PayPal IPN Issue, but works in Sandbox fine

2019-03-30 19:38发布

Using the code located on GitHub here: https://github.com/Quixotix/PHP-PayPal-IPN

Ok, so, I have a php page like so which I am using as my ipnlistener for the sa=paypal_verify part of the url within variables, in Step 3 of button creation like so:

notify_url=http://mydomain.com/index.php?page=paypaltest;sa=paypal_verify
return=http://mydomain.com/index.php?page=paypaltest;sa=thankyou
rm=2

Here is the code linking to http://mydomain.com/index.php?page=paypaltest

if (!empty($_REQUEST['sa']) && $_REQUEST['sa'] != 'thankyou')
{
        // Require file for loading up global variables, etc.  Might not be needed, but just in here in case.
    require_once('/public_html/Settings.php');

    global $smcFunc, $context, $scripturl, $boarddir, $modSettings, $txt;

    if ($_REQUEST['sa'] == 'paypal_verify')
    {
        ini_set('log_errors', true);
        ini_set('error_log', '/ipn_errors.log');

        // Here is where the actual IpnListener Class is defined
        // and uses cURL or fSocket to post back to paypal.
        require_once($boarddir . '/ipn/ipnlistener.php');
        $listener = new IpnListener();

        try {
            $listener->requirePostMethod();
            $verified = $listener->processIpn();
        } catch (Exception $e) {
            error_log($e->getMessage());
            exit(0);
        }


        /*
        The processIpn() method returned true if the IPN was "VERIFIED" and false if it
        was "INVALID".
        */
        if ($verified) {
            $errmsg = '';   // stores errors from fraud checks

            // 1. Make sure the payment status is "Completed" 
            if ($_POST['payment_status'] != 'Completed') { 
                // simply ignore any IPN that is not completed
                exit(0); 
            }

            // Database Query in here (excluded) that selects sellers_email, product_name, and price from within the database for the actual purchase, based on the $_POST['item_name'] from paypal.

            // If return results are 0 from database table than do following:
                $errmsg .= "Product Not Found in the database: ";
                $errmsg .= $_POST['item_name']."\n";
                // manually investigate errors from the fraud checking
                $body = "IPN failed fraud checks: \n$errmsg\n\n";
                $body .= $listener->getTextReport();
                mail('myemail@address.com', 'IPN Fraud Warning', $body);
                exit(0);

            // Set $sellers_email, $item_name, and $price variables from the database table.  And than free the mysql result.


            // 2. Make sure seller email matches your primary account email.
            if ($_POST['receiver_email'] != $sellers_email) {
                $errmsg .= "'receiver_email' does not match: ";
                $errmsg .= $_POST['receiver_email']."\n";
            }

            // 3. Make sure the amount(s) paid match
            if ($_POST['mc_gross'] != $price) {
                $errmsg .= "'mc_gross' does not match: ";
                $errmsg .= $_POST['mc_gross']."\n";
            }

            // 4. Make sure the currency code matches
            if ($_POST['mc_currency'] != 'USD') {
                $errmsg .= "'mc_currency' does not match: ";
                $errmsg .= $_POST['mc_currency']."\n";
            }

            // 5. Ensure the transaction is not a duplicate.
            // Attempt to grab a transaction id from the table where it goes.  The column is id_txn, if it exists
                $errmsg .= "'txn_id' has already been processed: ".$_POST['txn_id']."\n";

            // free database result.

            // Set $txn_id from paypal to the $txn_id variable.
            $txn_id = (string) $_POST['txn_id'];

            if (!empty($errmsg)) {

                // manually investigate errors from the fraud checking
                $body = "IPN failed fraud checks: \n$errmsg\n\n";
                $body .= $listener->getTextReport();
                mail('myemail@address.com', 'IPN Fraud Warning', $body);

            } else {

                // send email to buyer.
                // and just to be sure, send them to the sa=thankyou page!
            }
        } else {
            // send email to self with the errors listed
        }
    }
}

if (!empty($_REQUEST['sa']) && $_REQUEST['sa'] == 'thankyou')
    echo '
    <div class="information">Thank you for purchasing this product.  We sent you an email with your details and link to download this software.<br />';

echo '
<div class="cat_bar boardframe">
    <h3 class="catbg">
        PayPal Test Sale
    </h3>
</div>
<div class="roundframe blockframe">
This is just a Test Selling Page to be sure that the PayPal electronic downloads actually work!
</div>
<span class="lowerframe"><span><!-- // --></span></span>
<br />
<div class="cat_bar boardframe">
    <h3 class="catbg">
        Purchase at 0.01 USD
    </h3>
</div>
<div class="roundframe blockframe">
    <form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
        <input type="hidden" name="cmd" value="_s-xclick">
        <input type="hidden" name="hosted_button_id" value="FJAGXAC7GCFSY">
        <input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_paynow_SM.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!" style="border: none; background: none;">
        <img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
    </form>
</div>
<span class="lowerframe"><span><!-- // --></span></span>
<br class="clear" />';

The ipnlistener.php file has the following code in it:

class IpnListener {

    /**
     *  If true, the recommended cURL PHP library is used to send the post back 
     *  to PayPal. If flase then fsockopen() is used. Default true.
     *
     *  @var boolean
     */
    public $use_curl = true;

    /**
     *  If true, explicitly sets cURL to use SSL version 3. Use this if cURL
     *  is compiled with GnuTLS SSL.
     *
     *  @var boolean
     */
    public $force_ssl_v3 = true;     

    /**
     *  If true, cURL will use the CURLOPT_FOLLOWLOCATION to follow any 
     *  "Location: ..." headers in the response.
     *
     *  @var boolean
     */
    public $follow_location = false;     

    /**
     *  If true, an SSL secure connection (port 443) is used for the post back 
     *  as recommended by PayPal. If false, a standard HTTP (port 80) connection
     *  is used. Default true.
     *
     *  @var boolean
     */
    public $use_ssl = true;

    /**
     *  If true, the paypal sandbox URI www.sandbox.paypal.com is used for the
     *  post back. If false, the live URI www.paypal.com is used. Default false.
     *
     *  @var boolean
     */
    public $use_sandbox = false; 

    /**
     *  The amount of time, in seconds, to wait for the PayPal server to respond
     *  before timing out. Default 30 seconds.
     *
     *  @var int
     */
    public $timeout = 30;       

    private $post_data = array();
    private $post_uri = '';     
    private $response_status = '';
    private $response = '';

    const PAYPAL_HOST = 'www.paypal.com';
    const SANDBOX_HOST = 'www.sandbox.paypal.com';

    /**
     *  Post Back Using cURL
     *
     *  Sends the post back to PayPal using the cURL library. Called by
     *  the processIpn() method if the use_curl property is true. Throws an
     *  exception if the post fails. Populates the response, response_status,
     *  and post_uri properties on success.
     *
     *  @param  string  The post data as a URL encoded string
     */
    protected function curlPost($encoded_data) {

        if ($this->use_ssl) {
            $uri = 'https://'.$this->getPaypalHost().'/cgi-bin/webscr';
            $this->post_uri = $uri;
        } else {
            $uri = 'http://'.$this->getPaypalHost().'/cgi-bin/webscr';
            $this->post_uri = $uri;
        }

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_CAINFO, 
                    dirname(__FILE__)."/cert/api_cert_chain.crt");
        curl_setopt($ch, CURLOPT_URL, $uri);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded_data);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $this->follow_location);
        curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, true);

        if ($this->force_ssl_v3) {
            curl_setopt($ch, CURLOPT_SSLVERSION, 3);
        }

        $this->response = curl_exec($ch);
        $this->response_status = strval(curl_getinfo($ch, CURLINFO_HTTP_CODE));

        if ($this->response === false || $this->response_status == '0') {
            $errno = curl_errno($ch);
            $errstr = curl_error($ch);
            throw new Exception("cURL error: [$errno] $errstr");
        }
    }

    /**
     *  Post Back Using fsockopen()
     */
    protected function fsockPost($encoded_data) {

        if ($this->use_ssl) {
            $uri = 'ssl://'.$this->getPaypalHost();
            $port = '443';
            $this->post_uri = $uri.'/cgi-bin/webscr';
        } else {
            $uri = $this->getPaypalHost(); // no "http://" in call to fsockopen()
            $port = '80';
            $this->post_uri = 'http://'.$uri.'/cgi-bin/webscr';
        }

        $fp = fsockopen($uri, $port, $errno, $errstr, $this->timeout);

        if (!$fp) { 
            // fsockopen error
            throw new Exception("fsockopen error: [$errno] $errstr");
        } 

        $header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
        $header .= "Host: ".$this->getPaypalHost()."\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Content-Length: ".strlen($encoded_data)."\r\n";
        $header .= "Connection: Close\r\n\r\n";

        fputs($fp, $header.$encoded_data."\r\n\r\n");

        while(!feof($fp)) { 
            if (empty($this->response)) {
                // extract HTTP status from first line
                $this->response .= $status = fgets($fp, 1024); 
                $this->response_status = trim(substr($status, 9, 4));
            } else {
                $this->response .= fgets($fp, 1024); 
            }
        } 

        fclose($fp);
    }

    private function getPaypalHost() {
        if ($this->use_sandbox) return self::SANDBOX_HOST;
        else return self::PAYPAL_HOST;
    }

    /**
     *  Get POST URI
     */
    public function getPostUri() {
        return $this->post_uri;
    }

    /**
     *  Get Response
     *
     *  Returns the entire response from PayPal as a string including all the
     *  HTTP headers.
     *
     *  @return string
     */
    public function getResponse() {
        return $this->response;
    }

    /**
     *  Get Response Status 200 if Successful
     */
    public function getResponseStatus() {
        return $this->response_status;
    }

    /**
     *  Get Text Report
     */
    public function getTextReport() {

        $r = '';

        // date and POST url
        for ($i=0; $i<80; $i++) { $r .= '-'; }
        $r .= "\n[".date('m/d/Y g:i A').'] - '.$this->getPostUri();
        if ($this->use_curl) $r .= " (curl)\n";
        else $r .= " (fsockopen)\n";

        // HTTP Response
        for ($i=0; $i<80; $i++) { $r .= '-'; }
        $r .= "\n{$this->getResponse()}\n";

        // POST vars
        for ($i=0; $i<80; $i++) { $r .= '-'; }
        $r .= "\n";

        foreach ($this->post_data as $key => $value) {
            $r .= str_pad($key, 25)."$value\n";
        }
        $r .= "\n\n";

        return $r;
    }

    /**
     *  Process IPN
     *
     *  Handles the IPN post back to PayPal and parsing the response. Call this
     *  method from your IPN listener script. Returns true if the response came
     *  back as "VERIFIED", false if the response came back "INVALID", and 
     *  throws an exception if there is an error.
     *
     *  @param array
     *
     *  @return boolean
     */    
    public function processIpn($post_data=null) {

        $encoded_data = 'cmd=_notify-validate';

        if ($post_data === null) {
            if (!empty($_POST)) {
                $this->post_data = $_POST;
                $encoded_data .= '&'.file_get_contents('php://input');
            } else {
                throw new Exception("No POST data found.");
            }
        } else {
            $this->post_data = $post_data;

            foreach ($this->post_data as $key => $value) {
                $encoded_data .= "&$key=".urlencode($value);
            }
        }

        if ($this->use_curl) $this->curlPost($encoded_data); 
        else $this->fsockPost($encoded_data);

        if (strpos($this->response_status, '200') === false) {
            throw new Exception("Invalid response status: ".$this->response_status);
        }

        if (strpos($this->response, "VERIFIED") !== false) {
            return true;
        } elseif (strpos($this->response, "INVALID") !== false) {
            return false;
        } else {
            throw new Exception("Unexpected response from PayPal.");
        }
    }

    public function requirePostMethod() {
        // require POST requests
        if ($_SERVER['REQUEST_METHOD'] && $_SERVER['REQUEST_METHOD'] != 'POST') {
            header('Allow: POST', true, 405);
            throw new Exception("Invalid HTTP request method.");
        }
    }
}

There are no errors recorded in the error_log (this file actually gets created if an error occurs, so it doesn't even exist). And it never adds anything into the database. I know that the item_name is correct on the PayPal Server and should match in the database table as well.

Seems there is a problem with the ipnlistener or how it is linking to it? Or what is being returned from it?

Is there a way I can test the return value exactly from the ipnlistener.php file? So that I can actually see it? Maybe if I could even see what gets sent to the following url (from Paypal): http://mydomain.com/index.php?page=paypaltest;sa=paypal_verify

I'm trying to be able to track this so that I can figure out exactly where it is going wrong at... Any ideas?

Perhaps it has something to do with this line in the cURL:

curl_setopt($ch, CURLOPT_CAINFO, 
                    dirname(__FILE__)."/cert/api_cert_chain.crt");

I never heard tell of attaching an SSL Certificate within the actual file structure of a server.

Also, it might be possible, because it has index.php within the notify_url that it is adding additional headers. If so, can I somehow flush everything out of the file before it grabs the encoded data from PayPal and returns it back to paypal for verification? That way it's sure to be exactly the same as when PayPal sent it to the notify_url.

I don't understand why this has to be so damn complicated! I've been pulling out my hair now on this for 7 days straight with almost no sleep!

This I know: cURL is enabled on my server. If I browse to the actual notify_url page it is BLANK (Like a completely empty page), which I believe is correct. When viewing the Notification History, it says, Sent, and HTTP Response code is 200.

This I don't know: Where it is failing at? Where is the PayPal Notification failing at exactly? Why is it not touching my database? I get no errors anywhere...

3条回答
ゆ 、 Hurt°
2楼-- · 2019-03-30 20:07
smile是对你的礼貌
3楼-- · 2019-03-30 20:10

I found the problem, OMG, it was in the certification file:

Just changed the following:

curl_setopt($ch, CURLOPT_CAINFO, 
    dirname(__FILE__)."/cert/api_cert_chain.crt");

To the actual filepath on my server starting at /home/

curl_setopt($ch, CURLOPT_CAINFO, "FULL FILE PATH/cert/api_cert_chain.crt");

OMG I'm stupid! Works perfectly now! Yayyy!

查看更多
Deceive 欺骗
4楼-- · 2019-03-30 20:10

I was struggling with this for a while, and it turned out to be my firewall.

During testing I'd added two rules to allow HTTPS outbound to www.paypal.com and www.sandbox.paypal.com, and it seemed to work fine. In hindsight that was just because the sandbox only has one IP address!

So when I added www.paypal.com to the firewall in iptables it resolved the IP then, but of course later the IP I got from DNS was different, so the firwall started blocking.

As @Latha says, you should use ipnpb.paypal.com instead of www.paypal.com. This is because they list the IP addresses for this domain so you can add them to your firewall spec. They are currently...

173.0.84.8
173.0.84.40
173.0.88.8
173.0.88.40

They are soon (25 March 2014) going to add a lot more, you can find a full list here...

https://ppmts.custhelp.com/app/answers/detail/a_id/92

查看更多
登录 后发表回答