Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Payment/PayPal.php
<?php

namespace XF\Payment;

use
XF\Entity\PurchaseRequest;
use
XF\Mvc\Controller;
use
XF\Purchasable\Purchase;
use
XF\Util\Arr;

use function
count;

class
PayPal extends AbstractProvider
{
    public function
getTitle()
    {
        return
'PayPal';
    }

    public function
getApiEndpoint()
    {
        return
$this->getPayPalUrlPrefix() . 'cgi-bin/webscr';
    }

    public function
getPayPalUrlPrefix(): string
   
{
        if (\
XF::config('enableLivePayments'))
        {
            return
'https://www.paypal.com/';
        }
        else
        {
            return
'https://www.sandbox.paypal.com/';
        }
    }

    public function
verifyConfig(array &$options, &$errors = [])
    {
        if (empty(
$options['primary_account']))
        {
           
$errors[] = \XF::phrase('you_must_provide_primary_paypal_account_email_to_set_up_this_payment');
            return
false;
        }

       
$emails = [];
        if (!empty(
$options['alternate_accounts']))
        {
           
$emails = Arr::stringToArray($options['alternate_accounts'], '#\r?\n#');
        }
       
$emails[] = $options['primary_account'];

       
$validator = \XF::app()->validator('Email');
        foreach (
$emails AS $email)
        {
            if (!
$validator->isValid($email))
            {
               
$errors[] = \XF::phrase('value_x_is_not_valid_email_address', ['email' => $email]);
            }
        }

        if (
$errors)
        {
            return
false;
        }

        return
true;
    }

    protected function
getPaymentParams(PurchaseRequest $purchaseRequest, Purchase $purchase)
    {
       
$paymentProfile = $purchase->paymentProfile;
       
$purchaser = $purchase->purchaser;

       
$params = [
           
'business' => $paymentProfile->options['primary_account'],
           
'currency_code' => $purchase->currency,
           
'item_name' => $purchase->title,
           
'quantity' => 1,
           
'no_note' => 1,

           
// 2 = required, 1 = not required, 0 = optional
           
'no_shipping' => !empty($paymentProfile->options['require_address']) ? 2 : 1,

           
'custom' => $purchaseRequest->request_key,

           
'charset' => 'utf8',
           
'email' => $purchaser->email,

           
'return' => $purchase->returnUrl,
           
'cancel_return' => $purchase->cancelUrl,
           
'notify_url' => $this->getCallbackUrl()
        ];
        if (
$purchase->recurring)
        {
           
$params['cmd'] = '_xclick-subscriptions';
           
$params['a3'] = $purchase->cost;
           
$params['p3'] = $purchase->lengthAmount;

            switch (
$purchase->lengthUnit)
            {
                case
'day': $params['t3'] = 'D'; break;
                case
'week': $params['t3'] = 'W'; break;
                case
'month': $params['t3'] = 'M'; break;
                case
'year': $params['t3'] = 'Y'; break;
                default:
$params['t3'] = ''; break;
            }

           
$params['src'] = 1;
           
$params['sra'] = 1;
        }
        else
        {
           
$params['cmd'] = '_xclick';
           
$params['amount'] = $purchase->cost;
        }

        return
$params;
    }

    public function
initiatePayment(Controller $controller, PurchaseRequest $purchaseRequest, Purchase $purchase)
    {
       
$params = $this->getPaymentParams($purchaseRequest, $purchase);

       
$endpointUrl = $this->getApiEndpoint();
       
$endpointUrl .= '?' . http_build_query($params);
        return
$controller->redirect($endpointUrl, '');
    }

    public function
renderCancellationTemplate(PurchaseRequest $purchaseRequest)
    {
       
$data = [
           
'purchaseRequest' => $purchaseRequest,
           
'endpoint' => $this->getPayPalUrlPrefix()
        ];
        return \
XF::app()->templater()->renderTemplate('public:payment_cancel_recurring_paypal', $data);
    }

   
/**
     * @param \XF\Http\Request $request
     *
     * @return CallbackState
     */
   
public function setupCallback(\XF\Http\Request $request)
    {
       
$state = new CallbackState();

       
$state->business = $request->filter('business', 'str');
       
$state->receiverEmail = $request->filter('receiver_email', 'str');
       
$state->payerEmail = $request->filter('payer_email', 'str');
       
$state->transactionType = $request->filter('txn_type', 'str');
       
$state->parentTransactionId = $request->filter('parent_txn_id', 'str');
       
$state->transactionId = $request->filter('txn_id', 'str');
       
$state->subscriberId = $request->filter('subscr_id', 'str');
       
$state->paymentCountry = $request->filter('residence_country', 'str');
       
$state->costAmount = $request->filter('mc_gross', 'unum');
       
$state->taxAmount = $request->filter('tax', 'unum');
       
$state->costCurrency = $request->filter('mc_currency', 'str');
       
$state->paymentStatus = $request->filter('payment_status', 'str');

       
$state->requestKey = $request->filter('custom', 'str');
       
$state->testMode = $request->filter('test_ipn', 'bool');

        if (\
XF::config('enableLivePayments'))
        {
           
// explicitly disable test mode if live payments are enabled
           
$state->testMode = false;
        }

       
$state->ip = $request->getIp();
       
$state->_POST = $_POST;

        return
$state;
    }

    public function
validateCallback(CallbackState $state)
    {
        try
        {
           
$params = ['form_params' => $state->_POST + ['cmd' => '_notify-validate']];
           
$client = \XF::app()->http()->client();
            if (
$state->testMode)
            {
               
$url = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
            }
            else
            {
               
$url = 'https://ipnpb.paypal.com/cgi-bin/webscr';
            }
           
$response = $client->post($url, $params);
            if (!
$response || $response->getBody()->getContents() != 'VERIFIED' || $response->getStatusCode() != 200)
            {
               
$host = \XF\Util\Ip::getHost($state->ip);
                if (
preg_match('#(^|\.)paypal.com$#i', $host))
                {
                   
$state->logType = 'error';
                   
$state->logMessage = 'Request not validated';
                }
                else
                {
                   
$state->logType = false;
                   
$state->logMessage = 'Request not validated (from unknown source)';
                }
                return
false;
            }
        }
        catch (\
GuzzleHttp\Exception\RequestException $e)
        {
           
$state->logType = 'error';
           
$state->logMessage = 'Connection to PayPal failed: ' . $e->getMessage();
            return
false;
        }

        return
true;
    }

    public function
validateTransaction(CallbackState $state)
    {
        if (!
$state->requestKey)
        {
           
$state->logType = 'info';
           
$state->logMessage = 'No purchase request key. Unrelated payment, no action to take.';
            return
false;
        }

        if (
$state->legacy)
        {
           
// The custom data in legacy calls is <user_id>,<user_upgrade_id>,<validation_type>,<validation>.
            // We only need the user_id and user_upgrade_id but we can at least verify it's a familiar format.
           
if (!preg_match(
               
'/^(?P<user_id>\d+),(?P<user_upgrade_id>\d+),token,.*$/',
               
$state->requestKey,
               
$itemParts)
                &&
count($itemParts) !== 3 // full match + 2 groups
           
)
            {
               
$state->logType = 'info';
               
$state->logMessage = 'Invalid custom field. Unrelated payment, no action to take.';
                return
false;
            }

           
$user = \XF::em()->find('XF:User', $itemParts['user_id']);
            if (!
$user)
            {
               
$state->logType = 'error';
               
$state->logMessage = 'Could not find user with user_id ' . $itemParts['user_id'] . '.';
                return
false;
            }
           
$state->purchaser = $user;

           
$state->userUpgrade = \XF::em()->find('XF:UserUpgrade', $itemParts['user_upgrade_id']);
            if (!
$state->userUpgrade)
            {
               
$state->logType = 'error';
               
$state->logMessage = 'Could not find user upgrade with user_upgrade_id ' . $itemParts['user_upgrade_id'] . '.';
                return
false;
            }
        }
        else
        {
            if (!
$state->getPurchaseRequest())
            {
               
$state->logType = 'info';
               
$state->logMessage = 'Invalid request key. Unrelated payment, no action to take.';
                return
false;
            }
        }

        if (!
$state->transactionId && !$state->subscriberId)
        {
           
$state->logType = 'info';
           
$state->logMessage = 'No transaction or subscriber ID. No action to take.';
            return
false;
        }

       
$paymentRepo = \XF::repository('XF:Payment');
       
$matchingLogsFinder = $paymentRepo->findLogsByTransactionIdForProvider($state->transactionId, $this->providerId);
        if (
$matchingLogsFinder->total())
        {
           
$logs = $matchingLogsFinder->fetch();
            foreach (
$logs AS $log)
            {
                if (
$log->log_type == 'cancel' && $state->paymentStatus == 'Canceled_Reversal')
                {
                   
// This is a cancelled transaction we've already seen, but has now been reversed.
                    // Let it go through.
                   
return true;
                }
            }

           
$state->logType = 'info';
           
$state->logMessage = 'Transaction already processed. Skipping.';
            return
false;
        }

        return
true;
    }

    public function
validatePurchaseRequest(CallbackState $state)
    {
       
// validated in validateTransaction
       
return true;
    }

    public function
validatePurchasableHandler(CallbackState $state)
    {
        if (
$state->legacy)
        {
           
$purchasable = \XF::em()->find('XF:Purchasable', 'user_upgrade');
           
$state->purchasableHandler = $purchasable->handler;

           
// For legacy payments, all we can do is get the user upgrade handler. We don't have
            // a purchase request or anything so no other validation can be done.
           
return true;
        }
        return
parent::validatePurchasableHandler($state);
    }

    public function
validatePaymentProfile(CallbackState $state)
    {
        if (
$state->legacy)
        {
           
$finder = \XF::finder('XF:PaymentProfile')
                ->
where('provider_id', 'paypal');
            foreach (
$finder->fetch() AS $profile)
            {
                if (!empty(
$profile->options['legacy']))
                {
                   
$state->paymentProfile = $profile;
                    break;
                }
            }
        }
        return
parent::validatePaymentProfile($state);
    }

    public function
validatePurchaser(CallbackState $state)
    {
        if (
$state->legacy)
        {
           
// validated in validateTransaction
           
return true;
        }
        else
        {
            return
parent::validatePurchaser($state);
        }
    }

    public function
validatePurchasableData(CallbackState $state)
    {
       
$paymentProfile = $state->getPaymentProfile();

       
$business = strtolower($state->business);
       
$receiverEmail = strtolower($state->receiverEmail);
       
$payerEmail = strtolower($state->payerEmail);

       
$options = $paymentProfile->options;
       
$accounts = Arr::stringToArray($options['alternate_accounts'], '#\r?\n#');
       
$accounts[] = $options['primary_account'];

       
$matched = false;
        foreach (
$accounts AS $account)
        {
           
$account = trim(strtolower($account));
            if (!
$account)
            {
                continue;
            }

            if (
$business == $account || $receiverEmail == $account)
            {
               
$matched = true;
                break;
            }

            if (
$state->transactionType == 'adjustment' && $payerEmail == $account)
            {
               
$matched = true;
                break;
            }
        }
        if (!
$matched)
        {
           
$state->logType = 'error';
           
$state->logMessage = 'Invalid business or receiver_email.';
            return
false;
        }

        return
true;
    }

    public function
validateCost(CallbackState $state)
    {
        if (
$state->legacy)
        {
           
$upgrade = $state->userUpgrade;

           
$upgradeRecord = \XF::em()->findOne('XF:UserUpgradeActive', [
               
'user_upgrade_id' => $upgrade->user_upgrade_id,
               
'user_id' => $state->purchaser->user_id
           
]);

            if (!
$upgradeRecord && $state->subscriberId)
            {
               
$logFinder = \XF::finder('XF:PaymentProviderLog')
                    ->
where('subscriber_id', $state->subscriberId)
                    ->
order('log_date', 'DESC');
               
                foreach (
$logFinder->fetch() AS $log)
                {
                    if (
is_numeric($log->purchase_request_key))
                    {
                       
$upgradeRecord = \XF::em()->find('XF:UserUpgradeExpired', $log->purchase_request_key);
                        if (
$upgradeRecord)
                        {
                           
$state->userUpgradeRecordId = $upgradeRecord->user_upgrade_record_id;
                            break;
                        }
                    }
                }
            }

            if (!
$upgradeRecord && $state->parentTransactionId)
            {
               
$logFinder = \XF::finder('XF:PaymentProviderLog')
                    ->
where('transaction_id', $state->parentTransactionId)
                    ->
order('log_date', 'DESC');

                foreach (
$logFinder->fetch() AS $log)
                {
                    if (
is_numeric($log->purchase_request_key))
                    {
                       
$upgradeRecord = \XF::em()->find('XF:UserUpgradeExpired', $log->purchase_request_key);
                        if (
$upgradeRecord)
                        {
                           
$state->userUpgradeRecordId = $upgradeRecord->user_upgrade_record_id;
                            break;
                        }
                    }
                }
            }

           
$cost = $upgrade->cost_amount;
           
$currency = $upgrade->cost_currency;
        }
        else
        {
           
$upgradeRecord = false;
           
$purchaseRequest = $state->getPurchaseRequest();
           
$cost = $purchaseRequest->cost_amount;
           
$currency = $purchaseRequest->cost_currency;
        }

        switch (
$state->transactionType)
        {
            case
'web_accept':
            case
'subscr_payment':
               
$costValidated = (
                   
round(($state->costAmount - $state->taxAmount), 2) == round($cost, 2)
                    &&
$state->costCurrency == $currency
               
);

                if (
$state->legacy && !$costValidated && $upgradeRecord && $upgradeRecord->extra)
                {
                   
$cost = $upgradeRecord->extra['cost_amount'];
                   
$currency = $upgradeRecord->extra['cost_currency'];

                   
$costValidated = (
                       
round(($state->costAmount - $state->taxAmount), 2) == round($cost, 2)
                        &&
$state->costCurrency == strtoupper($currency)
                    );
                    if (
$costValidated)
                    {
                       
// the upgrade's cost has changed, but we need to continue as if it hasn't
                       
$state->extraData = [
                           
'cost_amount' => round($state->costAmount, 2),
                           
'cost_currency' => $state->costCurrency
                       
];
                    }
                }

                if (!
$costValidated)
                {
                   
$state->logType = 'error';
                   
$state->logMessage = 'Invalid cost amount. PayPal may be erroneously adding additional shipping and handling charges';
                    return
false;
                }
        }
        return
true;
    }

    public function
getPaymentResult(CallbackState $state)
    {
        switch (
$state->transactionType)
        {
            case
'web_accept':
            case
'subscr_payment':
                if (
$state->paymentStatus == 'Completed')
                {
                   
$state->paymentResult = CallbackState::PAYMENT_RECEIVED;
                }
                break;

            case
'adjustment':
                if (
$state->paymentStatus == 'Completed')
                {
                   
$state->paymentResult = CallbackState::PAYMENT_REVERSED;
                }
                break;
        }

        if (
$state->paymentStatus == 'Refunded' || $state->paymentStatus == 'Reversed')
        {
           
$state->paymentResult = CallbackState::PAYMENT_REVERSED;
        }
        else if (
$state->paymentStatus == 'Canceled_Reversal')
        {
           
$state->paymentResult = CallbackState::PAYMENT_REINSTATED;
        }
    }

    public function
prepareLogData(CallbackState $state)
    {
       
$state->logDetails = $state->_POST;
    }

    protected function
getSupportedRecurrenceRanges()
    {
        return [
           
'day' => [1, 90],
           
'week' => [1, 52],
           
'month' => [1, 24],
           
'year' => [1, 5]
        ];
    }
}