Page 4 of 6

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 21, 2013 3:05 am
by simonmlewis
Thanks.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 21, 2013 3:08 am
by simonmlewis
I've put that code at the foot of the code - is that right?
Are you expecting there to be a file on the server in the same folder (the http folder) ??

Nothing there. Or do I need to create that file and then run it again?

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 21, 2013 5:16 am
by simonmlewis
Great news, someone on the PHP Developer support site sent me a link and this works:

Code: Select all

<?php

// CONFIG: Enable debug mode. This means we'll log requests into 'ipn.log' in the same directory.
// Especially useful if you encounter network errors or other intermittent problems with IPN (validation).
// Set this to 0 once you go live or don't require logging.
define("DEBUG", 1);

// Set to 0 once you're ready to go live
define("USE_SANDBOX", 1);


define("LOG_FILE", "./ipn.log");


// Read POST data
// reading posted data directly from $_POST causes serialization
// issues with array data in POST. Reading raw POST data from input stream instead.
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
        $keyval = explode ('=', $keyval);
        if (count($keyval) == 2)
                $myPost[$keyval[0]] = urldecode($keyval[1]);
}
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
if(function_exists('get_magic_quotes_gpc')) {
        $get_magic_quotes_exists = true;
}
foreach ($myPost 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 IPN data back to PayPal to validate the IPN data is genuine
// Without this step anyone can fake IPN data

if(USE_SANDBOX == true) {
        $paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr";
} else {
        $paypal_url = "https://www.paypal.com/cgi-bin/webscr";
}

$ch = curl_init($paypal_url);
if ($ch == FALSE) {
        return FALSE;
}

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_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);

if(DEBUG == true) {
        curl_setopt($ch, CURLOPT_HEADER, 1);
        curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
}

// CONFIG: Optional proxy configuration
//curl_setopt($ch, CURLOPT_PROXY, $proxy);
//curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);

// Set TCP timeout to 30 seconds
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));

// CONFIG: Please download 'cacert.pem' from "http://curl.haxx.se/docs/caextract.html" and set the directory path
// of the certificate as shown below. Ensure the file is readable by the webserver.
// This is mandatory for some environments.

//$cert = __DIR__ . "./cacert.pem";
//curl_setopt($ch, CURLOPT_CAINFO, $cert);

$res = curl_exec($ch);
if (curl_errno($ch) != 0) // cURL error
        {
        if(DEBUG == true) {        
                error_log(date('[Y-m-d H:i e] '). "Can't connect to PayPal to validate IPN message: " . curl_error($ch) . PHP_EOL, 3, LOG_FILE);
        }
        curl_close($ch);
        exit;

} else {
                // Log the entire HTTP response if debug is switched on.
                if(DEBUG == true) {
                        error_log(date('[Y-m-d H:i e] '). "HTTP request of validation request:". curl_getinfo($ch, CURLINFO_HEADER_OUT) ." for IPN payload: $req" . PHP_EOL, 3, LOG_FILE);
                        error_log(date('[Y-m-d H:i e] '). "HTTP response of validation request: $res" . PHP_EOL, 3, LOG_FILE);

                        // Split response headers and payload
                        list($headers, $res) = explode("\r\n\r\n", $res, 2);
                }
                curl_close($ch);
}

// Inspect IPN validation result and act accordingly

if (strcmp ($res, "VERIFIED") == 0) {
        // check whether the payment_status is Completed
        // check that txn_id has not been previously processed
        // check that receiver_email is your PayPal email
        // check that payment_amount/payment_currency are correct
        // process payment and mark item as paid.

        // 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(DEBUG == true) {
                error_log(date('[Y-m-d H:i e] '). "Verified IPN: $req ". PHP_EOL, 3, LOG_FILE);
        }
} else if (strcmp ($res, "INVALID") == 0) {
        // log for manual investigation
        // Add business logic here which deals with invalid IPN messages
        if(DEBUG == true) {
                error_log(date('[Y-m-d H:i e] '). "Invalid IPN: $req" . PHP_EOL, 3, LOG_FILE);
        }
}

?>
Question in, now that is giving me:
[text]IPN sent successfully[/text]
What do I do next to make it listen for the Price, and maybe the product ID ?

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 21, 2013 5:35 am
by simonmlewis
I am getting rather bamboozled with this, but trying to learn.

So, the IPN Listener is working.
I've just read all this from PayPal:
[text]
Verify that you are the intended recipient of the IPN message.
To do this, check the email address in the message. This check prevents another merchant from accidentally or intentionally using your listener.
Verify that the IPN is not a duplicate.
To do this, save the transaction ID and last payment status in each IPN message in a database and verify that the current IPN's values for these fields are not already in this database.
Note: You can't rely on transaction ID alone to screen out duplicates, as this scenario shows: 1) PayPal sends you an IPN notifying you of a pending payment. 2) PayPal later sends you a second IPN telling you that the payment has completed. However, both IPNs contain the same transaction ID; therefore, if you were using just transaction ID to identify IPNs, you would to treat the "completed payment" IPN as a duplicate.
Ensure that you receive an IPN whose payment status is "completed" before shipping merchandise or enabling download of digital goods.
Because IPN messages can be sent at various stages in a transaction's progress, you must wait for the IPN whose status is "completed' before handing over merchandise to a customer.
Verify that the payment amount in an IPN matches the price you intend to charge.
If you do not encrypt your button code, it's possible for someone to capture a button-click message and change the price it contains. If you don't check the price in an IPN against the real price, you could accept a lower payment than you want.
[/text]

It sounds like I need a separate DB table that at some stage stores the transaction ID ? As well as maybe other fields. And the IPN somehow queries that???

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 21, 2013 3:34 pm
by Eric!
Yup. And read all those items carefully and test them with sandbox. Also if you send out emails, make sure you filter all the $_POST data. I've seen more than one Palpal listener get hijacked by a spammer injecting headers into fake paypal data. In fact, you should be filtering that data anyway.

As I've mentioned several times too. You need to integrate your user management system with your backend using some kind of user specific token that you pass via the 'custom' variable. Additional double checks can also be made between the front end and back end to verify a buyer's identity. Don't rely on the email address coming from paypal their users often have old addresses or use different addresses to register with your site.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Fri Nov 22, 2013 3:11 am
by simonmlewis
Slow down, I've no idea what you are saying here.
I've got it to listen. Next question is, how do I know what fields I have to put _POST variables in my Listener? Becauase I am sure 90% of the data I don't even use.

"Check the email address in the message". How?
"Save the transaction ID and last payment status in each IPN Message in a database"... how? I probably sound dumb here to you, but this is alien to me. How do I capture the transaction ID (is it in that _POST?).
I don't know what it means by "ensure that you receive an IPN whose payment status is completed. Does it mean I have to capture the content of a variable, and if that content says "Completed", then mark my item as sold?

Is the $price in a _POST within the IPN code somewhere, so I can check that code? ie. if $IPN_Price == $row->price { SUCCESS! }
??

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Fri Nov 22, 2013 3:16 am
by simonmlewis
I've seen this in line 113-120 of the code:

Code: Select all

        //$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'];
Do uncomment these items, and then query the ones I want to?
Such as:

Code: Select all

// query db
if ($mc_gross == $row->price) { // update database to mark it as sold }
How do I tell PayPal that the amount is in fact correct?
The transaction ID is confusing me a bit. Sounds like I have to store things in a db, perhaps temporarily, and then query it within the same momentary process.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Fri Nov 22, 2013 11:35 am
by Eric!
Have you not worked with $_POST or $_GET data before like with forms? Paypal is POSTING data to your script. You can see everything that is available by looking in the $_POST array. This is raw unfiltered data, and anyone from anywhere can stick stuff in it so you need to be careful how you use it.

To see all the fields that paypal is sending to you, just look in your debug file.

Keeping in line with the code you've previously posted you could also add something like this to dump out the $_POST contents which will make it more clear what all the keys are to the posted array data:

Code: Select all

if(DEBUG == true) {        
                error_log('POST ARRAY CONTAINS:\r\n'.print_r($_POST,true), LOG_FILE);
}
Then you get the data just like the commented lines are suggesting to you in your code. However I recommend you keep most of the IPN data that comes via $_POST for your records.

See PHP's filter_var functions for some basic methods to sanitize/verify these fields.

99% of what I'm explaining can be found with a quick google search.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 28, 2013 4:55 am
by simonmlewis
Yes I've worked with $_POST and $_GET before.
So far then, I've learnt how to make the IPN test page run fine, and the test via the IPN Simulator works.

My next job is to make that actually happen for real.

So, where do I tell the PayPal button to use that URL (my paypalipntest.php) to run the checks?
If I can get that working in tests, I can then see what it's posting, create the variables, then query the variables I want to use - and I might then be getting somewhere.

BTW I use XAMPP locally, trying to find the Debug this end. Not sure how to find Debug on the live server as never really needed it.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 28, 2013 6:47 am
by simonmlewis
Do I put my "IPN Listener" file location in here?
At the moment, this is where the user is taken back to, and then our system processes it.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 28, 2013 7:08 am
by simonmlewis

Code: Select all

if (strcmp ($res, "VERIFIED") == 0) {
        // check whether the payment_status is Completed
        // check that txn_id has not been previously processed
        // check that receiver_email is your PayPal email
        // check that payment_amount/payment_currency are correct
        // process payment and mark item as paid.

        // 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(DEBUG == true) {
                error_log(date('[Y-m-d H:i e] '). "Verified IPN: $req ". PHP_EOL, 3, LOG_FILE);
        }
} else if (strcmp ($res, "INVALID") == 0) {
        // log for manual investigation
        // Add business logic here which deals with invalid IPN messages
        if(DEBUG == true) {
                error_log(date('[Y-m-d H:i e] '). "Invalid IPN: $req" . PHP_EOL, 3, LOG_FILE);
        }
Just reading through this again.
So I can query "$payment_amount" against my own databse to ensure what they paid, is what is in my system???
Where within this PayPal code, do I run my internal DB query? Can you please show me?
I'm assuming the VERIFIED bit means it's passed thru PayPal ok, but now I need to ensure my end, that the money spent is what's on the system.

Am I nearly there...??

Also, how do I actually generate the button, and have the Transaction ID generated from PayPAl?
Or do I have to generate that transaction code?

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 28, 2013 12:02 pm
by Eric!
The code that you've borrowed from someone tells you right there in the comments what you need to implement. Naturally you can't do anything with the database until you've retrieved the post data, so this would imply that you need to do your database work after you get the post data.

Again you should also use filter_var on the post data to make sure it is clean. And Paypal provides a lot of information that you are ignoring because you're just copying what someone else did. I suggest you look at all the of posted data that comes from paypal (as I provided some code to do that previously in this thread) and decide if you should record some of the other transaction post data. You also might want a separate table in your database just to log all the PayPal activity for your clients' records.

You can generate the button by creating the html <form> as I outlined earlier. The transaction id will come from paypal via the posted data to your listener.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 28, 2013 12:06 pm
by simonmlewis
1) Where in that code I pasted, do I put the DB query?
2) how does the transaction ID come thru to me, BEFORE I have even submitted anything to PayPal on the product page?

I'm assuming there is a "standard" paypal button I put on my product page??

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Thu Nov 28, 2013 5:34 pm
by Eric!
Huh? Put the database stuff after your read the data from paypal in your listener script. I'll just keep programming for you...and send the bill later.

Code: Select all

if (strcmp ($res, "VERIFIED") == 0) {
        // 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']; //sometimes users use more than one email address (one for paypal and one for your site...use the 'custom' variable to track who they really are
        $payer_custom= $_POST['custom']; // I added this so you can verify which user really made the purchase
// now that you have the data, check it against your database
// I suggest you look at all the $_POST data and log some of it to a table in your database to track the history
// Now you still need to do all the things the code comments helpfully tell you to do:

        // check whether the payment_status is Completed
        // check that txn_id has not been previously processed
        // check that receiver_email is your PayPal email
        // check that payment_amount/payment_currency are correct
        // process payment and mark item as paid.

        if(DEBUG == true) {
                error_log(date('[Y-m-d H:i e] '). "Verified IPN: $req ". PHP_EOL, 3, LOG_FILE);
        }
}
The transaction ID comes from paypal to your listener. Your front end buttons on your HTML page should be made according to my earlier example. viewtopic.php?f=1&t=138708&start=15#p689210

NOTE THESE FIELDS:
action='https://www.sandbox.paypal.com/cgi-bin/webscr' -- this is for testing paypal only in sandbox
notify_url -- points pay pal to your listener
custom -- is your way to identify which logged in user is making the purchase
sandbox -- for testing

And figure out how to use filter_var on the POST data....last time I'm going to mention this...promise.

Re: PayPal buttons: how do I protect sellers' sales?

Posted: Fri Nov 29, 2013 9:39 am
by simonmlewis
Let me try to think this thru straight now.

This code is what I enter into my product.inc page.

Code: Select all

<form action='https://www.sandbox.paypal.com/cgi-bin/webscr' method='post'>
<input type='hidden' name='business' value='$row->selleremail' />

// this is what does the processing after it's been finalised and updates my DB.
<input type='hidden' name='notify_url' value='http://YOURDOMAIN/paypal_ipn/process.php' />
<input type='hidden' name='currency_code' value='USD' />
<input type='hidden' name='lc' value='US' />
<input type='hidden' name='item_name' value='$row->title' />
<input type='hidden' name='amount' value='$15.00' />
<input type='hidden' name='sandbox' value='1' />
<input type='hidden' name='type' value='paynow' />
<input type='hidden' name='class' value='signup' />
<input type='hidden' name='custom' value='64b1add1e6349e3a2900fd4a9cf90e1f9e9090c54a983fdd56a3978812e66115' />

// what is difference between this and notify_url?
<input type='hidden' name='return_url' value='/Purchases' />

// we don't use this, but perhaps it's just the $row->id of the product in our database?
<input type='hidden' name='item_number' value='24adf829-15e6-11e3-b5bd-e0cb4e1c48db' />
<input type='hidden' name='cmd' value='_xclick' />
<input  type="submit" value="Buy Now"/>
</form>
"The transaction id will come from paypal via the posted data to your listener."
How does that come through to my site, if I have yet to send anything to PayPal?

And do I have to put each transId into a DB field, each time the product page loads??

I understand about reading all the _POST and _GET data. Whether I need them or not, I accept that I need to do that.