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'));
Related
I am using Paypal's two IPN scripts from github here: https://github.com/paypal/ipn-code-samples/tree/master/php however they aren't working.
I send the test IPN using Paypal's tool here: https://developer.paypal.com/developer/ipnSimulator/ and it says it was succesful: "IPN was sent and the handshake was verified."
However, I can't do what I want after that. I've tried inserting into a database or even just sending a success email. Both of these work fine when done on their own but when I add it to the IPN script nothing happens.
PaypalIPN.php (copied from the github)
<?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;
}
}
}
This is my IPN handler URL, example_usage.php (also copied from the github, I just added the one line to send an email, which does nothing when I send the test IPN):
<?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/
*/
//THIS IS THE ONLY PART I HAVE ADDED, BUT NOTHING IS HAPPENING
$name = '' //Get name from form input
$email = '' //Get email from form input
$amount = '' //Get amount from hidden form input
$subject = 'New order from' . $name
$message = 'New order. Amount: ' . $amount
//For some reason this isn't being sent, even if I change $subject and $message to my own string.
$sendemail= mail("***#***.com", $subject, $message);
}
// Reply with an empty 200 response to indicate to paypal the IPN was
received correctly.
header("HTTP/1.1 200 OK");
?>
Does anyone have an idea of what the problem is? Thanks if so
You need to make sure the IPNs are triggering as expected, and are being sent to the URL you expect.
Follow the steps outlined here and you'll be able to find the problem, which includes:
Local Testing
IPN Simulator
Sandbox Transaction Testing
Deployment
Additional tips for troubleshooting.
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.
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.
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.
What I'd like to do is find out what is the last/final URL after following the redirections.
I would prefer not to use cURL. I would like to stick with pure PHP (stream wrappers).
Right now I have a URL (let's say http://domain.test), and I use get_headers() to get specific headers from that page. get_headers will also return multiple Location: headers (see Edit below). Is there a way to use those headers to build the final URL? or is there a PHP function that would automatically do this?
Edit: get_headers() follows redirections and returns all the headers for each response/redirections, so I have all the Location: headers.
function getRedirectUrl ($url) {
stream_context_set_default(array(
'http' => array(
'method' => 'HEAD'
)
));
$headers = get_headers($url, 1);
if ($headers !== false && isset($headers['Location'])) {
return $headers['Location'];
}
return false;
}
Additionally...
As was mentioned in a comment, the final item in $headers['Location'] will be your final URL after all redirects. It's important to note, though, that it won't always be an array. Sometimes it's just a run-of-the-mill, non-array variable. In this case, trying to access the last array element will most likely return a single character. Not ideal.
If you are only interested in the final URL, after all the redirects, I would suggest changing
return $headers['Location'];
to
return is_array($headers['Location']) ? array_pop($headers['Location']) : $headers['Location'];
... which is just if short-hand for
if(is_array($headers['Location'])){
return array_pop($headers['Location']);
}else{
return $headers['Location'];
}
This fix will take care of either case (array, non-array), and remove the need to weed-out the final URL after calling the function.
In the case where there are no redirects, the function will return false. Similarly, the function will also return false for invalid URLs (invalid for any reason). Therefor, it is important to check the URL for validity before running this function, or else incorporate the redirect check somewhere into your validation.
/**
* get_redirect_url()
* Gets the address that the provided URL redirects to,
* or FALSE if there's no redirect.
*
* #param string $url
* #return string
*/
function get_redirect_url($url){
$redirect_url = null;
$url_parts = #parse_url($url);
if (!$url_parts) return false;
if (!isset($url_parts['host'])) return false; //can't process relative URLs
if (!isset($url_parts['path'])) $url_parts['path'] = '/';
$sock = fsockopen($url_parts['host'], (isset($url_parts['port']) ? (int)$url_parts['port'] : 80), $errno, $errstr, 30);
if (!$sock) return false;
$request = "HEAD " . $url_parts['path'] . (isset($url_parts['query']) ? '?'.$url_parts['query'] : '') . " HTTP/1.1\r\n";
$request .= 'Host: ' . $url_parts['host'] . "\r\n";
$request .= "Connection: Close\r\n\r\n";
fwrite($sock, $request);
$response = '';
while(!feof($sock)) $response .= fread($sock, 8192);
fclose($sock);
if (preg_match('/^Location: (.+?)$/m', $response, $matches)){
if ( substr($matches[1], 0, 1) == "/" )
return $url_parts['scheme'] . "://" . $url_parts['host'] . trim($matches[1]);
else
return trim($matches[1]);
} else {
return false;
}
}
/**
* get_all_redirects()
* Follows and collects all redirects, in order, for the given URL.
*
* #param string $url
* #return array
*/
function get_all_redirects($url){
$redirects = array();
while ($newurl = get_redirect_url($url)){
if (in_array($newurl, $redirects)){
break;
}
$redirects[] = $newurl;
$url = $newurl;
}
return $redirects;
}
/**
* get_final_url()
* Gets the address that the URL ultimately leads to.
* Returns $url itself if it isn't a redirect.
*
* #param string $url
* #return string
*/
function get_final_url($url){
$redirects = get_all_redirects($url);
if (count($redirects)>0){
return array_pop($redirects);
} else {
return $url;
}
}
And, as always, give credit:
http://w-shadow.com/blog/2008/07/05/how-to-get-redirect-url-in-php/
While the OP wanted to avoid cURL, it's best to use it when it's available. Here's a solution which has the following advantages
uses curl for all the heavy lifting, so works with https
copes with servers which return lower cased location header name (both xaav and webjay's answers do not handle this)
allows you to control how deep you want you go before giving up
Here's the function:
function findUltimateDestination($url, $maxRequests = 10)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRequests);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
//customize user agent if you desire...
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Link Checker)');
curl_setopt($ch, CURLOPT_URL, $url);
curl_exec($ch);
$url=curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close ($ch);
return $url;
}
Here's a more verbose version which allows you to inspect the redirection chain rather than let curl follow it.
function findUltimateDestination($url, $maxRequests = 10)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
//customize user agent if you desire...
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Link Checker)');
while ($maxRequests--) {
//fetch
curl_setopt($ch, CURLOPT_URL, $url);
$response = curl_exec($ch);
//try to determine redirection url
$location = '';
if (in_array(curl_getinfo($ch, CURLINFO_HTTP_CODE), [301, 302, 303, 307, 308])) {
if (preg_match('/Location:(.*)/i', $response, $match)) {
$location = trim($match[1]);
}
}
if (empty($location)) {
//we've reached the end of the chain...
return $url;
}
//build next url
if ($location[0] == '/') {
$u = parse_url($url);
$url = $u['scheme'] . '://' . $u['host'];
if (isset($u['port'])) {
$url .= ':' . $u['port'];
}
$url .= $location;
} else {
$url = $location;
}
}
return null;
}
As an example of redirection chain which this function handles, but the others do not, try this:
echo findUltimateDestination('http://dx.doi.org/10.1016/j.infsof.2016.05.005')
At the time of writing, this involves 4 requests, with a mixture of Location and location headers involved.
xaav answer is very good; except for the following two issues:
It does not support HTTPS protocol => The solution was proposed as a comment in the original site: http://w-shadow.com/blog/2008/07/05/how-to-get-redirect-url-in-php/
Some sites will not work since they will not recognise the underlying user agent (client browser)
=> This is simply fixed by adding a User-agent header field: I added an Android user agent (you can find here http://www.useragentstring.com/pages/useragentstring.php other user agent examples according you your need):
$request .= "User-Agent: Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30\r\n";
Here's the modified answer:
/**
* get_redirect_url()
* Gets the address that the provided URL redirects to,
* or FALSE if there's no redirect.
*
* #param string $url
* #return string
*/
function get_redirect_url($url){
$redirect_url = null;
$url_parts = #parse_url($url);
if (!$url_parts) return false;
if (!isset($url_parts['host'])) return false; //can't process relative URLs
if (!isset($url_parts['path'])) $url_parts['path'] = '/';
$sock = fsockopen($url_parts['host'], (isset($url_parts['port']) ? (int)$url_parts['port'] : 80), $errno, $errstr, 30);
if (!$sock) return false;
$request = "HEAD " . $url_parts['path'] . (isset($url_parts['query']) ? '?'.$url_parts['query'] : '') . " HTTP/1.1\r\n";
$request .= 'Host: ' . $url_parts['host'] . "\r\n";
$request .= "User-Agent: Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30\r\n";
$request .= "Connection: Close\r\n\r\n";
fwrite($sock, $request);
$response = '';
while(!feof($sock)) $response .= fread($sock, 8192);
fclose($sock);
if (preg_match('/^Location: (.+?)$/m', $response, $matches)){
if ( substr($matches[1], 0, 1) == "/" )
return $url_parts['scheme'] . "://" . $url_parts['host'] . trim($matches[1]);
else
return trim($matches[1]);
} else {
return false;
}
}
/**
* get_all_redirects()
* Follows and collects all redirects, in order, for the given URL.
*
* #param string $url
* #return array
*/
function get_all_redirects($url){
$redirects = array();
while ($newurl = get_redirect_url($url)){
if (in_array($newurl, $redirects)){
break;
}
$redirects[] = $newurl;
$url = $newurl;
}
return $redirects;
}
/**
* get_final_url()
* Gets the address that the URL ultimately leads to.
* Returns $url itself if it isn't a redirect.
*
* #param string $url
* #return string
*/
function get_final_url($url){
$redirects = get_all_redirects($url);
if (count($redirects)>0){
return array_pop($redirects);
} else {
return $url;
}
}
Added to code from answers #xaav and #Houssem BDIOUI: 404 Error case and case when URL with no response. get_final_url($url) in that cases return strings: 'Error: 404 Not Found' and 'Error: No Responce'.
/**
* get_redirect_url()
* Gets the address that the provided URL redirects to,
* or FALSE if there's no redirect,
* or 'Error: No Responce',
* or 'Error: 404 Not Found'
*
* #param string $url
* #return string
*/
function get_redirect_url($url)
{
$redirect_url = null;
$url_parts = #parse_url($url);
if (!$url_parts)
return false;
if (!isset($url_parts['host']))
return false; //can't process relative URLs
if (!isset($url_parts['path']))
$url_parts['path'] = '/';
$sock = #fsockopen($url_parts['host'], (isset($url_parts['port']) ? (int)$url_parts['port'] : 80), $errno, $errstr, 30);
if (!$sock) return 'Error: No Responce';
$request = "HEAD " . $url_parts['path'] . (isset($url_parts['query']) ? '?' . $url_parts['query'] : '') . " HTTP/1.1\r\n";
$request .= 'Host: ' . $url_parts['host'] . "\r\n";
$request .= "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36\r\n";
$request .= "Connection: Close\r\n\r\n";
fwrite($sock, $request);
$response = '';
while (!feof($sock))
$response .= fread($sock, 8192);
fclose($sock);
if (stripos($response, '404 Not Found') !== false)
{
return 'Error: 404 Not Found';
}
if (preg_match('/^Location: (.+?)$/m', $response, $matches))
{
if (substr($matches[1], 0, 1) == "/")
return $url_parts['scheme'] . "://" . $url_parts['host'] . trim($matches[1]);
else
return trim($matches[1]);
} else
{
return false;
}
}
/**
* get_all_redirects()
* Follows and collects all redirects, in order, for the given URL.
*
* #param string $url
* #return array
*/
function get_all_redirects($url)
{
$redirects = array();
while ($newurl = get_redirect_url($url))
{
if (in_array($newurl, $redirects))
{
break;
}
$redirects[] = $newurl;
$url = $newurl;
}
return $redirects;
}
/**
* get_final_url()
* Gets the address that the URL ultimately leads to.
* Returns $url itself if it isn't a redirect,
* or 'Error: No Responce'
* or 'Error: 404 Not Found',
*
* #param string $url
* #return string
*/
function get_final_url($url)
{
$redirects = get_all_redirects($url);
if (count($redirects) > 0)
{
return array_pop($redirects);
} else
{
return $url;
}
}
After hours of reading Stackoverflow and trying out all custom functions written by people as well as trying all the cURL suggestions and nothing did more than 1 redirection, I managed to do a logic of my own which works.
$url = 'facebook.com';
// First let's find out if we just typed the domain name alone or we prepended with a protocol
if (preg_match('/(http|https):\/\/[a-z0-9]+[a-z0-9_\/]*/',$url)) {
$url = $url;
} else {
$url = 'http://' . $url;
echo '<p>No protocol given, defaulting to http://';
}
// Let's print out the initial URL
echo '<p>Initial URL: ' . $url . '</p>';
// Prepare the HEAD method when we send the request
stream_context_set_default(array('http' => array('method' => 'HEAD')));
// Probe for headers
$headers = get_headers($url, 1);
// If there is a Location header, trigger logic
if (isset($headers['Location'])) {
// If there is more than 1 redirect, Location will be array
if (is_array($headers['Location'])) {
// If that's the case, we are interested in the last element of the array (thus the last Location)
echo '<p>Redirected URL: ' . $headers['Location'][array_key_last($headers['Location'])] . '</p>';
$url = $headers['Location'][array_key_last($headers['Location'])];
} else {
// If it's not an array, it means there is only 1 redirect
//var_dump($headers['Location']);
echo '<p>Redirected URL: ' . $headers['Location'] . '</p>';
$url = $headers['Location'];
}
} else {
echo '<p>URL: ' . $url . '</p>';
}
// You can now send get_headers to the latest location
$headers = get_headers($url, 1);