The IPN Script
PayPal provides a simple example script for PHP(4.1+) to be used with IPN that contains the following code:
$value) { $value = urlencode(stripslashes($value)); $req .= "&$key=$value"; } // post back to PayPal system to validate $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; $fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30); // 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 (!$fp) { // HTTP ERROR } else { fputs ($fp, $header . $req); while (!feof($fp)) { $res = fgets ($fp, 1024); 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 } } fclose ($fp); } ?>
This example provides the very basic requirement to check whether or not a transaction is valid, and provides some suggestions on what to check against before processing the payment. However we’re going to modify this some to be more secure and better suited for an automatic saving of orders.
Create a Verification Function
Having a separate function for IPN verifications will help keep your code clean as your needs become more complex (and also helps in code reuse across projects). Since the data to be validated is already stored in the global scope there is no need to pass any information to the verification function, but rather simply await a boolean result.
function verify() { $verify = false; $req = 'cmd=_notify-validate'; foreach ($_POST as $key => $value) { $value = urlencode(stripslashes($value)); $req .= "&$key=$value"; } // post back to PayPal system to validate $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($req) . "\r\n\r\n"; $fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30); if(!$fp) return false; // http error fputs ($fp, $header . $req); while (!feof($fp)) { $res = fgets ($fp, 1024); if (strcmp ($res, "VERIFIED") == 0) { $verify = true; break; } else if (strcmp ($res, "INVALID") == 0) { break; } } fclose ($fp); return $verify; // false by default unless true due to verification }
The above function has one very simple task… to send the data that had just been received right back at paypal on a secure connection, and then in return get notified if the transaction and all the included information is verified, or invalid. This prevents an attacker from sending fake information in hope of making a purchase without actually sending any money.
Create a data function.
This function should further check the transaction details and insert the data into the database based on that information. The code below is an example from my KBLinker ipn script.
function insert_data() { //Stash the information into variables for easier readability $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']; $name = $_POST['first_name']." ".$_POST['last_name']; //The next variable is based on the second custom option //I had in my buy now button, in this case, contact email $contact = $_POST['option_selection2']; //Open a database connection for insertion $db = @mysql_connect("localhost", "username", "password"); mysql_select_db("database_name", $db); //Check to see if the transaction already exists in the database. //If so retrieve the ID of the record, otherwise set to zero $result = mysql_query("SELECT id FROM orders WHERE txn = '".$txn_id."' LIMIT 1;"); if(!($result === false)) { $row_count = mysql_num_rows($result); if($row_count > 0) { $row = mysql_fetch_array($result, MYSQL_NUM); $id = $row[0]; } else $id = 0; mysql_free_result($result); } else $id = 0; if($id > 0) { //Transaction already exists, update status mysql_query("UPDATE ipn set `status` = '".$payment_status."' WHERE id = ".$id); } else { //Transaction does not yet exists, insert $access = 0; //The following lines sets specific parameters based on product option //It is usually a good idea to make sure the item number matches the //item's actual price since web forms can be easily altered. if(($payment_amount == 50) && ($item_number == "KB-SL")) $access = 1; else if(($payment_amount == 75) && ($item_number == "KB-ML2")) $access = 2; else if(($payment_amount == 100) && ($item_number == "KB-ML3")) $access = 3; if($access > 0) { //If an appropriate item number matches insert into the database mysql_query("INSERT INTO ipn (`txn`, `name`, `email`, `access`, `price`, `notes`, `status`) VALUES('".$txn_id."', '".$name."', '".$contact."', ".$access.", ".$payment_amount.", '".$item_number."', '".$payment_status."')"); } else { //If the item purchased cannot be determined, flag the transaction for manual inspection //In this case I email myself, It's also a good idea to attach the transaction details mail("my-email", "KBlinker IPN Notice", "There was a problem determining item type"); } } //Close the database connection mysql_close($db); }