<?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(4, 'Paypal callback invoked');
        
        $logOK = $this->_logPaypalCallback();
        
        if ($this->settings->get('plugin_paypal_Use PayPal Sandbox') == '0'
            && isset($_POST['test_ipn']) && $_POST['test_ipn'] == '1')
        {
            $this->logger->log(4, "** Paypal sandbox callback but account is in production mode => callback discarded");
            return;
        }

        if (!isset($_POST['txn_type'])) {
            $this->logger->log(4, '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'];

        $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)
        
        $customValues = explode("_", $_POST["custom"]);
        $tInvoiceID         = $customValues[0];
        $tIsRecurring       = $customValues[1];
        $tGenerateInvoice   = $customValues[2];
        $tRecurringExclude  = '';
        if(isset($customValues[3])){
            $tRecurringExclude = $customValues[3];
        }
        
        $this->logger->log(4, "\$ppTransType: $ppTransType; \$ppTransID: $ppTransID; \$ppPayStatus: $ppPayStatus;");
        $this->logger->log(4, "\$ppPayAmount: $ppPayAmount; \$tInvoiceID: $tInvoiceID; \$tIsRecurring: $tIsRecurring; \$tGenerateInvoice: $tGenerateInvoice \$tRecurringExclude: $tRecurringExclude");
        
        if(!$logOK){
            return;
        }
        
        // Create Plugin class object to interact with CE.
        $cPlugin = new Plugin($tInvoiceID, 'paypal', $this->user);

        // Comfirm the callback before assuming anything
        $this->logger->log(4, "Requesting callback confirmation; sending request: $paypal_url?$req");
        try{
            $res = $this->_requestConfirmation($paypal_url, $header.$req, $testing);
        }catch (Exception $e){
            $customerid = $cPlugin->m_Invoice->getUserID();
            
            require_once 'modules/admin/models/Error_EventLog.php';
            $errorLog = Error_EventLog::newInstance(
                (isset($customerid))? $customerid : false,
                $tInvoiceID,
                ERROR_EVENTLOG_PAYPAL_REQUEST_CONFIRMATION,
                NE_EVENTLOG_USER_SYSTEM,
                serialize($this->_utf8EncodePaypalCallback($_POST))
            );
            $this->logger->addEventLog($errorLog);
            exit;
        }
        $this->log(2,"Request Confirmation Returned: ".$res);
        if (strcmp ($res, "VERIFIED") == 0) {
            $this->logger->log(4, "Callback has been verified successfully");
        }elseif (strcmp ($res, "INVALID") == 0) {
            $this->logger->log(4, "Callback verification returned 'INVALID'");
            $transaction = "Paypal IPN returned INVALID to Confirmation.";
            $cPlugin->PaymentRejected($transaction);
            return;
        }else {
            $this->logger->log(1, "Callback not returning verification code of 'VERIFIED' or 'INVALID'. Original Paypal callback details: ".print_r($_POST, true));
            return;
        }
        // Comfirm the callback before assuming anything

        if ($ppTransType == 'subscr_signup') {  // Subscription started
            // 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.
            
            //Lets mark the recurring fees as active subscription.
            $cPlugin->setRecurring($tRecurringExclude);
            
            $cPlugin->setSubscriptionId($_POST['subscr_id'], $tRecurringExclude);
            
            $transaction = "Started paypal subscription. Subscription ID: ".$_POST['subscr_id'];
            $cPlugin->logSubscriptionStarted($transaction, $_POST['subscr_id']);
            
            $this->logger->log(4, 'Paypal subscr_signup callback ignored');
            return;
        }

        $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') {
            $cPlugin->setSubscriptionId($_POST['subscr_id'], $tRecurringExclude);
            
            $this->logger->log(4, '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(4, 'Invoice already exists, and is marked as pending');
                    }
                } else {
                    $this->logger->log(4, 'Generating new invoice');
                    $newInvoice = $cPlugin->GenerateNewInvoice($ppPayAmount, '', $ppTransID, $tRecurringExclude);
                    $this->logger->log(4, 'New invoice generated');
                    
                    if($newInvoice === false){
                        $customerid = $cPlugin->m_Invoice->getUserID();
                        
                        require_once 'modules/admin/models/Error_EventLog.php';
                        $errorLog = Error_EventLog::newInstance(
                            (isset($customerid))? $customerid : false,
                            $tInvoiceID,
                            ERROR_EVENTLOG_PAYPAL_CALLBACK,
                            NE_EVENTLOG_USER_SYSTEM,
                            serialize($this->_utf8EncodePaypalCallback($_POST))
                        );
                        $this->logger->addEventLog($errorLog);
                        exit;
                    }else{
                        $cPlugin->updateDateRecurringEntry();
                    }
                }
            } else {
                $this->logger->log(1, 'Error: exiting Paypal callback invocation');
                exit;
            }
        }
        
        //Add plugin details
        $cPlugin->setAmount($ppPayAmount);
        $cPlugin->m_TransactionID = $ppTransID;
        $cPlugin->m_Action = "charge";
        $cPlugin->m_Last4 = "NA";
        
        // Manage the payment
        // 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':  // Subscription payment received
                    switch($ppPayStatus) {
                        case "Completed": // The payment has been completed, and the funds have been added successfully to your account balance.
                            $transaction = "Paypal payment of $ppPayAmount was accepted. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                            $cPlugin->PaymentAccepted($ppPayAmount,$transaction,$ppTransID, $testing);
                        break;
                        case "Pending": // The payment is pending. See pending_reason for more information.
                            $transaction = "Paypal payment of $ppPayAmount was marked 'pending' by Paypal. Reason: ".$_POST['pending_reason'].". Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                            $cPlugin->PaymentPending($transaction,$ppTransID);
                        break;
                        case "Failed":  // The payment has failed. This happens only if the payment was made from your customer’s bank account.
                            $transaction = "Paypal payment of $ppPayAmount was rejected. Original Signup Invoice: $tInvoiceID (OrderID:".$ppTransID.")";
                            $cPlugin->PaymentRejected($transaction);
                        break;
                    }
                    break;
                case 'subscr_eot':    // Subscription expired
                    $transaction = "Paypal subscription has expired. Subscription ID: ".$_POST['subscr_id'];
                    $cPlugin->logSubscriptionExpired($transaction, $_POST['subscr_id']);
                    break;
                case 'subscr_cancel': // Subscription canceled
                    $this->logger->log(4, "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(4, '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, $tRecurringExclude);
                    break;
                case 'subscr_failed': // Subscription signup 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;
            }
        }
    }
    
    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;
    }

    //return true if can add the event log
    //return false if can not add the event log
    function _logPaypalCallback(){
        if(!isset($_POST["custom"])){
            if(!isset($_POST["txn_type"]) || $_POST["txn_type"] != "new_case"){
                require_once 'modules/admin/models/Error_EventLog.php';
                $errorLog = Error_EventLog::newInstance(
                    0,
                    0,
                    ERROR_EVENTLOG_PAYPAL_CALLBACK,
                    NE_EVENTLOG_USER_SYSTEM,
                    serialize($this->_utf8EncodePaypalCallback($_POST))
                );
                $this->logger->addEventLog($errorLog);
            }
            
            return false;
        }
        
        // search the customer id based on the invoice id
        $customValues = explode("_", $_POST["custom"]);
        $tInvoiceID         = $customValues[0];
        
        $query = "SELECT `customerid` "
                ."FROM `invoice` "
                ."WHERE `id` = ? ";
        $result = $this->db->query($query, $tInvoiceID);
        list($customerid) = $result->fetch();
        
        $invoiceNotFound = false;
        
        if(!isset($customerid)){
            $invoiceNotFound = true;
            
            // search the customer id based on the email address
            $query = "SELECT `id` "
                    ."FROM `users` "
                    ."WHERE `email` = ? ";
            $result = $this->db->query($query, $_POST["payer_email"]);
            list($customerid) = $result->fetch();
        }
        
        if(!isset($customerid) || $invoiceNotFound){
            require_once 'modules/admin/models/Error_EventLog.php';
            $errorLog = Error_EventLog::newInstance(
                (isset($customerid))? $customerid : 0,
                $tInvoiceID,
                ERROR_EVENTLOG_PAYPAL_CALLBACK,
                NE_EVENTLOG_USER_SYSTEM,
                serialize($this->_utf8EncodePaypalCallback($_POST))
            );
            $this->logger->addEventLog($errorLog);
            return false;
        }else{
            require_once 'modules/billing/models/Invoice_EventLog.php';
            $invoiceLog = Invoice_EventLog::newInstance(
                $customerid,
                $tInvoiceID,
                INVOICE_EVENTLOG_PAYPAL_CALLBACK,
                NE_EVENTLOG_USER_SYSTEM,
                serialize($this->_utf8EncodePaypalCallback($_POST))
            );
            $this->logger->addEventLog($invoiceLog);
            return true;
        }
    }

    //return array with the utf8 encoded values of the original array
    function _utf8EncodePaypalCallback($callbackPost){
        if(is_array($callbackPost)){
            foreach($callbackPost as $postKey => $postValue){
                $callbackPost[$postKey] = utf8_encode($postValue);
            }
        }else{
            $callbackPost = array();
        }
        
        return $callbackPost;
    }
}

?>
