Gwendydd Gwendydd - 2 months ago 16
PHP Question

PayPal IPN runs repeatedly

I am writing a WordPress plugin that processes payments through PayPal. I have a PayPal IPN script that sends an email notification (in addition to PayPal's email notification) when a payment is successful. Some of the users of my plugin are reporting that they receive multiple copies of this email notification over several days.

I discovered this problem early on when I was developing the plugin, and the solution I found was to immediately send PayPal a 200 response. (Here is some discussion of the issue: https://www.paypal-community.com/t5/About-Settings-Archive/Paypal-repeats-identical-IPN-posts/td-p/465559 ). This seems to be working fine on my test site, but obviously isn't working for all of my users.

When I use the PayPal IPN simulator, it doesn't give me any error messages.

Aside from sending the 200 response right away, is there anything I can do to stop PayPal from repeating the IPN request over and over?

Here is my code:

<?php

// Create a query var so PayPal has somewhere to go
// https://willnorris.com/2009/06/wordpress-plugin-pet-peeve-2-direct-calls-to-plugin-files
function cdashmm_register_query_var($vars) {
$vars[] = 'cdash-member-manager';
return $vars;
}
add_filter('query_vars', 'cdashmm_register_query_var');


// If PayPal has gone to our query var, check that it is correct and process the payment
function cdashmm_parse_paypal_ipn_request($wp) {
// only process requests with "cdash-member-manager=paypal-ipn"
if (array_key_exists('cdash-member-manager', $wp->query_vars) && $wp->query_vars['cdash-member-manager'] == 'paypal-ipn') {

if( !isset( $_POST['txn_id'] ) ) {
// send a 200 message to PayPal IPN so it knows this happened
header('HTTP/1.1 200 OK');
// POST data isn't there, so we aren't going to do anything else
} else {
// we have valid POST, so we're going to do stuff with it
// send a 200 message to PayPal IPN so it knows this happened
header('HTTP/1.1 200 OK');

// process the request.
$req = 'cmd=_notify-validate';
foreach($_POST as $key => $value) :
$value = urlencode(stripslashes($value));
$req .= "&$key=$value";
endforeach;

$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Host: www.paypal.com\r\n";
$header .= "Connection: close\r\n\r\n";
$fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30);

if(!$fp) {
// HTTP ERROR
} else {
fputs ($fp, $header . $req);
while(!feof($fp)) {

$res = fgets ($fp, 1024);

$fh = fopen('result.txt', 'w');
fwrite($fh, $res);
fclose($fh);

if (strcmp (trim($res), "VERIFIED") == 0) {

/* Do a bunch of WordPress stuff - create some posts, send some emails */

}

elseif(strcmp (trim($res), "INVALID") == 0) {
// probably ought to do something here

}

}
fclose ($fp);
}
}
}
}
add_action('parse_request', 'cdashmm_parse_paypal_ipn_request');

?>

Answer

You cannot stop Paypal from repeating the request. This is part of the IPN system to make sure that the transactions clear even if the site goes down. Therefore, you should store this transaction ID in the database and check to be sure you have not encountered it in the past. If you have encountered it previously, you can log that you are seeing a repeat. Otherwise, process it.

Simple idea of this using a Transactions class:

foreach ($_POST as $key => $value) {
  $value = urlencode(stripslashes($value));
  $req .= "&$key=$value";
  $value = urldecode($value);
  foreach ($pp_vars as $search) {
    if ($key == $search)
      $$key = $value;
  }
  if (preg_match("/txn_id/", $key)) {
      $txn_id = $value;
  }
  if (preg_match("/item_number/", $key)) {
    $item_number = $value;
  }
}

$model = new Transactions();
if ($model->exists('txid', $txn_id)) {
    $res = "REPEAT";
}
$model->action[0] = $res;
$model->txid[0] = $txn_id;
$model->description[0] = $req;
$model->price[0] = $payment_gross;
$model->reviewed[0] = 0;
$model->user_id[0] = $user->id;
$model->created_at[0] = date("Y-m-d H:i:s");
$model->updated_at[0] = $model->created_at;
$model->save();    
Comments