11

As mentioned in one of the comments in an answer below, I tried following this tutorial. So now I have the following:


The ipn.php file:

<?php

    $ipn_post_data = $_POST;

    $url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';

    // Set up request to PayPal
    $request = curl_init();
    curl_setopt_array($request, array
    (
        CURLOPT_URL => $url,
        CURLOPT_POST => TRUE,
        CURLOPT_POSTFIELDS => http_build_query(array('cmd' => '_notify-validate') + $ipn_post_data),
        CURLOPT_RETURNTRANSFER => TRUE,
        CURLOPT_HEADER => FALSE,
        CURLOPT_SSL_VERIFYPEER => TRUE,
        CURLOPT_CAINFO => 'cacert.pem',
    ));

    // Execute request and get response and status code
    $response = curl_exec($request);
    $status   = curl_getinfo($request, CURLINFO_HTTP_CODE);

    // Close connection
    curl_close($request);

    if($status == 200 && $response == 'VERIFIED')
    {
        $subject = "valid";
        $message = "good";
    }
    else
    {
        $subject = "invalid";
        $message = "bad";
    }

    $to = "oshirowanen@mail.com";
    $from = "me@desktop.com";

    $header  = 'MIME-Version: 1.0' . "\r\n";
    $header .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
    $header .= 'To: Oshirowanen <oshirowanen@mail.com>' . "\r\n";
    $header .= 'From: Me <me@desktop.com>' . "\r\n";

    mail($to,$subject,$message,$header);

?>

The received email:

Subject "invalid"
Message "bad"
oshirowanen
  • 15,297
  • 82
  • 198
  • 350
  • Updated question showing values being received via the $_POST which contains an array within an array as expected by @Enzino. – oshirowanen Aug 05 '12 at 07:57
  • I still can't get it to work. So I will award the bounty to the answer which contains a working example of a ipn.php file which gives me a `valid` via email with the sandbox account. – oshirowanen Aug 09 '12 at 18:31
  • Updated question based on tutorial http://www.geekality.net/2011/05/28/php-tutorial-paypal-instant-payment-notification-ipn/ which was mentioned in a comment for an answer below. – oshirowanen Aug 11 '12 at 11:52

9 Answers9

14

Edit:

Now that I can see the array you've outputted, try replacing this to get rid of the PHP array error:

foreach ($_POST as $key => $value) {
    if (!is_array($value)) {
        $value = urlencode(stripslashes($value));
        $req .= "&$key=$value";
    }
    else if (is_array($value)) {
        $paymentArray = explode(' ', $value[0]);
        $paymentCurrency = urlencode(stripslashes($paymentArray[0]));
        $paymentGross = urlencode(stripslashes($paymentArray[1]));
        $req .= '&mc_currency=' . $paymentCurrency . '&mc_gross=' . $paymentGross;
    }
}

Here is the edited code in full:

// read the post from PayPal system and add 'cmd'
$req = 'cmd=' . urlencode('_notify-validate');

foreach ($_POST as $key => $value) {
    if (!is_array($value)) {
        $value = urlencode(stripslashes($value));
        $req .= "&$key=$value";
    }
    else if (is_array($value)) {
        $paymentArray = explode(' ', $value[0]);
        $paymentCurrency = urlencode(stripslashes($paymentArray[0]);
        $paymentGross = urlencode(stripslashes($paymentArray[1]);
        $req .= '&mc_currency=' . $paymentCurrency . '&mc_gross=' . $paymentGross;
    }
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://www.paypal.com/cgi-bin/webscr');
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Host: www.paypal.com'));
$res = curl_exec($ch);
curl_close($ch);


// assign posted variables to local variables
$item_name = $_POST['item_name'];
$item_number = $_POST['item_number'];
$payment_status = $_POST['payment_status'];
$payment_amount = $_POST['mc_gross'];
$payment_currency = $_POST['mc_currency'];
$txn_id = $_POST['txn_id'];
$receiver_email = $_POST['receiver_email'];
$payer_email = $_POST['payer_email'];


if (strcmp ($res, "VERIFIED") == 0) {
    // check the payment_status is Completed
    // check that txn_id has not been previously processed
    // check that receiver_email is your Primary PayPal email
    // check that payment_amount/payment_currency are correct
    // process payment
}
else if (strcmp ($res, "INVALID") == 0) {
    // log for manual investigation
}

Check this out!

Edit: Check out the PayPal troubleshooting tips:

https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_admin_IPNTesting

Doug
  • 3,312
  • 1
  • 24
  • 31
Stegrex
  • 4,004
  • 1
  • 17
  • 19
  • Done and updated the code in the original question. Any idea why I keep getting invalid as explained in the original question where you can see the email details I get, even though the payment clearly gets transferred from buyer to seller? – oshirowanen Aug 03 '12 at 08:56
  • @oshirowanen It seems like the PayPal Sandbox is pretty brittle and I keep seeing people who have the same issue. One thing to try is to print the $req value and see if all the POSTs have been added. Read this: (https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_admin_IPNTesting)[https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_admin_IPNTesting] – Stegrex Aug 03 '12 at 15:38
  • I tried your code again and had a closer look. `strtolower($res);` contains the value `invalid host header`. This explains why `if (strcmp ($res, "VERIFIED") == 0) {` is never true for me. But why would I be getting an `invalid host header` message? – oshirowanen Aug 03 '12 at 17:24
  • I've added further details to the original question. – oshirowanen Aug 03 '12 at 17:46
  • @oshirowanen Enzino added some explanations before I could get back to you. Check out their answer below. I agree that cURL is the better solution, and Enzino is right in putting in the host into the header to ensure there are no errors. I'm posting up the cURL solution from PayPal's code samples for you to see. – Stegrex Aug 04 '12 at 03:08
  • I think that this code is still missing the check on the http response status code. Have a look at [this answer](http://stackoverflow.com/questions/11593616/php-rest-do-not-return-headers-status-code-is-always-the-same/11767064#11767064) for further details. –  Aug 04 '12 at 09:49
  • @Stegrex, I've updated the question again with my latest attempt based on the cURL method. – oshirowanen Aug 04 '12 at 14:44
  • @Stegrex, Updated question showing values being received via the $_POST which contains an array within an array as expected by Enzino. – oshirowanen Aug 05 '12 at 07:58
  • I still can't get it to work. So I will award the bounty to the answer which contains a working example of a ipn.php file which gives me a `valid` via email with the sandbox account. – oshirowanen Aug 09 '12 at 18:32
  • @oshirowanen After checking your updated question with the print_r() of the post request, I've modified my code to prevent the PHP error from occurring. – Stegrex Aug 09 '12 at 19:14
  • I think SOF's bounty system is flawed, I just tried the answer here and 1. It had syntax errors which I corrected, but then after running the code, I get 8 error messages followed by an invalid email.. – oshirowanen Aug 11 '12 at 11:31
  • May I ask you why do you think that the bounty system is flawed? –  Aug 11 '12 at 16:09
  • @Enzino, 1. Problem has not been solved, 2. I lost points, 3. Someone got points automaitcally for not solving the problem... – oshirowanen Aug 11 '12 at 19:09
6

The problem is that you don't check the HTTP response code, so you are intepreting the "Invalid Host header" as the PayPal response, whilst it's the web server response (for the status code 400).
If you look at the PayPal documentation, there is a PHP example which is very similar to your code, since it uses the "fsockopen", "fputs" and "fgets" functions to communicate with the PayPal server.
But if you look carefully at the remark after the "fsockopen" call, you can read:

// Process validation from PayPal 
// TODO: This sample does not test the HTTP response code. All 
// HTTP response codes must be handled or you should use an HTTP 
// library, such as cUrl

And this is exacty your problem: you don't check that the HTTP response code is 200 (OK), before parsing the response body.
Also, using the "strtolower" function is not correct, since the real response from the PayPal server is always uppercase, as shown in the above cited example.
Even if the PayPal example uses the "fsockopen" approach, I think it should be much better to use the PHP cURL library to implement your IPN listener.
Have also a look at the following answers:

However, if you really want to use the "fsockopen" function, you should always specify the "Host" header field in the POST request, as shown in the following snippet of code (taken from the PHP manual):

<?php
$fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET / HTTP/1.1\r\n";
    $out .= "Host: www.example.com\r\n";
    $out .= "Connection: Close\r\n\r\n";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    fclose($fp);
}
?>

UPDATE

Here is a simple function for recursive stripslashes/urlencoding:

<html>
<body>
<pre>
<?

$post = Array (
  "transaction" => Array("USD 20.00"),
  "payment_request_date" => "Sun Aug '05 08:49:20 PDT 2012",
  "return_url" => "http://000.000.000.000/success.php"
);

echo "before myUrlencode...\n";
print_r($post);

function myUrlencode($post) {
  foreach ($post as $key => $val) {
    if (is_array($val)) {
      $post[$key] = myUrlencode($val);
    } else {
      $post[$key] = urlencode(stripslashes($val));
    }
  }
  return($post);
}

echo "\nafter myUrlencode...\n";
print_r(myUrlencode($post));

?>
</pre>
</body>
</html>
Community
  • 1
  • 1
  • I've updated the question again with my latest attempt based on the cURL method. – oshirowanen Aug 04 '12 at 14:45
  • You should add the line `echo "
    ".print_r($_POST,true)."
    \n";` before the `foreach` statement, so that you can see the contents of the $_POST array. I suspect that one of the values of the $_POST array is an array itself, herefore the stripslashes() function fails.
    –  Aug 04 '12 at 18:35
  • As i've said in a previous comment, you should also add a check on the http status code, since if the PayPal server won't reply to your request with a 200 (OK) code, the $res variable could be blank (or FALSE), and you won't get any mail because none of the two `if (strcmp($res, ...` will return a zero value. –  Aug 04 '12 at 18:42
  • Updated question showing values being received via the $_POST which contains an array within an array as you expected. – oshirowanen Aug 05 '12 at 07:58
  • 1
    The stripslashes() is not recursive. If you want to apply this function to a multi-dimensional array, you need to use a recursive function. On the [PHP manual](http://php.net/manual/en/function.stripslashes.php) for the stripslashes() function you can find an example of how to do this (search the "stripslashes_deep" function). Be aware that you will have the same problem on the "urlencode" function. See also [this link](http://it.php.net/manual/en/function.urlencode.php) for an example of urlencoding of an array (search for "urlencode_array"). –  Aug 05 '12 at 09:29
  • Have a look also at [this code](https://github.com/Quixotix/PHP-PayPal-IPN), maybe it can help. –  Aug 05 '12 at 09:43
  • I've checked the [PayPal IPN Notification Guide](https://cms.paypal.com/cms_content/US/en_US/files/developer/IPNGuide.pdf) and I've discovered that effectively the "transaction" variable is an array (see page 61-62), so I'm surprised that in all the IPN code examples I've looked at, it was not managed. I've also found some useful references on [this tutorial](http://www.geekality.net/2011/05/28/php-tutorial-paypal-instant-payment-notification-ipn/). –  Aug 05 '12 at 11:56
  • I still can't get it to work. So I will award the bounty to the answer which contains a working example of a ipn.php file which gives me a `valid` via email with the sandbox account. – oshirowanen Aug 09 '12 at 18:32
  • Can you provide further details about the problem? The problem is still related to the stripslashes() function, or there is some other error? Can you show us the PHP code you're using for the IPN listener? –  Aug 09 '12 at 19:22
  • It's the same problem as before, I am getting errors in the apache log file, plus I am getting invalid instead of valid as an email. – oshirowanen Aug 11 '12 at 11:02
  • It seems that the status code is 200, but the response body is different from "VERIFIED". Try adding the following line just after the "curl_close()" statement: `echo "
    ".print_r($response,true)."
    \n";`, then post the output.
    –  Aug 11 '12 at 16:08
  • I won't be able to see that being outputted as paypal will call the ipn.php when they want. I won't know when it's being called. The best I can do is stick that echo into an email or file. Which I will do now. – oshirowanen Aug 11 '12 at 16:12
  • OK, done, but via an email instead of echo and I got the following in the email `
    `...
    – oshirowanen Aug 11 '12 at 19:08
  • So you're receiving an empty response. Could you add the line `CURLOPT_VERBOSE => TRUE` to the array argument of the curl_setopt_array() function and substitute the line `$message = "bad";` with `$message = $response;`? –  Aug 11 '12 at 19:36
  • Done, and I am getting an email with no body/message at all. – oshirowanen Aug 11 '12 at 21:24
  • Try commenting the lines `$header = 'MIME-VERSION...';` and `$header .= 'Content-type...';`. To view the response, the mail must be in text format, not in HTML format. –  Aug 11 '12 at 21:30
  • Done, and I am still getting an empty email with the subject `invalid`. – oshirowanen Aug 11 '12 at 21:38
  • Has the problem got anything to do with the part `Confusion about the above mentioned tutorial:` at the end of my question? I've not included that bit in the code at all. – oshirowanen Aug 11 '12 at 21:44
  • also, I am using a localhost with a static ip address instead of an external hosting company. Has this got anything to do with it. According to that tutorial url mentioned in my question, I downloaded cacert.pem and I am now getting `
    INVALID
    `.
    – oshirowanen Aug 12 '12 at 21:03
  • Try adding the statement `$ipn_post_data = $_POST;` just before the `// Choose url` line. The problem is not depending on the static IP address. Also, I don't think that the charset conversion is necessary, so you should remove the "confusing" snipped of code. –  Aug 13 '12 at 20:19
  • Sorry, that was a mistake in the question. In my actual code, I already have that line, I just forgot to copy it across to the question. I have also tried curl_error and I get no errors... – oshirowanen Aug 13 '12 at 21:27
  • Did you check if your PayPal sandbox account is "unverified"? Have a look at [this post](http://www.geekality.net/2010/09/24/getting-started-with-paypal-sandbox/) for further details. Have also a look at [this post](http://www.webmasterworld.com/ecommerce/4292847.htm) for a list of the possible reasons for the INVALID response, even if I don't think it can really help. –  Aug 15 '12 at 09:56
2
  1. Got it working using the basic sample code 4b,

  2. Cleared $ipnNotificationUrl = ""; from the basic sample code as I had a value in there which I added myself,

  3. Created a seller account instead of a business pro account in sandbox,

  4. Set the seller account to enable the ipn url,

  5. Used the following PHP 5.2 sample code for the ipn listener

  6. Added the 2 lines into the listener, as described here, the 2 lines can be seen below:

  7. Downloaded the cacert.pem certificate to my server from here and put it in the same directory as the ipn listener:

The 2 lines mentioned in point 6:

CURLOPT_SSL_VERIFYPEER => TRUE,
CURLOPT_CAINFO => 'cacert.pem',

I have no idea why the sandbox business pro account does not let me set an ipn url, but the seller account does.

oshirowanen
  • 15,297
  • 82
  • 198
  • 350
1

These links may resolve your problem,

Paypal: Invalid IPN problem

http://www.webmasterworld.com/ecommerce/4292847.htm

Paypal sandbox IPN return INVALID

Community
  • 1
  • 1
Stranger
  • 10,332
  • 18
  • 78
  • 115
1

I am not sure what is exactly wrong right now with your code, but I was strugling wuth the same while ago and my fixes was to add HOST in the header and host have to be www.paypal.com. I used fsockopen method and work fine now.

In Curl I had a problem before with ssl. And solution was to put those lines:

curl_setopt($curl, CURLOPT_COOKIEJAR, dirname(__FILE__) . "/cookies.txt");
curl_setopt($curl, CURLOPT_COOKIEFILE, dirname(__FILE__) . "/cookies.txt");

where of course file cookies.txt have to exists. and more over I had to run one connection to page to get session data and later send post data.

Below is a header what is working fine for me with fsockopen method

$header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Host: www.paypal.com\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
Rajcho
  • 1
  • 2
1

It's a problem with the + character, it often get wrongly fetched so I made that workaround, and it worked for me.

payment_data = Sat Jun 04 2016 15:11:16 GMT+0200 (CEST)

foreach ($_POST as $key => $value) {
if($key !== "payment_date"){
    $req .= '&' . $key . '=' . rawurlencode(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
}else{
    $req .= '&' . $key . '=' . rawurlencode(str_replace(array('GMT '),array('GMT+'),$value));
}}
0

Here's how to avoid these errors...

foreach ($_POST as $key => $value) {
     if ($key=='transaction')
          foreach ($value as $key2=>$value2) {
               $value['transaction'][$key2] = urlencode(stripslashes($value2));
     }
     else {
          $value = urlencode(stripslashes($value));
     }
     $req .= "&$key=$value";
 }
Martin
  • 1,193
  • 3
  • 12
  • 24
  • OK, that gets the array from within the array, but I still get an invalid from the `$res`. – oshirowanen Aug 09 '12 at 18:25
  • I still can't get it to work. So I will award the bounty to the answer which contains a working example of a ipn.php file which gives me a `valid` via email with the sandbox account. – oshirowanen Aug 09 '12 at 18:32
0

Hours of hair pulling until I saw Izudin's answer. He's right..The + in the date wasn't being transferred. Just to test, I removed it from the pre-populated field in the simulator and got a Verified at last.

0

I finally found an updated (August 5, 2016) working answer to this query. You can use this code as your final IPN for Sandbox or Live. With the following consideration:

  1. Be sure to place your IPN listener to ->My selling tools -> instant payment notifications Section.
  2. Do not use IPN Simulator in sandbox, it will always return INVALID.
  3. Create and Use an actual Sandbox Button, but DO NOT put your IPN listener to RETURN PAGE that says "Take customers to this URL when they finish checkout".

That's all of it. I hope this will help.

And here is the working code:

<?php
$post_data = file_get_contents('php://input');
$post_array = explode('&', $post_data);
$dataFromPayPal = array();
foreach ($post_array as $keyval) {
    $keyval = explode ('=', $keyval);
    if (count($keyval) == 2)
        $dataFromPayPal[$keyval[0]] = urldecode($keyval[1]);
}

$req = 'cmd=_notify-validate';
if(function_exists('get_magic_quotes_gpc')) {
    $get_magic_quotes_exists = true;
}
foreach ($dataFromPayPal 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";
}

$ch = curl_init('https://www.sandbox.paypal.com/cgi-bin/webscr');
//use https://www.sandbox.paypal.com/cgi-bin/webscr in case you are testing this on a PayPal Sanbox environment
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_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));

if( !($res = curl_exec($ch)) ) {
    curl_close($ch);
    exit;
}
curl_close($ch);



if (strcmp ($res, "INVALID") == 0) {
        echo "INVALID";
}
else if (strcmp ($res, "VERIFIED") == 0) {
        echo "VALID";
}

?>
Draeko
  • 101
  • 1
  • 12