diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 502b27e..c4fd362 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -51,8 +51,6 @@ jobs: docker exec -i magento mysql magento << "EOF" REPLACE INTO `admin_user` VALUES (1,'Example','Example','user@example.com','exampleuser','12b66e0132008acb13a9eb880f9ff4e7393b64b74722dcda435a03ec7e0dd780:TWLwiabBH2lIPcDkKLrhu37GsEBRpnSb:3_32_2_67108864','2024-09-23 01:02:08','2024-10-10 16:39:29','2024-10-10 16:39:29',1,0,1,NULL,NULL,NULL,'en_US',0,NULL,NULL); REPLACE INTO `admin_analytics_usage_version_log` VALUES (1,'2.4.6'); - REPLACE INTO `core_config_data` VALUES (4,'default',0,'currency/options/base','GBP','2024-10-11 11:26:34'),(5,'default',0,'currency/options/default','GBP','2024-10-11 11:26:34'),(6,'default',0,'currency/options/allow','GBP','2024-10-11 11:26:34'); - REPLACE INTO `core_config_data` VALUES (18,'default',0,'admin/usage/enabled','0','2024-10-10 16:43:40'),(19,'default',0,'carriers/flatrate/active','0','2024-10-10 16:43:40'),(20,'default',0,'carriers/flatrate/handling_fee',NULL,'2024-10-10 16:43:40'),(21,'default',0,'carriers/flatrate/specificcountry',NULL,'2024-10-10 16:43:40'),(22,'default',0,'carriers/flatrate/showmethod','0','2024-10-10 16:43:40'),(23,'default',0,'carriers/flatrate/sort_order',NULL,'2024-10-10 16:43:40'),(24,'default',0,'carriers/freeshipping/active','1','2024-10-10 16:43:40'),(25,'default',0,'carriers/freeshipping/free_shipping_subtotal',NULL,'2024-10-10 16:43:40'),(26,'default',0,'carriers/freeshipping/specificcountry',NULL,'2024-10-10 16:43:40'),(27,'default',0,'carriers/freeshipping/showmethod','0','2024-10-10 16:43:40'),(28,'default',0,'carriers/freeshipping/sort_order',NULL,'2024-10-10 16:43:40'),(29,'default',0,'carriers/tablerate/handling_fee',NULL,'2024-10-10 16:43:40'),(30,'default',0,'carriers/tablerate/specificcountry',NULL,'2024-10-10 16:43:40'),(31,'default',0,'carriers/tablerate/showmethod','0','2024-10-10 16:43:40'),(32,'default',0,'carriers/tablerate/sort_order',NULL,'2024-10-10 16:43:40'),(33,'default',0,'carriers/ups/shipper_number',NULL,'2024-10-10 16:43:40'),(34,'default',0,'carriers/ups/handling_fee',NULL,'2024-10-10 16:43:40'),(35,'default',0,'carriers/ups/free_shipping_enable','0','2024-10-10 16:43:40'),(36,'default',0,'carriers/ups/specificcountry',NULL,'2024-10-10 16:43:40'),(37,'default',0,'carriers/ups/showmethod','0','2024-10-10 16:43:40'),(38,'default',0,'carriers/ups/debug','0','2024-10-10 16:43:40'),(39,'default',0,'carriers/ups/sort_order',NULL,'2024-10-10 16:43:40'),(40,'default',0,'carriers/usps/userid',NULL,'2024-10-10 16:43:40'),(41,'default',0,'carriers/usps/password',NULL,'2024-10-10 16:43:40'),(42,'default',0,'carriers/usps/handling_fee',NULL,'2024-10-10 16:43:40'),(43,'default',0,'carriers/usps/free_shipping_enable','0','2024-10-10 16:43:40'),(44,'default',0,'carriers/usps/specificcountry',NULL,'2024-10-10 16:43:40'),(45,'default',0,'carriers/usps/debug','0','2024-10-10 16:43:40'),(46,'default',0,'carriers/usps/showmethod','0','2024-10-10 16:43:40'),(47,'default',0,'carriers/usps/sort_order',NULL,'2024-10-10 16:43:40'),(48,'default',0,'carriers/fedex/account',NULL,'2024-10-10 16:43:40'),(49,'default',0,'carriers/fedex/meter_number',NULL,'2024-10-10 16:43:40'),(50,'default',0,'carriers/fedex/key',NULL,'2024-10-10 16:43:40'),(51,'default',0,'carriers/fedex/password',NULL,'2024-10-10 16:43:40'),(52,'default',0,'carriers/fedex/handling_fee',NULL,'2024-10-10 16:43:40'),(53,'default',0,'carriers/fedex/residence_delivery','0','2024-10-10 16:43:40'),(54,'default',0,'carriers/fedex/smartpost_hubid',NULL,'2024-10-10 16:43:40'),(55,'default',0,'carriers/fedex/free_shipping_enable','0','2024-10-10 16:43:40'),(56,'default',0,'carriers/fedex/specificcountry',NULL,'2024-10-10 16:43:40'),(57,'default',0,'carriers/fedex/debug','0','2024-10-10 16:43:40'),(58,'default',0,'carriers/fedex/showmethod','0','2024-10-10 16:43:40'),(59,'default',0,'carriers/fedex/sort_order',NULL,'2024-10-10 16:43:40'),(60,'default',0,'carriers/dhl/id',NULL,'2024-10-10 16:43:40'),(61,'default',0,'carriers/dhl/password',NULL,'2024-10-10 16:43:40'),(62,'default',0,'carriers/dhl/handling_fee',NULL,'2024-10-10 16:43:40'),(63,'default',0,'carriers/dhl/free_method_nondoc',NULL,'2024-10-10 16:43:40'),(64,'default',0,'carriers/dhl/free_shipping_enable','0','2024-10-10 16:43:40'),(65,'default',0,'carriers/dhl/specificcountry',NULL,'2024-10-10 16:43:40'),(66,'default',0,'carriers/dhl/showmethod','0','2024-10-10 16:43:40'),(67,'default',0,'carriers/dhl/debug','0','2024-10-10 16:43:40'),(68,'default',0,'carriers/dhl/sandbox_mode','0','2024-10-10 16:43:40'),(69,'default',0,'carriers/dhl/sort_order',NULL,'2024-10-10 16:43:40'),(70,'default',0,'payment/truelayer/active','1','2024-10-10 16:44:45'),(71,'default',0,'payment/truelayer/merchant_account_name',NULL,'2024-10-10 16:44:45'),(72,'default',0,'payment/truelayer/mode','sandbox','2024-10-10 16:44:45'),(73,'default',0,'payment/truelayer/sandbox_client_id','${{ secrets.TEST_CLIENT_ID }}','2024-10-10 16:44:45'),(74,'default',0,'payment/truelayer/sandbox_client_secret','','2024-10-10 16:44:45'),(75,'default',0,'payment/truelayer/sandbox_key_id','${{ secrets.TEST_KID }}','2024-10-10 16:44:45'),(76,'default',0,'payment/truelayer/sandbox_private_key','sandbox/default/private-key.pem','2024-10-10 16:44:45'),(77,'default',0,'payment/truelayer/specificcountry','GB,IE,ES,FR,DE,NL,LT','2024-10-10 16:44:45'),(78,'default',0,'payment/truelayer/send_order_email','0','2024-10-10 16:44:45'),(79,'default',0,'payment/truelayer/send_invoice_email','0','2024-10-10 16:44:45'),(80,'default',0,'payment/truelayer/banking_providers','retail','2024-10-10 16:44:45'),(81,'default',0,'payment/truelayer/payment_page_primary_color','#000000','2024-10-10 16:44:45'),(82,'default',0,'payment/truelayer/payment_page_secondary_color','#e53935','2024-10-10 16:44:45'),(83,'default',0,'payment/truelayer/payment_page_tertiary_color','#32329f','2024-10-10 16:44:45'),(84,'default',0,'payment/truelayer/logging','0','2024-10-10 16:44:45'); REPLACE INTO `catalog_product_entity` VALUES (1,4,'simple','test-product',0,0,'2024-10-10 16:41:56','2024-10-10 16:41:56'); REPLACE INTO `catalog_product_entity_decimal` VALUES (1,77,0,1,0.010000),(2,82,0,1,1.000000); REPLACE INTO `catalog_product_entity_int` VALUES (1,97,0,1,1),(2,136,0,1,2),(3,99,0,1,4); @@ -64,12 +62,43 @@ jobs: REPLACE INTO `directory_currency_rate` VALUES ('EUR','EUR',1.000000000000),('EUR','USD',1.415000000000),('GBP','GBP',1.000000000000),('USD','EUR',0.706700000000),('USD','USD',1.000000000000); REPLACE INTO `inventory_low_stock_notification_configuration` VALUES ('default','test-product',NULL); REPLACE INTO `inventory_source_item` VALUES (1,'default','test-product',0.0000,0); - REPLACE INTO `ui_bookmark` VALUES (1,1,'product_listing','default',1,'Default View','{\"views\":{\"default\":{\"label\":\"Default View\",\"index\":\"default\",\"editable\":false,\"data\":{\"filters\":{\"applied\":{\"placeholder\":true}},\"paging\":{\"pageSize\":20,\"current\":1,\"options\":{\"20\":{\"value\":20,\"label\":20},\"30\":{\"value\":30,\"label\":30},\"50\":{\"value\":50,\"label\":50},\"100\":{\"value\":100,\"label\":100},\"200\":{\"value\":200,\"label\":200}},\"value\":20},\"search\":{\"value\":\"\"},\"columns\":{\"entity_id\":{\"visible\":true,\"sorting\":\"asc\"},\"name\":{\"visible\":true,\"sorting\":false},\"sku\":{\"visible\":true,\"sorting\":false},\"price\":{\"visible\":true,\"sorting\":false},\"websites\":{\"visible\":true,\"sorting\":false},\"qty\":{\"visible\":true,\"sorting\":false},\"short_description\":{\"visible\":false,\"sorting\":false},\"special_price\":{\"visible\":false,\"sorting\":false},\"cost\":{\"visible\":false,\"sorting\":false},\"weight\":{\"visible\":false,\"sorting\":false},\"meta_title\":{\"visible\":false,\"sorting\":false},\"meta_keyword\":{\"visible\":false,\"sorting\":false},\"meta_description\":{\"visible\":false,\"sorting\":false},\"url_key\":{\"visible\":false,\"sorting\":false},\"msrp\":{\"visible\":false,\"sorting\":false},\"actions\":{\"visible\":true,\"sorting\":false},\"ids\":{\"visible\":true,\"sorting\":false},\"type_id\":{\"visible\":true,\"sorting\":false},\"attribute_set_id\":{\"visible\":true,\"sorting\":false},\"visibility\":{\"visible\":true,\"sorting\":false},\"status\":{\"visible\":true,\"sorting\":false},\"manufacturer\":{\"visible\":false,\"sorting\":false},\"color\":{\"visible\":false,\"sorting\":false},\"custom_design\":{\"visible\":false,\"sorting\":false},\"page_layout\":{\"visible\":false,\"sorting\":false},\"country_of_manufacture\":{\"visible\":false,\"sorting\":false},\"custom_layout\":{\"visible\":false,\"sorting\":false},\"gift_message_available\":{\"visible\":false,\"sorting\":false},\"tax_class_id\":{\"visible\":false,\"sorting\":false},\"salable_quantity\":{\"visible\":true,\"sorting\":false},\"thumbnail\":{\"visible\":true,\"sorting\":false},\"updated_at\":{\"visible\":true,\"sorting\":false},\"special_from_date\":{\"visible\":false,\"sorting\":false},\"special_to_date\":{\"visible\":false,\"sorting\":false},\"news_from_date\":{\"visible\":false,\"sorting\":false},\"news_to_date\":{\"visible\":false,\"sorting\":false},\"custom_design_from\":{\"visible\":false,\"sorting\":false},\"custom_design_to\":{\"visible\":false,\"sorting\":false}},\"displayMode\":\"grid\",\"positions\":{\"ids\":0,\"entity_id\":1,\"thumbnail\":2,\"name\":3,\"type_id\":4,\"attribute_set_id\":5,\"sku\":6,\"price\":7,\"qty\":8,\"salable_quantity\":9,\"visibility\":10,\"status\":11,\"websites\":12,\"short_description\":13,\"special_price\":14,\"special_from_date\":15,\"special_to_date\":16,\"cost\":17,\"weight\":18,\"manufacturer\":19,\"meta_title\":20,\"meta_keyword\":21,\"meta_description\":22,\"color\":23,\"news_from_date\":24,\"news_to_date\":25,\"custom_design\":26,\"custom_design_from\":27,\"custom_design_to\":28,\"page_layout\":29,\"country_of_manufacture\":30,\"custom_layout\":31,\"url_key\":32,\"msrp\":33,\"gift_message_available\":34,\"tax_class_id\":35,\"updated_at\":36,\"actions\":37}},\"value\":\"Default View\"}}}','2024-10-10 16:40:36','2024-10-10 16:40:36'),(2,1,'product_listing','current',0,NULL,'{\"current\":{\"filters\":{\"applied\":{\"placeholder\":true}},\"paging\":{\"pageSize\":20,\"current\":1,\"options\":{\"20\":{\"value\":20,\"label\":20},\"30\":{\"value\":30,\"label\":30},\"50\":{\"value\":50,\"label\":50},\"100\":{\"value\":100,\"label\":100},\"200\":{\"value\":200,\"label\":200}},\"value\":20},\"search\":{\"value\":\"\"},\"columns\":{\"entity_id\":{\"visible\":true,\"sorting\":\"asc\"},\"name\":{\"visible\":true,\"sorting\":false},\"sku\":{\"visible\":true,\"sorting\":false},\"price\":{\"visible\":true,\"sorting\":false},\"websites\":{\"visible\":true,\"sorting\":false},\"qty\":{\"visible\":true,\"sorting\":false},\"short_description\":{\"visible\":false,\"sorting\":false},\"special_price\":{\"visible\":false,\"sorting\":false},\"cost\":{\"visible\":false,\"sorting\":false},\"weight\":{\"visible\":false,\"sorting\":false},\"meta_title\":{\"visible\":false,\"sorting\":false},\"meta_keyword\":{\"visible\":false,\"sorting\":false},\"meta_description\":{\"visible\":false,\"sorting\":false},\"url_key\":{\"visible\":false,\"sorting\":false},\"msrp\":{\"visible\":false,\"sorting\":false},\"actions\":{\"visible\":true,\"sorting\":false},\"ids\":{\"visible\":true,\"sorting\":false},\"type_id\":{\"visible\":true,\"sorting\":false},\"attribute_set_id\":{\"visible\":true,\"sorting\":false},\"visibility\":{\"visible\":true,\"sorting\":false},\"status\":{\"visible\":true,\"sorting\":false},\"manufacturer\":{\"visible\":false,\"sorting\":false},\"color\":{\"visible\":false,\"sorting\":false},\"custom_design\":{\"visible\":false,\"sorting\":false},\"page_layout\":{\"visible\":false,\"sorting\":false},\"country_of_manufacture\":{\"visible\":false,\"sorting\":false},\"custom_layout\":{\"visible\":false,\"sorting\":false},\"gift_message_available\":{\"visible\":false,\"sorting\":false},\"tax_class_id\":{\"visible\":false,\"sorting\":false},\"salable_quantity\":{\"visible\":true,\"sorting\":false},\"thumbnail\":{\"visible\":true,\"sorting\":false},\"updated_at\":{\"visible\":true,\"sorting\":false},\"special_from_date\":{\"visible\":false,\"sorting\":false},\"special_to_date\":{\"visible\":false,\"sorting\":false},\"news_from_date\":{\"visible\":false,\"sorting\":false},\"news_to_date\":{\"visible\":false,\"sorting\":false},\"custom_design_from\":{\"visible\":false,\"sorting\":false},\"custom_design_to\":{\"visible\":false,\"sorting\":false}},\"displayMode\":\"grid\",\"positions\":{\"ids\":0,\"entity_id\":1,\"thumbnail\":2,\"name\":3,\"type_id\":4,\"attribute_set_id\":5,\"sku\":6,\"price\":7,\"qty\":8,\"salable_quantity\":9,\"visibility\":10,\"status\":11,\"websites\":12,\"short_description\":13,\"special_price\":14,\"special_from_date\":15,\"special_to_date\":16,\"cost\":17,\"weight\":18,\"manufacturer\":19,\"meta_title\":20,\"meta_keyword\":21,\"meta_description\":22,\"color\":23,\"news_from_date\":24,\"news_to_date\":25,\"custom_design\":26,\"custom_design_from\":27,\"custom_design_to\":28,\"page_layout\":29,\"country_of_manufacture\":30,\"custom_layout\":31,\"url_key\":32,\"msrp\":33,\"gift_message_available\":34,\"tax_class_id\":35,\"updated_at\":36,\"actions\":37}}}','2024-10-10 16:40:37','2024-10-10 16:40:37'); REPLACE INTO `url_rewrite` VALUES (5,'product',1,'test-product.html','catalog/product/view/id/1',0,1,NULL,1,NULL); EOF + - name: Add magento config + run: | + docker exec magento bin/magento config:set admin/usage/enabled 0; + docker exec magento bin/magento config:set admin/security/use_form_key 0; + docker exec magento bin/magento config:set general/country/default GB; + docker exec magento bin/magento config:set currency/options/allow GBP,EUR; + docker exec magento bin/magento config:set currency/options/base GBP; + docker exec magento bin/magento config:set currency/options/default GBP; + docker exec magento bin/magento config:set currency/options/allow GBP; + docker exec magento bin/magento config:set carriers/flatrate/active 0; + docker exec magento bin/magento config:set carriers/freeshipping/active 1; + docker exec magento bin/magento config:set carriers/freeshipping/specificcountry ""; + + - name: Add truelayer config + run: | + docker exec magento bin/magento config:set payment/truelayer/active 1; + docker exec magento bin/magento config:set payment/truelayer/mode sandbox; + docker exec magento bin/magento config:set payment/truelayer/send_order_email 0; + docker exec magento bin/magento config:set payment/truelayer/send_invoice_email 0; + docker exec magento bin/magento config:set payment/truelayer/banking_providers retail; + docker exec magento bin/magento config:set payment/truelayer/logging 0; + docker exec magento bin/magento config:set payment/truelayer/send_order_email 0; + docker exec magento bin/magento config:set payment/truelayer/send_invoice_email 0; + docker exec magento bin/magento config:set payment/truelayer/payment_page_primary_color \#000000; + docker exec magento bin/magento config:set payment/truelayer/payment_page_secondary_color \#e53935; + docker exec magento bin/magento config:set payment/truelayer/payment_page_tertiary_color \#32329f; + docker exec magento bin/magento config:set payment/truelayer/specificcountry GB,IE,ES,FR,DE,NL,LT; + docker exec magento bin/magento config:set payment/truelayer/sandbox_client_id ${{ secrets.TEST_CLIENT_ID }}; + docker exec magento bin/magento config:set payment/truelayer/sandbox_key_id ${{ secrets.TEST_KID }}; + - name: Set encrypted client secret - run: docker exec magento sh -c 'php bin/magento config:set payment/truelayer/sandbox_client_secret $(php -r "require \"vendor/autoload.php\"; require \"app/bootstrap.php\"; \$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, \$_SERVER); \$bootstrap->createApplication(\Magento\Framework\App\Http::class); \$encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Encryption\EncryptorInterface::class); echo \$encryptor->encrypt(\"${{ secrets.TEST_CLIENT_SECRET }}\");")' + run: | + docker exec magento sh -c 'php bin/magento config:set payment/truelayer/sandbox_client_secret $(php -r "require \"vendor/autoload.php\"; require \"app/bootstrap.php\"; \$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, \$_SERVER); \$bootstrap->createApplication(\Magento\Framework\App\Http::class); \$encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Encryption\EncryptorInterface::class); echo \$encryptor->encrypt(\"${{ secrets.TEST_CLIENT_SECRET }}\");")' + docker exec magento sh -c 'php bin/magento config:set payment/truelayer/sandbox_private_key $(php -r "require \"vendor/autoload.php\"; require \"app/bootstrap.php\"; \$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, \$_SERVER); \$bootstrap->createApplication(\Magento\Framework\App\Http::class); \$encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get(\Magento\Framework\Encryption\EncryptorInterface::class); echo \$encryptor->encrypt(\"${{ secrets.TEST_PEM }}\");")' - name: Reindex products run: docker exec magento php bin/magento indexer:reindex @@ -84,4 +113,4 @@ jobs: docker exec -w /data/extensions/magento2 magento npx playwright install - name: Run tests - run: docker exec --env PW_TEST_HTML_REPORT_OPEN=never -w /data/extensions/magento2 magento npx playwright test --project=chromium --reporter=line \ No newline at end of file + run: docker exec --env PW_TEST_HTML_REPORT_OPEN=never -w /data/extensions/magento2 magento npx playwright test --project=chromium --reporter=line diff --git a/Block/Adminhtml/System/Config/Field/Base64FileUpload.php b/Block/Adminhtml/System/Config/Field/Base64FileUpload.php new file mode 100644 index 0000000..8531602 --- /dev/null +++ b/Block/Adminhtml/System/Config/Field/Base64FileUpload.php @@ -0,0 +1,46 @@ +getHtmlId(); + $mode = $element->getData('field_config')['depends']['fields']['mode']['value'] ?? Mode::SANDBOX; + $fieldType = $element->getData('field_config')['type'] ?? 'text'; + $displayValue = $element->getValue(); + $disabled = $element->getDisabled(); + if ($displayValue && $fieldType == 'obscure') { + $displayValue = '******'; + } + + $this->setData([ + 'htmlTextInputId' => $htmlTextInputId, + 'mode' => $mode, + 'fieldType' => $fieldType, + 'displayValue' => $displayValue, + 'disabled' => $disabled, + ]); + return $this->_toHtml(); + } +} diff --git a/Controller/Adminhtml/Credentials/Check.php b/Controller/Adminhtml/Credentials/Check.php index f7cd6bc..2341d1e 100644 --- a/Controller/Adminhtml/Credentials/Check.php +++ b/Controller/Adminhtml/Credentials/Check.php @@ -154,52 +154,41 @@ private function getCredentials(): array $clientId = $this->getRequest()->getParam('sandbox_client_id'); $clientSecret = $this->getRequest()->getParam('sandbox_client_secret'); $keyId = $this->getRequest()->getParam('sandbox_key_id'); + $privateKey = $this->getRequest()->getParam('sandbox_private_key'); } else { $clientId = $this->getRequest()->getParam('production_client_id'); $clientSecret = $this->getRequest()->getParam('production_client_secret'); $keyId = $this->getRequest()->getParam('production_key_id'); + $privateKey = $this->getRequest()->getParam('production_private_key'); } $configCredentials = $this->configProvider->getCredentials($storeId, $mode === Mode::SANDBOX); if ($clientSecret == '******') { $clientSecret = $configCredentials['client_secret']; } + if ($privateKey == '******') { + $privateKey = $configCredentials['private_key']; + } else { + if ($privateKey) { + $decoded = base64_decode($privateKey, true); + if (@base64_encode($decoded) === $privateKey) { + $privateKey = $decoded; + } + } + } return [ 'store_id' => $storeId, 'credentials' => [ 'client_id' => $clientId, 'client_secret' => $clientSecret, - 'private_key' => $this->getPrivateKeyPath($configCredentials), + 'private_key' => $privateKey, 'key_id' => $keyId, 'cache_encryption_key' => $configCredentials['cache_encryption_key'] ] ]; } - /** - * @param array $configCredentials - * @return string - * @throws FileSystemException - */ - private function getPrivateKeyPath(array $configCredentials): string - { - if ($privateKey = $this->getRequest()->getParam('private_key')) { - $path = $this->directoryList->getPath('var') . self::PEM_UPLOAD_FILE; - $fileInfo = $this->file->getPathInfo($path); - - if (!$this->file->fileExists($fileInfo['dirname'])) { - $this->file->mkdir($fileInfo['dirname']); - } - - $this->file->write($path, $privateKey); - - return $path; - } - - return $configCredentials['private_key']; - } - /** * @return void * @throws FileSystemException diff --git a/Model/Config/System/ConnectionRepository.php b/Model/Config/System/ConnectionRepository.php index b3459e3..62be0ec 100644 --- a/Model/Config/System/ConnectionRepository.php +++ b/Model/Config/System/ConnectionRepository.php @@ -41,7 +41,7 @@ public function getCredentials(?int $storeId = null, ?bool $forceSandbox = null) return [ "client_id" => $this->getClientId($storeId, $isSandBox), "client_secret" => $this->getClientSecret($storeId, $isSandBox), - "private_key" => $this->getPathToPrivateKey($storeId, $isSandBox), + "private_key" => $this->getPrivateKey($storeId, $isSandBox), "key_id" => $this->getKeyId($storeId, $isSandBox), "cache_encryption_key" => $this->getCacheEncryptionKey($storeId) ]; @@ -52,18 +52,14 @@ public function getCredentials(?int $storeId = null, ?bool $forceSandbox = null) * @param bool $isSandBox * @return string */ - private function getPathToPrivateKey(?int $storeId = null, bool $isSandBox = false): string + private function getPrivateKey(?int $storeId = null, bool $isSandBox = false): string { $path = $isSandBox ? self::XML_PATH_SANDBOX_PRIVATE_KEY : self::XML_PATH_PRODUCTION_PRIVATE_KEY; - if (!$savedPrivateKey = $this->getStoreValue($path, $storeId)) { - return ''; + if ($value = $this->getStoreValue($path, $storeId)) { + return $this->encryptor->decrypt($value); } - try { - return $this->directoryList->getPath('var') . '/truelayer/' . $savedPrivateKey; - } catch (\Exception $exception) { - return ''; - } + return ''; } /** diff --git a/Model/System/Config/Backend/PrivateKey.php b/Model/System/Config/Backend/PrivateKey.php index d316b7a..27d95a8 100644 --- a/Model/System/Config/Backend/PrivateKey.php +++ b/Model/System/Config/Backend/PrivateKey.php @@ -7,178 +7,34 @@ namespace TrueLayer\Connect\Model\System\Config\Backend; -use Magento\Framework\App\Cache\TypeListInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\Config\Value; -use Magento\Framework\Data\Collection\AbstractDb; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem\Io\File; -use Magento\Framework\Model\Context; -use Magento\Framework\Model\ResourceModel\AbstractResource; -use Magento\Framework\Registry; -use TrueLayer\Connect\Api\Config\System\ConnectionInterface; +use Magento\Config\Model\Config\Backend\Encrypted; /** - * Backend model for saving certificate file + * Backend model for saving certificate */ -class PrivateKey extends Value +class PrivateKey extends Encrypted { - public const FILENAME = 'private-key.pem'; /** - * @var File - */ - private $file; - /** - * @var ReadInterface - */ - private $tmpDirectory; - /** - * @var ReadInterface - */ - private $varDirectory; - - /** - * @param Context $context - * @param Registry $registry - * @param ScopeConfigInterface $config - * @param TypeListInterface $cacheTypeList - * @param Filesystem $filesystem - * @param File $file - * @param AbstractResource|null $resource - * @param AbstractDb|null $resourceCollection - * @param array $data - */ - public function __construct( - Context $context, - Registry $registry, - ScopeConfigInterface $config, - TypeListInterface $cacheTypeList, - Filesystem $filesystem, - File $file, - AbstractResource $resource = null, - AbstractDb $resourceCollection = null, - array $data = [] - ) { - $this->file = $file; - $this->tmpDirectory = $filesystem->getDirectoryRead('sys_tmp'); - $this->varDirectory = $filesystem->getDirectoryRead('var'); - parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); - } - - /** - * Process additional data before save config. + * Decode and encrypt value before saving * - * @return $this - * @throws LocalizedException - */ - public function beforeSave(): self - { - $value = (array)$this->getValue(); - $sandbox = $this->getPath() === ConnectionInterface::XML_PATH_SANDBOX_PRIVATE_KEY; - $directory = $this->getDirectory($sandbox); - - if (!empty($value['delete'])) { - $this->deleteCertificateAndReset($this->isObjectNew() ? '' : $this->getOldValue()); - return $this; - } - - $tmpName = $this->getTmpName($sandbox); - $isUploading = (is_string($tmpName) && !empty($tmpName) && $this->tmpDirectory->isExist($tmpName)); - - if (!$isUploading) { - $this->setValue($this->isObjectNew() ? '' : $this->getOldValue()); - return $this; - } - - if ($isUploading) { - $tmpPath = $this->tmpDirectory->getAbsolutePath($tmpName); - if (!$this->tmpDirectory->stat($tmpPath)['size']) { - throw new LocalizedException(__('The TrueLayer certificate file is empty.')); - } - - $destinationPath = $this->varDirectory->getAbsolutePath('truelayer/' . $directory); - - $filePath = $directory . self::FILENAME; - $this->file->checkAndCreateFolder($destinationPath); - $this->file->mv( - $tmpPath, - $this->varDirectory->getAbsolutePath('truelayer/' . $filePath) - ); - $this->setValue($filePath); - } - - return $this; - } - - /** - * Delete the cert file from disk when deleting the setting. - * - * @return $this - */ - public function beforeDelete() - { - $returnValue = parent::beforeDelete(); - $filePath = $this->isObjectNew() ? '' : $this->getOldValue(); - if ($filePath) { - $absolutePath = $this->varDirectory->getAbsolutePath('truelayer/' . $filePath); - if ($this->file->fileExists($absolutePath)) { - $this->file->rm($absolutePath); - } - } - return $returnValue; - } - - /** - * Delete the cert file and unset the config value. - * - * @param string $filePath * @return void */ - private function deleteCertificateAndReset(string $filePath): void + public function beforeSave() { - if (!empty($filePath)) { - $absolutePath = $this->varDirectory->getAbsolutePath('truelayer/' . $filePath); - if ($this->file->fileExists($absolutePath)) { - $this->file->rm($absolutePath); + $this->_dataSaveAllowed = false; + $value = (string)$this->getValue(); + // don't save value, if an obscured value was received. This indicates that data was not changed. + if (!preg_match('/^\*+$/', $value) && !empty($value)) { + $this->_dataSaveAllowed = true; + $decoded = base64_decode($value, true); + if (!$decoded || @base64_encode($decoded) !== $value) { + $decoded = ''; } - } - - $this->setValue(''); - } - - /** - * Returns the directory based on set scope. - * - * @param bool $sandbox - * @return string - */ - private function getDirectory(bool $sandbox): string - { - $mode = $sandbox ? 'sandbox' : 'production'; - return $this->getScope() !== 'default' - ? sprintf('%s/%s/%s/', $mode, $this->getScope(), $this->getScopeId()) - : sprintf('%s/default/', $mode); - } - - /** - * Returns the path to the uploaded tmp_file based on set scope. - * - * @param bool $sandbox - * @return string - */ - private function getTmpName(bool $sandbox): ?string - { - $files = $_FILES; - if (empty($files)) { - return null; - } - try { - $tmpName = $files['groups']['tmp_name']['general']['fields'][$sandbox ? 'sandbox_private_key' : 'production_private_key']['value']; - return empty($tmpName) ? null : $tmpName; - } catch (\Exception $e) { - return null; + $encrypted = $decoded ? $this->_encryptor->encrypt($decoded) : null; + $this->setValue($encrypted); + } elseif (empty($value)) { + $this->setValue(null); + $this->_dataSaveAllowed = true; } } } diff --git a/Service/Client/ClientFactory.php b/Service/Client/ClientFactory.php index 1fa6450..6a391af 100644 --- a/Service/Client/ClientFactory.php +++ b/Service/Client/ClientFactory.php @@ -74,7 +74,7 @@ private function createClient(array $credentials, ?bool $forceSandbox = null): ? $clientFactory->clientId($credentials['client_id']) ->clientSecret($credentials['client_secret']) ->keyId($credentials['key_id']) - ->pemFile($credentials['private_key']) + ->pem($credentials['private_key']) ->useProduction(is_null($forceSandbox) ? !$this->configProvider->isSandbox() : !$forceSandbox); if ($cacheEncryptionKey) { diff --git a/Setup/UpgradeData.php b/Setup/UpgradeData.php new file mode 100644 index 0000000..c37fe9e --- /dev/null +++ b/Setup/UpgradeData.php @@ -0,0 +1,69 @@ +getVersion(); + + if(version_compare($setupVersion, '1.0.0', '<=')) { + $this->encryptPrivateKeys(); + } + } + + private function encryptPrivateKeys() + { + $this->dataCollection->addFilter('path', ConnectionInterface::XML_PATH_SANDBOX_PRIVATE_KEY, 'or'); + $this->dataCollection->addFilter('path', ConnectionInterface::XML_PATH_PRODUCTION_PRIVATE_KEY, 'or'); + $this->dataCollection->loadWithFilter(); + /** @var \Magento\Framework\App\Config\Value[] $configItems */ + $configItems = $this->dataCollection->getItems(); + $this->dataCollection->clear()->getSelect()->reset('where'); + + $configPaths = [ConnectionInterface::XML_PATH_PRODUCTION_PRIVATE_KEY,ConnectionInterface::XML_PATH_SANDBOX_PRIVATE_KEY]; + $varDirectory = $this->filesystem->getDirectoryRead('var'); + foreach ($configItems as $configItem) { + $configPath = $configItem->getPath(); + if (!in_array($configPath, $configPaths)) { + continue; + } + $configValue = $configItem->getValue(); + if (!$configValue) { + continue; + } + $isPath = str_starts_with($configValue, 'sandbox/') || str_starts_with($configValue, 'production/'); + $absPath = $isPath ? $varDirectory->getAbsolutePath('truelayer/' . $configValue) : null; + $fileExists = $absPath ? $this->file->fileExists($absPath, true) : false; + if ($fileExists) { + $privateKey = $this->file->read($absPath, null); + $encryptedKey = $this->encryptor->encrypt($privateKey); + $configItem->setValue($encryptedKey); + $this->file->rm($absPath); + } else { + $configItem->setValue(null); + } + $this->dataResourceModel->save($configItem); + } + } +} diff --git a/etc/adminhtml/system/general.xml b/etc/adminhtml/system/general.xml index d63387e..2e26e78 100644 --- a/etc/adminhtml/system/general.xml +++ b/etc/adminhtml/system/general.xml @@ -76,18 +76,22 @@ production - + + payment/truelayer/production_private_key + TrueLayer\Connect\Block\Adminhtml\System\Config\Field\Base64FileUpload TrueLayer\Connect\Model\System\Config\Backend\PrivateKey 1 production - + + payment/truelayer/sandbox_private_key + TrueLayer\Connect\Block\Adminhtml\System\Config\Field\Base64FileUpload TrueLayer\Connect\Model\System\Config\Backend\PrivateKey 1 diff --git a/etc/module.xml b/etc/module.xml index 02fb4d7..a76b59b 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -7,7 +7,7 @@ --> - + diff --git a/tests/e2e/fixtures/base-page.ts b/tests/e2e/fixtures/base-page.ts index f3fa550..75511fb 100644 --- a/tests/e2e/fixtures/base-page.ts +++ b/tests/e2e/fixtures/base-page.ts @@ -4,7 +4,6 @@ import { ProductPage } from "../pages/product-page"; import { CheckoutPage } from "../pages/checkout-page"; import { HostedPaymentsPage } from "../pages/hosted-payments-page"; import { MockUkBankPage } from "../pages/mock-uk-bank-page"; -import { PaymentConfirmationPage } from "../pages/payment-confirmation-page"; import { MockUkBankAccountsPage } from "../pages/mock-uk-bank-accounts-page"; import { OrderConfirmationPage } from "../pages/order-confirmation-page"; @@ -14,7 +13,6 @@ export const test = base.extend<{ hostedPaymentsPage: HostedPaymentsPage; mockUkBankPage: MockUkBankPage; mockUkBankAccountsPage: MockUkBankAccountsPage; - paymentConfirmationPage: PaymentConfirmationPage; orderConfirmationPage: OrderConfirmationPage; }>({ @@ -33,10 +31,7 @@ export const test = base.extend<{ mockUkBankAccountsPage: async ({ page }, use) => { await use(new MockUkBankAccountsPage(page)); }, - paymentConfirmationPage: async ({ page }, use) => { - await use(new PaymentConfirmationPage(page)); - }, orderConfirmationPage: async ({ page }, use) => { await use(new OrderConfirmationPage(page)); }, -}); \ No newline at end of file +}); diff --git a/tests/e2e/magento-e2e-tests.spec.ts b/tests/e2e/magento-e2e-tests-no-widget.spec.ts similarity index 90% rename from tests/e2e/magento-e2e-tests.spec.ts rename to tests/e2e/magento-e2e-tests-no-widget.spec.ts index 80614d6..ed6a461 100644 --- a/tests/e2e/magento-e2e-tests.spec.ts +++ b/tests/e2e/magento-e2e-tests-no-widget.spec.ts @@ -9,14 +9,13 @@ test.describe('Truelayer magento plugin E2E Tests', () => { hostedPaymentsPage, mockUkBankPage, mockUkBankAccountsPage, - paymentConfirmationPage, orderConfirmationPage, }) => { // arrange await productPage.navigateTo(); await productPage.addToCart(); await checkoutPage.navigateToShippingStep(); - await checkoutPage.fillShippingDetailsAndSubmit('truelayer@example.com'); + await checkoutPage.fillShippingDetailsAndSubmit('truelayer@example.com', isMobile); await checkoutPage.clickPaymentMethod(); await checkoutPage.clickPlaceOrderButton(); @@ -28,7 +27,6 @@ test.describe('Truelayer magento plugin E2E Tests', () => { } await mockUkBankPage.enterOnlineBankingDetailsAndContinue(); await mockUkBankAccountsPage.selectAccountAndContinue(); - // await paymentConfirmationPage.waitForProcessingAndContinue(); await orderConfirmationPage.waitForProcessingAndReturnToStore(); }) }); diff --git a/tests/e2e/pages/checkout-page.ts b/tests/e2e/pages/checkout-page.ts index 2430f2f..9db2317 100644 --- a/tests/e2e/pages/checkout-page.ts +++ b/tests/e2e/pages/checkout-page.ts @@ -24,7 +24,7 @@ export class CheckoutPage { // Methods - async fillShippingDetailsAndSubmit(email: string) { + async fillShippingDetailsAndSubmit(email: string, isMobile: boolean = false) { await this.emailField().isVisible(); await this.emailField().fill(email); await this.firstNameField().isVisible(); @@ -41,6 +41,9 @@ export class CheckoutPage { await this.addressField().fill('10 Downing Street') await this.postcodeField().isVisible(); await this.postcodeField().fill('SW1A 2AB'); + if (isMobile) { + await this.page.locator('body').click({position: {x: 0, y: 0}}); + } await this.submitShippingInfoAndWaitForPageLoad(); } @@ -52,11 +55,9 @@ export class CheckoutPage { } async submitShippingInfoAndWaitForPageLoad() { - await this.nextStepButton().isEnabled(); + await expect(this.nextStepButton()).toBeEnabled({timeout: 5000}); await this.nextStepButton().click(); - await this.page.waitForSelector('.loading-mask', { state: 'visible' }); - await this.page.waitForSelector('.loading-mask', { state: 'hidden' }); - await this.page.waitForSelector(this.paymentMethodSelector, { state: 'visible' }); + await this.page.waitForSelector(this.paymentMethodSelector, { state: 'visible', timeout: 15000 }); } async clickPaymentMethod() { @@ -72,4 +73,4 @@ export class CheckoutPage { await this.placeOrderButton().isEnabled(); await this.placeOrderButton().click(); } -} \ No newline at end of file +} diff --git a/tests/e2e/pages/hosted-payments-page.ts b/tests/e2e/pages/hosted-payments-page.ts index e3655be..589b5f1 100644 --- a/tests/e2e/pages/hosted-payments-page.ts +++ b/tests/e2e/pages/hosted-payments-page.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test'; +import { expect, Page } from '@playwright/test'; export class HostedPaymentsPage { page: Page; @@ -8,14 +8,14 @@ export class HostedPaymentsPage { } // Locators - mockBank = () => this.page.getByLabel('Select Mock UK Payments - Redirect Flow', { exact: true }); - continueButton = () => this.page.getByTestId('confirm-redirect-button'); - continueOnDesktopButton = () => this.page.getByTestId('continue-desktop'); + mockBank = () => this.page.getByText('Mock UK Payments - Redirect Flow', { exact: true }); + continueButton = () => this.page.getByTestId('go-to-bank-button'); + continueOnDesktopButton = () => this.page.getByText('on this device'); // Methods async selectMockBankAndContinueOnDesktop() { await this.selectMockBankAndContinue(); - await this.continueOnDesktopButton().isVisible() + await expect(this.continueOnDesktopButton()).toBeVisible({timeout: 10000}) await this.continueOnDesktopButton().click(); } @@ -24,9 +24,9 @@ export class HostedPaymentsPage { } private async selectMockBankAndContinue(){ - await this.mockBank().isVisible(); + await expect(this.mockBank()).toBeVisible({timeout: 10000}) await this.mockBank().click(); - await this.continueButton().isVisible(); + await expect(this.continueButton()).toBeVisible({timeout: 10000}) await this.continueButton().click(); } -} \ No newline at end of file +} diff --git a/tests/e2e/pages/order-confirmation-page.ts b/tests/e2e/pages/order-confirmation-page.ts index 5f1bfa5..ff86c40 100644 --- a/tests/e2e/pages/order-confirmation-page.ts +++ b/tests/e2e/pages/order-confirmation-page.ts @@ -14,6 +14,10 @@ export class OrderConfirmationPage { // Methods async waitForProcessingAndReturnToStore() { await this.paymentBeingProcessedText().isVisible(); - await expect(this.orderConfirmedText()).toBeVisible({timeout: 90000}); + await this.expectOrderConfirmed(90000); } -} \ No newline at end of file + + async expectOrderConfirmed(timeout: number) { + await expect(this.orderConfirmedText()).toBeVisible({timeout}); + } +} diff --git a/tests/e2e/pages/payment-confirmation-page.ts b/tests/e2e/pages/payment-confirmation-page.ts deleted file mode 100644 index 521c34f..0000000 --- a/tests/e2e/pages/payment-confirmation-page.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect, Page } from '@playwright/test'; - -export class PaymentConfirmationPage { - page: Page; - - constructor(page: Page) { - this.page = page; - } - - // Locators - paymentProcessingText = () => this.page.getByText('In progress'); - paymentConfirmedText = () => this.page.getByText('All done'); - continueButton = () => this.page.getByRole('button', {name: 'continue'}) - - // Methods - async waitForProcessingAndContinue() { - await expect(this.paymentProcessingText().or(this.paymentConfirmedText())).toBeVisible({timeout: 40000}) - await expect(this.continueButton()).toBeVisible(); - await this.continueButton().click(); - } -} \ No newline at end of file diff --git a/tests/e2e/pages/product-page.ts b/tests/e2e/pages/product-page.ts index 43dfc61..390a6ce 100644 --- a/tests/e2e/pages/product-page.ts +++ b/tests/e2e/pages/product-page.ts @@ -17,11 +17,11 @@ export class ProductPage { await this.addToCartButton().click(); const cartCounter = this.cartCounter() await cartCounter.isVisible(); - await expect(cartCounter).toHaveText('1', {timeout: 1000}) + await expect(cartCounter).toHaveText('1', {timeout: 5000}) } async navigateTo(){ const url = `${process.env.E2E_TEST_URL as string}/catalog/product/view/id/1/s/test-product/category/2/`; await this.page.goto(url); } -} \ No newline at end of file +} diff --git a/view/adminhtml/templates/system/config/button/base64-file-upload.phtml b/view/adminhtml/templates/system/config/button/base64-file-upload.phtml new file mode 100644 index 0000000..68f3353 --- /dev/null +++ b/view/adminhtml/templates/system/config/button/base64-file-upload.phtml @@ -0,0 +1,56 @@ +getData('mode'); +$htmlClass = 'truelayer_upload-file-as-string_'.$mode; +$scope = 'truelayer_private-key-upload_'.$mode; +$fieldType = $block->getData('fieldType'); +$displayValue = $block->getData('displayValue'); +$disabled = $block->getData('disabled'); +$disabledHtml = $disabled ? 'disabled="disabled"' : ""; + +$htmlTextInputId = $block->getData('htmlTextInputId'); +$htmlFileInputId = $htmlTextInputId.'_file'; +?> + + + + /> + + + + + + diff --git a/view/adminhtml/templates/system/config/button/credentials.phtml b/view/adminhtml/templates/system/config/button/credentials.phtml index 224257e..d5c97f6 100644 --- a/view/adminhtml/templates/system/config/button/credentials.phtml +++ b/view/adminhtml/templates/system/config/button/credentials.phtml @@ -22,16 +22,16 @@ use TrueLayer\Connect\Block\Adminhtml\System\Config\Button\Credentials; document.querySelector('#truelayer_general').addEventListener('change', (e) => { // Check mode - if (e.target.getAttribute('name').includes('[mode]')) { + if (e.target.getAttribute('name')?.includes('[mode]')) { truelayer_mode = e.target.value; } if (e.target.getAttribute('type') === 'file') { const FR = new FileReader(); - FR.onload = () => { - truelayer_mode === 'sandbox' - ? private_key_sandbox = FR.result + FR.onload = () => { + truelayer_mode === 'sandbox' + ? private_key_sandbox = FR.result : private_key_production = FR.result; } @@ -46,19 +46,20 @@ use TrueLayer\Connect\Block\Adminhtml\System\Config\Button\Credentials; jQuery("input[name='groups[general][fields][production_client_id][value]']").val(), "production_client_secret": jQuery("input[name='groups[general][fields][production_client_secret][value]']").val(), + "production_private_key": + jQuery("input[name='groups[general][fields][production_private_key][value]']").val(), "production_key_id": jQuery("input[name='groups[general][fields][production_key_id][value]']").val(), "sandbox_client_id": jQuery("input[name='groups[general][fields][sandbox_client_id][value]']").val(), "sandbox_client_secret": jQuery("input[name='groups[general][fields][sandbox_client_secret][value]']").val(), + "sandbox_private_key": + jQuery("input[name='groups[general][fields][sandbox_private_key][value]']").val(), "sandbox_key_id": jQuery("input[name='groups[general][fields][sandbox_key_id][value]']").val(), "mode": jQuery("select[name='groups[general][fields][mode][value]']").val(), - "private_key": truelayer_mode === 'sandbox' ? private_key_sandbox : private_key_production, - "delete_private_key": - jQuery("input[name='groups[general][fields][sandbox_private_key][value][delete]']").is(':checked'), }; new Ajax.Request('escapeUrl($block->getApiCheckUrl()) ?>', { diff --git a/view/adminhtml/web/js/handle-upload.js b/view/adminhtml/web/js/handle-upload.js new file mode 100644 index 0000000..94763a9 --- /dev/null +++ b/view/adminhtml/web/js/handle-upload.js @@ -0,0 +1,35 @@ +/** + * Copyright © TrueLayer Ltd, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', +], function (Component) { + 'use strict'; + + return Component.extend({ + clickUpload() { + document.getElementById(this.htmlFileInputId).click(); + }, + handleFile(object, event) { + const textInput = document.getElementById(this.htmlTextInputId); + // Get a reference to the file + const file = event?.target?.files[0]; + + if (!file) { + return; + } + // Encode the file using the FileReader API + const reader = new FileReader(); + reader.onloadend = () => { + // Use a regex to remove data url part + const base64String = reader.result + .replace(/^data:.+,/, ""); + + textInput.value = base64String; + }; + reader.readAsDataURL(file); + } + }); +});