Search code examples
phpcodeigniterdesign-patternspingback

CodeIgniter PaymentWall Response body does not match the expected pattern: OK


Pingback wasn't successful. Reason: Response body does not match the expected pattern: OK

Signature base string uid=currency=type=0ref=369e67e903ca0b2261cd342575b8979e

Signature = MD5(Signature base string) 2aa9f1c847d1492b18cd017cdf78290b

this is model.donate.php

<?php

in_file();

class Mdonate{
    protected $registry, $db, $config;
    private $vars = array();
    protected $hash_item = '';
    protected $paypal_ipn_url = 'https://www.paypal.com/cgi-bin/webscr';
    protected $paypal_ipn_url_ssl = 'www.paypal.com';
    protected $req = 'cmd=_notify-validate';
    protected $post = array();
    protected $paypal_response;
    public $order_details = array();
    protected $pw_ip_white_list = array('174.36.92.186', '66.220.10.3', '174.36.92.186', '174.36.96.66', '174.36.92.187', '174.36.92.192', '174.37.14.28');
    protected $pw_reason_list = array(0     => 'Invalid Reason',
                                      1     => 'Chargeback',
                                      2     => 'Credit Card fraud',
                                      3     => 'Order fraud',
                                      4     => 'Bad data entry',
                                      5     => 'Fake / proxy user',
                                      6     => 'Rejected by advertiser',
                                      7     => 'Duplicate conversions',
                                      8     => 'Goodwill credit taken back',
                                      9     => 'Cancelled order',
                                      10    => 'Partially reversed transaction');

    public function __construct(){
        $this->registry = registry::getInstance();
        $this->db = $this->registry->db;
        $this->config = $this->registry->config;
    }

    public function __set($key, $val){
        $this->vars[$key] = $val;
    }

    public function __get($name){
        return $this->vars[$name];
    }

    public function __isset($name){
        return isset($this->vars[$name]);
    }

    public function get_paypal_packages(){
        return $this->db->query('SELECT id, package, reward, price, currency FROM dmncms_donate_paypal_packages WHERE status = 1 ORDER BY orders ASC')->fetch_all();
    }

    public function check_package($id){
        $count = $this->db->snumrows('SELECT COUNT(id) as count FROM dmncms_donate_paypal_packages WHERE id = '.$this->db->escape($id).' AND status = 1');
        return ($count == 1);
    }

    public function insert_paypal_order($reward, $price, $currency){
        $this->hash_item = md5($_SESSION['name'].$price.$currency.uniqid(microtime(),1));
        $stmt = $this->db->prepare('INSERT INTO dmncms_donate_paypal_orders (amount, currency, credits, account, hash) VALUES(:amount, :currency, :credits, :account, :hash)');
        return $stmt->execute(array(':amount'   => $price, ':currency'  => $currency, ':credits'    => $reward, ':account'  => $_SESSION['name'], ':hash'   => $this->hash_item));  
    }

    public function get_paypal_data(){
        return array('email' => $this->config->load_xml_config('donate|pp_email'), 'item' => $this->hash_item, 'user' => $_SESSION['name']);
    }

    public function gen_post_fields($data){
        $data_array = explode('&', $data);
        foreach($data_array as $value){
            $value = explode ('=', $value);
            if(count($value) == 2)
                $this->post[$value[0]] = urldecode($value[1]);
        }
        foreach($this->post as $key => $value) {        
            $this->req .= "&".$key."=".urlencode($value);
        }
    }

    public function post_back_paypal_fsock(){
        $header = "POST /cgi-bin/webscr HTTP/1.0\r\n";  
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Host: ".$this->paypal_ipn_url_ssl."\r\n";
        $header .= "Content-Length: " . strlen($this->req) . "\r\n";
        $header .= "Connection: close\r\n\r\n";
        $fp = fsockopen('ssl://'.$this->paypal_ipn_url_ssl, 443, $errno, $errstr, 30);
        if(!$fp){
            $this->writelog('PayPal sent fsockopen error no. '.$errno.': '.$errstr.'','Paypal');
            return false;
        } 
        else{
            fputs($fp, $header.$this->req);
            while(!feof($fp)){
                $this->paypal_response = fgets($fp, 1024);
            }
            fclose($fp);
        }
        return true;
    }

    public function post_back_paypal_curl(){
        $request = curl_init();
        curl_setopt_array($request, array(CURLOPT_URL => $this->paypal_ipn_url,
                                          CURLOPT_POST => TRUE,
                                          CURLOPT_POSTFIELDS => $this->req,
                                          CURLOPT_RETURNTRANSFER => TRUE,
                                          CURLOPT_HTTPHEADER => array('Connection: Close'),
                                          CURLOPT_SSL_VERIFYPEER => TRUE,
                                          CURLOPT_SSL_VERIFYHOST => 2,
                                          CURLOPT_FORBID_REUSE => TRUE,
                                          CURLOPT_CAINFO => APP_PATH.DS.'data'.DS.'cacert.pem'));
        $this->paypal_response = curl_exec($request);
        if(curl_errno($request)){
            $this->writelog(curl_error($request), 'Paypal');
            return false;
        }
        curl_close($request);
        return true;
    }

    public function validate_paypal_payment(){
        if(stripos($this->paypal_response, "VERIFIED") !== false){
            if(!$this->check_email()){
                return false;   
            }
            if(!$this->check_order_number()){
                return false;
            }
            switch($this->vars['payment_status']){
                case 'Completed':
                    if($this->vars['tax'] > 0){
                        $this->vars['mc_gross'] -= $this->vars['tax']; 
                    }                   
                    if($this->vars['mc_gross'] == $this->order_details['amount']){
                        if($this->vars['mc_currency'] == $this->order_details['currency']){
                            if($this->check_completed_transaction()){
                                return false;
                            }   
                            if($this->check_pending_transaction()){
                                if($this->update_transaction_status()){
                                    return true;
                                }
                            }
                            else{
                                if($this->insert_transaction_status()){
                                    return true;
                                }
                            }
                        }
                    }
                break;
                case 'Pending':
                    if($this->vars['tax'] > 0){
                        $this->vars['mc_gross'] -= $this->vars['tax']; 
                    }
                    if(!$this->check_completed_transaction() && !$this->check_pending_transaction()){
                        $this->insert_transaction_status();
                    }
                break;
                case 'Reversed': case 'Refunded':
                    $this->decrease_credits($this->order_details['account'], $this->order_details['credits']);
                    $this->update_transaction_status();
                    if($this->config->load_xml_config('donate|pp_punish_player') == 1){
                        $this->block_user($this->order_details['account']);
                    }
                break;
            }
        }
        if(stripos($this->paypal_response, "INVALID") !== false){
            $this->writelog('PayPal sent [status: INVALID] [transaction id: '.$this->vars['txn_id'], 'Paypal');
        }
    }

    private function check_email(){
        if(strtolower($this->vars['receiver_email']) != strtolower($this->config->load_xml_config('donate|pp_email'))){
            $this->writelog('PayPal sent invalid reciever email: '.$this->vars['receiver_email'].'', 'Paypal');
            return false;
        }
        return true;
    }

    private function check_order_number(){
        $count = $this->db->snumrows('SELECT COUNT(id) AS count FROM dmncms_donate_paypal_orders where hash = '.$this->db->escape($this->vars['item_number']));
        if($count == 1){
            $this->order_details = $this->db->query('SELECT amount, currency, account, credits FROM dmncms_donate_paypal_orders where hash = '.$this->db->escape($this->vars['item_number']))->fetch();
            return true;
        }
        else{
            $this->writelog('PayPal sent invalid order [transaction id: '.$this->vars['txn_id'].']', 'Paypal');
            return false;
        }
    }

    private function check_completed_transaction(){
        $count = $this->db->snumrows('SELECT COUNT(id) AS count FROM dmncms_donate_paypal_transactions where transaction_id = '.$this->db->escape($this->vars['txn_id']).' and status = \'Completed\'');
        if($count > 0){
            return true;
        }
        return false;
    }

    private function check_pending_transaction(){
        $count = $this->db->snumrows('SELECT COUNT(id) AS count FROM dmncms_donate_paypal_transactions where transaction_id = '.$this->db->escape($this->vars['txn_id']).' and status = \'Pending\'');
        if($count > 0){
            return true;
        }
        return false;
    }

    private function update_transaction_status(){
        $stmt = $this->db->prepare('UPDATE dmncms_donate_paypal_transactions SET status = :status WHERE transaction_id = :trans_id');
        return $stmt->execute(array(':status' => $this->vars['payment_status'], ':trans_id' => $this->vars['txn_id']));
    }

    private function insert_transaction_status(){
        $stmt = $this->db1->prepare('INSERT INTO dmncms_donate_paypal_transactions (transaction_id, amount, currency, acc, credits, order_date, status, payer_email) VALUES (:trans_id, :gross, :currency, :account, :credits, :time, :payment_status, :payer_email)');
        return $stmt->execute(array(':trans_id' => $this->vars['txn_id'], ':gross' => $this->vars['mc_gross'], ':currency' => $this->vars['mc_currency'], ':account' => $this->order_details['account'], ':credits' => $this->order_details['credits'], ':time' => time(), ':payment_status' => $this->vars['payment_status'], ':payer_email' => $this->vars['payer_email']));
    }

    public function reward_user($acc, $credits){
        $stmt = $this->db->prepare('UPDATE bg_user SET cash = cash + :credits WHERE bg_user = :account');
        $stmt->execute(array(':account' => $acc, ':credits' => str_replace('-', '', $credits)));
    }

    private function decrease_credits($acc, $credits){
        $stmt = $this->db1->prepare('UPDATE bg_user SET cash = cash - :credits WHERE bg_user = :account');
        $stmt->execute(array(':credits' => str_replace('-', '', $credits), ':account' => $acc));
    }

    private function block_user($acc){
        return;
    }

    public function validate_ip_list(){
        return (in_array($_SERVER['REMOTE_ADDR'], $this->pw_ip_white_list));
    }

    public function validate_pw_signature(){
        return (md5('uid='.$this->vars['uid'].'currency='.$this->vars['currency'].'type='.$this->vars['type'].'ref='.$this->vars['ref'].$this->config->load_xml_config('donate|pw_secretkey')) == $this->vars['sig']);
    }

    public function validate_pw_payment(){
        if(!$this->check_reference()){
            if($this->log_pw_transaction()){
                return true;
            }
        }
        else{
            if($this->vars['type'] == 2){
                $this->change_pw_transaction_status();
                if($this->vars['reason'] == 2 || $this->vars['reason'] == 3){
                    $this->block_user($this->vars['uid']);
                }       
                $this->decrease_credits($this->vars['uid'], $this->vars['currency']);   
            }
        }
    }

    private function check_reference(){
        $count = $this->db->snumrows('SELECT COUNT(uid) AS count FROM dmncms_donate_paymentwall WHERE uid = '.$this->db->escape($this->vars['uid']).' AND ref = '.$this->db->escape($this->vars['ref']).'');
        if($count > 0){ 
            return true;
        }
        return false;
    }

    private function log_pw_transaction(){
        $prepare = $this->db->prepare('INSERT INTO dmncms_donate_paymentwall (uid, currency, type, ref, reason, order_date) VALUES (:uid, :currency, :type, :ref, :reason, :time)');
        return $prepare->execute(array(':uid' => $this->vars['uid'], ':currency' => $this->vars['currency'], ':type' => $this->vars['type'], ':ref' => $this->vars['ref'], ':reason' => 'Complete', ':time' => time()));
    }

    private function change_pw_transaction_status(){
        $stmt = $this->db->prepare('UPDATE dmncms_donate_paymentwall SET currency = :currency, reason = :reason, order_date = :order_date WHERE uid =:uid AND ref = :ref');
        $stmt->execute(array(':currency' => $this->vars['currency'], ':reason' => $this->pw_reason_list[$this->vars['reason']], ':order_date' => time(), ':uid' => $this->vars['uid'], ':ref' => $this->vars['ref']));
    }

    public function writelog($logentry, $lgname) {
        $log = '['.$_SERVER['REMOTE_ADDR'].'] ['.(isset($_SESSION['name']) ? $_SESSION['name'] : 'Unknown').'] '.$logentry.'';
        $log_name = APP_PATH.DS.'logs'.DS.$lgname.'_'.date("m-d-y").'.txt';
        $logfile = @fopen($log_name, "a+");
        if($logfile){
            fwrite($logfile, "[".date ("h:iA")."] $log\r\n");
            fclose($logfile);
        }
    }
}

This is view.paymentwall.php / http://domain.com/donate/paymentwall - im using it for pingback adress

<?php 
    if(load::get('errors') != false){
        foreach(load::get('errors') as $errors){
            echo '<div class="notification-box notification-box-error">'.$errors.'</div>';
        }
    }
    if(load::get('pw') == false || load::get('pw') == 0){
        echo '<div class="notification-box notification-box-error">This donation method is disabled.</div>';
    }
    else{
        echo '<div style="/* border: 1px dotted black; *//* -webkit-border-radius: 5px; */-moz-border-radius: 5px;/* border-radius: 5px; */margin-top: 10px;    padding: 10px;    height: auto;    background: rgba(55, 52, 55, 1);    box-shadow: 0 0 4px rgba(0,0,0,.6), 0 1px 1px rgba(0,0,0,.5), inset 0 0 0 1px rgba(255,255,255,.015), inset 0 1px 0 rgba(255,255,255,.05);    -webkit-border-radius: 5px;    -moz-border-radius: 5px;    border-radius: 5px;    /* margin-left: -38px; */    z-index: 1;">
                <div style="padding: 2px; text-align: center;"><iframe src="http://wallapi.com/api/ps/?key='.load::get('pw_apikey').'&uid='.$_SESSION['name'].'&widget='.load::get('pw_widget').'" width="'.load::get('pw_w_width').'" height="'.load::get('pw_w_height').'" frameborder="0"></iframe></div>
              </div>';
    }
?>

Solution

  • When Paymentwall sends a Pingback, it expects your server to respond with HTTP Status Code 200 and with response body only containing OK https://www.paymentwall.com/en/documentation/Virtual-Currency-API/711#http_pingback_return_value

    It looks like currently your script returns HTML code of the payment page as a response to Paymentwall's Pingback, so the problem is that the response body doesn't only contain OK.

    I recommend splitting the payment page and the pingback processing script and move the pingback processing script to something like domain/paymentwall-pingback

    On a separate note, to validate Paymentwal pingbacks easier, please feel free to use Paymentwall PHP Library. With Paymentwall PHP Library, validating the pingback signature, pingback origin and the parameters can be done with just a few lines:

    require_once('/path/to/paymentwall-php/lib/paymentwall.php');
    Paymentwall_Config::getInstance()->set(array(
      'api_type' => Paymentwall_Config::API_VC, //OR API_GOODS or API_CART
      'public_key' => 'YOUR_PUBLIC_KEY',
      'private_key' => 'YOUR_PRIVATE_KEY'
    ));
    $pingback = new Paymentwall_Pingback($_GET, $_SERVER['REMOTE_ADDR']);
    if ($pingback->validate()) {
      //product delivery logic
    }