<?php
/**
 *                       ######
 *                       ######
 * ############    ####( ######  #####. ######  ############   ############
 * #############  #####( ######  #####. ######  #############  #############
 *        ######  #####( ######  #####. ######  #####  ######  #####  ######
 * ###### ######  #####( ######  #####. ######  #####  #####   #####  ######
 * ###### ######  #####( ######  #####. ######  #####          #####  ######
 * #############  #############  #############  #############  #####  ######
 *  ############   ############  #############   ############  #####  ######
 *                                      ######
 *                               #############
 *                               ############
 *
 * Adyen Payment module (https://www.adyen.com/)
 *
 * Copyright (c) 2015 Adyen BV (https://www.adyen.com/)
 * See LICENSE.txt for license details.
 *
 * Author: Adyen <magento@adyen.com>
 */

namespace Adyen\Payment\Controller\Process;

use Adyen\Util\HmacSignature;
use Symfony\Component\Config\Definition\Exception\Exception;
use Magento\Framework\App\Request\Http as Http;

/**
 * Class Json extends Action
 */
class Json extends \Magento\Framework\App\Action\Action
{
    /**
     * @var \Magento\Framework\ObjectManagerInterface
     */
    protected $_objectManager;

    /**
     * @var \Magento\Framework\Controller\Result\RawFactory
     */
    protected $_resultFactory;

    /**
     * @var \Adyen\Payment\Helper\Data
     */
    protected $_adyenHelper;

    /**
     * @var \Adyen\Payment\Logger\AdyenLogger
     */
    protected $_adyenLogger;

    /**
     * @var \Magento\Framework\Serialize\SerializerInterface
     */
    private $serializer;

    /**
     * @var \Adyen\Payment\Helper\Config
     */
    protected $configHelper;

    /**
     * @var \Adyen\Payment\Helper\IpAddress
     */
    protected $ipAddressHelper;

    /**
     * @var HmacSignature
     */
    private $hmacSignature;

    /**
     * Json constructor.
     *
     * @param \Magento\Framework\App\Action\Context $context
     * @param \Adyen\Payment\Helper\Data $adyenHelper
     * @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger
     * @param \Magento\Framework\Serialize\SerializerInterface $serializer
     * @param \Adyen\Payment\Helper\Config $configHelper
     * @param \Adyen\Payment\Helper\IpAddress $ipAddressHelper
     * @param HmacSignature $hmacSignature
     */
    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Adyen\Payment\Helper\Data $adyenHelper,
        \Adyen\Payment\Logger\AdyenLogger $adyenLogger,
        \Magento\Framework\Serialize\SerializerInterface $serializer,
        \Adyen\Payment\Helper\Config $configHelper,
        \Adyen\Payment\Helper\IpAddress $ipAddressHelper,
        HmacSignature $hmacSignature
    ) {
        parent::__construct($context);
        $this->_objectManager = $context->getObjectManager();
        $this->_resultFactory = $context->getResultFactory();
        $this->_adyenHelper = $adyenHelper;
        $this->_adyenLogger = $adyenLogger;
        $this->serializer = $serializer;
        $this->configHelper = $configHelper;
        $this->ipAddressHelper = $ipAddressHelper;
        $this->hmacSignature = $hmacSignature;

        // Fix for Magento2.3 adding isAjax to the request params
        if (interface_exists(\Magento\Framework\App\CsrfAwareActionInterface::class)) {
            $request = $this->getRequest();
            if ($request instanceof Http && $request->isPost()) {
                $request->setParam('isAjax', true);
                $request->getHeaders()->addHeaderLine('X_REQUESTED_WITH', 'XMLHttpRequest');
            }
        }
    }

    /**
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function execute()
    {
        // if version is in the notification string show the module version
        $response = $this->getRequest()->getParams();
        if (isset($response['version'])) {
            $this->getResponse()
                ->clearHeader('Content-Type')
                ->setHeader('Content-Type', 'text/html')
                ->setBody($this->_adyenHelper->getModuleVersion());

            return;
        }

        try {
            $notificationItems = json_decode(file_get_contents('php://input'), true);

            $notificationMode = isset($notificationItems['live']) ? $notificationItems['live'] : "";

            if ($notificationMode !== "" && $this->_validateNotificationMode($notificationMode)) {
                foreach ($notificationItems['notificationItems'] as $notificationItem) {
                    $status = $this->_processNotification(
                        $notificationItem['NotificationRequestItem'],
                        $notificationMode
                    );

                    if ($status != true) {
                        $this->_return401();
                        return;
                    }

                    $acceptedMessage = "[accepted]";
                }
                $cronCheckTest = $notificationItems['notificationItems'][0]['NotificationRequestItem']['pspReference'];

                // Run the query for checking unprocessed notifications, do this only for test notifications coming
                // from the Adyen Customer Area
                if ($this->_isTestNotification($cronCheckTest)) {
                    $unprocessedNotifications = $this->_adyenHelper->getUnprocessedNotifications();
                    if ($unprocessedNotifications > 0) {
                        $acceptedMessage .= "\nYou have " . $unprocessedNotifications . " unprocessed notifications.";
                    }
                }

                $this->_adyenLogger->addAdyenNotification("The result is accepted");

                $this->getResponse()
                    ->clearHeader('Content-Type')
                    ->setHeader('Content-Type', 'text/html')
                    ->setBody($acceptedMessage);
                return;
            } else {
                if ($notificationMode == "") {
                    $this->_return401();
                    return;
                }
                throw new \Magento\Framework\Exception\LocalizedException(
                    __('Mismatch between Live/Test modes of Magento store and the Adyen platform')
                );
            }
        } catch (Exception $e) {
            throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()));
        }
    }

    /**
     * @param $notificationMode
     * @return bool
     */
    protected function _validateNotificationMode($notificationMode)
    {
        $mode = $this->_adyenHelper->getAdyenAbstractConfigData('demo_mode');

        // Notification mode can be a string or a boolean
        if (($mode == '1' && ($notificationMode == "false" || !$notificationMode))
            || ($mode == '0' && ($notificationMode == 'true' || $notificationMode))) {
            return true;
        }
        return false;
    }

    /**
     * save notification into the database for cronjob to execute notification
     *
     * @param $response
     * @param $notificationMode
     * @return bool
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    protected function _processNotification($response, $notificationMode)
    {
        if ($this->configHelper->getNotificationsIpHmacCheck()) {
            //Validate if the notification comes from a verified IP
            if (!$this->isIpValid()) {
                $this->_adyenLogger->addAdyenNotification(
                    "Notification has been rejected because the IP address could not be verified"
                );
                return false;
            }

            if ($this->hmacSignature->isHmacSupportedEventCode($response)) {
                //Validate the Hmac calculation
                if (!$this->hmacSignature->isValidNotificationHMAC($this->configHelper->getNotificationsHmacKey(),
                    $response)) {
                    $this->_adyenLogger->addAdyenNotification('HMAC key validation failed ' . print_r($response, 1));
                    return false;
                }
            }
        }

        // validate the notification
        if ($this->authorised($response)) {
            // log the notification
            $this->_adyenLogger->addAdyenNotification(
                "The content of the notification item is: " . print_r($response, 1)
            );

            // check if notification already exists
            if (!$this->_isDuplicate($response)) {
                try {
                    $notification = $this->_objectManager->create(\Adyen\Payment\Model\Notification::class);

                    if (isset($response['pspReference'])) {
                        $notification->setPspreference($response['pspReference']);
                    }
                    if (isset($response['originalReference'])) {
                        $notification->setOriginalReference($response['originalReference']);
                    }
                    if (isset($response['merchantReference'])) {
                        $notification->setMerchantReference($response['merchantReference']);
                    }
                    if (isset($response['eventCode'])) {
                        $notification->setEventCode($response['eventCode']);
                    }
                    if (isset($response['success'])) {
                        $notification->setSuccess($response['success']);
                    }
                    if (isset($response['paymentMethod'])) {
                        $notification->setPaymentMethod($response['paymentMethod']);
                    }
                    if (isset($response['amount'])) {
                        $notification->setAmountValue($response['amount']['value']);
                        $notification->setAmountCurrency($response['amount']['currency']);
                    }
                    if (isset($response['reason'])) {
                        $notification->setReason($response['reason']);
                    }

                    $notification->setLive($notificationMode);

                    if (isset($response['additionalData'])) {
                        $notification->setAdditionalData($this->serializer->serialize($response['additionalData']));
                    }
                    if (isset($response['done'])) {
                        $notification->setDone($response['done']);
                    }

                    // do this to set both fields in the correct timezone
                    $date = new \DateTime();
                    $notification->setCreatedAt($date);
                    $notification->setUpdatedAt($date);

                    $notification->save();

                    return true;
                } catch (Exception $e) {
                    throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()));
                }
            } else {
                // duplicated so do nothing but return accepted to Adyen
                return true;
            }
        }
        return false;
    }

    /**
     * HTTP Authentication of the notification
     *
     * @param $response
     * @return bool
     */
    protected function authorised($response)
    {
        // Add CGI support
        $this->_fixCgiHttpAuthentication();

        $internalMerchantAccount = $this->_adyenHelper->getAdyenAbstractConfigData('merchant_account');
        $username = $this->_adyenHelper->getAdyenAbstractConfigData('notification_username');
        $password = $this->_adyenHelper->getNotificationPassword();

        $submitedMerchantAccount = $response['merchantAccountCode'];

        if (empty($submitedMerchantAccount) && empty($internalMerchantAccount)) {
            if ($this->_isTestNotification($response['pspReference'])) {
                $this->_returnResult('merchantAccountCode is empty in magento settings');
            }
            return false;
        }

        // validate username and password
        if ((!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']))) {
            if ($this->_isTestNotification($response['pspReference'])) {
                $this->_returnResult(
                    'Authentication failed: PHP_AUTH_USER or PHP_AUTH_PW are empty. See Adyen Magento manual CGI mode'
                );
            }
            return false;
        }

        $usernameIsValid = hash_equals($username, $_SERVER['PHP_AUTH_USER']);
        $passwordIsValid = hash_equals($password, $_SERVER['PHP_AUTH_PW']);
        if ($usernameIsValid && $passwordIsValid) {
            return true;
        }

        // If notification is test check if fields are correct if not return error
        if ($this->_isTestNotification($response['pspReference'])) {
            $this->_returnResult(
                'username (PHP_AUTH_USER) and\or password (PHP_AUTH_PW) are not the same as Magento settings'
            );
        }
        return false;
    }

    /**
     * Checks if any of the possible remote IP address sending the notification is verified and returns the validation result
     *
     * @return bool
     */
    protected function isIpValid()
    {
        $ipAddress = [];

        //Getting remote and possibly forwarded IP addresses
        if (!empty($_SERVER['REMOTE_ADDR'])) {
            array_push($ipAddress, $_SERVER['REMOTE_ADDR']);
        }
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            array_push($ipAddress, $_SERVER['HTTP_X_FORWARDED_FOR']);
        }
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
            array_push($ipAddress, $_SERVER['HTTP_CLIENT_IP']);
        }

        return $this->ipAddressHelper->isIpAddressValid($ipAddress);
    }

    /**
     * If notification is already saved ignore it
     *
     * @param $response
     * @return mixed
     */
    protected function _isDuplicate($response)
    {
        $pspReference = trim($response['pspReference']);
        $eventCode = trim($response['eventCode']);
        $success = trim($response['success']);
        $originalReference = null;
        if (isset($response['originalReference'])) {
            $originalReference = trim($response['originalReference']);
        }
        $notification = $this->_objectManager->create(\Adyen\Payment\Model\Notification::class);
        return $notification->isDuplicate($pspReference, $eventCode, $success, $originalReference);
    }

    /**
     * Fix these global variables for the CGI
     */
    protected function _fixCgiHttpAuthentication()
    {
        // do nothing if values are already there
        if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
            return;
        } elseif (isset($_SERVER['REDIRECT_REMOTE_AUTHORIZATION']) &&
            $_SERVER['REDIRECT_REMOTE_AUTHORIZATION'] != ''
        ) {
            list(
                $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']
                ) =
                explode(':', base64_decode($_SERVER['REDIRECT_REMOTE_AUTHORIZATION']), 2);
        } elseif (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
            list(
                $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']
                ) =
                explode(':', base64_decode(substr($_SERVER['REDIRECT_HTTP_AUTHORIZATION'], 6)), 2);
        } elseif (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
            list(
                $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']
                ) =
                explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)), 2);
        } elseif (!empty($_SERVER['REMOTE_USER'])) {
            list(
                $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']
                ) =
                explode(':', base64_decode(substr($_SERVER['REMOTE_USER'], 6)), 2);
        } elseif (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
            list(
                $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']
                ) =
                explode(':', base64_decode(substr($_SERVER['REDIRECT_REMOTE_USER'], 6)), 2);
        }
    }

    /**
     * Return a 401 result
     */
    protected function _return401()
    {
        $this->getResponse()->setHttpResponseCode(401);
    }

    /**
     * If notification is a test notification from Adyen Customer Area
     *
     * @param $pspReference
     * @return bool
     */
    protected function _isTestNotification($pspReference)
    {
        if (strpos(strtolower($pspReference), "test_") !== false
            || strpos(strtolower($pspReference), "testnotification_") !== false
        ) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns the message to the browser
     *
     * @param $message
     */
    protected function _returnResult($message)
    {
        $this->getResponse()
            ->clearHeader('Content-Type')
            ->setHeader('Content-Type', 'text/html')
            ->setBody($message);
    }
}