diff --git a/Api/Data/MagentoDebugInfo.php b/Api/Data/MagentoDebugInfo.php index 2c4ccf8..fa89495 100644 --- a/Api/Data/MagentoDebugInfo.php +++ b/Api/Data/MagentoDebugInfo.php @@ -4,11 +4,17 @@ class MagentoDebugInfo implements MagentoDebugInfoInterface { + /** @var string */ protected $moduleVersion; + /** @var string */ protected $magentoVersion; + /** @var string */ protected $client; + /** @var string */ protected $session; + /** @var MagentoDebugInfo\ConfigurationInterface */ protected $configuration; + /** @var MagentoDebugInfo\MagentoModuleInterface[] */ protected $modules; /** diff --git a/Api/Data/MagentoDebugInfo/Configuration.php b/Api/Data/MagentoDebugInfo/Configuration.php index 39114b6..b23c27e 100644 --- a/Api/Data/MagentoDebugInfo/Configuration.php +++ b/Api/Data/MagentoDebugInfo/Configuration.php @@ -4,7 +4,9 @@ class Configuration implements ConfigurationInterface { + /** @var string */ protected $key; + /** @var string */ protected $secret; /** diff --git a/Api/Data/MagentoDebugInfo/MagentoModule.php b/Api/Data/MagentoDebugInfo/MagentoModule.php index 3c0fb0d..1db026b 100644 --- a/Api/Data/MagentoDebugInfo/MagentoModule.php +++ b/Api/Data/MagentoDebugInfo/MagentoModule.php @@ -4,7 +4,9 @@ class MagentoModule implements MagentoModuleInterface { + /** @var string */ private string $name; + /** @var string */ private string $setupVersion; /** diff --git a/Block/System/Config/Status.php b/Block/System/Config/Status.php index aa2f2b9..7ade1de 100644 --- a/Block/System/Config/Status.php +++ b/Block/System/Config/Status.php @@ -19,20 +19,33 @@ class Status extends Template implements RendererInterface public const CACHE_ID = 'postcode-eu-status'; public const CACHE_LIFETIME_SECONDS = 3600; + /** @var string */ protected $_template = 'PostcodeEu_AddressValidation::system/config/status.phtml'; + /** @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $_scopeConfig; + /** @var StoreConfigHelper */ protected $_storeConfigHelper; + /** @var ApiClientHelper */ protected $_apiClientHelper; + /** @var ConfigInterface */ protected $_resourceConfig; + /** @var CacheTypeList */ protected $_cacheTypeList; + /** @var CacheFrontendPool */ protected $_cacheFrontendPool; + /** @var SerializerInterface */ protected $_serializer; + /** @var DataHelper */ protected $_dataHelper; + /** @var UpdateNotifier */ protected $_updateNotifier; + /** @var array|null */ private $_cachedData; + /** @var array */ public array $accountInfo; + /** @var array */ public array $moduleInfo; /** @@ -191,13 +204,15 @@ public function isStatusActive(): bool private function _getCachedData(): array { $cache = $this->_cacheFrontendPool->get(\Magento\Framework\App\Cache\Type\Config::TYPE_IDENTIFIER); - $cachedData = $cache->load(self::CACHE_ID); + [$scopeType, $scopeId] = $this->_storeConfigHelper->getScopeFromRequest(); + $cacheId = implode('-', [self::CACHE_ID, $scopeType, $scopeId]); + $cachedData = $cache->load($cacheId); if ($cachedData === false) { $data = []; $data['accountInfo'] = $this->_getAccountInfo(); $data['moduleInfo'] = $this->_dataHelper->getModuleInfo(); - $cache->save($this->_serializer->serialize($data), self::CACHE_ID, [], self::CACHE_LIFETIME_SECONDS); + $cache->save($this->_serializer->serialize($data), $cacheId, [], self::CACHE_LIFETIME_SECONDS); return $data; } diff --git a/Controller/Adminhtml/Address/Api.php b/Controller/Adminhtml/Address/Api.php new file mode 100644 index 0000000..bc27138 --- /dev/null +++ b/Controller/Adminhtml/Address/Api.php @@ -0,0 +1,72 @@ +_resultJsonFactory = $resultJsonFactory; + $this->_postcodeModel = $postcodeModel; + $this->_serviceOutputProcessor = $serviceOutputProcessor; + } + + /** + * Call address API methods + * + * @return Json + */ + public function execute(): Json + { + $resultJson = $this->_resultJsonFactory->create(); + $request = $this->getRequest(); + + try { + switch ($request->getParam('method')) { + case 'postcode': + $serviceMethod = 'getNlAddress'; + $params = ['postcode', 'house_number']; + break; + case 'autocomplete': + $serviceMethod = 'getAddressAutocomplete'; + $params = ['context', 'term']; + break; + case 'address_details': + $serviceMethod = 'getAddressDetails'; + $params = ['context']; + break; + default: + throw new \Exception('Invalid service method'); + } + + $values = array_filter($request->getParams(), fn($key) => in_array($key, $params), ARRAY_FILTER_USE_KEY); + $result = $this->_postcodeModel->$serviceMethod(...array_values($values)); + $result = $this->_serviceOutputProcessor->process($result, PostcodeModelInterface::class, $serviceMethod); + return $resultJson->setData($result); + } catch (\Exception $e) { + return $resultJson->setHttpResponseCode(400)->setData(['error' => $e->getMessage()]); + } + } +} diff --git a/Cron/NotifyModuleUpdate.php b/Cron/NotifyModuleUpdate.php index 4381827..ce685ef 100644 --- a/Cron/NotifyModuleUpdate.php +++ b/Cron/NotifyModuleUpdate.php @@ -8,8 +8,11 @@ class NotifyModuleUpdate { + /** @var LoggerInterface */ protected $_logger; + /** @var DataHelper */ protected $_dataHelper; + /** @var UpdateNotifier */ protected $_updateNotifier; /** diff --git a/Cron/UpdateApiData.php b/Cron/UpdateApiData.php index 5d3adba..f08bb61 100644 --- a/Cron/UpdateApiData.php +++ b/Cron/UpdateApiData.php @@ -2,17 +2,39 @@ namespace PostcodeEu\AddressValidation\Cron; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use PostcodeEu\AddressValidation\Helper\ApiClientHelper; use PostcodeEu\AddressValidation\Helper\StoreConfigHelper; -use Magento\Framework\App\Config\Storage\WriterInterface; use Psr\Log\LoggerInterface; class UpdateApiData { + /** @var LoggerInterface */ protected $_logger; + /** @var ApiClientHelper */ protected $_apiClientHelper; + /** @var WriterInterface */ protected $_configWriter; + /** @var StoreConfigHelper */ protected $_storeConfigHelper; + /** @var StoreManagerInterface */ + protected $_storeManager; + /** @var CollectionFactory */ + protected $_configCollectionFactory; + /** @var EncryptorInterface */ + protected $_encryptor; + /** @var TypeListInterface */ + protected $_cacheTypeList; + /** @var bool */ + protected $_hasChanges = false; + /** @var array */ + protected $_existingConfig = []; /** * Constructor @@ -22,58 +44,199 @@ class UpdateApiData * @param ApiClientHelper $apiClientHelper * @param WriterInterface $configWriter * @param StoreConfigHelper $storeConfigHelper - * @return void + * @param StoreManagerInterface $storeManager + * @param CollectionFactory $configCollectionFactory + * @param EncryptorInterface $encryptor + * @param TypeListInterface $cacheTypeList */ public function __construct( LoggerInterface $logger, ApiClientHelper $apiClientHelper, WriterInterface $configWriter, - StoreConfigHelper $storeConfigHelper + StoreConfigHelper $storeConfigHelper, + StoreManagerInterface $storeManager, + CollectionFactory $configCollectionFactory, + EncryptorInterface $encryptor, + TypeListInterface $cacheTypeList ) { $this->_logger = $logger; $this->_apiClientHelper = $apiClientHelper; $this->_configWriter = $configWriter; $this->_storeConfigHelper = $storeConfigHelper; + $this->_storeManager = $storeManager; + $this->_configCollectionFactory = $configCollectionFactory; + $this->_encryptor = $encryptor; + $this->_cacheTypeList = $cacheTypeList; } /** - * Update Postcode.eu API account data. + * Update API data on each scope. */ public function execute(): void { - if (!$this->_storeConfigHelper->hasCredentials()) { - return; // No credentials so nothing to do here. + $this->_hasChanges = false; + $this->_preloadConfig(); + $scopesToProcess = [['type' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 'id' => 0, 'name' => 'Default']]; + $groupedScopes = []; + + foreach ($this->_storeManager->getWebsites() as $website) { + $scopesToProcess[] = [ + 'type' => ScopeInterface::SCOPE_WEBSITES, + 'id' => (int)$website->getId(), + 'name' => $website->getName(), + ]; } - $this->_logger->info(__('Postcode.eu API data update start')); + foreach ($this->_storeManager->getStores() as $store) { + $scopesToProcess[] = [ + 'type' => ScopeInterface::SCOPE_STORES, + 'id' => (int)$store->getId(), + 'name' => $store->getName(), + ]; + } - try { + foreach ($scopesToProcess as $scope) { + [$key, $secret] = $this->_getCredentials($scope['type'], $scope['id']); - $client = $this->_apiClientHelper->getApiClient(); - $accountInfo = $client->accountInfo(); - $this->_configWriter->save(StoreConfigHelper::PATH['account_name'], $accountInfo['name']); + if (!isset($key, $secret)) { + continue; + } - if ($accountInfo['hasAccess']) { + $hash = md5($key . ':' . $secret); + $groupedScopes[$hash] ??= ['key' => $key, 'secret' => $secret, 'scopes' => []]; + $groupedScopes[$hash]['scopes'][] = $scope; - $this->_configWriter->save(StoreConfigHelper::PATH['account_status'], ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE); - $countries = $client->internationalGetSupportedCountries(); - $this->_configWriter->save(StoreConfigHelper::PATH['supported_countries'], json_encode($countries)); - $iso2Codes = array_column($countries, 'iso2'); - $this->_logger->info(__('Postcode.eu API countries updated: ') . implode(', ', $iso2Codes)); + } - } else { + foreach ($groupedScopes as $group) { + $this->_updateApiDataForGroup($group['key'], $group['secret'], $group['scopes']); + } + + if ($this->_hasChanges) { + $this->_cacheTypeList->cleanType('config'); + } + } + + /** + * Update Postcode.eu API account data for a group of scopes sharing the same credentials. + * + * @param string $key + * @param string $secret + * @param array $scopes + */ + private function _updateApiDataForGroup(string $key, string $secret, array $scopes): void + { + $scopeNames = array_column($scopes, 'name'); + $scopeNamesJoined = implode(', ', $scopeNames); + + $this->_logger->info(sprintf( + 'Postcode.eu API data update start for scope(s): "%s".', + $scopeNamesJoined + )); + + $client = $this->_apiClientHelper->getApiClient(); + $client->setCredentials($key, $secret); + + try { + $accountInfo = $client->accountInfo(); + $accountName = $accountInfo['name'] ?? '[UNKNOWN]'; + $hasAccess = $accountInfo['hasAccess'] ?? false; + $accountStatus = $hasAccess + ? ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE + : ApiClientHelper::API_ACCOUNT_STATUS_INACTIVE; + $countriesJson = null; + + if ($hasAccess) { + $countries = $client->internationalGetSupportedCountries(); + $countriesJson = json_encode($countries, JSON_THROW_ON_ERROR); + $this->_logger->info(sprintf( + 'Postcode.eu API countries updated for scope(s) "%s": %s', + $scopeNamesJoined, + implode(', ', array_column($countries, 'iso2')) + )); + } - $this->_configWriter->save(StoreConfigHelper::PATH['account_status'], ApiClientHelper::API_ACCOUNT_STATUS_INACTIVE); + foreach ($scopes as $scope) { + $this->_saveIfChanged(StoreConfigHelper::PATH['account_name'], $accountName, $scope['type'], $scope['id']); + $this->_saveIfChanged(StoreConfigHelper::PATH['account_status'], $accountStatus, $scope['type'], $scope['id']); + $this->_saveIfChanged(StoreConfigHelper::PATH['supported_countries'], $countriesJson, $scope['type'], $scope['id']); } - $this->_logger->info(__('Postcode.eu API account info updated: ') . 'name: ' . $accountInfo['name'] . ', status: ' . ($accountInfo['hasAccess'] ? 'active' : 'inactive')); + $this->_logger->info(sprintf( + 'Postcode.eu API account info updated for scope(s) "%s": name "%s", status "%s".', + $scopeNamesJoined, + $accountName, + $accountStatus + )); + } catch (\Throwable $e) { + $this->_logger->error(sprintf( + 'Postcode.eu API data update FAILED for scope(s) "%s": %s', + $scopeNamesJoined, + $e->getMessage() + ), [ + 'exception' => $e, + ]); + } + } - } catch (\Exception $e) { + /** + * Pre-load all relevant config values into memory. + */ + private function _preloadConfig(): void + { + $collection = $this->_configCollectionFactory->create(); + $collection->addFieldToFilter('path', ['in' => [ + StoreConfigHelper::PATH['api_key'], + StoreConfigHelper::PATH['api_secret'], + StoreConfigHelper::PATH['account_name'], + StoreConfigHelper::PATH['account_status'], + StoreConfigHelper::PATH['supported_countries'] + ]]); + + $this->_existingConfig = []; + + foreach ($collection as $item) { + $this->_existingConfig[$item->getScope()][$item->getScopeId()][$item->getPath()] = $item->getValue(); + } + } - $this->_logger->error(__('Postcode.eu API data update FAILED: ') . json_encode($e->getMessage())); - return; + /** + * Save config value only if it has changed. + * + * @param string $path + * @param mixed $value + * @param string $scopeType + * @param int $scopeId + */ + private function _saveIfChanged(string $path, $value, string $scopeType, int $scopeId): void + { + $currentValue = $this->_existingConfig[$scopeType][$scopeId][$path] ?? null; + $normalizedValue = $value === null ? null : (string)$value; + + if ($currentValue !== $normalizedValue) { + if ($value === null) { + $this->_configWriter->delete($path, $scopeType, $scopeId); + } else { + $this->_configWriter->save($path, $value, $scopeType, $scopeId); + } + + $this->_existingConfig[$scopeType][$scopeId][$path] = $normalizedValue; + $this->_hasChanges = true; } + } + + /** + * Get credentials explicitly defined at the given scope. + * Does NOT fall back to parent scopes. + * + * @return array [key, secret] + */ + private function _getCredentials(string $scopeType, int $scopeId): array + { + $key = $this->_existingConfig[$scopeType][$scopeId][StoreConfigHelper::PATH['api_key']] ?? null; + $secretValue = $this->_existingConfig[$scopeType][$scopeId][StoreConfigHelper::PATH['api_secret']] ?? null; + $secret = $secretValue === null ? null : $this->_encryptor->decrypt($secretValue); - $this->_logger->info(__('Postcode.eu API data update complete')); + return [$key, $secret]; } } diff --git a/GraphQl/Exception/GraphQlHeaderException.php b/GraphQl/Exception/GraphQlHeaderException.php deleted file mode 100644 index 6344e12..0000000 --- a/GraphQl/Exception/GraphQlHeaderException.php +++ /dev/null @@ -1,51 +0,0 @@ -isSafe = $isSafe; - parent::__construct($phrase, $cause, $code); - } - - /** - * @inheritdoc - */ - public function isClientSafe(): bool - { - return $this->isSafe; - } - - /** - * @inheritdoc - */ - public function getCategory(): string - { - return self::EXCEPTION_CATEGORY; - } -} diff --git a/Helper/ApiClientHelper.php b/Helper/ApiClientHelper.php index eac8b10..2769e9a 100644 --- a/Helper/ApiClientHelper.php +++ b/Helper/ApiClientHelper.php @@ -25,18 +25,31 @@ class ApiClientHelper extends AbstractHelper public const API_ACCOUNT_STATUS_INACTIVE = 'inactive'; public const API_ACCOUNT_STATUS_ACTIVE = 'active'; + /** @var array|null */ protected $_modules; + /** @var ModuleListInterface */ protected $_moduleList; + /** @var Data */ protected $_developerHelper; + /** @var Request */ protected $_request; + /** @var Response */ protected $_response; + /** @var PostcodeApiClient */ protected $_client; + /** @var LocaleResolver */ protected $_localeResolver; + /** @var array */ protected $_countryCodeMap = []; + /** @var StoreConfigHelper */ protected $_storeConfigHelper; + /** @var ProductMetadataInterface */ protected $_productMetadata; + /** @var RegionFactory */ protected $_regionFactory; + /** @var AddressHelper */ protected $_addressHelper; + /** @var LoggerInterface */ protected $_logger; /** @@ -303,7 +316,6 @@ public function getNlAddress(string $zipCode, string $houseNumber): array } try { - $client = $this->getApiClient(); $address = $client->dutchAddressByPostcode($zipCode, $houseNumber, $houseNumberAddition); $status = 'valid'; @@ -312,6 +324,7 @@ public function getNlAddress(string $zipCode, string $houseNumber): array || (!empty($address['houseNumberAdditions']) && null === $address['houseNumberAddition']) ) { $status = 'houseNumberAdditionIncorrect'; + $unknownHouseNumberAddition = $houseNumberAddition; } } catch (NotFoundException $e) { return ['status' => 'notFound', 'address' => null]; @@ -322,12 +335,18 @@ public function getNlAddress(string $zipCode, string $houseNumber): array $formattedHouseNumberAdditions = []; foreach ($address['houseNumberAdditions'] ?? [] as $addition) { - $houseNumberWithAddition = rtrim($address['houseNumber'] . ' ' . $addition); - $formattedHouseNumberAdditions[] = [ - 'label' => $houseNumberWithAddition, - 'value' => $houseNumberWithAddition, - 'houseNumberAddition' => $addition, - ]; + $formattedHouseNumberAdditions[] = $this->_formatHouseNumberAdditionOption( + $address['houseNumber'], + $addition + ); + } + + if (isset($unknownHouseNumberAddition)) { + $formattedHouseNumberAdditions[] = $this->_formatHouseNumberAdditionOption( + $address['houseNumber'], + $unknownHouseNumberAddition, + '(' . __('unknown addition') . ')' + ); } $address['houseNumberAdditions'] = $formattedHouseNumberAdditions; @@ -344,6 +363,25 @@ public function getNlAddress(string $zipCode, string $houseNumber): array return $this->_prepareResponse($result, $client); } + /** + * Format house number with addition for use in select UI component. + * + * @access private + * @param int $houseNumber + * @param string $addition + * @param string $labelSuffix - Additional text to append to the label. + * @return array + */ + private function _formatHouseNumberAdditionOption(int $houseNumber, string $addition, string $labelSuffix = null): array + { + $houseNumberWithAddition = rtrim($houseNumber . ' ' . $addition); + return [ + 'label' => rtrim($houseNumberWithAddition . ' ' . ($labelSuffix ?? '')), + 'value' => $houseNumberWithAddition, + 'houseNumberAddition' => $addition, + ]; + } + /** * _handleClientException function. * @@ -353,8 +391,11 @@ public function getNlAddress(string $zipCode, string $houseNumber): array */ private function _handleClientException(\Exception $exception): array { - if (!$exception instanceof \PostcodeEu\AddressValidation\Service\Exception\NotFoundException) { + if ($exception instanceof NotFoundException) { + $this->_response->setHttpResponseCode(404); + } else { $this->_logger->error($exception->getMessage(), ['exception' => $exception]); + $this->_response->setHttpResponseCode(400); } $result = ['error' => true, 'message' => __('Something went wrong. Please try again.')]; @@ -473,6 +514,11 @@ public function getCountryIso3Code(string $iso2Code): ?string return $this->_countryCodeMap[$mapKey][strtoupper($iso2Code)] ?? null; } + /** + * Get account info + * + * @return array + */ public function getAccountInfo(): array { try { @@ -536,18 +582,16 @@ private function _getDebugInfo(): array ]; // Module version - $debug['moduleVersion'] = $this->_storeConfigHelper->getModuleVersion(); + $debug['module_version'] = $this->_storeConfigHelper->getModuleVersion(); // Magento version $version = $this->_productMetadata->getVersion(); - $debug['magentoVersion'] = 'Magento/' . $version; + $debug['magento_version'] = 'Magento/' . $version; if ($this->_getModuleInfo('Enterprise_CatalogPermissions') !== null) { - $debug['magentoVersion'] = 'MagentoEnterprise/' . $version; - + $debug['magento_version'] = 'MagentoEnterprise/' . $version; } elseif ($this->_getModuleInfo('Enterprise_Enterprise') !== null) { - - $debug['magentoVersion'] = 'MagentoProfessional/' . $version; + $debug['magento_version'] = 'MagentoProfessional/' . $version; } $debug['client'] = $this->getApiClient()->getUserAgent(); diff --git a/Helper/Data.php b/Helper/Data.php index a3f9503..ae85a4f 100644 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -68,54 +68,58 @@ public function __construct( * Check if formatted output is disabled. * * @access public + * @param int|string|null $storeId * @return bool */ - public function isFormattedOutputDisabled(): bool + public function isFormattedOutputDisabled($storeId = null): bool { return - $this->isDisabled() - || ShowHideAddressFields::FORMAT != $this->_storeConfigHelper->getValue('show_hide_address_fields'); + $this->isDisabled($storeId) + || ShowHideAddressFields::FORMAT != $this->_storeConfigHelper->getValue('show_hide_address_fields', $storeId); } /** * Check if Dutch API component is disabled. * * @access public + * @param int|string|null $storeId * @return bool */ - public function isNlComponentDisabled(): bool + public function isNlComponentDisabled($storeId = null): bool { return - $this->isDisabled() - || false === in_array('NL', $this->_storeConfigHelper->getEnabledCountries()) - || NlInputBehavior::ZIP_HOUSE != $this->_storeConfigHelper->getValue('nl_input_behavior'); + $this->isDisabled($storeId) + || false === in_array('NL', $this->_storeConfigHelper->getEnabledCountries($storeId)) + || NlInputBehavior::ZIP_HOUSE != $this->_storeConfigHelper->getValue('nl_input_behavior', $storeId); } /** * Check if the module is disabled. * * @access public + * @param int|string|null $storeId * @return bool */ - public function isDisabled(): bool + public function isDisabled($storeId = null): bool { return - false === $this->_storeConfigHelper->isSetFlag('enabled') - || ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE != $this->_storeConfigHelper->getValue('account_status'); + false === $this->_storeConfigHelper->isSetFlag('enabled', $storeId) + || ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE != $this->_storeConfigHelper->getValue('account_status', $storeId); } /** * Check if autofill bypass is disabled. * * @access public + * @param int|string|null $storeId * @return bool */ - public function isAutofillBypassDisabled(): bool + public function isAutofillBypassDisabled($storeId = null): bool { return - $this->isDisabled() - || ShowHideAddressFields::SHOW == $this->_storeConfigHelper->getValue('show_hide_address_fields') - || $this->_storeConfigHelper->isSetFlag('allow_autofill_bypass') === false; + $this->isDisabled($storeId) + || ShowHideAddressFields::SHOW == $this->_storeConfigHelper->getValue('show_hide_address_fields', $storeId) + || $this->_storeConfigHelper->isSetFlag('allow_autofill_bypass', $storeId) === false; } /** @@ -131,7 +135,7 @@ public function getModuleInfo(): array $data = $this->_getPackageData(); $latest_version = $data['packages'][self::VENDOR_PACKAGE][0]['version']; } catch (LocalizedException $e) { - $this->_logger->error(__('Failed to get package data: "%1".', $e->getMessage())); + $this->_logger->error('Failed to get package data:', ['exception' => $e]); $latest_version = $version; } diff --git a/Helper/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index 34c9d3e..1abd0ff 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -2,24 +2,38 @@ namespace PostcodeEu\AddressValidation\Helper; +use Magento\Backend\Model\UrlInterface as BackendUrlInterface; +use Magento\Developer\Helper\Data as DeveloperHelperData; +use Magento\Directory\Model\ResourceModel\Country\CollectionFactory as CountryCollectionFactory; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Developer\Helper\Data as DeveloperHelperData; +use Magento\Framework\App\State as AppState; +use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Directory\Model\ResourceModel\Country\CollectionFactory as CountryCollectionFactory; use Magento\Framework\Locale\ResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use PostcodeEu\AddressValidation\Model\Config\Source\NlInputBehavior; use PostcodeEu\AddressValidation\Model\Config\Source\ShowHideAddressFields; class StoreConfigHelper extends AbstractHelper { + /** @var StoreManagerInterface */ protected $_storeManager; + /** @var DeveloperHelperData */ protected $_developerHelper; + /** @var EncryptorInterface */ protected $_encryptor; + /** @var CountryCollectionFactory */ protected $_countryCollectionFactory; + /** @var ResolverInterface */ protected $_localeResolver; + /** @var FormKey */ + protected $_formKey; + /** @var AppState */ + protected $_appState; + /** @var BackendUrlInterface */ + protected $_backendUrl; public const PATH = [ // Status @@ -52,6 +66,9 @@ class StoreConfigHelper extends AbstractHelper * @param EncryptorInterface $encryptor * @param CountryCollectionFactory $countryCollectionFactory * @param ResolverInterface $localeResolver + * @param FormKey $formKey + * @param AppState $appState + * @param BackendUrlInterface $backendUrl */ public function __construct( Context $context, @@ -59,13 +76,19 @@ public function __construct( DeveloperHelperData $developerHelper, EncryptorInterface $encryptor, CountryCollectionFactory $countryCollectionFactory, - ResolverInterface $localeResolver + ResolverInterface $localeResolver, + FormKey $formKey, + AppState $appState, + BackendUrlInterface $backendUrl ) { $this->_storeManager = $storeManager; $this->_developerHelper = $developerHelper; $this->_encryptor = $encryptor; $this->_countryCollectionFactory = $countryCollectionFactory; $this->_localeResolver = $localeResolver; + $this->_formKey = $formKey; + $this->_appState = $appState; + $this->_backendUrl = $backendUrl; parent::__construct($context); } @@ -74,11 +97,13 @@ public function __construct( * * @access public * @param string $path - Full path or alias as specified in PATH constant. + * @param int|string|null $storeId * @return string|null */ - public function getValue($path): ?string + public function getValue($path, $storeId = null): ?string { - return $this->scopeConfig->getValue(static::PATH[$path] ?? $path, ScopeInterface::SCOPE_STORE); + [$scopeType, $scopeCode] = $this->_getScopeContext($storeId); + return $this->scopeConfig->getValue(static::PATH[$path] ?? $path, $scopeType, $scopeCode); } /** @@ -86,45 +111,50 @@ public function getValue($path): ?string * * @access public * @param string $path - Full path or alias as specified in PATH constant. + * @param int|string|null $storeId * @return bool */ - public function isSetFlag($path): bool + public function isSetFlag($path, $storeId = null): bool { - return $this->scopeConfig->isSetFlag(static::PATH[$path] ?? $path, ScopeInterface::SCOPE_STORE); + [$scopeType, $scopeCode] = $this->_getScopeContext($storeId); + return $this->scopeConfig->isSetFlag(static::PATH[$path] ?? $path, $scopeType, $scopeCode); } /** * Get enabled status. * * @access public + * @param int|string|null $storeId * @return bool */ - public function isEnabled(): bool + public function isEnabled($storeId = null): bool { - return $this->isSetFlag('enabled'); + return $this->isSetFlag('enabled', $storeId); } /** * Get supported countries from config. * * @access public + * @param int|string|null $storeId * @return array */ - public function getSupportedCountries(): array + public function getSupportedCountries($storeId = null): array { - return json_decode($this->getValue('supported_countries') ?? '[]'); + return json_decode($this->getValue('supported_countries', $storeId) ?? '[]'); } /** * Get supported countries, excluding disabled countries. * * @access public + * @param int|string|null $storeId * @return array */ - public function getEnabledCountries(): array + public function getEnabledCountries($storeId = null): array { - $supported = array_column($this->getSupportedCountries(), 'iso2'); - $disabled = $this->getValue('disabled_countries'); + $supported = array_column($this->getSupportedCountries($storeId), 'iso2'); + $disabled = $this->getValue('disabled_countries', $storeId); if (empty($disabled)) { return $supported; @@ -198,28 +228,48 @@ public function getModuleVersion(): string * Get settings to be used in frontend. * * @access public + * @param int|string|null $storeId * @return array */ - public function getJsinit(): array + public function getJsinit($storeId = null): array { - $baseUrl = $this->getCurrentStoreBaseUrl(); - $apiBaseUrl = $baseUrl . 'postcode-eu/V1/'; + $baseUrl = $this->getCurrentStoreBaseUrl($storeId); + $isAdmin = false; + + try { + $isAdmin = $this->_appState->getAreaCode() === \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE; + } catch (\Magento\Framework\Exception\LocalizedException $e) { + // Area code not set + } + + if ($isAdmin) { + $apiBaseUrl = $this->_backendUrl->getUrl('postcode_eu/address/api'); + $apiActions = [ + 'dutchAddressLookup' => $apiBaseUrl . 'method/postcode/postcode/{postcode}/house_number/{houseNumber}', + 'autocomplete' => $apiBaseUrl . 'method/autocomplete/context/{context}/term/{term}', + 'addressDetails' => $apiBaseUrl . 'method/address_details/context/{context}', + ]; + } else { + $apiBaseUrl = $baseUrl . 'postcode-eu/V1/'; + $formKey = $this->_formKey->getFormKey(); + $apiActions = [ + 'dutchAddressLookup' => $apiBaseUrl . 'nl/address/{postcode}/{houseNumber}?form_key=' . $formKey, + 'autocomplete' => $apiBaseUrl . 'international/autocomplete/{context}/{term}?form_key=' . $formKey, + 'addressDetails' => $apiBaseUrl . 'international/address/{context}?form_key=' . $formKey, + 'validate' => $apiBaseUrl . 'international/validate/{country}?form_key=' . $formKey, + ]; + } return [ - 'enabled_countries' => $this->getEnabledCountries(), - 'nl_input_behavior' => $this->getValue('nl_input_behavior') ?? NlInputBehavior::ZIP_HOUSE, - 'show_hide_address_fields' => $this->getValue('show_hide_address_fields') ?? ShowHideAddressFields::SHOW, + 'enabled_countries' => $this->getEnabledCountries($storeId), + 'nl_input_behavior' => $this->getValue('nl_input_behavior', $storeId) ?? NlInputBehavior::ZIP_HOUSE, + 'show_hide_address_fields' => $this->getValue('show_hide_address_fields', $storeId) ?? ShowHideAddressFields::SHOW, 'base_url' => $baseUrl, - 'api_actions' => [ - 'dutchAddressLookup' => $apiBaseUrl . 'nl/address', - 'autocomplete' => $apiBaseUrl . 'international/autocomplete', - 'addressDetails' => $apiBaseUrl . 'international/address', - 'validate' => $apiBaseUrl . 'international/validate', - ], - 'debug' => $this->isDebugging(), - 'change_fields_position' => $this->isSetFlag('change_fields_position'), - 'allow_pobox_shipping' => $this->isSetFlag('allow_pobox_shipping'), - 'split_street_values' => $this->isSetFlag('split_street_values'), + 'api_actions' => $apiActions, + 'debug' => $this->isDebugging($storeId), + 'change_fields_position' => $this->isSetFlag('change_fields_position', $storeId), + 'allow_pobox_shipping' => $this->isSetFlag('allow_pobox_shipping', $storeId), + 'split_street_values' => $this->isSetFlag('split_street_values', $storeId), ]; } @@ -227,11 +277,12 @@ public function getJsinit(): array * Get the base URL of the current store. * * @access public + * @param int|string|null $storeId * @return string */ - public function getCurrentStoreBaseUrl(): string + public function getCurrentStoreBaseUrl($storeId = null): string { - $currentStore = $this->_storeManager->getStore(); + $currentStore = $this->_storeManager->getStore($storeId); return $this->_urlBuilder->getBaseUrl(['_store' => $currentStore->getCode()]); } @@ -239,10 +290,59 @@ public function getCurrentStoreBaseUrl(): string * Check if debugging is active. * * @access public + * @param int|string|null $storeId * @return bool */ - public function isDebugging(): bool + public function isDebugging($storeId = null): bool + { + return $this->isSetFlag('api_debug', $storeId) && $this->_developerHelper->isDevAllowed(); + } + + /** + * Get scope from request params. + * + * @return array - Scope type and id + */ + public function getScopeFromRequest(): array { - return $this->isSetFlag('api_debug') && $this->_developerHelper->isDevAllowed(); + $storeId = $this->_request->getParam(ScopeInterface::SCOPE_STORE); + + if ($storeId !== null) { + return [ScopeInterface::SCOPE_STORES, (int)$storeId]; + } + + $websiteId = $this->_request->getParam(ScopeInterface::SCOPE_WEBSITE); + + if ($websiteId !== null) { + return [ScopeInterface::SCOPE_WEBSITES, (int)$websiteId]; + } + + return [\Magento\Framework\App\ScopeInterface::SCOPE_DEFAULT, 0]; + } + + /** + * Get scope type and code based on request. + * + * @param int|string|null $storeId + * @return array - Scope type and id + */ + private function _getScopeContext($storeId = null): array + { + if ($storeId !== null) { + return [ScopeInterface::SCOPE_STORES, $storeId]; + } + + try { + // Check for admin area. + if ($this->_appState->getAreaCode() === \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE) { + [$scope, $scopeId] = $this->getScopeFromRequest(); + + return [$scope, $scopeId ?: null]; + } + } catch (\Magento\Framework\Exception\LocalizedException $e) { + // Area code not set, fall through. + } + + return [ScopeInterface::SCOPE_STORES, null]; } } diff --git a/Model/PostcodeModel.php b/Model/PostcodeModel.php index 137e63c..4245c06 100644 --- a/Model/PostcodeModel.php +++ b/Model/PostcodeModel.php @@ -2,29 +2,34 @@ namespace PostcodeEu\AddressValidation\Model; -use PostcodeEu\AddressValidation\Helper\ApiClientHelper; -use PostcodeEu\AddressValidation\Api\PostcodeModelInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Webapi\Exception as WebapiException; use PostcodeEu\AddressValidation\Api\Data\Autocomplete as AutocompleteData; use PostcodeEu\AddressValidation\Api\Data\AutocompleteInterface as AutocompleteDataInterface; +use PostcodeEu\AddressValidation\Api\PostcodeModelInterface; +use PostcodeEu\AddressValidation\Helper\ApiClientHelper; +use PostcodeEu\AddressValidation\Service\CsrfValidator; class PostcodeModel implements PostcodeModelInterface { + /** @var ApiClientHelper */ + protected ApiClientHelper $_apiClientHelper; + /** @var CsrfValidator */ + protected CsrfValidator $_csrfValidator; /** - * @var ApiClientHelper - */ - protected $apiClientHelper; - - /** - * __construct function. + * Constructor * * @access public * @param ApiClientHelper $apiClientHelper * @return void */ - public function __construct(ApiClientHelper $apiClientHelper) - { - $this->apiClientHelper = $apiClientHelper; + public function __construct( + ApiClientHelper $apiClientHelper, + CsrfValidator $csrfValidator + ) { + $this->_apiClientHelper = $apiClientHelper; + $this->_csrfValidator = $csrfValidator; } /** @@ -32,7 +37,9 @@ public function __construct(ApiClientHelper $apiClientHelper) */ public function getAddressAutocomplete(string $context, string $term): AutocompleteDataInterface { - $result = $this->apiClientHelper->getAddressAutocomplete($context, $term); + $this->_validateRequest(); + + $result = $this->_apiClientHelper->getAddressAutocomplete($context, $term); return new AutocompleteData($result); } @@ -41,7 +48,9 @@ public function getAddressAutocomplete(string $context, string $term): Autocompl */ public function getAddressDetails(string $context): array { - $result = $this->apiClientHelper->getAddressDetails($context); + $this->_validateRequest(); + + $result = $this->_apiClientHelper->getAddressDetails($context); return [$result]; } @@ -50,7 +59,9 @@ public function getAddressDetails(string $context): array */ public function getAddressDetailsCountry(string $context, string $dispatchCountry): array { - $result = $this->apiClientHelper->getAddressDetails($context, $dispatchCountry); + $this->_validateRequest(); + + $result = $this->_apiClientHelper->getAddressDetails($context, $dispatchCountry); return [$result]; } @@ -59,7 +70,9 @@ public function getAddressDetailsCountry(string $context, string $dispatchCountr */ public function getNlAddress(string $zipCode, string $houseNumber): array { - $result = $this->apiClientHelper->getNlAddress($zipCode, $houseNumber); + $this->_validateRequest(); + + $result = $this->_apiClientHelper->getNlAddress($zipCode, $houseNumber); return [$result]; } @@ -75,7 +88,18 @@ public function validateAddress( ?string $region = null, ?string $streetAndBuilding = null ): array { - $result = $this->apiClientHelper->validateAddress(...func_get_args()); + $this->_validateRequest(); + + $result = $this->_apiClientHelper->validateAddress(...func_get_args()); return [$result]; } + + private function _validateRequest(): void + { + try { + $this->_csrfValidator->validate(); + } catch (LocalizedException $e) { + throw new WebapiException(__($e->getMessage()), 0, WebapiException::HTTP_FORBIDDEN); + } + } } diff --git a/Model/Resolver/AddressApiSettings.php b/Model/Resolver/AddressApiSettings.php deleted file mode 100644 index 6c3dbdb..0000000 --- a/Model/Resolver/AddressApiSettings.php +++ /dev/null @@ -1,39 +0,0 @@ -_storeConfigHelper = $storeConfigHelper; - } - - /** - * @inheritdoc - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - ?array $value = null, - ?array $args = null - ): array { - return $this->_storeConfigHelper->getJsinit(); - } -} diff --git a/Model/Resolver/DutchAddress.php b/Model/Resolver/DutchAddress.php deleted file mode 100644 index f354793..0000000 --- a/Model/Resolver/DutchAddress.php +++ /dev/null @@ -1,45 +0,0 @@ -_apiClientHelper = $apiClientHelper; - } - - /** - * @inheritdoc - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - ?array $value = null, - ?array $args = null - ): array { - $result = $this->_apiClientHelper->getNlAddress($args['postcode'], $args['houseNumber']); - if (!empty($result['error'])) { - throw new GraphQlInputException(__($result['message'] ?? 'Unknown error')); - } - - return $result; - } -} diff --git a/Model/Resolver/IntlAddress.php b/Model/Resolver/IntlAddress.php deleted file mode 100644 index a2b44c3..0000000 --- a/Model/Resolver/IntlAddress.php +++ /dev/null @@ -1,46 +0,0 @@ -_apiClientHelper = $apiClientHelper; - $this->_httpRequest = $httpRequest; - } - - /** - * @throws GraphQlHeaderException - */ - protected function requireSessionHeader(): void - { - $headerName = \PostcodeEu\AddressValidation\Service\PostcodeApiClient::SESSION_HEADER_KEY; - $headerValue = $this->_httpRequest->getHeader($headerName); - if (empty($headerValue)) { - throw new GraphQlHeaderException(__('%1 header not found.', $headerName)); - } - } -} diff --git a/Model/Resolver/IntlAddress/Details.php b/Model/Resolver/IntlAddress/Details.php deleted file mode 100644 index b9724eb..0000000 --- a/Model/Resolver/IntlAddress/Details.php +++ /dev/null @@ -1,33 +0,0 @@ -requireSessionHeader(); - - $result = $this->_apiClientHelper->getAddressDetails($args['context']); - if (isset($result['error'])) { - throw new GraphQlInputException(__($result['message'] ?? 'Unknown error')); - } - - return $result; - } -} diff --git a/Model/Resolver/IntlAddress/Matches.php b/Model/Resolver/IntlAddress/Matches.php deleted file mode 100644 index f24c6df..0000000 --- a/Model/Resolver/IntlAddress/Matches.php +++ /dev/null @@ -1,42 +0,0 @@ -requireSessionHeader(); - - $result = $this->_apiClientHelper->getAddressAutocomplete($args['context'], $args['term']); - if (isset($result['error'])) { - throw new GraphQlInputException(__($result['message'] ?? 'Unknown error')); - } - - if (count($result['matches']) > 0) { - foreach ($result['matches'] as &$match) { - foreach ($match['highlights'] as &$hl) { - $hl['start'] =& $hl[0]; - $hl['end'] =& $hl[1]; - } - } - } - - return $result; - } -} diff --git a/Model/Resolver/ValidatedAddress.php b/Model/Resolver/ValidatedAddress.php deleted file mode 100644 index 0486121..0000000 --- a/Model/Resolver/ValidatedAddress.php +++ /dev/null @@ -1,54 +0,0 @@ -_apiClientHelper = $apiClientHelper; - } - - /** - * @inheritdoc - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - ?array $value = null, - ?array $args = null - ): array { - $result = $this->_apiClientHelper->validateAddress( - $args['country'], - $args['postcode'] ?? null, - $args['locality'] ?? null, - $args['street'] ?? null, - $args['building'] ?? null, - $args['region'] ?? null, - $args['streetAndBuilding'] ?? null - ); - - if (!empty($result['error'])) { - throw new GraphQlInputException(__($result['message'] ?? 'Unknown error')); - } - - return $result; - } -} diff --git a/Model/System/Message/RebrandNotice.php b/Model/System/Message/RebrandNotice.php index daba2a5..0784352 100644 --- a/Model/System/Message/RebrandNotice.php +++ b/Model/System/Message/RebrandNotice.php @@ -30,10 +30,8 @@ public function getIdentity(): string */ public function isDisplayed(): bool { - foreach ($this->moduleList->getAll() as $module) - { - if (isset($module['sequence']) && in_array('Flekto_Postcode', $module['sequence'])) - { + foreach ($this->moduleList->getAll() as $module) { + if (isset($module['sequence']) && in_array('Flekto_Postcode', $module['sequence'])) { return true; } } diff --git a/Model/UpdateNotificationRepository.php b/Model/UpdateNotificationRepository.php index 8ad3f2f..4f3a60b 100644 --- a/Model/UpdateNotificationRepository.php +++ b/Model/UpdateNotificationRepository.php @@ -12,7 +12,9 @@ class UpdateNotificationRepository implements UpdateNotificationRepositoryInterface { + /** @var UpdateNotificationResource */ protected $_resource; + /** @var UpdateNotificationFactory */ protected $_notificationFactory; public function __construct( diff --git a/Observer/System/Config.php b/Observer/System/Config.php index fc704a1..a055fc6 100644 --- a/Observer/System/Config.php +++ b/Observer/System/Config.php @@ -10,18 +10,41 @@ use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Message\ManagerInterface; use Psr\Log\LoggerInterface; class Config implements ObserverInterface { + /** @var WriterInterface */ protected $_configWriter; + + /** @var LoggerInterface */ protected $_logger; + + /** @var ApiClientHelper */ protected $_apiClientHelper; + + /** @var TypeListInterface */ protected $_cacheTypeList; + + /** @var CacheFrontendPool */ protected $_cacheFrontendPool; + + /** @var StoreConfigHelper */ protected $_storeConfigHelper; + + /** @var RequestInterface */ protected $_request; + /** @var ManagerInterface */ + protected $_messageManager; + + /** @var string */ + protected $_scopeType; + + /** @var int */ + protected $_scopeId; + /** * Constructor * @@ -33,7 +56,7 @@ class Config implements ObserverInterface * @param ApiClientHelper $apiClientHelper * @param StoreConfigHelper $storeConfigHelper * @param RequestInterface $request - * @return void + * @param ManagerInterface $messageManager */ public function __construct( WriterInterface $configWriter, @@ -42,7 +65,8 @@ public function __construct( CacheFrontendPool $cacheFrontendPool, ApiClientHelper $apiClientHelper, StoreConfigHelper $storeConfigHelper, - RequestInterface $request + RequestInterface $request, + ManagerInterface $messageManager ) { $this->_configWriter = $configWriter; $this->_logger = $logger; @@ -51,6 +75,7 @@ public function __construct( $this->_apiClientHelper = $apiClientHelper; $this->_storeConfigHelper = $storeConfigHelper; $this->_request = $request; + $this->_messageManager = $messageManager; } /** @@ -58,63 +83,62 @@ public function __construct( */ public function execute(Observer $observer): void { + [$this->_scopeType, $this->_scopeId] = $this->_storeConfigHelper->getScopeFromRequest(); + if (empty($this->_request->getParam('refresh_api_data'))) { + $hasChangedCredentials = count(array_intersect( + $observer->getDataByKey('changed_paths') ?? [], + [StoreConfigHelper::PATH['api_key'], StoreConfigHelper::PATH['api_secret']], + )) > 0; - // Return if credentials didn't change. - if (empty(array_intersect($observer->getDataByKey('changed_paths'), [StoreConfigHelper::PATH['api_key'], StoreConfigHelper::PATH['api_secret']]))) { - return; + if (!$hasChangedCredentials) { + return; // Return if credentials didn't change. } // Credential(s) missing. Delete account info (status will fallback to "new" via default config). if (!$this->_storeConfigHelper->hasCredentials()) { - - $this->_configWriter->delete(StoreConfigHelper::PATH['account_name']); - $this->_configWriter->delete(StoreConfigHelper::PATH['account_status']); + $this->_configWriter->delete('account_name'); + $this->_configWriter->delete('account_status'); $this->_purgeCachedData(); - return; } } - try { + $hasAccess = false; + try { $client = $this->_apiClientHelper->getApiClient(); $accountInfo = $client->accountInfo(); + $hasAccess = $accountInfo['hasAccess'] ?? false; - $this->_configWriter->save(StoreConfigHelper::PATH['account_name'], $accountInfo['name']); + $this->_saveConfig('account_name', $accountInfo['name']); - if ($accountInfo['hasAccess']) { - $this->_configWriter->save(StoreConfigHelper::PATH['account_status'], ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE); + if ($hasAccess) { + $this->_saveConfig('account_status', ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE); } else { - $this->_configWriter->save(StoreConfigHelper::PATH['account_status'], ApiClientHelper::API_ACCOUNT_STATUS_INACTIVE); + $this->_saveConfig('account_status', ApiClientHelper::API_ACCOUNT_STATUS_INACTIVE); } - } catch (\PostcodeEu\AddressValidation\Service\Exception\AuthenticationException $e) { - - $this->_configWriter->save(StoreConfigHelper::PATH['account_status'], ApiClientHelper::API_ACCOUNT_STATUS_INVALID_CREDENTIALS); - $this->_configWriter->delete(StoreConfigHelper::PATH['account_name']); - + $this->_saveConfig('account_status', ApiClientHelper::API_ACCOUNT_STATUS_INVALID_CREDENTIALS); + $this->_deleteConfig('account_name'); } catch (\PostcodeEu\AddressValidation\Service\Exception\ClientException $e) { - - $this->_configWriter->delete(StoreConfigHelper::PATH['account_name']); - $this->_configWriter->delete(StoreConfigHelper::PATH['account_status']); - - } catch (\Exception $e) { - - $this->_logger->error(__('Postcode.eu update account info FAILED: ') . json_encode($e->getMessage())); - throw $e; // Shows exception message in error message on page. + $this->_deleteConfig('account_name'); + $this->_deleteConfig('account_status'); + } catch (\Throwable $e) { + $this->_logger->error('Postcode.eu update account info FAILED.', ['exception' => $e]); + $this->_messageManager->addErrorMessage(__('Failed to update account info: %1', $e->getMessage())); } - if (isset($accountInfo) && $accountInfo['hasAccess']) { - + if ($hasAccess) { try { $countries = $client->internationalGetSupportedCountries(); - $this->_configWriter->save(StoreConfigHelper::PATH['supported_countries'], json_encode($countries)); - - } catch (\Exception $e) { - - $this->_logger->error(__('Postcode.eu update countries FAILED: ') . json_encode($e->getMessage())); - throw $e; + $this->_saveConfig( + StoreConfigHelper::PATH['supported_countries'], + json_encode($countries, JSON_THROW_ON_ERROR), + ); + } catch (\Throwable $e) { + $this->_logger->error('Postcode.eu update countries FAILED.', ['exception' => $e]); + $this->_messageManager->addErrorMessage(__('Failed to update countries: %1', $e->getMessage())); } } @@ -123,8 +147,6 @@ public function execute(Observer $observer): void /** * Clean config cache. - * - * @return void */ protected function _cleanConfigCache(): void { @@ -133,13 +155,37 @@ protected function _cleanConfigCache(): void /** * Purge cached data. - * - * @return void */ private function _purgeCachedData(): void { $this->_cleanConfigCache(); $cache = $this->_cacheFrontendPool->get(\Magento\Framework\App\Cache\Type\Config::TYPE_IDENTIFIER); - $cache->remove(\PostcodeEu\AddressValidation\Block\System\Config\Status::CACHE_ID); + $cacheId = implode('-', [ + \PostcodeEu\AddressValidation\Block\System\Config\Status::CACHE_ID, + $this->_scopeType, + $this->_scopeId, + ]); + $cache->remove($cacheId); + } + + /** + * Save config value. + * + * @param string $path + * @param string $value + */ + private function _saveConfig(string $path, string $value): void + { + $this->_configWriter->save(StoreConfigHelper::PATH[$path] ?? $path, $value, $this->_scopeType, $this->_scopeId); + } + + /** + * Delete config value. + * + * @param string $path + */ + private function _deleteConfig(string $path): void + { + $this->_configWriter->delete(StoreConfigHelper::PATH[$path] ?? $path, $this->_scopeType, $this->_scopeId); } } diff --git a/Plugin/AddAddressAutofillToOrderCreateForm.php b/Plugin/AddAddressAutofillToOrderCreateForm.php index 1b8bebc..599c4df 100644 --- a/Plugin/AddAddressAutofillToOrderCreateForm.php +++ b/Plugin/AddAddressAutofillToOrderCreateForm.php @@ -52,11 +52,12 @@ public function __construct( */ public function afterGetForm(AddressBlock $subject, Form $form) { + $storeId = $subject->getStoreId(); $fieldset = $form->getElement('main'); - $autocompleteBehavior = $this->_storeConfigHelper->getValue('admin_address_autocomplete_behavior'); + $autocompleteBehavior = $this->_storeConfigHelper->getValue('admin_address_autocomplete_behavior', $storeId); if ($fieldset === null - || $this->_dataHelper->isDisabled() + || $this->_dataHelper->isDisabled($storeId) || $autocompleteBehavior === AdminAddressAutocompleteBehavior::DISABLE || $subject instanceof \Magento\Sales\Block\Adminhtml\Order\Address\Form // Exclude edit form. ) { @@ -70,7 +71,7 @@ public function afterGetForm(AddressBlock $subject, Form $form) $addressType = $subject->getIsShipping() ? 'shipping' : 'billing'; $fieldId = $addressType . '_address_autofill'; $countryId = $subject->getAddress()->getCountryId() ?? $this->_directoryHelper->getDefaultCountry(); - $isVisible = in_array($countryId, $this->_storeConfigHelper->getEnabledCountries()); + $isVisible = in_array($countryId, $this->_storeConfigHelper->getEnabledCountries($storeId)); if ($autocompleteBehavior !== AdminAddressAutocompleteBehavior::DEFAULT) { $isNlComponentDisabled = $autocompleteBehavior === AdminAddressAutocompleteBehavior::SINGLE_INPUT; @@ -81,14 +82,14 @@ public function afterGetForm(AddressBlock $subject, Form $form) $fieldId, 'postcode-eu-address-autofill', [ - 'settings' => $this->_storeConfigHelper->getJsinit(), + 'settings' => $this->_storeConfigHelper->getJsinit($storeId), 'htmlIdPrefix' => $form->getHtmlIdPrefix(), 'addressType' => $addressType, 'label' => __('Address autocomplete'), 'countryCode' => $countryId, 'visible' => $isVisible, 'css_class' => $isVisible ? '' : 'hidden', - 'isNlComponentDisabled' => $isNlComponentDisabled ?? $this->_dataHelper->isNlComponentDisabled(), + 'isNlComponentDisabled' => $isNlComponentDisabled ?? $this->_dataHelper->isNlComponentDisabled($storeId), ], 'country_id', ); diff --git a/Plugin/ValidatorPlugin.php b/Plugin/ValidatorPlugin.php index be5bb91..8d63bea 100644 --- a/Plugin/ValidatorPlugin.php +++ b/Plugin/ValidatorPlugin.php @@ -11,6 +11,7 @@ */ class ValidatorPlugin { + /** @var ProductMetadataInterface */ private $_productMetadata; /** diff --git a/README.md b/README.md index 9988059..056e515 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Other output options are: ## GraphQL Support -Our module now supports GraphQL, allowing you to query address data via Magento's GraphQL API. This enables integration with headless Magento setups, progressive web applications (PWAs), and other front-end technologies that leverage GraphQL. +We provide a [separate module for GraphQL support](https://github.com/postcode-nl/PostcodeEu_Api_Magento2_GraphQl), allowing you to query address data via Magento's GraphQL API. This enables integration with headless Magento setups, progressive web applications (PWAs), and other front-end technologies that leverage GraphQL. ## Compatibility diff --git a/Service/CsrfValidator.php b/Service/CsrfValidator.php new file mode 100644 index 0000000..1b5de34 --- /dev/null +++ b/Service/CsrfValidator.php @@ -0,0 +1,44 @@ +_formKeyValidator = $formKeyValidator; + $this->_request = $request; + $this->_appState = $appState; + } + + public function validate(): void + { + try { + if ($this->_appState->getAreaCode() === Area::AREA_ADMINHTML) { + return; + } + } catch (LocalizedException $e) { + // Area code not set. + } + + if (!$this->_request->isAjax() || !$this->_formKeyValidator->validate($this->_request)) { + throw new LocalizedException(__('Invalid request')); + } + } +} diff --git a/Service/PostcodeApiClient.php b/Service/PostcodeApiClient.php index ab706b3..9183552 100644 --- a/Service/PostcodeApiClient.php +++ b/Service/PostcodeApiClient.php @@ -46,8 +46,11 @@ class PostcodeApiClient */ protected $_userAgent; + /** @var Curl */ protected $_curl; + /** @var StoreConfigHelper */ protected $_storeConfigHelper; + /** @var ProductMetadataInterface */ protected $_productMetadata; public function __construct( @@ -213,6 +216,10 @@ public function validateAddress( 'streetAndBuilding' => $streetAndBuilding, ], fn($value) => $value !== null); + if (!in_array(strtoupper($country), array_column($this->_storeConfigHelper->getSupportedCountries(), 'iso3'))) { + throw new BadRequestException('Country not supported', 400); + } + return $this->_fetch( sprintf( 'international/v1/validate/%s?%s', diff --git a/Setup/Patch/Data/EncryptApiSecrets.php b/Setup/Patch/Data/EncryptApiSecrets.php index 6441eef..3f57152 100644 --- a/Setup/Patch/Data/EncryptApiSecrets.php +++ b/Setup/Patch/Data/EncryptApiSecrets.php @@ -8,8 +8,10 @@ class EncryptApiSecrets implements DataPatchInterface { - protected $_storeConfigHelper; + /** @var ConfigInterface */ protected $_resourceConfig; + + /** @var EncryptorInterface */ protected $_encryptor; /** diff --git a/Setup/Patch/Data/UpdateApiStatusConfig.php b/Setup/Patch/Data/UpdateApiStatusConfig.php index c098e36..809092f 100644 --- a/Setup/Patch/Data/UpdateApiStatusConfig.php +++ b/Setup/Patch/Data/UpdateApiStatusConfig.php @@ -10,9 +10,16 @@ class UpdateApiStatusConfig implements DataPatchInterface { + /** @var ApiClientHelper */ protected $_apiClientHelper; + + /** @var WriterInterface */ protected $_configWriter; + + /** @var StoreConfigHelper */ protected $_storeConfigHelper; + + /** @var ConfigInterface */ protected $_resourceConfig; /** @@ -44,7 +51,7 @@ public function __construct( */ public function getAliases(): array { - return ['Flekto\Postcode\Setup\Patch\Data\UpdateApiStatusConfig']; + return [\Flekto\Postcode\Setup\Patch\Data\UpdateApiStatusConfig::class]; } /** diff --git a/composer.json b/composer.json index 02494f4..676d27a 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,9 @@ { "name": "postcode-nl/api-magento2-module", - "version": "4.0.0", + "version": "4.1.0", "description": "Postcode.eu Address Validation module for Magento 2. Adds autocompletion for addresses in multiple countries using official postal data.", "require": { - "php": "^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5", + "php": "^7.4 || ^8.0", "magento/module-checkout": "~100.4" }, "type": "magento2-module", diff --git a/etc/config.xml b/etc/config.xml index 8cb62c5..a2f9899 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -3,7 +3,7 @@ - 4.0.0 + 4.1.0 new diff --git a/etc/module.xml b/etc/module.xml index 62825ab..de20f72 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -4,7 +4,6 @@ - diff --git a/etc/schema.graphqls b/etc/schema.graphqls deleted file mode 100644 index 8a349da..0000000 --- a/etc/schema.graphqls +++ /dev/null @@ -1,170 +0,0 @@ -type Query { - dutchAddress( - postcode: String! @doc(description: "Dutch postcode in the \"1234AB\" format.") - houseNumber: String! @doc(description: "House number. May include an addition.") - ): DutchAddressResult! @resolver(class: "PostcodeEu\\AddressValidation\\Model\\Resolver\\DutchAddress") @doc(description: "Get an address based on its unique combination of postcode, house number and house number addition.") - intlAddressMatches( - context: String! = "NL" @doc(description: "Initial autocomplete context. E.g. a country code to start searching in that country. The country code is not case sensitive and can be two or three characters. In subsequent requests, the context should be set to the context of the selected match.") - term: String! @doc(description: "The address search term. May be a complete or partial address. When a user selects a match, use the exact and unmodified \"value\" field of the selected match for the term parameter. In subsequent requests, the term should be set to the value of the selected match.") - ): IntlAddressMatches! @resolver(class: "PostcodeEu\\AddressValidation\\Model\\Resolver\\IntlAddress\\Matches") @doc(description: "Get a list of autocomplete matches based on a single (partial) address term. Depending on the input different types of matches may be returned, such as streets, postal codes or a specific address. Required session header: Both the autocomplete and getDetails methods require a custom HTTP header named X-Autocomplete-Session to be specified. Its value should identify a single address completion session (for example, a user filling out one checkout address) and remain the same for calls to both methods. The session header should be at least 8 characters and at most 64 characters long. The value must only contain alphanumeric characters (case insensitive), \"-\", \"_\" and/or \".\". Reusing the same session identifier for completing more than one address is not recommended as it may lead to increased costs for your account.") - intlAddressDetails( - context: String! @doc(description: "Context with precision address to get detailed address information.") - ): IntlAddressDetails! @resolver(class: "PostcodeEu\\AddressValidation\\Model\\Resolver\\IntlAddress\\Details") @doc(description: "Get address information based on the provided autocomplete context. The context must have the precision `address`. A HTTP status 400 response is given when the provided context does not resolve to a single address. This method is intended to provide address information using the result context from the autocomplete method. The result of this method can be used to complete an address in an order form for example. A session header is required, see IntlAddressMatches for details.") - addressApiSettings: AddressApiSettings! @doc(description: "Settings for the Postcode.eu Address API.") @resolver(class: "PostcodeEu\\AddressValidation\\Model\\Resolver\\AddressApiSettings") - validateAddress( - country: String! @doc(description: "ISO country code.") - postcode: String @doc(description: "The postcode.") - locality: String @doc(description: "The locality name.") - street: String @doc(description: "The street name, without building number or name.") - building: String @doc(description: "The full building number, including any additions.") - region: String @doc(description: "The region name. Depending on the country this can be a province, a state or another type of administrative region. This is not typically needed to find matches, but can sometimes be useful to distinguish between otherwise similar matches.") - streetAndBuilding: String @doc(description: "The street name and building number. Use this parameter if you do not have separate street and building data. This parameter is not allowed in combination with the street or building parameter.") - ): ValidatedAddressResult @resolver(class: "PostcodeEu\\AddressValidation\\Model\\Resolver\\ValidatedAddress") -} - -type DutchAddressResult { - address: DutchAddress @doc(description: "A Dutch address. Make sure to check the status field to see if the address was found and if the house number addition is correct.") - status: String! @doc(description: "The status of the address lookup. \"valid\" if found, \"houseNumberAdditionIncorrect\" for an invalid house number addition, \"notFound\" if the address cannot be found.") -} - -type DutchAddress { - street: String! @doc(description: "Street name in accordance with the BAG Registry (Dutch: BAG - Basisregistratie Adressen en Gebouwen). In capital and lowercase letters, including punctuation marks and accents. This field is at most 80 characters in length. Filled with \"Postbus\" in case it is a range of PO boxes.") - streetNen: String! @doc(description: "Street name in NEN-5825 notation, which has a lower maximum length. In capital and lowercase letters, including punctuation marks and accents. This field is at most 24 characters in length. Filled with \"Postbus\" in case it is a range of PO boxes.") - houseNumber: Int! @doc(description: "House number of a perceel. In case of a Postbus match the house number will always be 0. Range: 0-99999") - houseNumberAddition: String @doc(description: "Addition of the house number to uniquely define a location. These additions are officially recognized by the municipality. This field is at most 6 characters in length and null if the given addition was not found (see houseNumberAdditions result field). The addition \"\" is returned for an address without an addition.") - postcode: String! @doc(description: "Four digit neighborhood code (first part of a postcode). Range: 1000-9999 plus two character letter combination (second part of a postcode). Range: \"AA\"-\"ZZ\"") - city: String! @doc(description: "Official city name in accordance with the BAG Registry (Dutch: BAG - Basisregistratie Adressen en Gebouwen). In capital and lowercase letters, including punctuation marks and accents. This field is at most 80 characters in length.") - cityShort: String! @doc(description: "City name, shortened to fit a lower maximum length. In capital and lowercase letters, including punctuation marks and accents. This field is at most 24 characters in length.") - cityId: String! @doc(description: "Unique identifier for the city (Dutch: \"woonplaatscode\") as defined by the BAG Registry (Dutch: BAG - Basisregistratie Adressen en Gebouwen). Range \"0000\"-\"9999\"") - municipality: String! @doc(description: "Municipality name in accordance with the BAG Registry (Dutch: BAG - Basisregistratie Adressen en Gebouwen). In capital and lowercase letters, including punctuation marks and accents. This field is at most 80 characters in length. Examples: \"Baarle-Nassau\", \"'s-Gravenhage\", \"Haarlemmerliede en Spaarnwoude\".") - municipalityShort: String! @doc(description: "Municipality name, shortened to fit a lower maximum length. In capital and lowercase letters, including punctuation marks and accents. This field is at most 24 characters in length. Examples: \"Baarle-Nassau\", \"'s-Gravenhage\", \"Haarlemmerliede c.a.\".") - municipalityId: String! @doc(description: "Unique identifier for the municipality (Dutch: \"gemeentecode\") as defined by the National Office for Identity Data (Dutch: Rijksdienst voor Indentiteitsgegevens (RvIG)). Range \"0000\"-\"9999\"") - province: String! @doc(description: "Official name of the province, correctly cased and with dashes where applicable.") - rdX: Int @doc(description: "X coordinate according to Dutch Coordinate system \"(EPSG) 28992 Amersfoort / RD New\" (Dutch: Rijksdriehoeksmeting). Values range from 0 to 300000 meters. Null for PO Boxes.") - rdY: Int @doc(description: "Y coordinate according to Dutch Coordinate system \"(EPSG) 28992 Amersfoort / RD New\" (Dutch: Rijksdriehoeksmeting). Values range from 300000 to 620000 meters. Null for PO Boxes.") - latitude: Float @doc(description: "Latitude of address. Null for PO Boxes.") - longitude: Float @doc(description: "Longitude of address. Null for PO Boxes.") - bagNumberDesignationId: String @doc(description: "Unique identifier for address designation. (Dutch: Nummeraanduiding ID)") - bagAddressableObjectId: String @doc(description: "Unique identifier for addressable object designation (Dutch: Adresseerbaar object ID). If null no active object is currently registered.") - addressType: String! @doc(description: "Type of this address. See reference for possible values.") - purposes: [String] @doc(description: "List of all purposes (Dutch: gebruiksdoelen). Null or an array of text values. See reference for possible values.") - surfaceArea: Int @doc(description: "Surface in square meters. Null for PO Boxes.") - houseNumberAdditions: [DutchHouseNumberAddition]! @doc(description: "List of all house number additions having the postcode and houseNumber which was input. The addition \"\" is returned for an address without an addition.") -} - -type DutchHouseNumberAddition { - label: String! @doc(description: "House number and addition.") - value: String! @doc(description: "House number and addition.") - houseNumberAddition: String! @doc(description: "Just the house number addition.") -} - -type IntlAddressMatches { - newContext: String @doc(description: "New context that is required for further autocomplete requests. Null if no context update is required.") - matches: [IntlAddressMatch]! @doc(description: "List of matches for the specified context and term.") -} - -type IntlAddressMatch { - value: String! @doc(description: "The value represents all matched address information. If the user selects this match the current term input must be replaced with this value. ") - label: String! @doc(description: "Label describing this match. For example, a street or municipality name.") - description: String! @doc(description: "Additional information relevant to this match, helps with disambiguation between similar labels. For example, a postal code associated with a matched street.") - precision: String! @doc(description: "Match precision, used to know what type of address information is available. E.g. \"Locality\", \"PostalCode\", \"Street\", \"PartialAddress\", \"Address\".") - context: String! @doc(description: "If the user selects this match, use this as the context parameter in subsequent autocomplete call. Contexts may expire and should not be stored.") - highlights: [IntlAddressMatchHighlight]! @doc(description: "List of [start, end] character offsets to highlight in the label in order to indicate what was matched.") -} - -type IntlAddressMatchHighlight { - start: Int @doc(description: "Highlight start position.") - end: Int @doc(description: "Highlight end position.") -} - -type IntlAddressDetails { - language: String! @doc(description: "Language of the matched address, derived from context or falls back to default.") - address: IntlAddress! @doc(description: "Address information for the contexts address.") - mailLines: [String]! @doc(description: "List of address lines as they should appear in an address space. The country name is included when a dispatchCountry parameter is provided which does not correspond with the address country.") - streetLines: [String!]! @doc(description: "Formatted street lines. The amount of lines is limited by the configured number of lines in a street address.") - region: DirectoryRegion @doc(description: "Region id and name from Magento Directory, if found.") - location: IntlAddressLocation @doc(description: "WGS-84 coordinates for the address if available, null otherwise.") - isPoBox: Boolean! @doc(description: "Indicates if the address is a Post Office box.") - country: IntlAddressCountry! @doc(description: "Country information.") -} - -type IntlAddress { - country: String! @doc(description: "Country name of the of postal address in English.") - locality: String! @doc(description: "Name of primary locality used in postal address.") - street: String! @doc(description: "Name of primary street used in postal address.") - postcode: String! @doc(description: "Postcode used in postal address.") - building: String! @doc(description: "Number and possible addition of the building in the postal address.") - buildingNumber: Int @doc(description: "Building number of the postal address. Or null if not available.") - buildingNumberAddition: String @doc(description: "Building number addition of the postal address, if available.") -} - -type IntlAddressLocation { - latitude: Float! @doc(description: "Latitude coordinate.") - longitude: Float! @doc(description: "Longitude coordinate.") - precision: String! @doc(description: "Precision of location. One of \"Address\", \"Street\", \"PostalCode\", \"Locality\".") -} - -type IntlAddressCountry { - name: String! @doc(description: "The country name.") - iso2Code: String! @doc(description: "The ISO 3166-1 Alpha-2 code for the country.") - iso3Code: String! @doc(description: "The ISO 3166-1 Alpha-3 code for the country.") -} - -type ValidatedAddressResult { - country: IntlAddressCountry! @doc(description: "Country information.") - matches: [ValidatedAddressMatch!]! @doc(description: "A list of matches to the input address. Matches are ordered from best to worst match to the input parameters. For use-cases without user interaction you typically only need to consider the top match.") -} - -type ValidatedAddressMatch { - status: ValidatedAddressMatchStatus! @doc(description: "Status of the match.") - language: String! @doc(description: "Language code for the language used in the results.") - address: ValidatedAddress! @doc(description: "Address parts. Only validated elements are set.") - mailLines: [String!]! @doc(description: "Postal mail lines as used to address letters or packages to the address.") - streetLines: [String!]! @doc(description: "Formatted street lines. The amount of lines is limited by the configured number of lines in a street address.") - region: DirectoryRegion @doc(description: "Region id and name from Magento Directory, if found.") - location: IntlAddressLocation @doc(description: "WGS-84 coordinates for the address if available, null otherwise.") - isPoBox: Boolean! @doc(description: "Indicates if the address is a Post Office box.") - country: IntlAddressCountry! @doc(description: "Country information.") -} - -type ValidatedAddressMatchStatus { - grade: String! @doc(description: "Grade indicating how well the match corresponds to the input address: A - F, A indicating a perfect match, and F indicating an extremely poor match.") - validationLevel: String! @doc(description: "Indicates up to which address element the match is validated. Possible values: Building, BuildingPartial, Street, Locality, None.") - isAmbiguous: Boolean! @doc(description: "Indicates if this match is too similar in quality to other matches to be considered an unambiguous match to the input. This means there was not enough input data to decide between these matches.") -} - -type ValidatedAddress { - country: String! @doc(description: "Country name of the of postal address in English.") - locality: String @doc(description: "Name of primary locality used in postal address.") - street: String @doc(description: "Name of primary street used in postal address.") - postcode: String @doc(description: "Postcode used in postal address.") - building: String @doc(description: "The formatted building number of the postal address, including possible additions for the location. (United Kingdom: multiple lines are comma separated.)") - buildingNumber: Int @doc(description: "Building number of the postal address, or null if not available.") - buildingNumberAddition: String @doc(description: "Building number addition of the postal address, if available. (United Kingdom: multiple lines are comma separated)") - region: String @doc(description: "The region the address resides in. Depending on the country this can be a province, a state or another type of administrative region.") -} - -type DirectoryRegion { - id: Int @doc(description: "The region id if found in the directory.") - name: String @doc(description: "The name of the region as found in the directory. If not found, use region from address details if available.") -} - -type AddressApiSettings { - enabled_countries: [String]! @doc(description: "List of ISO2 codes of countries currently supported by the autocomplete API, excluding disabled countries.") - nl_input_behavior: String! @doc(description: "How to handle Dutch address. With \"zip_house\", use postcode and house number fields and the Dutch address API. With \"free\" use a single field and the international autocomplete API.") - show_hide_address_fields: String! @doc(description: "How to handle the standard address fields. E.g. show, hide or disable. The default is to keep fields hidden and show a formatted address instead.") - base_url: String! @doc(description: "Base URL for the current store.") - api_actions: ApiBaseUrls! @doc(description: "Base URL's to use for API requests.") - debug: Boolean! @doc(description: "Whether debugging is enabled or not.") - fixedCountry: String @doc(description: "Get fixed country (ISO2) if there's only one allowed country.") - change_fields_position: Boolean! @doc(description: "If true, address fields should be rearranged so that country selection comes before the autocomplete fields.") - allow_pobox_shipping: Boolean! @doc(description: "Allow shipping to PO boxes.") - split_street_values: Boolean! @doc(description: "Distribute street address values to available street fields.") -} - -type ApiBaseUrls { - dutchAddressLookup: String! @doc(description: "Dutch address lookup.") - autocomplete: String! @doc(description: "Autocomplete an address.") - addressDetails: String! @doc(description: "Get address details.") - validate: String! @doc(description: "Validate an address.") -} diff --git a/i18n/en_US.csv b/i18n/en_US.csv index 20a4cd6..3bef141 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -87,3 +87,4 @@ "Your Postcode.eu subscription is inactive. Please log in to your account to resolve this.","Your Postcode.eu subscription is inactive. Please log in to your account to resolve this." "Add manual entry link","Add manual entry link" "Allows users to skip the autocomplete field and manually enter an address. Enabling this option may lead to invalid addresses. Applicable to free address input only.","Allows users to skip the autocomplete field and manually enter an address. Enabling this option may lead to invalid addresses. Applicable to free address input only." +"unknown addition","unknown addition" diff --git a/i18n/nl_BE.csv b/i18n/nl_BE.csv index 7ac8956..adeaae2 100644 --- a/i18n/nl_BE.csv +++ b/i18n/nl_BE.csv @@ -87,4 +87,4 @@ "Your Postcode.eu subscription is inactive. Please log in to your account to resolve this.","Uw Postcode.eu abonnement is inactief. Log in op uw account om dit op te lossen." "Add manual entry link","Link voor handmatige invoer toevoegen" "Allows users to skip the autocomplete field and manually enter an address. Enabling this option may lead to invalid addresses. Applicable to free address input only.","Hiermee kunnen gebruikers het veld voor automatisch aanvullen overslaan en handmatig een adres invoeren. Het inschakelen van deze optie kan leiden tot ongeldige adressen. Alleen van toepassing op vrije adresinvoer." - +"unknown addition","onbekende toevoeging" diff --git a/i18n/nl_NL.csv b/i18n/nl_NL.csv index 7ac8956..adeaae2 100644 --- a/i18n/nl_NL.csv +++ b/i18n/nl_NL.csv @@ -87,4 +87,4 @@ "Your Postcode.eu subscription is inactive. Please log in to your account to resolve this.","Uw Postcode.eu abonnement is inactief. Log in op uw account om dit op te lossen." "Add manual entry link","Link voor handmatige invoer toevoegen" "Allows users to skip the autocomplete field and manually enter an address. Enabling this option may lead to invalid addresses. Applicable to free address input only.","Hiermee kunnen gebruikers het veld voor automatisch aanvullen overslaan en handmatig een adres invoeren. Het inschakelen van deze optie kan leiden tot ongeldige adressen. Alleen van toepassing op vrije adresinvoer." - +"unknown addition","onbekende toevoeging" diff --git a/view/base/web/css/lib/postcode-eu-autocomplete-address.css b/view/base/web/css/lib/postcode-eu-autocomplete-address.css index 01fbf52..fb0d59c 100644 --- a/view/base/web/css/lib/postcode-eu-autocomplete-address.css +++ b/view/base/web/css/lib/postcode-eu-autocomplete-address.css @@ -1,7 +1,7 @@ .postcodenl-autocomplete-menu { display: none; position: absolute; - z-index: 99; + z-index: 9999; background-position: right .85em bottom .3em; box-shadow: 0 .5em .75em rgba(0, 0, 0, .15); background-color: #fff; @@ -54,7 +54,7 @@ input[class].postcodenl-autocomplete-address-input.postcodenl-autocomplete-addre } input[class].postcodenl-autocomplete-address-input.postcodenl-autocomplete-loading { - background-image: url(data:image/gif;base64,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); + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48c3R5bGU+LnNwaW5uZXJfT1NtV3t0cmFuc2Zvcm0tb3JpZ2luOmNlbnRlcjthbmltYXRpb246c3Bpbm5lcl9UNm1BIDFzIHN0ZXAtZW5kIGluZmluaXRlfUBrZXlmcmFtZXMgc3Bpbm5lcl9UNm1BezguMyV7dHJhbnNmb3JtOnJvdGF0ZSgzMGRlZyl9MTYuNiV7dHJhbnNmb3JtOnJvdGF0ZSg2MGRlZyl9MjUle3RyYW5zZm9ybTpyb3RhdGUoOTBkZWcpfTMzLjMle3RyYW5zZm9ybTpyb3RhdGUoMTIwZGVnKX00MS42JXt0cmFuc2Zvcm06cm90YXRlKDE1MGRlZyl9NTAle3RyYW5zZm9ybTpyb3RhdGUoMTgwZGVnKX01OC4zJXt0cmFuc2Zvcm06cm90YXRlKDIxMGRlZyl9NjYuNiV7dHJhbnNmb3JtOnJvdGF0ZSgyNDBkZWcpfTc1JXt0cmFuc2Zvcm06cm90YXRlKDI3MGRlZyl9ODMuMyV7dHJhbnNmb3JtOnJvdGF0ZSgzMDBkZWcpfTkxLjYle3RyYW5zZm9ybTpyb3RhdGUoMzMwZGVnKX0xMDAle3RyYW5zZm9ybTpyb3RhdGUoMzYwZGVnKX19PC9zdHlsZT48ZyBjbGFzcz0ic3Bpbm5lcl9PU21XIiBmaWxsPSIjNDU1NTZjIj48cmVjdCB4PSIxMSIgeT0iMSIgd2lkdGg9IjIiIGhlaWdodD0iNSIgb3BhY2l0eT0iLjE0Ii8+PHJlY3QgeD0iMTEiIHk9IjEiIHdpZHRoPSIyIiBoZWlnaHQ9IjUiIHRyYW5zZm9ybT0icm90YXRlKDMwIDEyIDEyKSIgb3BhY2l0eT0iLjI5Ii8+PHJlY3QgeD0iMTEiIHk9IjEiIHdpZHRoPSIyIiBoZWlnaHQ9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDEyIDEyKSIgb3BhY2l0eT0iLjQzIi8+PHJlY3QgeD0iMTEiIHk9IjEiIHdpZHRoPSIyIiBoZWlnaHQ9IjUiIHRyYW5zZm9ybT0icm90YXRlKDkwIDEyIDEyKSIgb3BhY2l0eT0iLjU3Ii8+PHJlY3QgeD0iMTEiIHk9IjEiIHdpZHRoPSIyIiBoZWlnaHQ9IjUiIHRyYW5zZm9ybT0icm90YXRlKDEyMCAxMiAxMikiIG9wYWNpdHk9Ii43MSIvPjxyZWN0IHg9IjExIiB5PSIxIiB3aWR0aD0iMiIgaGVpZ2h0PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgMTIgMTIpIiBvcGFjaXR5PSIuODYiLz48cmVjdCB4PSIxMSIgeT0iMSIgd2lkdGg9IjIiIGhlaWdodD0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTgwIDEyIDEyKSIvPjwvZz48L3N2Zz4K); background-position: center right .6em; background-repeat: no-repeat; background-size: .8em; @@ -91,10 +91,6 @@ input[class].postcodenl-autocomplete-address-input.postcodenl-autocomplete-loadi color: #666; } -.postcodenl-autocomplete-item-description { - white-space: nowrap; -} - .postcodenl-autocomplete-item-more { background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgoJPHBhdGggZD0ibTUgMTQgNi02LTYtNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNzc3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K); background-position: center right .25em; diff --git a/view/base/web/css/source/_shared.less b/view/base/web/css/source/_shared.less index 3ec24c4..3aec875 100644 --- a/view/base/web/css/source/_shared.less +++ b/view/base/web/css/source/_shared.less @@ -1,7 +1,3 @@ -.postcodenl-autocomplete-menu { - z-index: 9999; -} - .postcodenl-autocomplete-menu, .address-autofill-intl-input input[class].input-text.postcodenl-autocomplete-address-input-blank { background-size: 100px; diff --git a/view/base/web/js/form/components/address-autofill-nl.js b/view/base/web/js/form/components/address-autofill-nl.js index a80f1aa..ad64295 100644 --- a/view/base/web/js/form/components/address-autofill-nl.js +++ b/view/base/web/js/form/components/address-autofill-nl.js @@ -116,14 +116,14 @@ define([ return AddressNlModel.houseNumberRegex.test(this.childHouseNumber().value()); }, - getAddress: function () { - const postcode = encodeURIComponent( - AddressNlModel.postcodeRegex.exec(this.childPostcode().value())[0].replace(/\s/g, '') - ), - houseNumber = encodeURIComponent( - AddressNlModel.houseNumberRegex.exec(this.childHouseNumber().value())[0].trim() - ), - url = `${this.settings.api_actions.dutchAddressLookup}/${postcode}/${houseNumber}`; + getAddress: function (acceptUnknownAddition = false) { + const postcode = AddressNlModel.postcodeRegex.exec(this.childPostcode().value())[0].replace(/\s/g, ''), + houseNumber = AddressNlModel.houseNumberRegex.exec(this.childHouseNumber().value())[0].trim(), + url = new URL( + this.settings.api_actions.dutchAddressLookup + .replace('{postcode}', postcode) + .replace('{houseNumber}', houseNumber) + ); this.resetInputAddress(); this.address(null); @@ -132,7 +132,7 @@ define([ this.childHouseNumber().error(false); $.get({ - url: url, + url, cache: true, dataType: 'json', success: ([response]) => { @@ -152,7 +152,15 @@ define([ this.address(response.address); if (this.status() === AddressNlModel.status.ADDITION_INCORRECT) { - this.childHouseNumberSelect().setOptions(response.address.houseNumberAdditions); + if (acceptUnknownAddition) { + this.status(AddressNlModel.status.VALID); + this.address().houseNumberAddition = AddressNlModel.houseNumberRegex.exec( + this.childHouseNumber().value() + )[1].trim(); + this.address.valueHasMutated(); + } else { + this.childHouseNumberSelect().setOptions(response.address.houseNumberAdditions); + } } else { this.toggleFields(true); } diff --git a/view/base/web/js/ko/bindings/init-intl-autocomplete.js b/view/base/web/js/ko/bindings/init-intl-autocomplete.js index 987e132..bee6ed2 100644 --- a/view/base/web/js/ko/bindings/init-intl-autocomplete.js +++ b/view/base/web/js/ko/bindings/init-intl-autocomplete.js @@ -21,6 +21,22 @@ define([ showLogo: viewModel.showLogo ?? true, }); + // Override methods to process URL template. + viewModel.intlAutocompleteInstance.getSuggestions = function (context, term, response) { + context = encodeURIComponent(context); + term = encodeURIComponent(term); + + return this.xhrGet( + // See client/helper for language and buildingListMode parameters. + this.options.autocompleteUrl.replace('{context}', context).replace('{term}', term), + response + ); + }; + + viewModel.intlAutocompleteInstance.getDetails = function (...args) { + return this.xhrGet(this.options.addressDetailsUrl.replace('{context}', args[0]), args.at(-1)); + }; + viewModel.inputElement = element; function getAddressDetails(context, callback) { @@ -66,6 +82,10 @@ define([ viewModel.resetInputAddress(); viewModel.address(null); }); + + element.addEventListener('autocomplete-xhr-send', ({detail: xhr}) => { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + }); } }; diff --git a/view/base/web/js/lib/postcode-eu-autocomplete-address.js b/view/base/web/js/lib/postcode-eu-autocomplete-address.js index f676dd2..ee168aa 100644 --- a/view/base/web/js/lib/postcode-eu-autocomplete-address.js +++ b/view/base/web/js/lib/postcode-eu-autocomplete-address.js @@ -8,7 +8,7 @@ * https://www.tldrlegal.com/license/apple-mit-license-aml * * @author Postcode.nl - * @version 1.4.2 + * @version 1.4.3 */ (function (root, factory) { @@ -31,13 +31,12 @@ const document = window.document, $ = function (selector) { return document.querySelectorAll(selector); }, elementData = new WeakMap(), - VERSION = '1.4.2', + VERSION = '1.4.3', EVENT_NAMESPACE = 'autocomplete-', PRECISION_ADDRESS = 'Address', KEY_ESC = 'Escape', KEY_ESC_LEGACY = 'Esc', KEY_ENTER = 'Enter', - KEY_TAB = 'Tab', KEY_UP = 'ArrowUp', KEY_UP_LEGACY = 'Up', KEY_DOWN = 'ArrowDown', @@ -894,6 +893,9 @@ xhr.open('GET', url); xhr.setRequestHeader('X-Autocomplete-Session', sessionId); + activeElement?.dispatchEvent( + new CustomEvent(EVENT_NAMESPACE + 'xhr-send', {detail: xhr, bubbles: true}) + ); xhr.send(); return xhr; @@ -1140,6 +1142,9 @@ return; // Menu is not part of the DOM, assume already destroyed. } + // Prevent potential race condition with pending timeout scheduled by searchDebounced(). + window.clearTimeout(searchTimeoutId); + document.body.removeChild(liveRegion); for (let i = 0, element; (element = inputElements[i++]);) @@ -1241,14 +1246,6 @@ menu.close(true); break; - case KEY_TAB: - if (menu.hasFocus) - { - menu.select(); - e.preventDefault(); - } - break; - case KEY_ENTER: if (menu.hasFocus) { @@ -1313,7 +1310,15 @@ { menu.open(element); data.context = e.detail.context; - window.setTimeout(search, 0, element); + window.setTimeout(() => { + if (!e.defaultPrevented) + { + // Make sure value isn't changed (e.g. via React render). + element.value = data.match.value; + } + + search(element); + }); } }); diff --git a/view/base/web/js/model/address-nl.js b/view/base/web/js/model/address-nl.js index 508316e..7f5cf02 100644 --- a/view/base/web/js/model/address-nl.js +++ b/view/base/web/js/model/address-nl.js @@ -4,7 +4,7 @@ define([], function () { return Object.defineProperties({}, { lookupDelay: { value: 750 }, postcodeRegex: { value: /[1-9][0-9]{3}\s*[a-z]{2}/i }, - houseNumberRegex: { value: /[1-9]\d{0,4}(?:\D.*)?$/i }, + houseNumberRegex: { value: /[1-9]\d{0,4}(\D.*)?$/i }, status: { value: Object.defineProperties({}, { VALID: { value: 'valid' }, diff --git a/view/frontend/web/js/action/customer/address/get-validated-address.js b/view/frontend/web/js/action/customer/address/get-validated-address.js index 2eede4c..d2673bc 100644 --- a/view/frontend/web/js/action/customer/address/get-validated-address.js +++ b/view/frontend/web/js/action/customer/address/get-validated-address.js @@ -3,17 +3,17 @@ define([ ], function (Registry) { 'use strict'; - function validateAddress(country, streetAndBuilding, postcode, locality) { - const params = [ - 'streetAndBuilding=' + encodeURIComponent(streetAndBuilding ?? ''), - 'postcode=' + encodeURIComponent(postcode ?? ''), - 'locality=' + encodeURIComponent(locality ?? ''), - ].join('&'), - url = `${Registry.get('address_autofill').settings.api_actions.validate}/${country}?${params}`; + function validateAddress(country, streetAndBuilding = '', postcode = '', locality = '') { + const settings = Registry.get('address_autofill').settings, + url = new URL(settings.api_actions.validate.replace('{country}', country)), + headers = {'X-Requested-With': 'XMLHttpRequest'}; - return fetch(url).then((response) => { - if (response.ok) - { + url.searchParams.set('streetAndBuilding', streetAndBuilding); + url.searchParams.set('postcode', postcode); + url.searchParams.set('locality', locality); + + return fetch(url.toString().replaceAll('+', '%20'), {headers}).then((response) => { + if (response.ok) { return response.json(); } @@ -31,8 +31,7 @@ define([ && !top.status.isAmbiguous && top.status.grade < 'C' && ['Building', 'BuildingPartial'].includes(top.status.validationLevel) - ) - { + ) { return top; } diff --git a/view/frontend/web/js/form/components/customer/address/address-autofill-nl.js b/view/frontend/web/js/form/components/customer/address/address-autofill-nl.js index ab2d0f5..11168df 100644 --- a/view/frontend/web/js/form/components/customer/address/address-autofill-nl.js +++ b/view/frontend/web/js/form/components/customer/address/address-autofill-nl.js @@ -23,7 +23,7 @@ define([ if (this.countryCode === 'NL') { Promise.all([this.prefillPostcode(), this.prefillHouseNumber()]) - .then(this.getAddress.bind(this)) + .then(() => this.getAddress(true)) .catch(() => { if (AddressNlModel.houseNumberRegex.test(this.inputs.getStreetValue())) { // Fall back to Validate API for ambiguous house number cases.