I am currently developing a software modification for open source forums. This modification allows the user to donate through this forum software.
However, a user recently reported a problem that might be caused by my code. I use another open source library to handle the IPN connection - the IPN Listener PHP class .
The user who reported this problem receives the following email:
Hi <My Name> ,
Please check your server, which processes instant payment PayPal Notifications (IPN). Instant payments sent to the following URLs are not processed:
http://www.MySite.com/donate/handler.php
If you do not recognize this URL, you can use a service provider that uses IPN on your behalf. Contact your service provider with the above information. If this problem continues, IPN may be disabled for your account.
Thank you for your attention to this issue.
Regards, PayPal
I am afraid that the problem is coming from my side, so I must study this and make sure.
I slightly modified the IPN script listener, which makes me think that my modification is causing this problem. Recently, there have been some changes in Paypal that could trigger this problem.
This is how the class looks short-lived:
/** * 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']); } } }
Are there any problems with this script that causes this problem?
PS: The url donate / handler.php is really an IPN handler / listener file, so it is a recognized url.