IPN listener for PayPal donations not being called properly PHP

67 Views Asked by At

I have a listener for PayPal donation IPNs. It however is not being called and there are no logs in the PayPal sandbox.The code is:
donations.php:

<?php
     // For test payments we want to enable the sandbox mode. If you want to put live
    // payments through then this setting needs changing to `false`.
    $enableSandbox = true;

    // Database settings. Change these for your database configuration.
    $dbConfig = [
        'host' => 'localhost',
        'username' => 'xxxx',
        'password' => 'xxxx',
        'name' => 'xxxx'
    ];

    // PayPal settings. Change these to your account details and the relevant URLs
    // for your site.
    $paypalConfig = [
        'email' => $enableSandbox ? '[email protected]' : '[email protected]',
        'return_url' => 'https://example.com/index.php?page=/donation-successful.php',
        'cancel_url' => 'https://example.com/index.php?page=/donation-cancelled.php',
        'notify_url' => 'https://example.com/donations.php',
    ];

    $paypalUrl = $enableSandbox ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' : 'https://ipnpb.paypal.com/cgi-bin/webscr';

    function verifyTransaction($data) {
        global $paypalUrl;
        $req = 'cmd=_notify-validate';
        foreach ($data as $key => $value) {
            $value = urlencode(stripslashes($value));
            //$value = preg_replace('/(.*[^%^0^D])(%0A)(.*)/i', '${1}%0D%0A${3}', $value); // IPN fix
            $req .= "&$key=$value";
        }
        $ch = curl_init($paypalUrl);
        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_SSLVERSION, 6);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
        $res = curl_exec($ch);
        if (!$res) {
            $errno = curl_errno($ch);
            $errstr = curl_error($ch);
            curl_close($ch);
            throw new Exception("cURL error: [$errno] $errstr");
        }
        $info = curl_getinfo($ch);
        // Check the http response
        $httpCode = $info['http_code'];
        if ($httpCode != 200) {
            throw new Exception("PayPal responded with http code $httpCode");
        }
        curl_close($ch);
        return $res == 'VERIFIED';
    }
    
    function checkTxnid($txnid) {
        $db = new mysqli($dbConfig['host'], $dbConfig['username'], $dbConfig['password'], $dbConfig['name']);
        $txnid = $db->real_escape_string($txnid);
        $results = $db->query('SELECT * FROM `donations` WHERE txnid = \'' . $txnid . '\'');
        return ! $results->num_rows;
    }
    
    function addPayment($data) {
        $db = new mysqli($dbConfig['host'], $dbConfig['username'], $dbConfig['password'], $dbConfig['name']);
        if (is_array($data)) {
            if ($db->query("INSERT INTO 'donations' (txnid, payment_amount, payment_status, createdtime) VALUES('" . $data["txn_id"] . "', '" . $data["payment_amount"] . "', '" . $data["payment_status"] . "', '" . date("Y-m-d H:i:s") . "')")) {
                return true;
            }
        }
        return false;
    }

    // Check if paypal request or response
    // STEP 1: read POST data
    // Reading POSTed data directly from $_POST causes serialization issues with array data in the POST.
    // Instead, read raw POST data from the input stream.
    $raw_post_data = file_get_contents('php://input');
    $raw_post_array = explode('&', $raw_post_data);
    $data = array();
    foreach ($raw_post_array as $keyval) {
        $keyval = explode ('=', $keyval);
        if (count($keyval) == 2) {
            $data[$keyval[0]] = urldecode($keyval[1]);
        }
    }
    if (!isset($data["txn_id"]) && !isset($data["txn_type"])) {
        // Set the PayPal account.
        $data['business'] = $paypalConfig['email'];
        // Set the PayPal return addresses.
        $data['return'] = stripslashes($paypalConfig['return_url']);
        $data['cancel_return'] = stripslashes($paypalConfig['cancel_url']);
        $data['notify_url'] = stripslashes($paypalConfig['notify_url']);
        // Set the currency so that these aren't overridden by the form data.
        $data['currency_code'] = 'GBP';
        // Build the query string from the data.
        $queryString = http_build_query($data);
        // Redirect to paypal IPN
        header('location:' . $paypalUrl . '?' . $queryString);
        exit();
    } else {
        // Handle the PayPal response.
        // Create a connection to the database.
        $db = new mysqli($dbConfig['host'], $dbConfig['username'], $dbConfig['password'], $dbConfig['name']);
        // We need to verify the transaction comes from PayPal and check we've not
        // already processed the transaction before adding the payment to our
        // database.
        if (verifyTransaction($data) && checkTxnid($data['txn_id'])) {
            // Assign posted variables to local data array.
            $data = [
                'item_name' => $data['item_name'],
                'item_number' => $data['item_number'],
                'payment_status' => $data['payment_status'],
                'payment_amount' => $data['mc_gross'],
                'payment_currency' => $data['mc_currency'],
                'txn_id' => $data['txn_id'],
                'receiver_email' => $data['receiver_email'],
                'payer_email' => $data['payer_email'],
            ];
            if (addPayment($data) !== false) {
                // Payment successfully added.
            }
        }
    }
?>

Replaced sensitive info in places but you get the idea. Nothing is in the PayPal logs. Nothing is added to my donations database. Payment shows up as completed on the transactions list.

1

There are 1 best solutions below

0
DarkestSoul1992 On

FIXED:
Here is the fixed code for anyone who is interested.

donations.php:

<?php
     // For test payments we want to enable the sandbox mode. If you want to put live
    // payments through then this setting needs changing to `false`.
    $enableSandbox = true;

    // Database settings. Change these for your database configuration.
    $dbConfig = [
        'host' => 'localhost',
        'username' => 'xxxx',
        'password' => 'xxxx',
        'name' => 'xxxx'
    ];

    // PayPal settings. Change these to your account details and the relevant URLs
    // for your site.
    $paypalConfig = [
        'email' => $enableSandbox ? '[email protected]' : '[email protected]',
        'return_url' => 'https://example.com/index.php?page=/donation-successful.php',
        'cancel_url' => 'https://example.com/index.php?page=/donation-cancelled.php',
        'notify_url' => 'https://example.com/donations.php',
    ];

    $paypalUrl = $enableSandbox ? 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' : 'https://ipnpb.paypal.com/cgi-bin/webscr';

    function verifyTransaction($data) {
        global $paypalUrl;
        $req = 'cmd=_notify-validate';
        foreach ($data as $key => $value) {
            $value = urlencode(stripslashes($value));
            //$value = preg_replace('/(.*[^%^0^D])(%0A)(.*)/i', '${1}%0D%0A${3}', $value); // IPN fix
            $req .= "&$key=$value";
        }
        $ch = curl_init($paypalUrl);
        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_SSLVERSION, 6);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_CAINFO, "../certs/cacert.pem");
        curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('User-Agent: PHP-IPN-Verification-Script', 'Connection: Close'));
        $res = curl_exec($ch);
        if (!$res) {
            $errno = curl_errno($ch);
            $errstr = curl_error($ch);
            curl_close($ch);
            throw new Exception("cURL error: [$errno] $errstr");
        }
        $info = curl_getinfo($ch);
        // Check the http response
        $httpCode = $info['http_code'];
        if ($httpCode != 200) {
            throw new Exception("PayPal responded with http code $httpCode");
        }
        curl_close($ch);
        return $res == 'VERIFIED';
    }
    
    function checkTxnid($txnid) {
        global $db;
        $txnid = $db->real_escape_string($txnid);
        $results = $db->query('SELECT * FROM `donations` WHERE txnid = \'' . $txnid . '\'');
        return ! $results->num_rows;
    }
    
    function addPayment($data) {
        global $db;
        if (is_array($data)) {
            $data['date'] = date('Y-m-d H:i:s');
            $stmt = $db->prepare('INSERT INTO `donations` (txnid, payment_amount, payment_status, createdtime) VALUES(?, ?, ?, ?)');
            $stmt->bind_param(
                'sdss',
                $data['txn_id'],
                $data['payment_amount'],
                $data['payment_status'],
                $data['date'],
            );
            $stmt->execute();
            $stmt->close();
            return $db->insert_id;
        }
        return false;
    }

    // Check if paypal request or response
    // STEP 1: read POST data
    // Reading POSTed data directly from $_POST causes serialization issues with array data in the POST.
    // Instead, read raw POST data from the input stream.
    $raw_post_data = file_get_contents('php://input');
    $raw_post_array = explode('&', $raw_post_data);
    $data = array();
    foreach ($raw_post_array as $keyval) {
        $keyval = explode ('=', $keyval);
        if (count($keyval) == 2) {
            $data[$keyval[0]] = urldecode($keyval[1]);
        }
    }
    if (!isset($data["txn_id"]) && !isset($data["txn_type"])) {
        // Set the PayPal account.
        $data['business'] = $paypalConfig['email'];
        // Set the PayPal return addresses.
        $data['return'] = stripslashes($paypalConfig['return_url']);
        $data['cancel_return'] = stripslashes($paypalConfig['cancel_url']);
        $data['notify_url'] = stripslashes($paypalConfig['notify_url']);
        // Set the currency so that these aren't overridden by the form data.
        $data['currency_code'] = 'GBP';
        // Build the query string from the data.
        $queryString = http_build_query($data);
        // Redirect to paypal IPN
        header('location:' . $paypalUrl . '?' . $queryString);
        exit();
    } else {
        // Handle the PayPal response.
        // Create a connection to the database.
        $db = new mysqli($dbConfig['host'], $dbConfig['username'], $dbConfig['password'], $dbConfig['name']);
        // We need to verify the transaction comes from PayPal and check we've not
        // already processed the transaction before adding the payment to our
        // database.
        if (verifyTransaction($data) && checkTxnid($data['txn_id'])) {
            // Assign posted variables to local data array.
            $data = [
                'item_name' => $data['item_name'],
                'item_number' => $data['item_number'],
                'payment_status' => $data['payment_status'],
                'payment_amount' => $data['mc_gross'],
                'payment_currency' => $data['mc_currency'],
                'txn_id' => $data['txn_id'],
                'receiver_email' => $data['receiver_email'],
                'payer_email' => $data['payer_email'],
            ];
            if (addPayment($data)) {
                // Payment successfully added.
            }
        }
    }
?>

Donations now get updated through PayPal's IPN system and added to my donations DB.