<?php
/**
 * @package     CMGiftCard
 * @subpackage  plg_cmgiftcard_paypal
 * @copyright   Copyright (C) 2016-2024 CMExension
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

require_once JPATH_ROOT . '/administrator/components/com_cmgiftcard/helpers/plugin/cmgiftcard.php';

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;

/**
 * PayPal payment method for CMGiftCard.
 *
 * @package     CMGiftCard
 * @subpackage  plg_cmgiftcard_paypal
 * @since       1.0.0
 */
class PlgCMGiftCardPayPal extends CMGiftCardPlugin
{
	/**
	 * Constructor
	 *
	 * @param   object  &$subject  The object to observe
	 * @param   array   $config    An optional associative array of configuration settings.
	 *                             Recognized key values include 'name', 'group', 'params', 'language'
	 *                             (this list is not meant to be comprehensive).
	 *
	 * @since   1.2.0
	 */
	public function __construct(&$subject, $config = array())
	{
		$this->gatewayId = 'paypal';
		$this->gatewayName = 'PLG_CMGIFTCARD_PAYPAL_NAME';

		parent::__construct($subject, $config);
	}

	/**
	 * Method to get payment form.
	 *
	 * @param   array  $data  Billing data.
	 *
	 * @return  array
	 *
	 * @since   1.0.0
	 */
	public function onCMGCGetPaymentForm($data)
	{
		$gatewayId = isset($data['billing']['payment_method']) ? $data['billing']['payment_method'] : '';

		if ($gatewayId != $this->gatewayId)
			return false;

		$gatewayName = $this->params->get('name', Text::_($this->gatewayName));
		$merchantEmail = $this->params->get('merchant_email');
		$currencyCode = $this->params->get('currency_code', 'USD');
		$environment = $this->params->get('environment', 'live');
		$secondsToWait = (int) $this->params->get('seconds_to_wait', 5);

		$clickHereLink = '<a href="#" onClick="document.checkOutForm.submit(); return false;">' . Text::_('COM_CMGIFTCARD_CLICK_HERE') . '</a>';
		$checkoutMessage = Text::sprintf('COM_CMGIFTCARD_CHECKOUT_MESSAGE', $gatewayName, $secondsToWait, $clickHereLink);

		if ($environment == 'live')
		{
			$url = 'https://www.paypal.com/cgi-bin/webscr';
		}
		else
		{
			$url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
		}

		$itemNumber = $data['billing']['order_number'];
		$siteUrl = str_replace(Uri::root(true), '', substr(Uri::root(), 0, -1));
		$returnUrl = $siteUrl . CMGiftCardHelper::prepareRoute('index.php?option=com_cmgiftcard&view=checkout') . '?layout=success';
		$cancelUrl = $siteUrl . CMGiftCardHelper::prepareRoute('index.php?option=com_cmgiftcard&view=checkout') . '?layout=cancel';
		$notifyUrl = Uri::root() . 'index.php?option=com_cmgiftcard&task=callback&gateway=' . $this->gatewayId;

		$formData = array(
			'cmd'			=> "_cart",
			'upload'		=> 1,
			'charset'		=> "utf-8",
			'currency_code'	=> $currencyCode,
			'business'		=> $merchantEmail,
			'custom'		=> $itemNumber,
			'no_shipping'	=> 1,
			'rm'			=> 2,
			'return'		=> $returnUrl,
			'cancel_return'	=> $cancelUrl,
			'notify_url'	=> $notifyUrl,
			'first_name'	=> $data['billing']['first_name'],
			'last_name'		=> $data['billing']['last_name'],
			'address1'		=> isset($data['billing']['address_1']) ? $data['billing']['address_1'] : '',
			'address2'		=> isset($data['billing']['address_2']) ? $data['billing']['address_2'] : '',
			'city'			=> isset($data['billing']['city']) ? $data['billing']['city'] : '',
			'state'			=> isset($data['billing']['state']) ? $data['billing']['state'] : '',
			'zip'			=> isset($data['billing']['postal_code']) ? $data['billing']['postal_code'] : '',
			'email'			=> $data['billing']['email'],
		);

		foreach ($data['items'] as $n => $item)
		{
			$formData['amount_' . ($n + 1)] = $item['amount'];
			$formData['item_name_' . ($n + 1)] = $item['design_title'];
		}

		foreach ($formData as $key => $value)
		{
			$formData[$key] = htmlspecialchars($value, ENT_QUOTES);
		}

		@ob_start();
		include dirname(__FILE__) . '/form.php';
		$html = @ob_get_clean();

		return $html;
	}

	/**
	 * Method to get validate PayPal callback.
	 *
	 * @return  boolean
	 *
	 * @since   1.0.0
	 */
	public function onCMGCValidateCallback()
	{
		$jinput = Factory::getApplication()->input;
		$gatewayId = $jinput->get('gateway', '');
		$data = $jinput->post->getArray();

		$result = array(
			'validation'		=> false,
			'gateway_id'		=> '',
			'transaction_id'	=> '',
			'order_id'			=> ''
		);

		if ($gatewayId != $this->gatewayId || empty($data))
		{
			return $result;
		}

		list($isValid, $httpCode) = $this->isValidIPN($data);

		if (!$isValid && $httpCode == '200')
		{
			$reason = 'PayPal reported transaction as invalid';
		}
		elseif (!$isValid && $httpCode == '400')
		{
			// PayPal's problem, sometime it happens, consider transaction is valid.
			$isValid = true;
		}

		$db = Factory::getDbo();
		$query = $db->getQuery(true);

		if ($isValid)
		{
			if ($data['payment_status'] == 'Completed')
			{
				$orderId = (int) $data['custom'];

				$query->clear()
					->select('*')
					->from($db->qn('#__cmgiftcard_orders'))
					->where($db->qn('order_number') . ' = ' . $db->q($orderId));
				$db->setQuery($query);
				$order = $db->loadObject();

				if (!isset($order->id))
				{
					$isValid = false;
					$reason = "Order number $orderId didn't exist";
				}

				if (isset($order->order_status) && $order->order_status == 'paid')
				{
					$isValid = false;
					$reason = "Order number $orderId was already paid.";
				}

				if (isset($order->order_status) && $order->order_status == 'refunded')
				{
					$isValid = false;
					$reason = "Order number $orderId was already refunded.";
				}
			}
			else
			{
				$isValid = false;
				$reason = "Transaction was not completed";
			}
		}

		if ($isValid)
		{
			$query->clear()
				->select('*')
				->from($db->qn('#__cmgiftcard_cards'))
				->where($db->qn('order_id') . ' = ' . $db->quote($order->id));
			$db->setQuery($query);
			$cards = $db->loadObjectList();

			if (count($cards))
			{
				$amount = 0;

				foreach ($cards as $card)
				{
					$amount += $card->amount;
				}

				$mc_gross = floatval($data['mc_gross']);
				$isValid = ($amount- $mc_gross) < 0.01;

				if (!$isValid)
				{
					$reason = "Invalid paid amount, expected $amount, got $mc_gross";
				}
			}
			else
			{
				$isValid = false;
				$reason = "No gift cards found";
			}

			if ($isValid)
			{
				$merchantEmail = $this->params->get('merchant_email', '');
				$merchantEmail = strtolower($merchantEmail);
				$receiverEmail = strtolower($data['receiver_email']);

				if ($merchantEmail != $receiverEmail)
				{
					$isValid = false;
					$reason = "Invalid merchant email, expected $merchantEmail, got $receiverEmail";
				}
			}

			if ($isValid)
			{
				$currencyCode = strtoupper($this->params->get('currency_code', ''));
				$mc_currency = strtoupper($data['mc_currency']);

				if ($currencyCode != $mc_currency)
				{
					$isValid = false;
					$reason = "Invalid currency, expected $currencyCode, got $mc_currency";
				}
			}
		}

		$result['valid'] = $isValid;

		if ($isValid)
		{
			$result['gateway_id'] = $this->gatewayId;
			$result['transaction_id'] = $data['txn_id'];
			$result['order_id'] = $order->id;
		}
		else
		{
			$data['failure_reason'] = $reason;
		}

		$this->logNotification($data, $isValid);

		return $result;
	}

	/**
	 * Method to check with PayPal to see if callback data is valid.
	 *
	 * @param   array  $data  Callback data.
	 *
	 * @return  boolean
	 *
	 * @since   1.0.0
	 */
	private function isValidIPN($data)
	{
		$environment = $this->params->get('environment', 'live');

		$url = ($environment == 'live') ?
			'https://ipnpb.paypal.com/cgi-bin/webscr' :
			'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';

		$raw_post_data = file_get_contents('php://input');
		$raw_post_array = explode('&', $raw_post_data);
		$myPost = array();

		foreach ($raw_post_array as $keyval)
		{
			$keyval = explode('=', $keyval);

			if (count($keyval) == 2)
			{
				// Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
				if ($keyval[0] === 'payment_date')
				{
					if (substr_count($keyval[1], '+') === 1)
					{
						$keyval[1] = str_replace('+', '%2B', $keyval[1]);
					}
				}

				$myPost[$keyval[0]] = urldecode($keyval[1]);
			}
		}

		// Build the body of the verification post request, adding the _notify-validate command.
		$req = 'cmd=_notify-validate';

		foreach ($myPost as $key => $value)
		{
			$value = urlencode($value);

			$req .= "&$key=$value";
		}

		$ch = curl_init($url);
		curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
		curl_setopt($ch, CURLOPT_SSLVERSION, 6);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
		curl_setopt($ch, CURLOPT_HTTPHEADER, array(
			'User-Agent: PHP-IPN-Verification-Script',
			'Connection: Close',
		));

		if (!($res = curl_exec($ch)))
		{
			$logData = array('failure_reason' => 'CURL error: ' . curl_error($ch));
			$this->logNotification($logData, false);
			curl_close($ch);

			return [false, 0];
		}

		$info = curl_getinfo($ch);
		$httpCode = $info['http_code'];

		curl_close($ch);

		if (strcmp ($res, "VERIFIED") == 0)
		{
			return [true, $httpCode];
		}
		else
		{
			$this->logNotification(['res' => $res], false);

			return [false, $httpCode];
		}
	}
}
