<?php
require_once 'modules/admin/models/PluginCallback.php';
require_once 'modules/billing/models/class.gateway.plugin.php';

class PluginPaypalCallback extends PluginCallback
{

    function processCallback()
    {
        
        if (!isset($GLOBALS['testing'])) {
            $testing = false;
        } else {
            $testing = $GLOBALS['testing'];
        }
        
        $this->logger->log(2, 'Paypal callback invoked');

        if ($this->settings->get('plugin_paypal_Use PayPal Sandbox') == '0'
            && isset($_POST['test_ipn']) && $_POST['test_ipn'] == '1')
        {
            $this->logger->log(2, "** Paypal sandbox callback but account is in production mode => callback discarded");
            return;
        }

        if (!isset($_POST['txn_type'])) {
            $this->logger->log(2, 'Paypal callback ignored: txn_type is not defined');
            return;
        }
        
        ////////////////////////////////////
        // read post from PayPal system and add 'cmd'
        $req = 'cmd=_notify-validate';
        
        foreach ($_POST as $key => $value) {
          $value = urlencode(stripslashes($value));
          $req .= "&$key=$value";
        }
        
        // post back to PayPal system to validate
        if (isset($_POST['test_ipn']) && $_POST['test_ipn'] == 1) {
            $paypal_url = 'http://www.sandbox.paypal.com';
        } else {
            $paypal_url = 'http://www.paypal.com';
        }
        $header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= 'Content-Length: ' . strlen($req) . "\r\n\r\n";
        
        // Assign IPN Variables
        // Different possible transaction types (txn_type) for subscriptions:
        // 1. 'subscr_signup': issued just after the client has paid the inicial amount. We ignore this.
        // 2. 'subscr_payment': issued just after the previous one and for every subsequent payment.
        $ppTransType = $_POST['txn_type'];

        if ($ppTransType == 'subscr_signup') {
            // Here we should update a field in the first invoice with the subscription date:
            //     $_POST['subscr_date']
            // Time/Date stamp generated by PayPal , in the following format: HH:MM:SS DD Mmm YY, YYYY PST
            //                                                                22:21:09 Oct 20, 2009 PDT
            // However, I am not believing in this param now, because seems that the documentacion says
            // one thing different than what I am really getting in this parameter.
            
            $this->logger->log(2, 'Paypal subscr_signup callback ignored');
            return;
        }

        $ppTransID = @$_POST['txn_id']; // Transaction ID (Unique) (not defined for subscription cancellations)
        $ppPayStatus = @$_POST['payment_status']; // Payment Status (not defined for subscription cancellations)
        $ppPayAmount = @$_POST['mc_gross']; // Total paid for this transaction (not defined for subscription cancellations)
        list($tInvoiceID,$tIsRecurring,$tGenerateInvoice) = explode("_", $_POST["custom"]);
        $this->logger->log(2, "\$ppTransType: $ppTransType; \$ppTransID: $ppTransID; \$ppPayStatus: $ppPayStatus;");
        $this->logger->log(2, "\$ppPayAmount: $ppPayAmount; \$tInvoiceID: $tInvoiceID; \$tIsRecurring: $tIsRecurring; \$tGenerateInvoice: $tGenerateInvoice");
        
        // Create Plugin class object to interact with CE.
        $cPlugin = new Plugin($tInvoiceID, 'paypal', $this->user);

        $newInvoice = false;
        
        // Check to see if this Invoice is already paid.
        // Skip if this is a call for a cancellation
        if($cPlugin->IsPaid()==1 && $ppTransType == 'subscr_payment') {
            $this->logger->log(2, 'Previous subscription invoice is already paid');
            // If it is, then check to see if the GenerateInvoice variable was passed to the script
            if($tGenerateInvoice && $tIsRecurring) {
                // If it was and is TRUE (1), Generate a New Invoice, and set internal Plugin object
                // variables to the new invoice's information.
                if ($cPlugin->transExists($ppTransID)) {
                    $newInvoice = $cPlugin->retrieveInvoiceForTransaction($ppTransID);
                    
                    if ($newInvoice && $cPlugin->m_Invoice->isPending()) {
                        $this->logger->log(2, 'Invoice already exists, and is marked as pending');
                    }
                } else {
                    $this->logger->log(2, 'Generating new invoice');
                    $newInvoice = $cPlugin->GenerateNewInvoice($ppPayAmount, '', $ppTransID);
                    
                    if($newInvoice === false){
                        $subject = 'Error generating a new paypal invoice';
                        $message = "There was an error when trying to generate a new paypal invoice.\n"
                                  ."It was not possible to retrieve the invoice entries.\n"
                                  ."Maybe the invoice that started the subscription (".$tInvoiceID.") has been deleted.\n"
                                  ."\n"
                                  ."This is the current information in the callback:\n"
                                  ."\$ppTransType: ".$ppTransType."\n"
                                  ."\$ppTransID: ".$ppTransID."\n"
                                  ."\$ppPayStatus: ".$ppPayStatus."\n"
                                  ."\n"
                                  ."\$ppPayAmount: ".$ppPayAmount."\n"
                                  ."\$tInvoiceID: ".$tInvoiceID."\n"
                                  ."\$tIsRecurring: ".$tIsRecurring."\n"
                                  ."\$tGenerateInvoice: ".$tGenerateInvoice."\n";
                        
                        $cPlugin->createTicket($ppTransID, $subject, $message, $this->user);
                        exit;
                    }else{
                        $cPlugin->updateDateRecurringEntry();
                    }
                }
            } else {
                $this->logger->log(1, 'Error: exiting Paypal callback invocation');
                exit;
            }
        }
        
        $this->logger->log(2, "Requesting callback confirmation; sending request: $paypal_url?$req");
        $res = $this->_requestConfirmation($paypal_url, $header.$req, $testing);
        $this->log(2,"Request Confirmation Returned: ".$res);
        
        //Add plugin details
        $cPlugin->m_Amount = $ppPayAmount;
        $cPlugin->m_TransactionID = $ppTransID;
        $cPlugin->m_Action = "charge";
        $cPlugin->m_Last4 = "NA";
        
        if (strcmp ($res, "VERIFIED") == 0) {
            $this->logger->log(2, "Callback has been verified successfully");
            // TODO check that receiver_email is an email address in your PayPal account
            if($tIsRecurring) {
                // Uncomment to test payment failures through subscription cancellations
                // if ($ppTransType == 'subscr_cancel') $ppTransType = 'subscr_failed';
                switch($ppTransType) {
                    case 'subscr_payment':
                        switch($ppPayStatus) {
                            case "Completed":
                                $transaction = "Paypal payment of $ppPayAmount was accepted. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                                $cPlugin->PaymentAccepted($ppPayAmount,$transaction,$ppTransID, $testing);
                            break;
                            case "Pending":
                                $transaction = "Paypal payment of $ppPayAmount was marked 'pending' by Paypal. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                                $cPlugin->PaymentPending($transaction,$ppTransID);
                            break;
                            case "Failed":
                                $transaction = "Paypal payment of $ppPayAmount was rejected. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                                $cPlugin->PaymentRejected($transaction);
                            break;
                        }
                        break;
                    case 'subscr_cancel':
                        $this->logger->log(2, "Subscription has been cancelled on Paypal's side.");
                        $tUser = new User($cPlugin->m_Invoice->m_UserID);
                        if ($tUser->getStatus() == USER_STATUS_CANCELLED) {
                            $this->logger->log(2, 'User is already cancelled. Ignore callback.');
                            exit;
                        }
                        $subject = 'Gateway recurring payment cancelled';
                        $message = "Recurring payment for invoice $tInvoiceID, corresponding to package \"{$_POST['item_name']}\" has been cancelled by customer.";
                        // If createTicket returns false it's because this transaction has already been done
                        // Use subscr_id because txn_id is not sent on cancellation IPNs from Paypal
                        if (!$cPlugin->createTicket($_POST['subscr_id'], $subject, $message, $tUser)) exit;
        
                        // Reset recurringfee entry so that invoice entry gets generated without the IPN call.
                        // We don't use the nextbilldate in the recurringfee table cause there was a bug in this plugin's
                        // early implementation that wasn't updating that field. So we calculate it from scratch every time.
                        // First we need to determine the next due date.
                        $cycles = array(
                            'D' => 'day',
                            'M' => 'month',
                            'Y' => 'year',
                        );
                        // Warning: don't use $_POST['subscr_date'] here: when $_POST['txn_type'] is 'subscr_cancel' this contains
                        // the cancellation date, not the subscription date.
                        
                        /* There are four possible cases that a subscription cancellation can occur.
                         *   1) The subscription is cancelled before the next due date
                         *   2) The subscription is cancelled on the due date (before the payment has gone through)
                         *   3) The subscription is cancelled on the due data (after the payment has gone through)
                         *   4) The subscription is cancelled after two failed payment attempts (payment overdue)
                         *
                         * Cases 1 & 4 are similiar and cases 2 & 3 are similiar.  We need to distinguish between each set of cases.
                         */
                        $nextDueDate = $cPlugin->m_Invoice->getDate('timestamp');
                        
                        if (isset($_POST['period1'])) {
                            $period = explode(' ', $_POST['period1']);
                            $nextDueDate = strtotime("+{$period[0]} {$cycles[$period[1]]}", $nextDueDate);
                        }
                        
                        // Search for the date of the last transaction with a payment for the subscription
                        $query = "SELECT UNIX_TIMESTAMP(`transactiondate`) "
                                ."FROM `invoicetransaction` "
                                ."WHERE `response` LIKE '%accepted. Original Signup Invoice: ".$tInvoiceID." (%' "
                                ."ORDER BY `transactiondate` DESC "
                                ."LIMIT 1 ";
                        $result = $this->db->query($query);
                        list($transactiondate) = $result->fetch();
                        
                        // If we got the transaction date, then lets use it to calculate the new Next Due Date
                        if($transactiondate != ''){
                            $period = explode(' ', $_POST['period3']);
                            while ($nextDueDate <= $transactiondate) {
                                $nextDueDate = strtotime("+{$period[0]} {$cycles[$period[1]]}", $nextDueDate);
                            }
                        }else{
                            $today = mktime(0, 0, 0, date("m") , date("d"), date("Y"));
                            // It should be $_POST['subscr_date'] instead of today:
                            //     $today = strtotime($_POST['subscr_date']);
                            // when $_POST['txn_type'] is 'subscr_cancel', $_POST['subscr_date'] contains the cancellation date
                            // Time/Date stamp generated by PayPal , in the following format: HH:MM:SS DD Mmm YY, YYYY PST
                            //                                                                22:21:09 Oct 20, 2009 PDT
                            // However, I am not believing in this param now, because seems that the documentacion says
                            // one thing different than what I am really getting in this parameter.
                            // (My log is getting the subscription date in this date, instead of the cancelation date,
                            // so better ovoid using it.)
                            
                            if ($nextDueDate <= $today) {
                                $period = explode(' ', $_POST['period3']);
                                while ($nextDueDate < $today) {
                                    $nextDueDate = strtotime("+{$period[0]} {$cycles[$period[1]]}", $nextDueDate);
                                }
                                
                                // If nextduedate is today then we must check to see if the payment has already gone through
                                if ($nextDueDate == $today) {
                                    // Cases 2 & 3:
                                    //    if case 2 then do nothing.
                                    //    if case 3 we must push the date forward one cycle.
                                    
                                    // Check for case 3 by seeing if there is a subscription payment for the proper amount already paid today.
                                    $query = "SELECT customerid, amount FROM invoice WHERE id=?";
                                    $result = $this->db->query($query, $tInvoiceID);
                                    list ($customerId, $amount) = $result->fetch();
                                    // If there is already an invoice marked paid today for that customer and the same invoice amount
                                    // then update the nextduedate one cycle in the future.
                                    $query = "SELECT id FROM invoice WHERE amount<=? AND amount>=? AND UNIX_TIMESTAMP(billdate)=? AND customerid = ?";
                                    $result = $this->db->query($query, $amount + 0.0001, $amount - 0.0001, $today, $customerId);
                                    
                                    if ($result->getNumRows() == 1) {
                                        // Case 3
                                        $nextDueDate = strtotime("+{$period[0]} {$cycles[$period[1]]}", $nextDueDate);
                                    }
                                } else {
                                    // Cases 1 & 4:   if case 1 then do nothing.  if case 4 we must roll back the date one cycle.
                                    // Check for case 4 by seeing if there is a paid invoice one cycle in the past.
                                    $lastDueDate = strtotime("-{$period[0]} {$cycles[$period[1]]}", $nextDueDate);
                                    
                                    $query = "SELECT customerid, amount FROM invoice WHERE id=?";
                                    $result = $this->db->query($query, $tInvoiceID);
                                    list ($customerId, $amount) = $result->fetch();
                                    // If there is not already an invoice marked paid on the last due date for that customer and the same invoice amount
                                    // then update the nextduedate one cycle in the past.
                                    $query = "SELECT id FROM invoice WHERE amount<=? AND amount>=? AND UNIX_TIMESTAMP(billdate)=? AND customerid = ?";
                                    $result = $this->db->query($query, $amount + 0.0001, $amount - 0.0001, $lastDueDate, $customerId);
                                    if ($result->getNumRows() == 0) {
                                        // Case 4
                                        $nextDueDate = $lastDueDate;
                                    }
                                }
                            }
                        }
                        $transaction = "Paypal subscription cancelled by customer. Original Signup Invoice: $tInvoiceID";
                        $cPlugin->resetRecurring($transaction, $_POST['subscr_id'], $nextDueDate);
                        break;
                    case 'subscr_failed':
                        // this is caused by lack of funds for example
                        $subject = 'Gateway recurring payment failed';
                        $reason = isset($_POST['pending_reason'])? $_POST['pending_reason'] : 'unknown';
                        $message = "Recurring payment for invoice $tInvoiceID, corresponding to package \"{$_POST['item_name']}\" has failed.\n";
                        $message .= "Reason: $reason.";
                        $tUser = new User($cPlugin->m_Invoice->m_UserID);
                        // if createTicket returns false it's because this transaction has already been done
                        if (!$cPlugin->createTicket($_POST['subscr_id'], $subject, $message, $tUser)) exit;
                        // log failed transaction
                        $transaction = "Recurring subscription payment failed. Reason: $reason.";
                        $cPlugin->logFailedSubscriptionPayment($transaction, $_POST['subscr_id']);
                        break;
                }
            } else {
                // Add Code for Normal Payment
                switch($ppPayStatus) {
                    case "Completed":
                        $transaction = "Paypal payment of $ppPayAmount was accepted. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                        $cPlugin->PaymentAccepted($ppPayAmount,$transaction,$ppTransID, $testing);
                    break;
                    case "Pending":
                        $transaction = "Paypal payment of $ppPayAmount was marked 'pending' by Paypal. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                        $cPlugin->PaymentPending($transaction,$ppTransID);
                    break;
                    case "Failed":
                        $transaction = "Paypal payment of $ppPayAmount was rejected. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                        $cPlugin->PaymentRejected($transaction);
                    break;
                }
            }
        } elseif (strcmp ($res, "INVALID") == 0) {
            $this->logger->log(2, "Callback verification returned 'INVALID'");
            $transaction = "Paypal IPN returned INVALID to Confirmation.";
            $cPlugin->PaymentRejected($transaction);
        }else {
        	$this->logger->log(1, "Callback not returning verification code of 'VERIFIED' or 'INVALID'");
        }
    }
    
    function _requestConfirmation($paypal_url, $request, $testing)
    {
        if ($testing) {
            return 'VERIFIED';
        } else {
            // no credit card info transmitted, so no need to use SSL

            $urlParsed = parse_url($paypal_url);
            if ($urlParsed['scheme'] == 'https')
            {
                $host = 'ssl://' . $urlParsed['host'];
                $port =  443;
            }
            else
            {
                $host = $urlParsed['host'];
                $port = 80;
            }

            $this->logger->log(4,"fsockopen host:".$host." port:".$port);
            $fp = fsockopen ($host, $port, $errno, $errstr, 30);
            
            if (!$fp) {
                echo "$errstr ($errno)\n";
                exit;
            }
            fputs($fp, $request);
            
            $res = '';
            $got = '';
            $this->logger->log(4,"Parsing confirmation handshake result");
            while (!feof($fp)) {
                $res = fgets($fp, 1024);
                $got.=$res;
                $this->logger->log(4,"Looping:".$res);
            }

            //commented out below because it was giving errors
            //but I'm not sure if we need it or not
            //while (!preg_match('/\\r\\n\\r\\n$/', $got));
            
            // Parse the headers
            $newurl = $this->_checkRedirect($got);
            if($newurl){
                $this->log(4,"Paypal sent header information to redirect where to get verification");
                $res = $this->_requestConfirmation($newurl, $request, $testing);
            }
            fclose($fp);
            return $res;
        }
    }

    //return false if we do not need to do anything
    //return url of redirect if we do
    function _checkRedirect($responseHeader){
        $newurl="";
        // Break up the headers
        $headers = explode("\r\n", $responseHeader);
        //get status
        // Get resposne status
        if(!mb_eregi($match = "^http/[0-9]+\\.[0-9]+[ \t]+([0-9]+)[ \t]*(.*)\$", $headers[0], $matches))
        {
            $this->logger->log(1,'Unexpected HTTP response status');
            return FALSE;
        }
        $status = $matches[1];
        array_shift($headers);
        if($status!="301" && $status!="302"){
            return false;
        }
        // Prepare all the other headers
        foreach ($headers as $header)
        {
            if($header!=""){
                $name  = strtok($header, ':');
                $value = trim(rtrim(strtok("\r\n")));
                if(strcmp(strtolower($name),"location")==0){
                    $newurl = $value;
                    $this->logger->log(4,"Paypal callback response header requesting ".$status." redirect to url:".$newurl);
                }
            }
        }
        return $newurl;
    }


}

?>
