From c18168c3d06794257da75eb26cad2b67b13262be Mon Sep 17 00:00:00 2001 From: Matt75 <5262628+Matt75@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:46:19 +0100 Subject: [PATCH] PAYSHIP-3146 - Improve the Apple Pay Setup --- config/common.yml | 11 + .../AdminAjaxPrestashopCheckoutController.php | 25 ++ src/PayPal/ApplePay/AppleSetup.php | 223 ++++++++++++++++++ .../Exception/ApplePaySetupException.php | 37 +++ src/System/SystemConfiguration.php | 65 +++++ tests/Unit/PayPal/ApplePay/AppleSetupTest.php | 180 ++++++++++++++ 6 files changed, 541 insertions(+) create mode 100644 src/PayPal/ApplePay/AppleSetup.php create mode 100644 src/PayPal/ApplePay/Exception/ApplePaySetupException.php create mode 100644 src/System/SystemConfiguration.php create mode 100644 tests/Unit/PayPal/ApplePay/AppleSetupTest.php diff --git a/config/common.yml b/config/common.yml index 013d923e1..05aa5b33f 100644 --- a/config/common.yml +++ b/config/common.yml @@ -486,3 +486,14 @@ services: public: true arguments: - '@PrestaShop\Module\PrestashopCheckout\Translations\Translations' + + PrestaShop\Module\PrestashopCheckout\System\SystemConfiguration: + class: 'PrestaShop\Module\PrestashopCheckout\System\SystemConfiguration' + public: true + + PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\AppleSetup: + class: 'PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\AppleSetup' + public: true + arguments: + - '@PrestaShop\Module\PrestashopCheckout\System\SystemConfiguration' + - '@PrestaShop\Module\PrestashopCheckout\PayPal\PayPalConfiguration' diff --git a/controllers/admin/AdminAjaxPrestashopCheckoutController.php b/controllers/admin/AdminAjaxPrestashopCheckoutController.php index b107e13aa..ea5804eba 100755 --- a/controllers/admin/AdminAjaxPrestashopCheckoutController.php +++ b/controllers/admin/AdminAjaxPrestashopCheckoutController.php @@ -37,6 +37,8 @@ use PrestaShop\Module\PrestashopCheckout\Order\State\Exception\OrderStateException; use PrestaShop\Module\PrestashopCheckout\Order\State\OrderStateInstaller; use PrestaShop\Module\PrestashopCheckout\Order\State\Service\OrderStateMapper; +use PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\AppleSetup; +use PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\Exception\ApplePaySetupException; use PrestaShop\Module\PrestashopCheckout\PayPal\Mode; use PrestaShop\Module\PrestashopCheckout\PayPal\Payment\Refund\Command\RefundPayPalCaptureCommand; use PrestaShop\Module\PrestashopCheckout\PayPal\Payment\Refund\Exception\PayPalRefundException; @@ -1067,4 +1069,27 @@ public function ajaxProcessDownloadLogs() readfile($file->getRealPath()); exit; } + + public function ajaxProcessSetupApplePay() + { + /** @var AppleSetup $appleSetup */ + $appleSetup = $this->module->getService(AppleSetup::class); + + try { + $appleSetup->setup(); + } catch (ApplePaySetupException $e) { + $this->exitWithResponse([ + 'httpCode' => 500, + 'status' => false, + 'error' => [ + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + ], + ]); + } + + $this->exitWithResponse([ + 'status' => true, + ]); + } } diff --git a/src/PayPal/ApplePay/AppleSetup.php b/src/PayPal/ApplePay/AppleSetup.php new file mode 100644 index 000000000..6b04213a2 --- /dev/null +++ b/src/PayPal/ApplePay/AppleSetup.php @@ -0,0 +1,223 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay; + +use Configuration; +use Exception; +use Hook; +use Module; +use PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\Exception\ApplePaySetupException; +use PrestaShop\Module\PrestashopCheckout\PayPal\PayPalConfiguration; +use PrestaShop\Module\PrestashopCheckout\System\SystemConfiguration; +use Shop; + +class AppleSetup +{ + /** + * @var SystemConfiguration + */ + private $systemConfiguration; + + /** + * @var PayPalConfiguration + */ + private $payPalConfiguration; + + public function __construct(SystemConfiguration $systemConfiguration, PayPalConfiguration $payPalConfiguration) + { + $this->systemConfiguration = $systemConfiguration; + $this->payPalConfiguration = $payPalConfiguration; + } + + /** + * @return void + * + * @throws ApplePaySetupException + */ + public function setup() + { + if ($this->systemConfiguration->isApacheServer() && !$this->checkWellKnownFileExist()) { + $this->registerModuleRoutesHook(); + } else { + $this->copyWellKnownFile(); + } + } + + /** + * @return void + * + * @throws ApplePaySetupException + */ + public function registerModuleRoutesHook() + { + try { + $module = Module::getInstanceByName('ps_checkout'); + $shopList = Shop::getCompleteListOfShopsID(); + if (!Hook::registerHook($module, 'moduleRoutes', $shopList)) { + throw new ApplePaySetupException('Failed to register moduleRoutes hook for ps_checkout.', ApplePaySetupException::FAILED_REGISTER_HOOK); + } + } catch (Exception $e) { + throw new ApplePaySetupException('Failed to register moduleRoutes hook for ps_checkout.', ApplePaySetupException::ERROR_REGISTER_HOOK, $e); + } + } + + /** + * @param string $wellKnownDir + * + * @return string + */ + public function getDestinationFile($wellKnownDir) + { + return $wellKnownDir . '/apple-developer-merchantid-domain-association'; + } + + /** + * @return bool + * + * @throws ApplePaySetupException + */ + public function checkWellKnownFileExist() + { + $rootDir = $this->getPrestaShopRootDir(); + $wellKnownDir = $this->getWellKnownDir($rootDir); + $destinationFile = $this->getDestinationFile($wellKnownDir); + + return file_exists($destinationFile); + } + + /** + * @return void + * + * @throws ApplePaySetupException + */ + public function copyWellKnownFile() + { + $rootDir = $this->getPrestaShopRootDir(); + $this->checkPrestaShopIsAtDomainRoot(); + $wellKnownDir = $this->getWellKnownDir($rootDir); + $sourceFile = $this->getSourceFile(); + $destinationFile = $this->getDestinationFile($wellKnownDir); + + if (!$this->isWritable($destinationFile)) { + throw new ApplePaySetupException('The Apple Domain Association file is not writable in the PrestaShop root directory.', ApplePaySetupException::APPLE_DOMAIN_FILE_NOT_WRITABLE); + } + + if (!copy($sourceFile, $destinationFile)) { + throw new ApplePaySetupException('Failed to copy the "apple-developer-merchantid-domain-association" file to the PrestaShop root directory.', ApplePaySetupException::FAILED_COPY_APPLE_DOMAIN_FILE); + } + } + + /** + * @return string + * + * @throws ApplePaySetupException + */ + public function getPrestaShopRootDir() + { + $rootDir = defined('_PS_ROOT_DIR_') ? constant('_PS_ROOT_DIR_') : null; + + if (!$rootDir || !is_dir($rootDir)) { + throw new ApplePaySetupException('Unable to retrieve the PrestaShop Root directory path.', ApplePaySetupException::UNABLE_RETRIEVE_ROOT_DIR); + } + + return $rootDir; + } + + /** + * @return void + * + * @throws ApplePaySetupException + */ + public function checkPrestaShopIsAtDomainRoot() + { + $defaultShop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT')); + + if (!$defaultShop->physical_uri) { + throw new ApplePaySetupException('Unable to retrieve the base URI of the shop.', ApplePaySetupException::UNABLE_RETRIEVE_BASE_URI); + } + + if ($defaultShop->physical_uri !== '/') { + throw new ApplePaySetupException('PrestaShop is not installed at the domain root.', ApplePaySetupException::PRESTASHOP_NOT_AT_DOMAIN_ROOT); + } + } + + /** + * @param string $rootDir + * + * @return string + * + * @throws ApplePaySetupException + */ + public function getWellKnownDir($rootDir) + { + $wellKnownDir = $rootDir . '/.well-known'; + + if (!is_dir($wellKnownDir)) { + if (!$this->createDir($wellKnownDir)) { + throw new ApplePaySetupException('Failed to create the ".well-known" directory in the PrestaShop root directory.', ApplePaySetupException::FAILED_CREATE_WELL_KNOWN_DIR); + } + } + + if (!$this->isWritable($wellKnownDir)) { + throw new ApplePaySetupException('The ".well-known" directory is not writable in the PrestaShop root directory.', ApplePaySetupException::WELL_KNOWN_DIR_NOT_WRITABLE); + } + + return $wellKnownDir; + } + + /** + * @return string + * + * @throws ApplePaySetupException + */ + public function getSourceFile() + { + $moduleWellKnownDir = _PS_MODULE_DIR_ . 'ps_checkout/.well-known'; + $paypalEnvironment = $this->payPalConfiguration->getPaymentMode(); + $sourceFile = "${$moduleWellKnownDir}/apple-${$paypalEnvironment}-merchantid-domain-association"; + + if (!file_exists($sourceFile)) { + throw new ApplePaySetupException('The Apple Domain Association file could not be found in the module directory.', ApplePaySetupException::APPLE_DOMAIN_FILE_NOT_FOUND); + } + + return $sourceFile; + } + + /** + * @param string $wellKnownDir + * + * @return bool + */ + public function createDir($wellKnownDir) + { + return mkdir($wellKnownDir, 0755, true); + } + + /** + * @param string $wellKnownDir + * + * @return bool + */ + public function isWritable($wellKnownDir) + { + return is_writable($wellKnownDir); + } +} diff --git a/src/PayPal/ApplePay/Exception/ApplePaySetupException.php b/src/PayPal/ApplePay/Exception/ApplePaySetupException.php new file mode 100644 index 000000000..1129c7a5b --- /dev/null +++ b/src/PayPal/ApplePay/Exception/ApplePaySetupException.php @@ -0,0 +1,37 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\Exception; + +use PrestaShop\Module\PrestashopCheckout\Exception\PsCheckoutException; + +class ApplePaySetupException extends PsCheckoutException +{ + const FAILED_REGISTER_HOOK = 1001; + const ERROR_REGISTER_HOOK = 1002; + const UNABLE_RETRIEVE_ROOT_DIR = 2001; + const FAILED_CREATE_WELL_KNOWN_DIR = 2002; + const WELL_KNOWN_DIR_NOT_WRITABLE = 2003; + const PRESTASHOP_NOT_AT_DOMAIN_ROOT = 2004; + const UNABLE_RETRIEVE_BASE_URI = 2005; + const APPLE_DOMAIN_FILE_NOT_FOUND = 3001; + const APPLE_DOMAIN_FILE_NOT_WRITABLE = 3002; + const FAILED_COPY_APPLE_DOMAIN_FILE = 3003; +} diff --git a/src/System/SystemConfiguration.php b/src/System/SystemConfiguration.php new file mode 100644 index 000000000..a4f5291a0 --- /dev/null +++ b/src/System/SystemConfiguration.php @@ -0,0 +1,65 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\Module\PrestashopCheckout\System; + +class SystemConfiguration +{ + /** + * @return bool + */ + public function isApacheServer() + { + if (php_sapi_name() === 'apache2handler') { + return true; + } + + if (function_exists('apache_get_version')) { + return true; + } + + if (isset($_SERVER['SERVER_SOFTWARE']) && stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== false) { + return true; + } + + if (isset($_SERVER['HTTPD_SERVER_ADMIN']) || isset($_SERVER['HTTPD_SERVER_NAME'])) { + return true; + } + + $headers = $this->getAllHeaders(); + if (isset($headers['Server']) && stripos($headers['Server'], 'apache') !== false) { + return true; + } + + return false; + } + + /** + * @return array + */ + public function getAllHeaders() + { + if (function_exists('getallheaders')) { + return getallheaders(); + } + + return []; + } +} diff --git a/tests/Unit/PayPal/ApplePay/AppleSetupTest.php b/tests/Unit/PayPal/ApplePay/AppleSetupTest.php new file mode 100644 index 000000000..21d022eb0 --- /dev/null +++ b/tests/Unit/PayPal/ApplePay/AppleSetupTest.php @@ -0,0 +1,180 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace Tests\Unit\PayPal\ApplePay; + +use PHPUnit\Framework\TestCase; +use PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\AppleSetup; +use PrestaShop\Module\PrestashopCheckout\PayPal\ApplePay\Exception\ApplePaySetupException; +use PrestaShop\Module\PrestashopCheckout\PayPal\PayPalConfiguration; +use PrestaShop\Module\PrestashopCheckout\System\SystemConfiguration; + +class AppleSetupTest extends TestCase +{ + private $systemConfigurationMock; + private $payPalConfigurationMock; + + protected function setUp() + { + $this->systemConfigurationMock = $this->createMock(SystemConfiguration::class); + $this->payPalConfigurationMock = $this->createMock(PayPalConfiguration::class); + } + + public function testSetupInvokesRegisterModuleRoutesHookWhenApacheServerAndWellKnownFileNotExist() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(true); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['checkWellKnownFileExist', 'registerModuleRoutesHook']) + ->getMock(); + + $appleSetupMock->method('checkWellKnownFileExist')->willReturn(false); + $appleSetupMock->expects($this->once())->method('registerModuleRoutesHook'); + + $appleSetupMock->setup(); + } + + public function testSetupInvokesCopyWellKnownFileWhenApacheServerAndWellKnownFileExist() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(true); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['checkWellKnownFileExist', 'copyWellKnownFile']) + ->getMock(); + + $appleSetupMock->method('checkWellKnownFileExist')->willReturn(true); + $appleSetupMock->expects($this->once())->method('copyWellKnownFile'); + + $appleSetupMock->setup(); + } + + public function testSetupInvokesCopyWellKnownFileWhenNotApacheServer() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(false); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['checkWellKnownFileExist', 'copyWellKnownFile']) + ->getMock(); + + $appleSetupMock->method('checkWellKnownFileExist')->willReturn(false); + $appleSetupMock->expects($this->once())->method('copyWellKnownFile'); + + $appleSetupMock->setup(); + } + + public function testSetupThrowsExceptionWhenRegisterModuleRoutesHookFails() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(true); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['checkWellKnownFileExist', 'registerModuleRoutesHook']) + ->getMock(); + + $appleSetupMock->method('checkWellKnownFileExist')->willReturn(false); + $appleSetupMock->method('registerModuleRoutesHook')->will($this->throwException(new ApplePaySetupException('Failed to register moduleRoutes hook for ps_checkout.', ApplePaySetupException::FAILED_REGISTER_HOOK))); + + $this->expectException(ApplePaySetupException::class); + $this->expectExceptionMessage('Failed to register moduleRoutes hook for ps_checkout.'); + $appleSetupMock->setup(); + } + + public function testSetupThrowsExceptionWhenPrestaShopRootDirNotFound() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(false); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['getPrestaShopRootDir']) + ->getMock(); + + $appleSetupMock->method('getPrestaShopRootDir')->will($this->throwException(new ApplePaySetupException('Unable to retrieve the PrestaShop Root directory path.', ApplePaySetupException::UNABLE_RETRIEVE_ROOT_DIR))); + + $this->expectException(ApplePaySetupException::class); + $this->expectExceptionMessage('Unable to retrieve the PrestaShop Root directory path.'); + $appleSetupMock->setup(); + } + + public function testSetupThrowsExceptionWhenPrestaShopNotAtDomainRoot() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(false); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['checkPrestaShopIsAtDomainRoot', 'getPrestaShopRootDir']) + ->getMock(); + + $appleSetupMock->method('getPrestaShopRootDir')->willReturn('/path/to/prestashop/root'); + $appleSetupMock->method('checkPrestaShopIsAtDomainRoot')->will($this->throwException(new ApplePaySetupException('PrestaShop is not installed at the domain root.', ApplePaySetupException::PRESTASHOP_NOT_AT_DOMAIN_ROOT))); + + $this->expectException(ApplePaySetupException::class); + $this->expectExceptionMessage('PrestaShop is not installed at the domain root.'); + $appleSetupMock->setup(); + } + + public function testSetupThrowsExceptionWhenWellKnownDirNotWritable() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(false); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['getWellKnownDir', 'getPrestaShopRootDir', 'checkPrestaShopIsAtDomainRoot']) + ->getMock(); + + $appleSetupMock->method('getPrestaShopRootDir')->willReturn('/path/to/prestashop/root'); + $appleSetupMock->method('checkPrestaShopIsAtDomainRoot')->willReturn(true); + $appleSetupMock->method('getWellKnownDir')->will($this->throwException(new ApplePaySetupException('The ".well-known" directory is not writable in the PrestaShop root directory.', ApplePaySetupException::WELL_KNOWN_DIR_NOT_WRITABLE))); + + $this->expectException(ApplePaySetupException::class); + $this->expectExceptionMessage('The ".well-known" directory is not writable in the PrestaShop root directory.'); + $appleSetupMock->setup(); + } + + public function testSetupThrowsExceptionWhenAppleDomainFileNotWritable() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(false); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['getDestinationFile', 'copyWellKnownFile']) + ->getMock(); + + $appleSetupMock->method('getDestinationFile')->willReturn('/path/to/destination/file'); + $appleSetupMock->method('copyWellKnownFile')->will($this->throwException(new ApplePaySetupException('The Apple Domain Association file is not writable in the PrestaShop root directory.', ApplePaySetupException::APPLE_DOMAIN_FILE_NOT_WRITABLE))); + + $this->expectException(ApplePaySetupException::class); + $this->expectExceptionMessage('The Apple Domain Association file is not writable in the PrestaShop root directory.'); + $appleSetupMock->setup(); + } + + public function testSetupThrowsExceptionWhenAppleDomainFileNotFound() + { + $this->systemConfigurationMock->method('isApacheServer')->willReturn(false); + $appleSetupMock = $this->getMockBuilder(AppleSetup::class) + ->setConstructorArgs([$this->systemConfigurationMock, $this->payPalConfigurationMock]) + ->setMethods(['getSourceFile', 'getPrestaShopRootDir', 'checkPrestaShopIsAtDomainRoot', 'createDir', 'isWritable']) + ->getMock(); + + $appleSetupMock->method('getPrestaShopRootDir')->willReturn('/path/to/prestashop/root'); + $appleSetupMock->method('checkPrestaShopIsAtDomainRoot')->willReturn(true); + $appleSetupMock->method('createDir')->willReturn(true); + $appleSetupMock->method('isWritable')->willReturn(true); + $appleSetupMock->method('getSourceFile')->will($this->throwException(new ApplePaySetupException('The Apple Domain Association file could not be found in the module directory.', ApplePaySetupException::APPLE_DOMAIN_FILE_NOT_FOUND))); + + $this->expectException(ApplePaySetupException::class); + $this->expectExceptionMessage('The Apple Domain Association file could not be found in the module directory.'); + $appleSetupMock->setup(); + } +}