Verifying a Paypal transaction via POST information - php

I'm at a complete loss. I think I might be getting "mis-informed", but I'll try explain my situation as best I can.
The Idea
We have a form for users to purchase credits. Type in credit number,
click pp button.
Upon click of button, a post is made to set the
transaction log information and set it as pending (works fine).
Upon valid post return it continues to submit the paypal form (works also).
The user is redirected to paypal page and makes payment (so far so good).
after payment made, they click the return and are directed toward the "success" page (still working).
upon reaching this page I take in post data from pp (uh oh, here's where it gets sticky)
verify the data is "true" pp data and update the transaction log (HOW!?)
What I'm being told & what i've tried
I was initially going to use IPN to do a post back to paypal to verify the recieved data (ensure it wasn't spoofed), however, I'm being told that for cost purposes and having to setup an "ipn server" we can't use IPN ....
Ok, so I was gonna use PDT, except either I missed a major step in my attempt or it ISNT working right at all because I'm not doing somthing right. Here is where I'm lost, i've tried a dozen different things, including a direct link post, using sparks (for CI) to set the data and call to paypal link, and etc ...
I've looked over every paypal question on here and a half dozen other forums and can't seem to get anything going.
Can anyone "clearly" tell me how I can verify the POST data of a successful paypal transaction and maybe even tell me if i'm being misinformed about the IPN, cause I looked over the docs and I can't find what i've been told, nor can I really find my solution.
I feel stupid, please help.

When your user clicks a PayPal button and goes to PayPal, when they complete the transaction, an IPN POST is made to a URL of your choosing. So you don't have to have another web server.
When the IPN request comes in, PayPal wants you to re-send the entire POST they made to you back to them, including all of the fields, in the exact order, at which point they will return the word 'VERIFIED' or 'INVALID.' If verified, then do whatever it is that you need to do to toggle your txn log from pending to verified. Also, any information you include in your button (your button is actually a form so you can include your own fields) is included in the POST. Useful for keeping a 'transaction id' or some other identifier for mapping back to your transaction.
If the IPN fails it will resend in n+4 minute increments (where n is how long it waited the last time - 4 minutes, next after 8 minutes, next after 12 minutes, etc) for a few days.

Finally made it work correctly thanks to the update in info on IPN.
My solution added the following line to my form:
<input type="hidden" name="notify_url" value="<?= site_url('payment/notifyTest'); ?>">
Then in the notifyTest function i ran this:
$pDat = $this->input->post(NULL, TRUE);
$isSandBox = array_key_exists('test_ipn', $pDat) && 1 === (int)$pDat['test_ipn'] ? TRUE : FALSE;
$verifyURL = $isSandBox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr';
$token = random_string('unique');
$request = curl_init();
curl_setopt_array($request, array
(
CURLOPT_URL => $verifyURL,
CURLOPT_POST => 0,
CURLOPT_POSTFIELDS => http_build_query(array('cmd' => '_notify-validate') + $pDat),
CURLOPT_RETURNTRANSFER => 0,
CURLOPT_HEADER => 0,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_CAINFO => 'cacert.pem',
));
$response = curl_exec($request);
$status = curl_getinfo($request, CURLINFO_HTTP_CODE);
curl_close($request);
if($status == 200 && $response == 'VERIFIED') {
// SUCCESS
$data = array (
... => ...
);
$this->db->insert('transactions', $data);
}
else {
// FAILED
$data = array (
... => ...
);
$this->db->insert('transactions', $data);
};
THE IMPORTANT DIFFERENCE AS WE FOUND -> DO NOT SET YOUR CURL VARS TO TRUE OR FALSE
USE 0 FOR TRUE AND 1 FOR FALSE, IT MIGHT SOUND STUPID, BUT IT WOIKED!!!

Related

Verify POST data comes from PayPal

How to verify POST data comes from PayPal ?
I am working on a store that sells some products. The payment gateway is PayPal.
Initially I set up a custom PayPal form and used the IPN responses to validate the data that is sent to me from PayPal.
Now my client has bought PayPal Advance Payment that uses PayPal PayFlow. The responses are not sent anymore through IPN (or are they?) instead they are returned by SILENT POST, basically when a transaction is perfomed on their end it is sent to a link of my choice and I process data through that link.
How do I validate the source of the POST data, so I know it is coming from PayPal and not a bad intentions user. I can not find any documentation on this. Also I want the same think when a users clicks "Cancel" button on paypal page and it is redirected to cancelation page on my website. I want that POST data source also verified.
Any thoughts on this ?
First solution than I found, also some PayPal support guy has mentioned something similar but he could not offer details as he said he is not an expert.
Basically you have to run a TRANSACTION of type INQUIRY with the received PNREF from SILENT POST, if the response returns ORIGRESULT equal to 0 then the transaction exists in PayPal database under your account.
I also send the SECURE TOKEN ID and SECURE TOKEN with the inquiry, I do not know if it helps or not, I saw this as a solution somewhere and I tried sending just TOKEN and TOKEN ID without ORIGID and sometimes it worked sometimes not.
On the developer reference from PayPal is specified that on a TRXTYPE=I (INQUIRY TRANSACTION) the ORIGID must be specified.
Code below:
//GET POST DATA FROM SILENT POST
$data = $_POST;
$result = $data['RESULT'];
$pnref = $data['PNREF'];
$secure_token = $data['SECURETOKEN'];
$secure_token_id = $data['SECURETOKENID'];
$fee_amount = $data['AMT'];
if(!isset($result) || $result != '0'){
//DO TRANSACTION FAILED OPERATIONS
exit;
}
//CHECK IF PNREF ID EXISTS ON PAYPAL
$post_data = "PARTNER=yourpartnerid"
."&VENDOR=your_vendor"
."&USER=your_user"
."&PWD=your_password"
."&SECURETOKENID=" . $secure_token_id
."&SECURETOKEN=" . $secure_token
."&ORIGID=" . $pnref
."&TRXTYPE=I"
."&VERBOSITY=HIGH";
$ch = curl_init('https://payflowpro.paypal.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
$resp = curl_exec($ch);
$inquiry = array();
parse_str($resp, $inquiry);
//IF ORIGRESULT is 0 then PNREF/transaction exists.
if($inquiry['ORIGRESULT'] == '0' && $inquiry['RESPMSG'] == 'Approved'){ $validated = true; }
else{ $validated = false; }
if($result != 0 || $amount != 'your_correct_fee' || $validated == false){
// DO TRANSACTION NOT VALID OR HAS FAILED OPERATIONS
exit;
}
//DO TRANSACTION SUCCESSFULL OPERATIONS
The response from a INQUIRY looks this way:
RESULT=0&PNREF=ETHPC0BBF5FB&TRANSSTATE=8&ORIGRESULT=0&ORIGPNREF=ELFPB0E766F5&RESPMSG=Approved&AVSADDR=N&AVSZIP=N&ORIGPPREF=8GT035513B296200N&CORRELATIONID=97306f6456378&SETTLE_DATE=2014-07-09 14:11:36&TRANSTIME=2014-07-09 14:11:36&FIRSTNAME=John&LASTNAME=doe&AMT=0.0
Another way of doing it is checking the IP from which the SILENT POST is coming.
I noticed all SILENT POST data comes from 173.0.81.65
$ip_address = $_SERVER['REMOTE_ADDR'];
if($ip_address != '173.0.81.65'){ exit; }
#Andrew Angel
SILENT POST and NotifyURL is not the same thing.
From the NotifyURL you can use IPN and data verification it is done very differently there because you receive different kind of post data string back.
I talked with PayPal support and they said NotifyURL and IPN is not available for PayPal Advanced Payments only for PayPal Payments Pro.
Indeed they both do the same function, but they are different things and source validation is done differently.
Hope this helps someone, waiting on opinions.

Duplicate Request in DoExpressCheckoutPayment (Digital Goods Checkout Express)

Long story short, I believe I've implemented the flow correctly, but on the final DoExpressCheckoutPayment I am getting:
ACK => SuccessWithWarning
L_ERRORCODE0 => 11607
L_SHORTMESSAGE0 => Duplicate Request
L_LONGMESSAGE0 => A successful transaction has already been completed for this token
Is this simply because I'm doing a GetExpressCheckoutDetails request before this? (the GetExpressCheckoutDetails ACK is "Success")
Note that the other data returned from DoExpressCheckoutPayment looks good:
PAYMENTINFO_0_PAYMENTSTATUS => Completed
PAYMENTINFO_0_ACK => Success
Should I just look for PAYMENTINFO_0_ACK and ignore the rest?
Sidenote- In case it's of interest, I'm using the PHP lib at https://github.com/thenbrent/paypal-digital-goods though I changed the stuff in the examples return.php to GetExpressCheckoutDetails on a new class since, of course, it made no sense to use the same purchase data every time and it has to be dynamic
EDIT: Okay I'm baffled. If I only call the GetExpressCheckoutDetails, then the response is:
CHECKOUTSTATUS => PaymentActionNotInitiated
However, if I call GetExpressCheckoutDetails and then DoExpressCheckoutPayment, the response of the preceding GetExpressCheckoutDetails becomes:
CHECKOUTSTATUS => PaymentActionCompleted (and it follows that the result of the subsequent DoExpressCheckoutPayment has the error of Duplicate Request)
How does that even make sense?! Did vanilla PHP just become asynchronous? Has paypal allocated enough money to buy a time machine? I'm probably missing something very basic, but I really don't see it yet :\
EDIT 2 Some Sample Code (didn't strip it to make it 100% vanilla, but should be pretty straightforward):
public static function completePaypalPurchase() {
self::configurePaypal(''); // Not relevent, just some setting of API keys and stuff
$paypalAPI = new PayPal_Purchase(); // Just to get purchase info so we can form the real purchase request
$response = $paypalAPI->get_checkout_details(); // Uses token from GET automatically
echo("RESPONSE FROM GET CHECKOUT");
print_r($response);
$ack = strtoupper($response['ACK']);
$userID = (int)$response['CUSTOM']; // This was passed earlier and is retrieved correctly
$numCredits = (int)$response['L_QTY0'];
//NOTE: If I comment out the below, then the $response above has CHECKOUTSTATUS => PaymentActionNotInitiated
// BUT If I do not comment it out, leaving it as-is then the $response above has CHECKOUTSTATUS => PaymentActionCompleted
// That's the core of the problem and where I'm stuck
if($ack == "SUCCESS" && $numCredits && $userID && $userID == $loggedInUserID) {
$paypalAPI = self::getPaypalPurchaseCredits($userID, $numCredits); // This creates a new PayPal_Purchase() with this info. In fact, it's the same method and therefore should return the same sort of object as the one used at the beginning of the flow
$response = $paypalAPI->process_payment();
$ack = strtoupper($response['ACK']);
echo("RESPONSE FROM DO PAYMENT");
print_r($response);
if(isset($response['PAYMENTINFO_0_TRANSACTIONID']) && $ack == "SUCCESS") {
$transactionID = $response['PAYMENTINFO_0_TRANSACTIONID'];
return(new APIReturn(true, array('ack'=>$ack, 'userid'=>$userID, 'numcredits'=>$numCredits, 'transactionid'=>$transactionID)));
}
}
return(new APIReturn(false, self::ERROR_NORESULT));
}
The correct order the calls is SetExpressCheckout, GetExpressCheckoutDetails, and then DoExpressCheckoutPayment. If you're geting a duplicate order error then you must be calling DECP twice somehow. You need to step through your code and see exactly how that's happening. It may be something in the class that you're using.
On that note, you may be interested in taking a look at my class instead. It makes everything very simple as it turns it all into PHP arrays and handles the gritty work for you.
If you don't want to start over with new class, though, then again, you need to step through what's happening with your code and through the class methods to see where it's getting posted twice.
Another thing I notice is that you're only checking for ACK = Success. That means when ACK = SuccessWithWarning it'll be treated as a failure. You need to handle both Success and SuccessWithWarning (which a decent class library would handle for you.)
Sorry I don't have a more definitive answer, but again, somewhere either in your code or in the library it must be getting posted twice. Are you logging the raw API requests and responses along the way? If so you'd be able to confirm it's getting hit twice because you'd have 2 sets of DECP requests and responses logged.

Passing 'Custom' Value back from Paypal

I'm attempting to pass order IDs to Paypal from an order page, then back to the thanks page so I can mark an order as paid, but I'm running into problems. Currently I'm passing these variables to Paypal like this:
$vars = array(
'cmd' => "_cart",
'upload' => "1",
'business' => "paypalemail#gmail.com",
'item_name_1' => "Test Item",
'amount_1' => "1.00",
'return' => "http://www.website.com/thanks.php",
'custom' => $order_id,
);
header('Location: https://www.paypal.com/cgi-bin/webscr?' . http_build_query($vars));
When they payment clears and I'm redirected, it doesn't appear that the order id is being sent back, however. This is the url I'm given:
thanks.php?tx=61G92224EW780500P&st=Completed&amt=1.00&cc=USD&cm=57&item_number=
I expected to see a "custom=" followed by the order number somewhere in this string. But it isn't present. Did I do something wrong?
Working on the same today :-)
You obviously want to use PDT instead of IPN, there are in fact some cases when this makes sense (e.g. small stores). PayPal PDT returns ONLY the tx value in GET format. It then expects that you construct a POST containing:
tx you just received
id token of the account
cmd with the fixed value "_notify-synch"
Only this post is answered with a body containing the variables, one each line.
See https://cms.paypal.com/cms_content/en_US/files/developer/PP_OrderMgmt_IntegrationGuide.pdf page 19.
The reason you are not getting the 'custom' GET parameter is because for some reason on the return URL, PayPal renames it to 'cm' so in the original example above, I assume that the custom parameter had been sent to PayPal will a value of 57 and you can see cm=57 in the return URL.
I think the custom parameter is still called 'custom' in the IPN call.

How do I get the Security Token for PayPal Payments Advanced gateway integration?

We have PayPal Payments Advanced and I'm unable to get past the first gateway integration step. Perhaps I'm missing something simple that should be obvious.
All the official PayPal documents I've been able to find for integrating the gateway for Advanced say the first step is to obtain a Secure Token. The page at
https://developer.paypal.com/webapps/developer/docs/classic/payflow/gs_ppa_hosted_pages/
for example.
I'm posting my test script below (sensitive info modified).
Every time I run the test script, I get a "Error: Your transaction can no longer be processed. Please return to the merchant's web site or contact the merchant. Error: 160" error message.
According to the PayPal Gateway Developer Guide and Reference, error 160 is, "Secure Token already been used. Indicates that the secure token has expired due to either a successful transaction or the token has been used three times while trying to successfully process a transaction. You must generate a new secure token."
Yet, the secure token has not already been used. A new one is generated every time the script is run.
"Enable Secure Token" is set to "Yes" in PayPal Manager.
Here is the script. What am I doing wrong?
<?php
$url = 'https://payflowlink.paypal.com';
#$url = 'https://pilot-payflowlink.paypal.com';
$token = md5( 'Will Bontrager' . time() );
/* $info assignment is all one line. Multi-line here for readability */
$info = "PARTNER=PayPal&
VENDOR=CertainReservations&
USER=ABC123&
PWD=321cba&
TRXTYPE=S&
AMT=23.45&
CREATESECURETOKEN=Y&
SECURETOKENID=$token";
echo "<pre>Value:$info</pre>";
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_VERBOSE => false,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $info
);
$ch = curl_init($url);
curl_setopt_array($ch,$options);
$content = curl_exec($ch);
$err = curl_errno($ch);
$errmsg = curl_error($ch) ;
$info = curl_getinfo($ch);
curl_close($ch);
if( $err )
{
echo "<pre>Error. $err\n$errmsg\n";
print_r($info);
echo '</pre>';
}
echo $content;
?>
Thank you very much for any guidance.
I think I must be missing some critical information.
Will
Just wanted to add that error 160 is also thrown when no secure token is passed to step 2.
Had this happen: host blocked curl calls, so the Paypal iframe was requested without the secure token which resulted in error 160.
According to Page 31 of this Payflow Gateway documentation this might work. I've copied the content here in the event he PDF is removed or moved without a proper 301 redirect.
To create a secure token, pass all parameters that you need to process
the transaction except for payment details parameters such as the
credit card number, expiration date, and check number.
In addition, pass the following Payflow parameters to create the
secure token.
Set SECURETOKENID to a unique alphanumeric value up to 36 characters in length.
SECURETOKENID=9a9ea8208de1413abc3d60c86cb1f4c5
Set CREATESECURETOKEN to the value Y to request that the Gateway server return a token.
CREATESECURETOKEN=Y
Set SILENTTRAN to the value TRUE to suppress the display of hosted pages.
SILENTTRAN=TRUE
Successful transactions will return RESULT=0. From page 33.
A Payflow Secure Token will expire:
If the same Secure Token is passed to Payflow a total of 3 times.
20 minutes after the Secure Token was generated.
When the token is used in a successful transaction
It's likely the formatting... here's a little about the parameters you're passing (from page 51):
Because the ampersand (&) and equal sign (=) characters have special
meanings, they are invalid in a name-value pair value.
The following are invalid:
COMPANYNAME=Ruff & Johnson COMMENT1=Level=5
To include special characters in the value portion of a name-value
pair, use a length tag. The length tag specifies the exact number of
characters and spaces that appear in the value. The following are
valid.
COMPANYNAME[14]=Ruff & Johnson
COMMENT1[7]=Level=5
NOTE: Do not use quotation marks ("") even if you use a length tag.
As far as I can tell (this is not documented clearly that I have found though this is helpful) you can't get a secure token from https://pilot-payflowlink.paypal.com but https://pilot-payflowpro.paypal.com seems to work just fine. Once you have your token you can use it with payflowlink.

How to protect sensitive pages for paypal payment processing?

I'm using the paypal express checkout, which follows this flow:
When they submit on the paypal website it follows a return url on my site which shows the order review with a confirm button, and two GET variables are passed back from paypal: token and payerId. The token gives me permission to request shipping info & later finalize the payment.
The first problem is I can access the 'checkout with paypal' page directly by typing in the URL into the address bar and it will submit the request to paypal, if the $_SESSION['Payment_Amount'] variable is not set it processes with the payment amount as 0 and throws an error.
SetExpressCheckout API call failed. Detailed Error Message: This transaction cannot be processed. The amount to be charged is zero.
I know I can set another session variable on the cart page to make sure they visit the cart first, and then clear the variable after checking for it, but another problem remains that the user only needs to visit the cart page once and the variable will be set to allow them to visit the sensitive page which sends a token request to paypal.
The next problem is that after going through all the steps and the user pressing the 'confirm order' button, the request is sent to paypal to process the order/money for that token. The user can press the 'BACK' button on the page and see the order-review again, then the user can press confirm order again and an error will show that an order was already processed for that token.
GetExpressCheckoutDetails API call failed. Detailed Error Message: A successful transaction has already been completed for this token.
That's clearly a good thing but what should I implement to prevent the user from accessing sensitive pages? Will I need to track certain keys in my back-end database?
At the moment i'm working on localhost with paypal's sandbox.
You have to create a process somehow that guarantees that the user follows the needed steps in the right order and prevents him from jumping out of this order.
Tracking the steps in the users session seems like the natural thing to do. If the session does not allow the step requested, redirect him elsewhere instead of asking paypal.
The deluxe version would be you implemented a state machine for easier improvements later on. State machines have the disadvantage of looking like huge overhead at first, and are too much hassle to implement later if you initially took a different approach. That's why it is important to think about using one from the start.
What if you want to add another payment provider later? A state machine could be easily extended for this - anything else might be a mess then.
Edit:
Actually, the only thing paypal expects you to send to them after the user is back on your site is the amount you want to charge. This info can be passed by putting it into the return url you send to paypal. Try adding some checksum there to prevent data errors and easy tampering (Paypal lets the process fail if the amount is incorrect nevertheless), and you are basically done. No session at all needed.
Edit2: Here is an excerpt of my code that defines the nvp parameters for paypals first step. You need the necessary auth stuff inside, too.
public function preparePayment(...) {
$nvp = array(
'METHOD' => 'SetExpressCheckout',
'VERSION' => '52.0',
'RETURNURL' => 'https://'.$request->server['HTTP_HOST'].'/'.$request->getLanguage().'/paypal/success/'.$this->hashAmount($amount),
'CANCELURL' => 'https://'.$request->server['HTTP_HOST'].'/'.$request->getLanguage().'/paypal/cancel',
'CURRENCYCODE' => $amount->getCurrency(),
'AMT' => number_format($amount->getAmount(), 2, '.', ''),
'ITEMAMT' => number_format($amount->getNettoAmount(), 2, '.', ''),
'TAXAMT' => number_format($amount->getVatAmount(), 2, '.', ''),
'PAYMENTACTION' => 'Sale',
'LOCALECODE' => strtoupper($request->getLanguage())
);
}
protected function hashAmount(Currency_Class $amount) {
return urlencode(
sprintf(
'%s-%s-%s-%u',
number_format($amount->getNettoAmount(), 2, '', ''),
number_format($amount->getVatAmount(), 2, '', ''),
strtoupper($amount->getCurrency()),
$this->makeChecksumString(number_format($amount->getNettoAmount(), 2, '', ''), strtoupper($amount->getCurrency()))
)
);
}
protected function makeChecksumString($amount, $currency) {
return crc32(sprintf('%sSaltValue%s', $amount, $currency));
}
protected function dehashAmount($string) {
$parts = array();
$found = preg_match('/^(\d+)\-(\d+)\-([A-Z]+)\-(\d+)$/', $string, $parts);
if ($found) {
$check = sprintf('%u', $this->makeChecksumString($parts[1], $parts[3]));
if ($check == $parts[4]) {
$netto = floatval(substr($parts[1], 0, -2) .'.'. substr($parts[1], -2));
$vat = floatval(substr($parts[2], 0, -2) .'.'. substr($parts[2], -2));
}
}
return ...
}

Categories