I've made an attempt at writing a small PayPal IPN handler however I'm always getting 'INVALID' when using the IPN simulator https://developer.paypal.com/webapps/developer/applications/ipn_simulator . I'm not sure what about the URL I'm sending back isn't right. I have looked at some Gists for this topic but they all seem a little long winded. Their docs on this are here https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/#protocol_and_arch and I think that I have followed all the steps for the protocol.
Any help is appreciated
<?php
/**
* Validates a PayPal IPN notification
* #param array $req The $_POST variable for the request
* #return String Returns the validity: 'VALID' or 'INVALID'
*/
function verifyPayPalIPN($req) {
// Base URL for the php validation
$baseURL = 'https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate';
// Loop through the POST parameters to
// create the validaton URL
$postVars = '';
foreach ($req as $key => $value) {
$postVars .= $key . '=' . urlencode($value) . '&';
}
// Strip the last & off of the URL
$postVars = substr($postVars, 0, -1);
// Send the request to PayPal to confirm
// whether the request to the script is
// from PayPal
$requestValidity = system("curl --data '$postVars' $baseURL");
return $requestValidity;
}
file_put_contents('/tmp/result.txt', verifyPayPalIPN($_POST));
?>
cmd=_notify-validate should be part of your postVars string
$baseURL = 'https://www.paypal.com/cgi-bin/webscr';
// Loop through the POST parameters to
// create the validaton URL
$postVars = 'cmd=_notify-validate';
foreach ($req as $key => $value) {
$postVars .= $key . '=' . urlencode($value) . '&';
}
The code that I have used in the past is this:
$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) {
// HTTP ERROR
} else {
fputs ($fp, $header . $req);
while (!feof($fp)) {
$res = fgets ($fp, 1024);
if (strcmp ($res, "VERIFIED") == 0) {
} else if (strcmp ($res, "INVALID") == 0) {
}
}
fclose($fp);
}
Note the port no. 443 also
firstly, check if your paying enviroment and validating enviroment is the same (both sandbox or both production), if they are not, say your customer paid their order in an production enviroment, but then your PHP code trying to request a sandbox enviroment end point to verify the IPN request, then it will always end as INVALIDE
Related
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');
?>
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();
So I'm trying to send a custom variable with a Paypal adaptive payment and when Paypal does its IPN with the file that I specified in the selling preferences on my Paypal account, the custom variable is empty. My Curl code works fine and the response envelope returns successful. I echoed the post fields before sending them and this is what I got (sensitive data censored):
actionType=PAY&clientDetails.applicationId=[app
ID]&clientDetails.ipAddress=[my ip]
¤cyCode=USD&feesPayer=EACHRECEIVER&memo=[my
memo]&custom=1Ad2Ad8Ad9&receiverList.receiver(0).amount=1&receiverList.receiver(0).email=[email1]&receiverList.receiver(0).primary=true&receiverList.receiver(1).amount=0.2&receiverList.receiver(1).email=[email2]&receiverList.receiver(1).primary=false&receiverList.receiver(2).amount=0.19&receiverList.receiver(2).email=[email3]&receiverList.receiver(2).primary=false&receiverList.receiver(3).amount=0.16&receiverList.receiver(3).email=[email4]&receiverList.receiver(3).primary=false&requestEnvelope.errorLanguage=en_US&returnUrl=[return
url]&cancelUrl=[cancel url]
The custom variable is in tact and exactly as I want it. On the other hand I noticed that the currencyCode field was changed to "¤cyCode" somehow. I have no clue why but when I changed it to an array and used print_r, it said currencyCode.
Anyway the problem that I'm having comes up when Paypal does its instant notification to my Confirm.php and runs the following code:
// read the post from PayPal system and add 'cmd'
$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) {
// HTTP ERROR
} else {
fputs ($fp, $header . $req);
while (!feof($fp)) {
$res = fgets ($fp, 1024);
if (strcmp ($res, "VERIFIED") == 0) {
$strEmail = $_POST[ "payer_email" ];
if( isset( $_POST[ "custom" ] ) == true )
{
$strQuery = "UPDATE TTest SET TestField = 'posted: " . $_POST[ "custom" ] . "' WHERE id = 1";
}
// Execute
mysql_query( $strQuery );
}
else if (strcmp ($res, "INVALID") == 0) {
}
}
fclose ($fp);
}
The $_POST[ "custom" ] variable appears to be empty because it only sets TestField to "posted: " every time.
Can someone please tell me whats wrong with this code.
I am trying for payment integration with Paypal with html form. I have specified the notify_url, payment goes all right but i am not able to enter in this block if (strcmp($res, "VERIFIED") == 0){}
// Response from Paypl
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
$value = urlencode(stripslashes($value));
$value = preg_replace('/(.*[^%^0^D])(%0A)(.*)/i', '${1}%0D%0A${3}', $value); // IPN fix
$req .= "&$key=$value";
}
// assign posted variables to local variables
$data['item_name'] = $_POST['item_name'];
// 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.sandbox.paypal.com', 443, $errno, $errstr, 30);
if (!$fp) {
// HTTP ERROR
echo 'HTTP ERROR';
} else {
file_put_contents('test1.txt', 'test');
fputs($fp, $header . $req);
while (!feof($fp)) {
$res = fgets($fp, 1024);
if (strcmp($res, "VERIFIED") == 0) {
echo 'SUCCESS';
} else if (strcmp($res, "INVALID") == 0) {
echo 'INVALID';
}
}
fclose($fp);
}
Use CuRl method to POST data back instead of fsock here is link:https://developer.paypal.com/webapps/developer/docs/classic/ipn/ht_ipn/ or My suggestion is Use AngellaEye library which is more effective and useful for Paypal integration here is link:http://www.angelleye.com/how-to-integrate-paypal-with-php-class-library/
Download it from this link and go throgh it.
How do I debug the ipn.php file when using the paypal sandbox ipn simulator tool?
The code looks like this:
// read the post from PayPal system and add 'cmd'
$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.sandbox.paypal.com', 443, $errno, $errstr, 30);
if (!$fp) {
// HTTP ERROR
} else {
fputs($fp, $header.$req);
while (!feof($fp)) {
$res = fgets($fp, 1024);
if (strcmp($res, "VERIFIED") == 0) {
$DBH = new PDO("mysql:host=localhost;dbname=db", "user", "pass");
$DBH - > setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$STH = $DBH - > prepare("update table2 set status = :status where tracking_id = :tracking_id");
$status = 1;
parse_str($req, $data);
$STH - > bindParam(':status', $status, PDO::PARAM_INT, 1);
$STH - > bindParam(':tracking_id', 'id_goes_here', PDO::PARAM_STR, 50);
$STH - > execute();
$DBH = null;
} else if (strcmp($res, "INVALID") == 0) {
// do something else
}
}
fclose($fp);
}
I normally use netbeans debug facility to debug, but how do I debug using the sandbox simulator? When I click send ipn from the sandbox ipn simulator, I get a message in the sandbox saying IPN successfully sent., but when I then go into my database to check the status, it's still 0.
Maybe problem is in parameter binding
$STH - > bindParam(':tracking_id', 'id_goes_here', PDO::PARAM_STR, 50);
if table 'table2' do not contain row with tracking_id = 'id_goes_here' update action will fail.
Try this
<?php
$testMode = false;
$url = 'https://www.paypal.com/cgi-bin/webscr';
if ($testMode === true)
$url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
$ipnResponse = ''; // holds the IPN response from paypal
$ipnData = array(); // array will contain the POST values for IPN
$urlParsed = parse_url($url);
$req = 'cmd=_notify-validate'; // Add 'cmd' to req (ipn command)
// Read the post from PayPal system and add them to req
foreach ($_POST as $key => $value) {
$ipnData["$key"] = $value;
$value = urlencode(stripslashes($value));
$req .= "&" . $key . "=" . $value;
}
// Open the connection to paypal
$fp = fsockopen($urlParsed['host'], "80", $errno, $errstr, 30);
// If could open the connection and check response
if ($fp) {
fputs($fp, "POST " . $urlParsed['path'] . " HTTP/1.1\r\n");
fputs($fp, "Host: " . $urlParsed['host'] . "\r\n");
fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
fputs($fp, "Content-length: " . strlen($req) . "\r\n");
fputs($fp, "Connection: close\r\n\r\n");
fputs($fp, $req . "\r\n\r\n");
// Loop through the response from the server and append to variable
while (!feof($fp)) {
$ipnResponse .= fgets($fp, 1024);
}
fclose($fp);
// Valid IPN transaction.
if (preg_match('/^VERIFIED/', $ipnResponse)) {
// Some action on IPN validation - update payment status etc
die("OK. IPN Validation: Success");
}
// Invalid IPN transaction
else {
// Some action on IPN validation - update payment status etc
die("ERROR. IPN Validation: Failed");
}
}
// Else no connection, so maybe wrong url or other reasons, you can do another call later
else {
die("ERROR. IPN Connection: fsockopen error");
}
?>
In general when I want to debug a script that doesn't provide direct output in the browser, I call a simple logging function throughout the script to output variable values or help determine where errors are occurring.
When I have no idea where the script is dying, I write a line to a log file after each significant line of code. After running the script and then checking the log file I can see exactly which line caused the script to die, a very simplified example is below. Of course after everything is running smoothly all of the logging should be removed.
public function logToFile($msg){
$file = 'log.txt';
$current = file_get_contents($file);
$current .= $msg . "\n";
file_put_contents($file, $current);
}
public function doOtherStuff(){
foreach ($_POST as $key => $value) {
$ipnData["$key"] = $value;
$value = urlencode(stripslashes($value));
$req .= "&" . $key . "=" . $value;
}
logToFile("1");
$fp = fsockopen($urlParsed['host'], "80", $errno, $errstr, 30);
logToFile("2");
if ($fp) {
logToFile("3");
fputs($fp, "POST " . $urlParsed['path'] . " HTTP/1.1\r\n");
logToFile("4");
fputs($fp, "Host: " . $urlParsed['host'] . "\r\n");
logToFile("5");
fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
logToFile("6");
fputs($fp, "Content-length: " . strlen($req) . "\r\n");
logToFile("7");
fputs($fp, "Connection: close\r\n\r\n");
logToFile("8");
fputs($fp, $req . "\r\n\r\n");
logToFile("9");
while (!feof($fp)) {
logToFile("10");
$ipnResponse .= fgets($fp, 1024);
}
logToFile("11");
fclose($fp);
}
}
I am not sure the title of this question covers what I mean.
In this Joomla component I am writing I have built in the ability for customers to buy via PayPal. At first I wrote a seperate view for the IPN, but even though the script worked without a flaw, it kept sending a 503 back to IPN (probably because the ipn-url was something like www.example.com/index.php?option=com_component&view=paypal) so i rewrote part of the script and now the IPN-url is www.example.com/paypal.php. Since this is an actual page it now correctly sends a 200 instead of a 503 back to PayPal.
But...now I don't know how to call the rest of my script which handles all the emailing and database storing of a payment. Since this paypal.php is called directly (and not via index.php) it works completely seperate from Joomla so I cannot call in a Model (or at least I don't know how to do that).
This is my paypal.php file:
<?php
$header = "";
$req = 'cmd=_notify-validate';
$get_magic_quotes_exists = false;
if (function_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
foreach ($_POST 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 back to PayPal 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.sandbox.paypal.com', 443, $errno, $errstr, 30);
if ($fp) {
fputs($fp, $header . $req);
while (!feof($fp)) {
$res = fgets($fp, 1024);
if (strcmp($res, "VERIFIED") == 0) {
// Here I must process the payment (emails, database, etc.)
}
else {
// Error
}
}
}
fclose($fp);
Now at the place where it says 'Here I must process payment' I must be able to get data from the database and store data into the database.
So how do I make it so this file acts as part of my component so I can call methods from my Model(s)? Or is there some other way I can integrate IPN into my model while ensuring a 200 instead of a 503.
UPDATE:
Someone mentioned using curl so i tried that. The handler now looks like this:
<?php
$header = "";
$req = 'cmd=_notify-validate';
$postData = 'option=com_component&view=buy&layout=paypal';
$get_magic_quotes_exists = false;
if (function_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
foreach ($_POST 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";
$postData .= "&$key=$value";
}
}
$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.sandbox.paypal.com', 443, $errno, $errstr, 30);
if ($fp) {
fputs($fp, $header . $req);
while (!feof($fp)) {
$res = fgets($fp, 1024);
if (strcmp($res, "VERIFIED") == 0) {
$ch = curl_init("http://www.example.com/index.php");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
$output = curl_exec($ch);
if ($output == FALSE) {
// Error
}
curl_close($ch);
}
else {
// Error
}
}
}
fclose($fp);
The IPN still works fine, but the component is not 'executed'. I never used curl before so maybe it is a fault in the script?
Got it myself finally; so somewhere I found out that to be able to access most of the basic functionality of Joomla from any file you simply need to include 2 files:
/includes/defines.php
/includes/framework.php
Then you simply initialise the framework like so:
$framework = & JFactory::getApplication('site');
$framework->initialise();
And then I import the model which contains all the database/email functionality:
JLoader::import('joomla.application.component.model');
JLoader::import('modelname', 'path_to_my/models');
$model= JModel::getInstance('ModelName', 'ComponentnameModel');
And now I can access the methods from that model (and thus the database) from my IPN-handler.
I just had a similar issue with a paypal component on my website and figured out where the 503 notification originated from.
This issue could have to do with the online/offline status of your website. If your website is offline (meaning you have to log in as admin to have a look at your website) and you're not logged in (PayPal isn't logged in as well) Joomla is generating a standard message displaying a message like"
This site is down for maintenance.
Please check back again soon.
This message is send with a 503 notification.
Depending on how your component is developed, the ipn message from PayPal can be processed by your website, while still sending a 503 error to PayPal.
Hope this helps you out.
For a component com_mycomponent, your mycomponent.php should look like
// Require the com_content helper library
require_once (JPATH_COMPONENT.DS.'controller.php');
// Create the controller
$controller = new MyComponentController();
// Perform the Request task
$controller->execute(JRequest::geCmd('task'));
// Redirect if set by the controller
$controller->redirect();
In controller.php, then use
class MyComponentCOntroller extends JController{
function processPaypalPayment(){
//paste your code here
}
}
In Paypal set your IPN to:
http://mysite.com/?option=com_mycomponent&task=processPaypalPayment