简体   繁体   English

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

[英]Paypal IPN issue - Not handling some payments

I am currently developing a modification for an open source forum software. 我目前正在开发一个开源论坛软件的修改。 This modification allows an user to donate through that forum software. 此修改允许用户通过该论坛软件进行捐赠。

However, recently an user reported an issue which may be caused by my code. 但是,最近用户报告了可能由我的代码引起的问题。 I use another open source library to handle the IPN connection - An IPN Listener PHP class . 我使用另一个开源库来处理IPN连接 - 一个IPN侦听器PHP类

The user who reported this issue is receiving the following e-mail: 报告此问题的用户正在收到以下电子邮件:

Hello <My Name> , 你好<My Name>

Please check your server that handles PayPal Instant Payment Notifications (IPN). 请检查处理PayPal即时付款通知(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. 如果您无法识别此URL,则可能正在使用代表您使用IPN的服务提供商。 Please contact your service provider with the above information. 请通过上述信息与您的服务提供商联系。 If this problem continues, IPNs may be disabled for your account. 如果此问题仍然存在,则可能会为您的帐户停用IPN。

Thank you for your prompt attention to this issue. 感谢您及时关注此问题。

Sincerely, PayPal 真诚的,PayPal

I am fearing that the issue comes from my side, therefore I have to look into this and make sure. 我担心问题来自我的方面,因此我必须对此进行调查并确保。

I lightly modified the IPN Listener script, which leads me to think that my modification is causing this issue. 我轻轻地修改了IPN Listener脚本,这让我认为我的修改导致了这个问题。 Paypal also had some changes recently which might have provoked this problem. Paypal最近也有一些变化可能会引发这个问题。

This is how the class looks like momentarily: 这是怎样的类看起来像瞬间:

/**
* 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']);
        }
    }
}

Is there any issue with this script which is causing this problem? 此脚本是否存在导致此问题的问题?

PS: The URL donate/handler.php is indeed the IPN handler/listener file, so it's a recognized URL. PS:URL donate / handler.php确实是IPN处理程序/侦听器文件,因此它是一个可识别的URL。

For the debug part 对于调试部分

You can also check on Paypal the state of your IPN. 您还可以在Paypal上查看您的IPN状态。

My Account > History > IPN History. 我的帐户>历史> IPN历史。

It will list all IPN that were sent to your server. 它将列出发送到您服务器的所有IPN。 You will see a status for each of them. 您将看到每个人的状态。 It might help. 它可能有所帮助。 But as Andrew Angell says, take a look at your log. 但正如Andrew Angell所说,请看看你的日志。

For the PHP part 对于PHP部分

Paypal provide a lots a goodness stuff on their Github . Paypal在他们的Github上提供很多好东西。 You should definitively take a closer look. 你应该明确地仔细看看。

They have a dead simple IPNLister sample that you should use (instead of a custom one - even if it seems good). 他们有一个简单的 IPNLister样本你应该使用(而不是自定义的 - 即使看起来不错)。 It use built-in function from Paypal itself. 它使用Paypal本身的内置功能。 And I personally use it too. 我个人也使用它。 You shouldn't re-invent the wheel :) 你不应该重新发明轮子 :)

<?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");   
}

As you can see, it's simple. 如您所见,这很简单。

I use it in a slightly different way: 我以稍微不同的方式使用它:

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

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

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

The IPNListener class is custom to me: it does handle what to do with the IPN. IPNListener类对我来说是自定义的:它确实处理了如何处理IPN。 It parse the response and do action depending on the state: 它解析响应并根据状态执行操作:

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
  }
}

Check your web server logs. 检查Web服务器日志。 That will show you what result is coming up when the IPN script is hit, and since it's failing you must be getting some sort of 500 internal server error. 这将显示当IPN脚本被命中时会出现什么结果,并且由于它失败,您必须得到某种500内部服务器错误。 The logs will give you the error info that you would normally see on screen, like a syntax error, line number, etc. 日志将为您提供通常在屏幕上看到的错误信息,如语法错误,行号等。

What I like to do for troubleshooting, too, is create a simulator of my own by building a basic HTML form with the action set to the URL of my IPN listener. 我喜欢做的故障排除方法是通过构建一个基本的HTML表单来创建我自己的模拟器,其中操作设置为我的IPN侦听器的URL。 Add hidden fields with the names/values you'd expect to get from an IPN and then you can load that in a browser and submit it directly so that you can see the result on screen. 添加隐藏字段,其中包含您希望从IPN获取的名称/值,然后您可以在浏览器中加载它并直接提交,以便您可以在屏幕上看到结果。 You'll probably find that you have an error in your code somewhere causing the script to be unable to complete. 您可能会发现代码中的某个错误导致脚本无法完成。

Keep in mind that when testing this way the data isn't coming from PayPal so it will not be verified. 请记住,在以这种方式测试时,数据不是来自PayPal,因此无法进行验证。 You'll need to make sure your code logic is setup to handle that accordingly. 您需要确保设置代码逻辑以相应地处理它。

Once you're able to get everything running smoothly testing that way, I'd use the PayPal IPN listener as another confirmation, and then you can rest assured that you've fixed the issue. 一旦你能够以这种方式顺利运行一切,我就会使用PayPal IPN监听器作为另一个确认,然后你可以放心,你已经解决了这个问题。

I highly recommend using the IPN simulator in the Paypal developer site. 我强烈建议在Paypal开发者网站中使用IPN模拟器 It can build an IPN request for you and send it to your server and report back what it got. 它可以为您构建IPN请求并将其发送到您的服务器并报告它所获得的内容。

Check your logs under your Paypal account, it will show a list of IPN requests sent and also the results. 检查您的Paypal帐户下的日志,它将显示已发送的IPN请求列表以及结果。 If you have some major issues you can use the GET string provided to test out use cases. 如果您遇到一些重大问题,可以使用提供的GET字符串来测试用例。

There was indeed an issue with my code. 我的代码确实存在问题。 I did some casting mistake and it caused the IPN to fail. 我做了一些铸造错误,导致IPN失败。 This issue has been resolved. 此问题已得到解决。

Thanks for the help everyone. 感谢大家的帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM