I am using this listener I downloaded here Paypal-IPN-listener. The problem is I always get an "INVALID" response. I've checked my host provider and they confirmed that the server has OpenSSL v1.0.1 which supports TLS1.2. Any help would be much appreciated.
The response I get:
HTTP/1.1 200 OK Date: Fri, 10 Jun 2016 03:32:59 GMT Server: Apache
X-Frame-Options: SAMEORIGIN Set-Cookie:
c9MWDuvPtT9GIMyPc3jwol1VSlO=M5RZRbR9O0p3RyZcCd57VOjVO7vQahSBxti78obb5GWSaRge7FbInlJm9xeF6VIGA8jPMUyenYAZxlLOd4-YqTmucvLu0i7lWMg5RM94Q0SFZOVZ1_ysAo5Y_G3ISqMX_oc5Ts8Xqy5z9q8YKYW-RCBOKKo6ZpkXYUKF2RyjbOQIV3jeUWUSBBtkl4SxeDFi4MUm49opYTa6CvKAqQZ4vr6cWpOQghPpmEsz0s175Z8k6UtTfB99JV14faev1SBZUafmOkyNWCT3XSUF_dRWx-RAQ9L_T8MPetd5beRMm5fXBY1rvehFXw33aUvhTxckwGBJrdrKW2bOQa0Ry6YaRdp1uvdMC6uCuesozTjKXBFgSf0g0cDdHXoxV0rgVPTMlTRoVoV9xNJJ1CNKZzxeCvaFBZNSQdSTT888w0;
domain=.paypal.com; path=/; Secure; HttpOnly Set-Cookie:
cookie_check=yes; expires=Mon, 08-Jun-2026 03:32:59 GMT;
domain=.paypal.com; path=/; Secure; HttpOnly Set-Cookie:
navcmd=_notify-validate; domain=.paypal.com; path=/; Secure; HttpOnly
Set-Cookie: navlns=0.0; expires=Sun, 10-Jun-2018 03:32:59 GMT;
domain=.paypal.com; path=/; Secure; HttpOnly Set-Cookie:
Apache=10.72.108.11.1465529579669687; path=/; expires=Sun, 03-Jun-46
03:32:59 GMT Vary: Accept-Encoding,User-Agent X-Cnection: close
HTTP_X_PP_AZ_LOCATOR: sandbox.slc Paypal-Debug-Id: 5d6d91989f423
Set-Cookie:
X-PP-SILOVER=name%3DSANDBOX3.WEB.1%26silo_version%3D1880%26app%3Dappdispatcher%26TIME%3D3946076759%26HTTP_X_PP_AZ_LOCATOR%3Dsandbox.slc;
Expires=Fri, 10 Jun 2016 04:03:00 GMT; domain=.paypal.com; path=/;
Secure; HttpOnly Set-Cookie: X-PP-SILOVER=; Expires=Thu, 01 Jan 1970
00:00:01 GMT Strict-Transport-Security: max-age=14400
Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8
INVALID
-------------------------------------------------------------------------------- payment_type instant payment_date Fri Jun 10
2016 11:02:14 GMT 0800 (PHT) payment_status Completed
address_status confirmed payer_status verified
first_name John last_name Smith
payer_email buyer#paypalsandbox.com payer_id
TESTBUYERID01 address_name John Smith address_country
United States address_country_code US address_zip
95131 address_state CA address_city San Jose
address_street 123 any street business
seller#paypalsandbox.com receiver_email
seller#paypalsandbox.com receiver_id
seller#paypalsandbox.com residence_country US item_name1
something item_number1 AK-1234 tax
2.02 mc_currency USD mc_fee 0.44 mc_gross 12.34 mc_gross_1 12.34
mc_handling 2.06 mc_handling1 1.67
mc_shipping 3.02 mc_shipping1 1.02 txn_type
cart txn_id 331854069 notify_version 2.1
custom xyz123 invoice abc1234
test_ipn 1 verify_sign
AFcWxV21C7fd0v3bYYYRCpSSRl31AfWDVp14NjdjOS60QpDQiiFYCMSm
ipn.php:
<?php
include('listener/ipnlistener.php');
$listener = new IpnListener();
$listener->use_sandbox = true;
try {
$listener->requirePostMethod();
$verified = $listener->processIpn();
} catch (Exception $e) {
$file = fopen("log.txt","w");
fwrite($file,$e->getMessage());
fclose($file);
exit(0);
}
if ($verified) {
$file = fopen("log.txt","w");
fwrite($file,$listener->getTextReport());
fclose($file);
} else {
$file = fopen("log.txt","w");
fwrite($file,$listener->getTextReport());
fclose($file);
}
?>
ipnlistener.php
<?php
/**
* 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) 2012 - Micah Carrick
* #version 2.1.0
*/
class IpnListener {
/**
* 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 = true;
/**
* The amount of time, in seconds, to wait for the PayPal server to respond
* before timing out. Default 30 seconds.
*
* #var int
*/
public $timeout = 30;
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) {
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_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO,
dirname(__FILE__)."/cert/cacert.pem");
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, 6);
}
$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("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) {
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("fsockopen error: [$errno] $errstr");
}
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Host: ".$this->getPaypalHost()."\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: ".strlen($encoded_data)."\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 self::SANDBOX_HOST;
else return self::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) {
$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("No POST data found.");
}
} 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("Invalid response status: ".$this->response_status);
}
if (strpos($this->response, "VERIFIED") !== false) {
return true;
} elseif (strpos($this->response, "INVALID") !== false) {
return false;
} else {
throw new Exception("Unexpected response from PayPal.");
}
}
/**
* Require Post Method
*
* Throws an exception and sets a HTTP 405 response header if the request
* method was not POST.
*/
public function requirePostMethod() {
// require POST requests
if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST') {
header('Allow: POST', true, 405);
throw new Exception("Invalid HTTP request method.");
}
}
}
Debugging steps
Check if the script is pointing to the correct path in both your IPN listener and the output generation script.
ssl://www.sandbox.paypal.com:443/cgi-bin/webscr for Sandbox (inc. IPN simulator)
ssl://www.paypal.com:443/cgi-bin/webscr for the live site
Only SSL connection is permitted for Sandbox.
The HTTP protocol can still be used for the live site at the time of writing.
Check if you server supports TLS 1.2
PayPal is updating its services to require TLS v1.2 for all HTTPS/TLS
if you use fsockopen()m makes sure the function has been enabled in your PHP configuration
if you use curl() functions makes sure, they are enabled in your PHP configuration
Check if curl_setopt($ch, CURLOPT_SSLVERSION, ) is set to the correct version, i.e. 6 at the time of writing
If all the above has been checked and you still can't get it to work, you can trying putting NULL in the payment date field of the IPN simulator, to see if it returns a VERIFIED result.
The plus sign used for timezone in payment date can cause problem
when urlencode() is used to parse the URL.
Related
PayPal Sandbox testing continuously provides the following error:
http 400 - Bad Request Your browser sent a request that this server
could not understand.
I am using the git provided sample code from PayPal and am unable to get around this error.
Code [PayPalIPN.php]
<?php
class PaypalIPN
{
/** #var bool Indicates if the sandbox endpoint is used. */
private $use_sandbox = false;
/** #var bool Indicates if the local certificates are used. */
private $use_local_certs = true;
/** Production Postback URL */
const VERIFY_URI = 'https://ipnpb.paypal.com/cgi-bin/webscr';
/** Sandbox Postback URL */
const SANDBOX_VERIFY_URI = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
/** Response from PayPal indicating validation was successful */
const VALID = 'VERIFIED';
/** Response from PayPal indicating validation failed */
const INVALID = 'INVALID';
/**
* Sets the IPN verification to sandbox mode (for use when testing,
* should not be enabled in production).
* #return void
*/
public function useSandbox()
{
$this->use_sandbox = true;
}
/**
* Sets curl to use php curl's built in certs (may be required in some
* environments).
* #return void
*/
public function usePHPCerts()
{
$this->use_local_certs = false;
}
/**
* Determine endpoint to post the verification data to.
*
* #return string
*/
public function getPaypalUri()
{
if ($this->use_sandbox) {
return self::SANDBOX_VERIFY_URI;
} else {
return self::VERIFY_URI;
}
}
/**
* Verification Function
* Sends the incoming post data back to PayPal using the cURL library.
*
* #return bool
* #throws Exception
*/
public function verifyIPN()
{
if ( ! count($_POST)) {
throw new Exception("Missing POST Data");
}
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval);
if (count($keyval) == 2) {
// Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
if ($keyval[0] === 'payment_date') {
if (substr_count($keyval[1], '+') === 1) {
$keyval[1] = str_replace('+', '%2B', $keyval[1]);
}
}
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
}
// Build the body of the verification post request, adding the _notify-validate command.
$req = 'cmd=_notify-validate';
$get_magic_quotes_exists = false;
if (function_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
foreach ($myPost as $key => $value) {
if ($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
// Post the data back to PayPal, using curl. Throw exceptions if errors occur.
$ch = curl_init($this->getPaypalUri());
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// This is often required if the server is missing a global cert bundle, or is using an outdated one.
if ($this->use_local_certs) {
curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . "/cert/cacert.pem");
}
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'User-Agent: PHP-IPN-Verification-Script',
'Connection: Close',
));
$res = curl_exec($ch);
if ( ! ($res)) {
$errno = curl_errno($ch);
$errstr = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: [$errno] $errstr");
}
$info = curl_getinfo($ch);
$http_code = $info['http_code'];
if ($http_code != 200) {
throw new Exception("PayPal responded with http code $http_code");
}
curl_close($ch);
// Check if PayPal verifies the IPN data, and if so, return true.
if ($res == self::VALID) {
return true;
} else {
return false;
}
}
}
?>
Code [PaypalListener.php]
<?php namespace Listener;
require('PaypalIPN.php');
use PaypalIPN;
$ipn = new PaypalIPN();
// Use the sandbox endpoint during testing.
$ipn->useSandbox();
$verified = $ipn->verifyIPN();
if ($verified) {
/*
* Process IPN
* A list of variables is available here:
* https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*/
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly.
header("HTTP/1.1 200 OK");
?>
When using PayPals IPN Simulator, the following data gets posted to the PayPalIPN page:
payment_type=instant&payment_date=14%3A37%3A40%20Aug%2004%2C%202021%20PDT&payment_status=Completed&address_status=confirmed&payer_status=verified&first_name=John&last_name=Smith&payer_email=buyer#paypalsandbox.com&payer_id=TESTBUYERID01&address_name=John%20Smith&address_country=United%20States&address_country_code=US&address_zip=95131&address_state=CA&address_city=San%20Jose&address_street=123%20any%20street&business=seller#paypalsandbox.com&receiver_email=seller#paypalsandbox.com&receiver_id=seller#paypalsandbox.com&residence_country=US&item_name1=something&item_number1=AK-1234&tax=2.02&mc_currency=USD&mc_fee=0.44&mc_gross=12.34&mc_gross_1=12.34&mc_handling=2.06&mc_handling1=1.67&mc_shipping=3.02&mc_shipping1=1.02&txn_type=cart&txn_id=746114854¬ify_version=2.1&custom=xyz123&invoice=abc1234&test_ipn=1&verify_sign=AqXxBxXlRxzzzTc3OqYNzyZnI8SaAdBMjOvZCSXfOj1cWp11HHkIrMRX
And the $req returned data (that should be being sent back to PayPal) turns out as:
cmd=_notify-validate&payment_type=instant&payment_date=14%3A37%3A40+Aug+04%2C+2021+PDT&payment_status=Completed&address_status=confirmed&payer_status=verified&first_name=John&last_name=Smith&payer_email=buyer%40paypalsandbox.com&payer_id=TESTBUYERID01&address_name=John+Smith&address_country=United+States&address_country_code=US&address_zip=95131&address_state=CA&address_city=San+Jose&address_street=123+any+street&business=seller%40paypalsandbox.com&receiver_email=seller%40paypalsandbox.com&receiver_id=seller%40paypalsandbox.com&residence_country=US&item_name1=something&item_number1=AK-1234&tax=2.02&mc_currency=USD&mc_fee=0.44&mc_gross=12.34&mc_gross_1=12.34&mc_handling=2.06&mc_handling1=1.67&mc_shipping=3.02&mc_shipping1=1.02&txn_type=cart&txn_id=746114854¬ify_version=2.1&custom=xyz123&invoice=abc1234&test_ipn=1&verify_sign=AqXxBxXlRxzzzTc3OqYNzyZnI8SaAdBMjOvZCSXfOj1cWp11HHkIrMRX
I noticed in the return encode, the "+" symbols were originally %20 and the "#" symbols have been replaced with %40, I have replaced these to match the original code received but with no luck in any change of error.
I am unable to find a solution to this problem on PayPal forums nor otherwise.
What am I missing? Please help!
This issue is related to PayPal's recent sandbox migration to the cloud. For now, change the verification postback hostname from ipnpb to www:
const SANDBOX_VERIFY_URI = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
I am just having a little trouble with the PayPal IPN. I used their sample code from GitHub. However when I test it in the IPN Simulator, it just says "IPN was not sent, and the handshake was not verified. Review your information."
In the paypalIPN.php, I made $use_local_certs equal false for now as I didn't implement them.
In the routes.php, I commented out the sandbox line for now.
I also did some testing with the server. And everything looks fine. I think I'm just missing something and I just can't put my finger on it.
New, TLSv1/SSLv3, Cipher is AES256-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
Protocol : TLSv1.2
Cipher : AES256-SHA256
Session-ID: 1EE4015F32E83402862D7C1618B15C0AF9826CAB32B642D05C177EAEEA41AC1B
Session-ID-ctx:
Master-Key: 7C9C9915C2D933F5E28F566F639BD72E3B591037EE22C6801CBBEB4AFD90784E32BA0DE5989FD9EBA81911589CCE87D3
Key-Arg : None
PSK identity: None
PSK identity hint: None
SRP username: None
Start Time: 1501993978
Timeout : 300 (sec)
Verify return code: 0 (ok)
Here is the routes.php
<?php namespace Listener;
require('PaypalIPN.php');
use PaypalIPN;
$ipn = new PaypalIPN();
// Use the sandbox endpoint during testing.
//$ipn->useSandbox();
$verified = $ipn->verifyIPN();
if ($verified) {
/*
* Process IPN
* A list of variables is available here:
* https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*/
require ('../database.php');
$sql = "INSERT INTO sales (firstName) values (?)";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", "HELLO HUMANS");
$stmt->execute();
$stmt->store_results();
$stmt->close();
$mysqli->close();
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly.
header("HTTP/1.1 200 OK");
And here is the paypalIPN.php
<?php
class PaypalIPN
{
/**
* #var bool $use_sandbox Indicates if the sandbox endpoint is used.
*/
private $use_sandbox = false;
/**
* #var bool $use_local_certs Indicates if the local certificates are used.
*/
private $use_local_certs = false;
/** Production Postback URL */
const VERIFY_URI = 'https://ipnpb.paypal.com/cgi-bin/webscr';
/** Sandbox Postback URL */
const SANDBOX_VERIFY_URI = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
/** Response from PayPal indicating validation was successful */
const VALID = 'VERIFIED';
/** Response from PayPal indicating validation failed */
const INVALID = 'INVALID';
/**
* Sets the IPN verification to sandbox mode (for use when testing,
* should not be enabled in production).
* #return void
*/
public function useSandbox()
{
$this->use_sandbox = true;
}
/**
* Sets curl to use php curl's built in certs (may be required in some
* environments).
* #return void
*/
public function usePHPCerts()
{
$this->use_local_certs = false;
}
/**
* Determine endpoint to post the verification data to.
* #return string
*/
public function getPaypalUri()
{
if ($this->use_sandbox) {
return self::SANDBOX_VERIFY_URI;
} else {
return self::VERIFY_URI;
}
}
/**
* Verification Function
* Sends the incoming post data back to PayPal using the cURL library.
*
* #return bool
* #throws Exception
*/
public function verifyIPN()
{
if ( ! count($_POST)) {
throw new Exception("Missing POST Data");
}
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval);
if (count($keyval) == 2) {
// Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
if ($keyval[0] === 'payment_date') {
if (substr_count($keyval[1], '+') === 1) {
$keyval[1] = str_replace('+', '%2B', $keyval[1]);
}
}
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
}
// Build the body of the verification post request, adding the _notify-validate command.
$req = 'cmd=_notify-validate';
$get_magic_quotes_exists = false;
if (function_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
foreach ($myPost as $key => $value) {
if ($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
// Post the data back to PayPal, using curl. Throw exceptions if errors occur.
$ch = curl_init($this->getPaypalUri());
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// This is often required if the server is missing a global cert bundle, or is using an outdated one.
if ($this->use_local_certs) {
curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . "/cert/cacert.pem");
}
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
$res = curl_exec($ch);
if ( ! ($res)) {
$errno = curl_errno($ch);
$errstr = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: [$errno] $errstr");
}
$info = curl_getinfo($ch);
$http_code = $info['http_code'];
if ($http_code != 200) {
throw new Exception("PayPal responded with http code $http_code");
}
curl_close($ch);
// Check if PayPal verifies the IPN data, and if so, return true.
if ($res == self::VALID) {
return true;
} else {
return false;
}
}
}
The reason is that when you test the direct IPN URL using this script above, ie. enter a direct URL to the IPN simulator like: example.com/routes.php then there are not POST data sent. But it should work in the Live environment.
Paypal IPN is returning an empty response. Here is my code.
<?php
ini_set("log_errors", 1);
ini_set("error_log", "php-error.log");
/**
* PayPal IPN Listener
*
* A class to listen for and handle Instant Payment Notifications (IPN) from
* the PayPal server.
*
* Forked from the great Quixotix PayPal IPN script. This fork plans to
* fix the current issues with the original repo, as well as update the code
* for use according to PayPal's documentation, and today's standards.
*
* #package PHP-PayPal-IPN
* #link https://github.com/WadeShuler/PHP-PayPal-IPN
* #forked https://github.com/Quixotix/PHP-PayPal-IPN
* #author Wade Shuler
* #copyright Copyright (c) 2015, Wade Shuler
* #license http://choosealicense.com/licenses/gpl-2.0/
* #version 2.5.2
*/
class IpnListener
{
/**
* 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 = false;
/**
* If true, cURL will use the CURLOPT_FOLLOWLOCATION to follow any
* "Location: ..." headers in the response.
*
* #var boolean
*/
public $follow_location = false;
/**
* 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 = 30;
/**
* If true, enable SSL certification validation when using cURL
*
* #var boolean
*/
public $verify_ssl = true;
private $_errors = array();
private $post_data;
private $rawPostData; // raw data from php://input
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.
*
* #todo add URL param so function is more dynamic
*
* #param string The post data as a URL encoded string
*/
protected function curlPost($encoded_data)
{
$uri = 'https://'.$this->getPaypalHost().'/cgi-bin/webscr';
$this->post_uri = $uri;
$ch = curl_init();
if ($this->verify_ssl) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO, dirname(dirname(__FILE__)) . '/cert/api_cert_chain.crt');
}
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);
$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("cURL error: [$errno] $errstr");
}
return $this->response;
}
/**
* 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.
*
* #todo add URL param so function is more dynamic
*
* #param string The post data as a URL encoded string
*/
protected function fsockPost($encoded_data)
{
$uri =$this->getPaypalHost();
$port = '443';
$this->post_uri = $uri.'/cgi-bin/webscr';
$fp = fsockopen($uri, $port, $errno, $errstr, $this->timeout);
if (!$fp) {
// fsockopen error
throw new Exception("fsockopen error: [$errno] $errstr");
}
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Host: ".$this->getPaypalHost()."\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: ".strlen($encoded_data)."\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);
return $this->response;
}
private function getPaypalHost()
{
return ($this->use_sandbox) ? self::SANDBOX_HOST : self::PAYPAL_HOST;
}
public function getErrors()
{
return $this->_errors;
}
private function addError($error)
{
$this->_errors[] .= $error;
}
public function getPostData()
{
return $this->post_data;
}
public function getRawPostData()
{
return $this->rawPostData;
}
/**
* 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)
{
try
{
$this->requirePostMethod(); // processIpn() should check itself if data is POST
// Read POST data
// reading posted data directly from $_POST causes serialization
// issues with array data in POST. Reading raw POST data from input stream instead.
if ($post_data === null) {
$raw_post_data = file_get_contents('php://input');
} else {
$raw_post_data = $post_data;
}
$this->rawPostData = $raw_post_data; // set raw post data for Class use
// if post_data is php input stream, make it an array.
if ( ! is_array($raw_post_data) ) {
$raw_post_array = explode('&', $raw_post_data);
$this->post_data = $raw_post_array; // use post array because it's same as $_POST
} else {
$this->post_data = $raw_post_data; // use post array because it's same as $_POST
}
$myPost = array();
if (isset($raw_post_array)) {
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval);
if (count($keyval) == 2) {
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
}
}
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
//XXX Debug log
$file = fopen('lastresponse.log', 'w');
fwrite($file, $req);
fclose($file);
if ($this->use_curl) {
$res = $this->curlPost($req);
} else {
$res = $this->fsockPost($req);
}
if (strpos($res, '200') === false) {
throw new Exception("Invalid response status: " . $res);
}
// Split response headers and payload, a better way for strcmp
$tokens = explode("\r\n\r\n", trim($res));
$res = trim(end($tokens));
if (strpos ($res, "VERIFIED") !== false) {
return true;
} else if (strpos ($res, "INVALID") !== false) {
return false;
} else {
throw new Exception("Unexpected response from PayPal: " . $res);
}
} catch (Exception $e) {
$this->addError($e->getMessage());
return false;
}
return false;
}
/**
* Require Post Method
*
* Throws an exception and sets a HTTP 405 response header if the request
* method was not POST.
*/
public function requirePostMethod()
{
// require POST requests
if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST') {
header('Allow: POST', true, 405);
throw new Exception("Invalid HTTP request method.");
}
}
}
$file = fopen('lastrequest.log', 'w');
fwrite($file, file_get_contents('php://input'));
fclose($file);
$listener = new IpnListener();
$listener->use_sandbox = true;
$listener->use_curl = false;
header('HTTP/1.1 200 OK');
if ($verified = $listener->processIpn())
{
// Valid IPN
/*
1. Check that $_POST['payment_status'] is "Completed"
2. Check that $_POST['txn_id'] has not been previously processed
3. Check that $_POST['receiver_email'] is your Primary PayPal email
4. Check that $_POST['payment_amount'] and $_POST['payment_currency'] are correct
*/
$transactionRawData = $listener->getRawPostData(); // raw data from PHP input stream
$transactionData = $listener->getPostData(); // POST data array
// Feel free to modify path and filename. Make SURE THE DIRECTORY IS WRITEABLE!
// For security reasons, you should use a path above/outside of your webroot
file_put_contents('ipn_success.log', print_r($transactionData, true) . PHP_EOL, LOCK_EX | FILE_APPEND);
} else {
// Invalid IPN
$errors = $listener->getErrors();
// Feel free to modify path and filename. Make SURE THE DIRECTORY IS WRITEABLE!
// For security reasons, you should use a path above/outside of your webroot
file_put_contents('ipn_errors.log', print_r($errors, true) . PHP_EOL, LOCK_EX | FILE_APPEND);
echo print_r($errors, true);
}
file_put_contents("verified.log", $verified ? "VERIFIED" : "INVALID");
?>
I've tried checking multiple IPNs but none worked. So, I decided to use that one as it logs everything.
lastrequest.log:
cmd=_s-xclick&hosted_button_id=L9CJDZARMVPHA
lastresponse.log:
cmd=_notify-validate&cmd=_s-xclick&hosted_button_id=L9CJDZARMVPHA
verified.log:
INVALID
ipn_errors.log:
Array
(
[0] => Invalid response status:
)
I've tried using curl and fsock, but the result is always the same.
I get this same issue because PayPal got problem with POST message.
I was check this issue with POSTMAN and I was copy sample PayPal message from doc examples. An I send this message using POSTMAN - effect was simple server receive real POST message and script deal with this perfect. I don't know why PayPal have this error wit sending messages by POST.
My problem
I set up an IPN listener in PHP, but it always returns INAVLID when testing with PayPal's IPN Simulator.
I know that this is a frequently asked question, but I have spent an entire weekend reading 50+ similar questions and trying out their solutions, but not a single one of them worked for me.
Note: I have to use fsock, my server does not support cURL.
What I tried
Ensured that my server is sending the request to www.sandbox.paypal.com and not www.paypal.com.
Ensured that my server uses SSL and port 443.
Ensured that the Host header is not missing.
Ensured that my response is equal to PayPal's request, prefixed with cmd=_notify-validate&.
Ensured that my server is parsing the VERIFIED / INVALID response correctly (PayPal's new system sends 7\r\nINVALID\r\n0 instead of just INVALID).
My code
IPN listener class
<?php
/**
* PayPal IPN Listener
*
* A class to listen for and handle Instant Payment Notifications (IPN) from
* the PayPal server.
*
* Forked from the great Quixotix PayPal IPN script. This fork plans to
* fix the current issues with the original repo, as well as update the code
* for use according to PayPal's documentation, and today's standards.
*
* #package PHP-PayPal-IPN
* #link https://github.com/WadeShuler/PHP-PayPal-IPN
* #forked https://github.com/Quixotix/PHP-PayPal-IPN
* #author Wade Shuler
* #copyright Copyright (c) 2015, Wade Shuler
* #license http://choosealicense.com/licenses/gpl-2.0/
* #version 2.5.2
*/
class IpnListener
{
/**
* 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, cURL will use the CURLOPT_FOLLOWLOCATION to follow any
* "Location: ..." headers in the response.
*
* #var boolean
*/
public $follow_location = false;
/**
* 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 = 30;
/**
* If true, enable SSL certification validation when using cURL
*
* #var boolean
*/
public $verify_ssl = true;
private $_errors = array();
private $post_data;
private $rawPostData; // raw data from php://input
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.
*
* #todo add URL param so function is more dynamic
*
* #param string The post data as a URL encoded string
*/
protected function curlPost($encoded_data)
{
$uri = 'https://'.$this->getPaypalHost().'/cgi-bin/webscr';
$this->post_uri = $uri;
$ch = curl_init();
if ($this->verify_ssl) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO, dirname(dirname(__FILE__)) . '/cert/api_cert_chain.crt');
}
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);
$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("cURL error: [$errno] $errstr");
}
return $this->response;
}
/**
* 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.
*
* #todo add URL param so function is more dynamic
*
* #param string The post data as a URL encoded string
*/
protected function fsockPost($encoded_data)
{
$uri = 'ssl://'.$this->getPaypalHost();
$port = '443';
$this->post_uri = $uri.'/cgi-bin/webscr';
$fp = fsockopen($uri, $port, $errno, $errstr, $this->timeout);
if (!$fp) {
// fsockopen error
throw new Exception("fsockopen error: [$errno] $errstr");
}
$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Host: ".$this->getPaypalHost()."\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: ".strlen($encoded_data)."\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);
return $this->response;
}
private function getPaypalHost()
{
return ($this->use_sandbox) ? self::SANDBOX_HOST : self::PAYPAL_HOST;
}
public function getErrors()
{
return $this->_errors;
}
private function addError($error)
{
$this->_errors[] .= $error;
}
public function getPostData()
{
return $this->post_data;
}
public function getRawPostData()
{
return $this->rawPostData;
}
/**
* 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)
{
try
{
$this->requirePostMethod(); // processIpn() should check itself if data is POST
// Read POST data
// reading posted data directly from $_POST causes serialization
// issues with array data in POST. Reading raw POST data from input stream instead.
if ($post_data === null) {
$raw_post_data = file_get_contents('php://input');
} else {
$raw_post_data = $post_data;
}
$this->rawPostData = $raw_post_data; // set raw post data for Class use
// if post_data is php input stream, make it an array.
if ( ! is_array($raw_post_data) ) {
$raw_post_array = explode('&', $raw_post_data);
$this->post_data = $raw_post_array; // use post array because it's same as $_POST
} else {
$this->post_data = $raw_post_data; // use post array because it's same as $_POST
}
$myPost = array();
if (isset($raw_post_array)) {
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval);
if (count($keyval) == 2) {
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
}
}
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
//XXX Debug log
$file = fopen('lastresponse.log', 'w');
fwrite($file, $req);
fclose($file);
if ($this->use_curl) {
$res = $this->curlPost($req);
} else {
$res = $this->fsockPost($req);
}
if (strpos($res, '200') === false) {
throw new Exception("Invalid response status: " . $res);
}
// Split response headers and payload, a better way for strcmp
$tokens = explode("\r\n\r\n", trim($res));
$res = trim(end($tokens));
if (strpos ($res, "VERIFIED") !== false) {
return true;
} else if (strpos ($res, "INVALID") !== false) {
return false;
} else {
throw new Exception("Unexpected response from PayPal: " . $res);
}
} catch (Exception $e) {
$this->addError($e->getMessage());
return false;
}
return false;
}
/**
* Require Post Method
*
* Throws an exception and sets a HTTP 405 response header if the request
* method was not POST.
*/
public function requirePostMethod()
{
// require POST requests
if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST') {
header('Allow: POST', true, 405);
throw new Exception("Invalid HTTP request method.");
}
}
}
Actual IPN listener
<?php
ini_set("log_errors", 1);
ini_set("error_log", "php-error.log");
$file = fopen('lastrequest.log', 'w');
fwrite($file, file_get_contents('php://input'));
fclose($file);
require_once $_SERVER['DOCUMENT_ROOT'] . '/_includes/ipnlistener.php';
$listener = new IpnListener();
$listener->use_sandbox = true;
$listener->use_curl = false;
header('HTTP/1.1 200 OK');
if ($verified = $listener->processIpn())
{
// Valid IPN
/*
1. Check that $_POST['payment_status'] is "Completed"
2. Check that $_POST['txn_id'] has not been previously processed
3. Check that $_POST['receiver_email'] is your Primary PayPal email
4. Check that $_POST['payment_amount'] and $_POST['payment_currency'] are correct
*/
$transactionRawData = $listener->getRawPostData(); // raw data from PHP input stream
$transactionData = $listener->getPostData(); // POST data array
// Feel free to modify path and filename. Make SURE THE DIRECTORY IS WRITEABLE!
// For security reasons, you should use a path above/outside of your webroot
file_put_contents('ipn_success.log', print_r($transactionData, true) . PHP_EOL, LOCK_EX | FILE_APPEND);
} else {
// Invalid IPN
$errors = $listener->getErrors();
// Feel free to modify path and filename. Make SURE THE DIRECTORY IS WRITEABLE!
// For security reasons, you should use a path above/outside of your webroot
file_put_contents('ipn_errors.log', print_r($errors, true) . PHP_EOL, LOCK_EX | FILE_APPEND);
}
file_put_contents("verified.log", $verified ? "VERIFIED" : "INVALID");
My logs
PayPal's request
payment_type=instant&payment_date=Sun%20Dec%2006%202015%2020%3A05%3A21%20GMT%2B0100%20%28Mitteleurop%C3%A4ische%20Zeit%29&payment_status=Completed&address_status=confirmed&payer_status=verified&first_name=John&last_name=Smith&payer_email=buyer%40paypalsandbox.com&payer_id=TESTBUYERID01&address_name=John%20Smith&address_country=United%20States&address_country_code=US&address_zip=95131&address_state=CA&address_city=San%20Jose&address_street=123%20any%20street&business=seller%40paypalsandbox.com&receiver_email=seller%40paypalsandbox.com&receiver_id=seller%40paypalsandbox.com&residence_country=US&item_name1=something&item_number1=AK-1234&tax=2.02&mc_currency=USD&mc_fee=0.44&mc_gross=12.34&mc_gross1=12.34&mc_handling=2.06&mc_handling1=1.67&mc_shipping=3.02&mc_shipping1=1.02&txn_type=cart&txn_id=936522821¬ify_version=2.1&custom=xyz123&invoice=abc1234&test_ipn=1&verify_sign=AFcWxV21C7fd0v3bYYYRCpSSRl31A61b6KnaHJWRwuKxRGWvWo2Bos20
My server's response
cmd=_notify-validate&payment_type=instant&payment_date=Sun+Dec+06+2015+20%3A05%3A21+GMT%2B0100+%28Mitteleurop%C3%A4ische+Zeit%29&payment_status=Completed&address_status=confirmed&payer_status=verified&first_name=John&last_name=Smith&payer_email=buyer%40paypalsandbox.com&payer_id=TESTBUYERID01&address_name=John+Smith&address_country=United+States&address_country_code=US&address_zip=95131&address_state=CA&address_city=San+Jose&address_street=123+any+street&business=seller%40paypalsandbox.com&receiver_email=seller%40paypalsandbox.com&receiver_id=seller%40paypalsandbox.com&residence_country=US&item_name1=something&item_number1=AK-1234&tax=2.02&mc_currency=USD&mc_fee=0.44&mc_gross=12.34&mc_gross1=12.34&mc_handling=2.06&mc_handling1=1.67&mc_shipping=3.02&mc_shipping1=1.02&txn_type=cart&txn_id=936522821¬ify_version=2.1&custom=xyz123&invoice=abc1234&test_ipn=1&verify_sign=AFcWxV21C7fd0v3bYYYRCpSSRl31A61b6KnaHJWRwuKxRGWvWo2Bos20
I've spent almost 2 days on this issue and thought I would share with people what have worked for me hopefully It can help a few others who faced the same issue.
Problem
PayPal IPN simulator gives INVALID IPN response always.
Solution
When simulating change the payment_date in simulator fields to NULL and everything should start working if your setup is correct.
IPN Simulator uses Sandbox environment, but your IPN Listener class is setting to public $use_sandbox = false;, that means you are in Live environment, which could be the cause of the issue of your response always getting INVALID.
I'm trying to do a HTTP POST Request to a SMA Datalogger, that uses JSON-RPC to respond to HTTP requests.
Using hurl.it I can make a successful request, for example:
Destination: POST, http://aaa.no-ip.org:101/rpc, follow redirects:on.
Headers: Host: aaa.no-ip.org:101, Content-Type:text/plain.
Body: RPC={"proc":"GetPlantOverview","format":"JSON","version":"1.0","id":"1"}
Then hurl.it process's the following request:
Success
POST http://aaa.no-ip.org:101/rpc
200 OK 401 bytes 3.76 secs
HEADERS
Accept: */*
Accept-Encoding: application/json
Content-Length: 122
Content-Type: text/plain
Host: aaa.no-ip.org
User-Agent: runscope/0.1
BODY
RPC=%7B%22proc%22%3A%22GetPlantOverview%22%2C%22format%22%3A%22JSON%22%2C%22version%22%3A%221.0%22%2C%22id%22%3A%221%22%7D
And the response is:
HEADERS
Cache-Control: no-store, no-cache, max-age=0
Connection: keep-alive
Content-Length: 401
Content-Type: text/html
Date: Wed, 22 Oct 2014 14:15:50 GMT
Keep-Alive: 300
Pragma: no-cache
Server: Sunny WebBox
BODY
{"format":"JSON","result":{"overview":[{"unit":"W","meta":"GriPwr","name":"GriPwr","value":"99527"},{"unit":"kWh","meta":"GriEgyTdy","name":"GriEgyTdy","value":"842.849"},{"unit":"kWh","meta":"GriEgyTot","name":"GriEgyTot","value":"2851960.438"},{"unit":"","meta":"OpStt","name":"OpStt","value":""},{"unit":"","meta":"Msg","name":"Msg","value":""}]},"proc":"GetPlantOverview","version":"1.0","id":"1"}
My problem is, every time I try to replicate these requests I always get:
string(0) ""
It could be because I'm using a shared host. I tried cURL, plain PHP (socket and file_get_contents, and even jQuery.
Can someone please provide an example on how to do this request?
Either jquery or php, I don't even care anymore, I've been trying for 2 weeks, and so many attempts, and either I get code errors or just string(0)"".
PS: for previous attempts examples, see: https://stackoverflow.com/questions/26408153/solar-energy-monitoring-sma-webbox-json-post-request
Here is the JSONRPC Client I have used to successfully make JSON RPC HTTP Requests. Hope this helps:
You may need to change some things to work in your code.
Found here: https://code.google.com/p/pmvc-framework/source/browse/trunk/src/main/php/pmvc/remoting/jsonrpc/JsonRpcClient.class.php?r=328
<?php
use Exception;
use ReflectionClass;
/*
* A client for accessing JSON-RPC over HTTP servers.
*
*/
class JsonRpcClient
{
private $_url = false;
private $_reuseConnections = true;
private static $_curl = null;
private $_nextId = 99;
/*
* Creates the client for the given URL.
* #param string $_url
*/
public function __construct($url)
{
$this->_url = $url;
}
/*
* Returns a curl resource.
* #return resource
*/
private function getCurl()
{
if (!isset(self::$_curl)) {
// initialize
self::$_curl = curl_init();
// set options
curl_setopt(self::$_curl, CURLOPT_FAILONERROR, true);
curl_setopt(self::$_curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt(self::$_curl, CURLOPT_FORBID_REUSE, $this->_reuseConnections===false);
curl_setopt(self::$_curl, CURLOPT_FRESH_CONNECT, $this->_reuseConnections===false);
curl_setopt(self::$_curl, CURLOPT_CONNECTTIMEOUT, 5);
return self::$_curl;
}
}
/*
* Invokes the given method with the given arguments
* on the server and returns it's value. If {#code $returnType}
* is specified than an instance of the class that it names
* will be created passing the json object (stdClass) to it's
* constructor.
*
* #param string $method the method to invoke
* #param Array $params the parameters (if any) to the method
* #param string $id the request id
* #param Array $headers any additional headers to add to the request
*/
public function invoke($method, Array $params=Array(), $id=false, Array $headers=Array())
{
// get curl
$curl = $this->getCurl();
// set url
curl_setopt($curl, CURLOPT_URL, $this->_url);
// set post body
$request = json_encode(
Array(
'jsonrpc' => '2.0',
'method' => $method,
'params' => $params,
'id' => ($id===false) ? ++$this->nextId : $id
)
);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
// set headers
$curlHeaders = Array();
$curlHeaders []= "Content-type: application/json-rpc";
for (reset($headers); list($key, $value)=each($headers); ) {
$curlHeaders []= $key.": ".$value."\n";
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $curlHeaders);
// post the data
$response = curl_exec($curl);
if (!$response) {
throw new Exception('cURL error '.curl_error($curl).' while making request to '.$this->_url);
}
// decode json response
$response = json_decode($response);
if ($response==NULL || curl_error($curl)!=0) {
throw new Exception("JSON parsing error occured: ".json_last_error());
// throw errors
} else if (isset($response->error)) {
$msg = 'JSON-RPC error';
if (isset($response->error->message) && !empty($response->error->message)) {
$msg .= ': "' . $response->error->message . '"';
}
$msg .= "\n";
$msg .= 'URL: ' . $this->_url;
$msg .= "\n";
$msg .= 'Method: ' . $method;
$msg .= "\n";
$msg .= 'Arguments: ' . self::printArguments($params, 2);
if (isset($response->error->code)) {
throw new Exception($msg, intval($response->error->code));
} else {
throw new Exception($msg);
}
}
// get the headers returns (APPSVR, JSESSIONID)
$responsePlusHeaders = Array();
$responsePlusHeaders['result'] = $response->result;
$responsePlusHeaders['headers'] = curl_getinfo($curl);
// return the data
return $responsePlusHeaders;
}
/*
* Printing arguments.
* #param $arg
* #param $depth
*/
private static function printArguments($args, $depth=1)
{
$argStrings = Array();
foreach ($args as $arg) {
$argStrings[] = self::printArgument($arg, $depth);
}
return implode($argStrings, ', ');
}
/*
* Print an argument.
* #param $arg
* #param $depth
*/
private static function printArgument($arg, $depth=1)
{
if ($arg === NULL) {
return 'NULL';
} else if (is_array($arg)) {
if ($depth > 1) {
return '[' . self::printArguments($arg, ($depth - 1)) . ']';
} else {
return 'Array';
}
} else if (is_object($arg)) {
return 'Object';
} else if (is_bool($arg)) {
return ($arg === TRUE) ? 'true' : 'false';
} else if (is_string($arg)) {
return "'$arg'";
}
return strval($arg);
}
}
Usage would then be:
include JsonRpcClient.php
$client = new JsonRpcClient('http://aaa.no-ip.org:101/rpc');
$response = $client->invoke('GetPlantOverview', 1, array('Host: aaa.no-ip.org:101'));