PayPal buttons: how do I protect sellers' sales?

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

simonmlewis
DevNet Master
Posts: 4435
Joined: Wed Oct 08, 2008 3:39 pm
Location: United Kingdom
Contact:

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

Post by simonmlewis »

Sandbox. Gotcha.
So I can just put their id number into Custom and query $custom in the listener?
Cool. I did wonder if I could use that.
Love PHP. Love CSS. Love learning new tricks too.
All the best from the United Kingdom.
Eric!
DevNet Resident
Posts: 1146
Joined: Sun Jun 14, 2009 3:13 pm

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

Post by Eric! »

Good to hear it. This thread has exhausted me.
simonmlewis
DevNet Master
Posts: 4435
Joined: Wed Oct 08, 2008 3:39 pm
Location: United Kingdom
Contact:

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

Post by simonmlewis »

Got Custom working nicely now, but there is a slightly issue and might be one that IPN LIstener can pickup.

I am testing on the live one still (it's working so using it at the moment), and it's providing "invalid" payments when I do refunds of pennies.

I guess this is because the IPN is on. Do I need to alter the listener to pickup negatives, or refunds??
Love PHP. Love CSS. Love learning new tricks too.
All the best from the United Kingdom.
Eric!
DevNet Resident
Posts: 1146
Joined: Sun Jun 14, 2009 3:13 pm

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

Post by Eric! »

Impossible to say without seeing your code and your debug log and a longer description of your problem. But yes your IPN should handle all the possible paypal status conditions (pending, complete, etc. see the docs). If you are getting INVALID instead of VERIFIED at this line of your snippet:

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

Then you have other issues. Paypal is saying that the information you are sending back to them to verify either A. Didn't come Paypal, or B. Has been altered.

A guess would be you are reposting to the wrong URL, or have sandbox set on.
simonmlewis
DevNet Master
Posts: 4435
Joined: Wed Oct 08, 2008 3:39 pm
Location: United Kingdom
Contact:

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

Post by simonmlewis »

I'm not posting anything. Sandbox is off in my listener.
I'm just logging into the paypal account to do a refund. It comes thru as -£0.01, but as invalid.
I though the default listener code would have a REFUND 'option' in the code.
Love PHP. Love CSS. Love learning new tricks too.
All the best from the United Kingdom.
Eric!
DevNet Resident
Posts: 1146
Joined: Sun Jun 14, 2009 3:13 pm

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

Post by Eric! »

If you want help you have to provide more than just your theory of how you think something should work and why you think it isn't working. Otherwise I just have to continue guessing solutions and trying to understand your misconceptions. You shouldn't worry there is nothing in an IPN dataset or your PHP IPN listener that needs to be kept secret.

Your IPN listener DOES post information. It receives the IPN info from Paypal and then sends it back to have paypal verify it. If you are posting it back to the sandbox it will come back invalid if it was issued from the live URL.

What parameter comes back as invalid? Where in your code does this happen?

Are you doing this refund manually: Logging into the account and sending the money back? Did you capture ALL the paypal IPN data for the refund? There are other parameters that can be used to identify what/where/why this IPN error occurred too.
simonmlewis
DevNet Master
Posts: 4435
Joined: Wed Oct 08, 2008 3:39 pm
Location: United Kingdom
Contact:

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

Post by simonmlewis »

It's basically just posting to my IPN a value of minus, with either an empty product id, or not. But the fact it is a minus, my system sees it as as invalid payment because the price is not a negative!

Seems that refunds are passing thru the same section of code int he IPN Listener, as payments. Hence asking how I pic up if it is a refund or not.

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", 0);


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
        
        $first_name = $_POST['first_name'];
        $last_name = $_POST['last_name'];
        $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'];
        $custom = $_POST['custom'];
        
        $todaydate = (date('Y-m-d'));    
        
        include "dbconn.php";
       
        $query = "SELECT price FROM products WHERE id = :item_number AND price = :payment_amount";
        $result = $pdo->prepare($query);
        $result->execute(array(':item_number' => $item_number, ':payment_amount' => $payment_amount));
        $num_rows = $result->rowCount();
        if ($num_rows == 0) 
          { 
          
          mysql_query("INSERT INTO db_listener (datepayment, firstname, lastname, buyerid, emailpaypal, emailreceiver, price, itemname, productid, txnid, status) VALUES ('$todaydate', '$first_name', '$last_name', '$custom', '$payer_email', '$receiver_email', '$payment_amount', '$item_name', '$item_number', '$txn_id', 'Invalid')");
                  
          $to = "simon@site";
          $subject =  "site: Invalid Payment";
          $headers = "From: info@site\n";
          $body = "Hi

This brief email is to confirm that you have received an invalid payment of $payment_amount from $payer_email.

The item you have for sale is SAFE.  It is unsold.  Whether you choose to refund the buyer is down to you.

Should you need further assistance, please contact us.
";
          mail ($to, $subject, $body, $headers);
          }
        else
          {
          while ($row = $result->fetch(PDO::FETCH_OBJ)) 
            { 
            mysql_query("INSERT INTO db_listener (datepayment, firstname, lastname, buyerid, emailpaypal, emailreceiver, price, itemname, productid, txnid, status) VALUES ('$todaydate', '$first_name', '$last_name', '$custom', '$payer_email', '$receiver_email', '$payment_amount', '$item_name', '$item_number', '$txn_id', 'Valid')");
            
$to = "$payer_email";
              $subject =  "Purchase";
              $headers = "From: info@site.co.uk\n";
              $body = "Dear $first_name


Your payment via PayPal should be cleared very soon.

Regards
              mail ($to, $subject, $body, $headers);


              $to = "$receiver_email";
              $subject =  "Sale";
              $headers = "From: info@site.co.uk\n";
              $body = "Dear Seller


Regards


--------------------------

              mail ($to, $subject, $body, $headers);
              }
        }

       
       
        
        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);
        }
}

?>
It seems to be passing thru this:

Code: Select all

if (strcmp ($res, "VERIFIED") == 0) {
I guess I could just stop it running if the price is a negative, but surely I can track if it is a refund and maybe record that too.
Love PHP. Love CSS. Love learning new tricks too.
All the best from the United Kingdom.
Eric!
DevNet Resident
Posts: 1146
Joined: Sun Jun 14, 2009 3:13 pm

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

Post by Eric! »

Now I don't have to guess. I can see what you're doing and that takes a lot of mystery out of the question. Your code is setup to only handle a very simple case of a person buying 1 specific item at 1 specific price.

Your code does not (but should):
*Process Multiple items (could be ok, if you only are selling one item, one at a time and never support shopping carts)
*Check whether the payment_status is = "XXXX" (see table below) and modify the database as needed
*Check that txn_id has not been previously processed as payment_status="Completed"
*Check that receiver_email is your sellers correct email
*Process payment and mark item as paid, notify seller.
*Filter the incoming user data

For starters you need to process the payment_status values. Here's a list of their possible conditions from IPN variables found at paypal docs.
[text]*Canceled_Reversal: A reversal has been canceled. For example, you won a dispute with the customer, and the funds for the transaction that was reversed have been returned to you.
*Completed: The payment has been completed, and the funds have been added successfully to the account balance.
*Created: A German ELV payment is made using Express Checkout.
*Denied: The payment was denied. This happens only if the payment was previously pending because of one of the reasons listed for the pending_reason variable or the Fraud_Management_Filters_x variable.
*Expired: This authorization has expired and cannot be captured.
*Failed: The payment has failed. This happens only if the payment was made from your customer's bank account.
*Pending: The payment is pending. See pending_reason for more information.
*Refunded: You refunded the payment.
*Reversed: A payment was reversed due to a chargeback or other type of reversal. The funds have been removed from your account balance and returned to the buyer. The reason for the reversal is specified in the ReasonCode element.
*Processed: A payment has been accepted.
*Voided: This authorization has been voided.[/text]

From this list you can see that your db query (shown below) should not just put 'Valid' all the time in for payment_status. And in the case of "Pending" there is also a pending_reason value you might want to capture.

Code: Select all

mysql_query("INSERT INTO db_listener (datepayment, firstname, lastname, buyerid, emailpaypal, emailreceiver, price, itemname, productid, txnid, status) VALUES ('$todaydate', '$first_name', '$last_name', '$custom', '$payer_email', '$receiver_email', '$payment_amount', '$item_name', '$item_number', '$txn_id', 'Valid')");
To specifically answer your question, you use the payment_status to process your payment_status="Refunded" case. But there are more payment_status conditions you need to process. The basics: "Denied", "Pending", "Reversed", "Canceled_Reversal", "Refunded" and the most important "Completed" should all be checked and processed accordingly.

FWIW you can do all this testing in the sandbox using the IPN simulator, you don't need to do live transactions.
Post Reply