草庐IT

php - Paypal IPN 问题 - 不处理某些付款

coder 2024-04-09 原文

我目前正在为一个开源论坛软件开发修改版。此修改允许用户通过该论坛软件进行捐赠。

但是,最近有用户报告了一个问题,可能是由我的代码引起的。我使用另一个开源库来处理 IPN 连接 - An IPN Listener PHP class .

报告此问题的用户将收到以下电子邮件:

Hello <My Name>,

Please check your server that handles PayPal Instant Payment Notifications (IPN). Instant Payment Notifications sent to the following URL(s) are failing:

http://www.MySite.com/donate/handler.php

If you do not recognize this URL, you may be using a service provider that is using IPN on your behalf. Please contact your service provider with the above information. If this problem continues, IPNs may be disabled for your account.

Thank you for your prompt attention to this issue.

Sincerely, PayPal

我担心问题出在我这边,因此我必须调查并确定。

我稍微修改了IPN Listener脚本,这让我认为我的修改导致了这个问题。 Paypal 最近也有一些变化,可能会引发这个问题。

这是 how该类暂时看起来像:

/**
* PayPal IPN Listener
*
* A class to listen for and handle Instant Payment Notifications (IPN) from 
* the PayPal server.
*
* https://github.com/Quixotix/PHP-PayPal-IPN
*
* @package    PHP-PayPal-IPN
* @author     Micah Carrick
* @copyright  (c) 2011 - Micah Carrick
* @version    2.0.5
* @license    http://opensource.org/licenses/gpl-license.php
*
* This library is originally licensed under GPL v3, but I received
* permission from the author to use it under GPL v2.
*/
class ipn_handler 
{
    /**
     *  If true, the recommended cURL PHP library is used to send the post back 
     *  to PayPal. If flase then fsockopen() is used. Default true.
     *
     *  @var boolean
     */
    public $use_curl = true;     

    /**
     *  If true, explicitly sets cURL to use SSL version 3. Use this if cURL
     *  is compiled with GnuTLS SSL.
     *
     *  @var boolean
     */
    public $force_ssl_v3 = true;     

    /**
     *  If true, cURL will use the CURLOPT_FOLLOWLOCATION to follow any 
     *  "Location: ..." headers in the response.
     *
     *  @var boolean
     */
    public $follow_location = false;     

    /**
     *  If true, an SSL secure connection (port 443) is used for the post back 
     *  as recommended by PayPal. If false, a standard HTTP (port 80) connection
     *  is used. Default true.
     *
     *  @var boolean
     */
    public $use_ssl = true;      

    /**
     *  If true, the paypal sandbox URI www.sandbox.paypal.com is used for the
     *  post back. If false, the live URI www.paypal.com is used. Default false.
     *
     *  @var boolean
     */
    public $use_sandbox = false; 

    /**
     *  The amount of time, in seconds, to wait for the PayPal server to respond
     *  before timing out. Default 30 seconds.
     *
     *  @var int
     */
    public $timeout = 60;       

    private $post_data = array();
    private $post_uri = '';     
    private $response_status = '';
    private $response = '';

    const PAYPAL_HOST = 'www.paypal.com';
    const SANDBOX_HOST = 'www.sandbox.paypal.com';

    /**
     *  Post Back Using cURL
     *
     *  Sends the post back to PayPal using the cURL library. Called by
     *  the processIpn() method if the use_curl property is true. Throws an
     *  exception if the post fails. Populates the response, response_status,
     *  and post_uri properties on success.
     *
     *  @param  string  The post data as a URL encoded string
     */
    protected function curlPost($encoded_data) 
    {
        global $user;

        if ($this->use_ssl) 
        {
            $uri = 'https://' . $this->getPaypalHost() . '/cgi-bin/webscr';
            $this->post_uri = $uri;
        }
        else 
        {
            $uri = 'http://' . $this->getPaypalHost() . '/cgi-bin/webscr';
            $this->post_uri = $uri;
        }

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $uri);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded_data);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $this->follow_location);
        curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);

        if ($this->force_ssl_v3) 
        {
            curl_setopt($ch, CURLOPT_SSLVERSION, 3);
        }

        $this->response = curl_exec($ch);
        $this->response_status = strval(curl_getinfo($ch, CURLINFO_HTTP_CODE));

        if ($this->response === false || $this->response_status == '0') 
        {
            $errno = curl_errno($ch);
            $errstr = curl_error($ch);
            throw new Exception($user->lang['CURL_ERROR'] . "[$errno] $errstr");
        }
    }

    /**
     *  Post Back Using fsockopen()
     *
     *  Sends the post back to PayPal using the fsockopen() function. Called by
     *  the processIpn() method if the use_curl property is false. Throws an
     *  exception if the post fails. Populates the response, response_status,
     *  and post_uri properties on success.
     *
     *  @param  string  The post data as a URL encoded string
     */
    protected function fsockPost($encoded_data) 
    {
        global $user;

        if ($this->use_ssl) 
        {
            $uri = 'ssl://' . $this->getPaypalHost();
            $port = '443';
            $this->post_uri = $uri . '/cgi-bin/webscr';
        } 
        else 
        {
            $uri = $this->getPaypalHost(); // no "http://" in call to fsockopen()
            $port = '80';
            $this->post_uri = 'http://' . $uri . '/cgi-bin/webscr';
        }

        $fp = fsockopen($uri, $port, $errno, $errstr, $this->timeout);

        if (!$fp) 
        { 
            // fsockopen error
            throw new Exception($user->lang['FSOCKOPEN_ERROR'] . "[$errno] $errstr");
        } 

        $header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
        $header .= "Content-Length: " . strlen($encoded_data) . "\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Host: " . $this->getPaypalHost() . "\r\n";
        $header .= "Connection: close\r\n\r\n";

        fputs($fp, $header . $encoded_data . "\r\n\r\n");

        while(!feof($fp)) 
        { 
            if (empty($this->response)) 
            {
                // extract HTTP status from first line
                $this->response .= $status = fgets($fp, 1024); 
                $this->response_status = trim(substr($status, 9, 4));
            } 
            else 
            {
                $this->response .= fgets($fp, 1024); 
            }
        } 

        fclose($fp);
    }

    private function getPaypalHost() 
    {
        if ($this->use_sandbox) 
        {
            return ipn_handler::SANDBOX_HOST;
        }
        else
        {
            return ipn_handler::PAYPAL_HOST;
        }
    }

    /**
     *  Get POST URI
     *
     *  Returns the URI that was used to send the post back to PayPal. This can
     *  be useful for troubleshooting connection problems. The default URI
     *  would be "ssl://www.sandbox.paypal.com:443/cgi-bin/webscr"
     *
     *  @return string
     */
    public function getPostUri() 
    {
        return $this->post_uri;
    }

    /**
     *  Get Response
     *
     *  Returns the entire response from PayPal as a string including all the
     *  HTTP headers.
     *
     *  @return string
     */
    public function getResponse() 
    {
        return $this->response;
    }

    /**
     *  Get Response Status
     *
     *  Returns the HTTP response status code from PayPal. This should be "200"
     *  if the post back was successful. 
     *
     *  @return string
     */
    public function getResponseStatus() 
    {
        return $this->response_status;
    }

    /**
     *  Get Text Report
     *
     *  Returns a report of the IPN transaction in plain text format. This is
     *  useful in emails to order processors and system administrators. Override
     *  this method in your own class to customize the report.
     *
     *  @return string
     */
    public function getTextReport() 
    {
        $r = '';

        // date and POST url
        for ($i = 0; $i < 80; $i++) 
        { 
            $r .= '-'; 
        }

        $r .= "\n[" . date('m/d/Y g:i A') . '] - ' . $this->getPostUri();
        if ($this->use_curl) 
        {
            $r .= " (curl)\n";
        }
        else
        {
            $r .= " (fsockopen)\n";
        }

        // HTTP Response
        for ($i = 0; $i < 80; $i++) 
        { 
            $r .= '-'; 
        }

        $r .= "\n{$this->getResponse()}\n";

        // POST vars
        for ($i = 0; $i < 80; $i++) 
        { 
            $r .= '-'; 
        }

        $r .= "\n";

        foreach ($this->post_data as $key => $value) 
        {
            $r .= str_pad($key, 25) . "$value\n";
        }

        $r .= "\n\n";

        return $r;
    }

    /**
     *  Process IPN
     *
     *  Handles the IPN post back to PayPal and parsing the response. Call this
     *  method from your IPN listener script. Returns true if the response came
     *  back as "VERIFIED", false if the response came back "INVALID", and 
     *  throws an exception if there is an error.
     *
     *  @param array
     *
     *  @return boolean
     */    
    public function processIpn($post_data = null) 
    {
        global $user;

        $encoded_data = 'cmd=_notify-validate';

        if ($post_data === null) 
        { 
            // use raw POST data 
            if (!empty($_POST)) 
            {
                $this->post_data = $_POST;
                $encoded_data .= '&' . file_get_contents('php://input');
            } 
            else 
            {
                throw new Exception($user->lang['NO_POST_DATA']);
            }
        } 
        else 
        { 
            // use provided data array
            $this->post_data = $post_data;

            foreach ($this->post_data as $key => $value) 
            {
                $encoded_data .= "&$key=" . urlencode($value);
            }
        }

        if ($this->use_curl) 
        {
            $this->curlPost($encoded_data); 
        }
        else
        {
            $this->fsockPost($encoded_data);
        }

        if (strpos($this->response_status, '200') === false) 
        {
            throw new Exception($user->lang['INVALID_RESPONSE'] . $this->response_status);
        }

        if (strpos(trim($this->response), "VERIFIED") !== false) 
        {
            return true;
        } 
        elseif (trim(strpos($this->response), "INVALID") !== false) 
        {
            return false;
        } 
        else 
        {
            throw new Exception($user->lang['UNEXPECTED_ERROR']);
        }
    }

    /**
     *  Require Post Method
     *
     *  Throws an exception and sets a HTTP 405 response header if the request
     *  method was not POST. 
     */    
    public function requirePostMethod() 
    {
        global $user;

        // require POST requests
        if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST') 
        {
            header('Allow: POST', true, 405);
            throw new Exception($user->lang['INVALID_REQUEST_METHOD']);
        }
    }
}

这个脚本是否有任何问题导致了这个问题?

P.S:URL donate/handler.php 确实是 IPN 处理程序/监听器文件,因此它是一个可识别的 URL。

最佳答案

调试部分

您还可以在 Paypal 上查看您的 IPN 状态。

My Account > History > IPN History.

它将列出所有发送到您的服务器的 IPN。您将看到他们每个人的状态。它可能会有所帮助。但正如 Andrew Angell 所说,请查看您的日志。

对于 PHP 部分

Paypal 在其 Github 上提供了很多有用的东西.您绝对应该仔细看看。

他们有a dead simple IPNLister sample你应该使用(而不是自定义的 - 即使它看起来不错)。它使用 Paypal 本身的内置功能。我个人也使用它。 你不应该重新发明轮子 :)

<?php
require_once('../PPBootStrap.php');
// first param takes ipn data to be validated. if null, raw POST data is read from input stream
$ipnMessage = new PPIPNMessage(null, Configuration::getConfig());
foreach($ipnMessage->getRawData() as $key => $value) {
    error_log("IPN: $key => $value");
}

if($ipnMessage->validate()) {
    error_log("Success: Got valid IPN data");       
} else {
    error_log("Error: Got invalid IPN data");   
}

如您所见,这很简单。

我以略微不同的方式使用它:

$rawData    = file_get_contents('php://input');
$ipnMessage = new PPIPNMessage($rawData);

$this->forward404If(!$ipnMessage->validate(), 'IPN not valid.');

$ipnListener = new IPNListener($rawData);
$ipnListener->process();

IPNListener 类对我来说是自定义的:它确实处理如何处理 IPN。它解析响应并根据状态执行操作:

function __construct($rawData)
{
  $rawPostArray = explode('&', $rawData);
  foreach ($rawPostArray as $keyValue)
  {
    $keyValue = explode ('=', $keyValue);
    if (count($keyValue) == 2)
    {
      $this->ipnData[$keyValue[0]] = urldecode($keyValue[1]);
    }
  }

  // log a new IPN and save in case of error in the next process
  $this->ipn = new LogIpn();
  $this->ipn->setContent($rawData);
  $this->ipn->setType(isset($this->ipnData['txn_type']) ? $this->ipnData['txn_type'] : 'Not defined');
  $this->ipn->save();
}

/**
 * Process a new valid IPN
 *
 */
public function process()
{
  if (null === $this->ipnData)
  {
    throw new Exception('ipnData is empty !');
  }

  if (!isset($this->ipnData['txn_type']))
  {
    $this->ipn->setSeemsWrong('No txn_type.');
    $this->ipn->save();

    return;
  }

  switch ($this->ipnData['txn_type'])
  {
    // handle statues
  }
}

关于php - Paypal IPN 问题 - 不处理某些付款,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18935679/

有关php - Paypal IPN 问题 - 不处理某些付款的更多相关文章

  1. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  4. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  5. ruby - Fast-stemmer 安装问题 - 2

    由于fast-stemmer的问题,我很难安装我想要的任何ruby​​gem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=

  6. ruby-on-rails - 在 Rails 和 ActiveRecord 中查询时忽略某些字段 - 2

    我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr

  7. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

    当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

  8. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  9. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  10. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

随机推荐