From 882f06ca74a77a86ba7e4f9adaf83394eded6bae Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Tue, 7 Apr 2026 10:18:01 +0200 Subject: [PATCH 01/10] Add CSRF protection to autocomplete requests - Update autocomplete JS library. - Simplify PHP version constraint. --- Helper/StoreConfigHelper.php | 16 ++++++--- Model/PostcodeModel.php | 36 +++++++++++++++++-- Service/CsrfValidator.php | 29 +++++++++++++++ composer.json | 2 +- .../lib/postcode-eu-autocomplete-address.css | 8 ++--- view/base/web/css/source/_shared.less | 4 --- .../js/form/components/address-autofill-nl.js | 1 + .../js/ko/bindings/init-intl-autocomplete.js | 24 +++++++++++++ .../lib/postcode-eu-autocomplete-address.js | 29 ++++++++------- .../customer/address/get-validated-address.js | 8 +++-- 10 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 Service/CsrfValidator.php diff --git a/Helper/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index 34c9d3e..6071449 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -2,14 +2,15 @@ namespace PostcodeEu\AddressValidation\Helper; +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\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; @@ -20,6 +21,7 @@ class StoreConfigHelper extends AbstractHelper protected $_encryptor; protected $_countryCollectionFactory; protected $_localeResolver; + protected $_formKey; public const PATH = [ // Status @@ -52,6 +54,7 @@ class StoreConfigHelper extends AbstractHelper * @param EncryptorInterface $encryptor * @param CountryCollectionFactory $countryCollectionFactory * @param ResolverInterface $localeResolver + * @param FormKey $formKey */ public function __construct( Context $context, @@ -59,13 +62,15 @@ public function __construct( DeveloperHelperData $developerHelper, EncryptorInterface $encryptor, CountryCollectionFactory $countryCollectionFactory, - ResolverInterface $localeResolver + ResolverInterface $localeResolver, + FormKey $formKey, ) { $this->_storeManager = $storeManager; $this->_developerHelper = $developerHelper; $this->_encryptor = $encryptor; $this->_countryCollectionFactory = $countryCollectionFactory; $this->_localeResolver = $localeResolver; + $this->_formKey = $formKey; parent::__construct($context); } @@ -220,6 +225,7 @@ public function getJsinit(): array 'change_fields_position' => $this->isSetFlag('change_fields_position'), 'allow_pobox_shipping' => $this->isSetFlag('allow_pobox_shipping'), 'split_street_values' => $this->isSetFlag('split_street_values'), + 'form_key' => $this->_formKey->getFormKey(), ]; } diff --git a/Model/PostcodeModel.php b/Model/PostcodeModel.php index 137e63c..c091f30 100644 --- a/Model/PostcodeModel.php +++ b/Model/PostcodeModel.php @@ -2,13 +2,17 @@ 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 { + protected CsrfValidator $_csrfValidator; /** * @var ApiClientHelper @@ -22,9 +26,13 @@ class PostcodeModel implements PostcodeModelInterface * @param ApiClientHelper $apiClientHelper * @return void */ - public function __construct(ApiClientHelper $apiClientHelper) + public function __construct( + ApiClientHelper $apiClientHelper, + CsrfValidator $csrfValidator + ) { $this->apiClientHelper = $apiClientHelper; + $this->_csrfValidator = $csrfValidator; } /** @@ -32,6 +40,8 @@ public function __construct(ApiClientHelper $apiClientHelper) */ public function getAddressAutocomplete(string $context, string $term): AutocompleteDataInterface { + $this->_validateRequest(); + $result = $this->apiClientHelper->getAddressAutocomplete($context, $term); return new AutocompleteData($result); } @@ -41,6 +51,8 @@ public function getAddressAutocomplete(string $context, string $term): Autocompl */ public function getAddressDetails(string $context): array { + $this->_validateRequest(); + $result = $this->apiClientHelper->getAddressDetails($context); return [$result]; } @@ -50,6 +62,8 @@ public function getAddressDetails(string $context): array */ public function getAddressDetailsCountry(string $context, string $dispatchCountry): array { + $this->_validateRequest(); + $result = $this->apiClientHelper->getAddressDetails($context, $dispatchCountry); return [$result]; } @@ -59,6 +73,8 @@ public function getAddressDetailsCountry(string $context, string $dispatchCountr */ public function getNlAddress(string $zipCode, string $houseNumber): array { + $this->_validateRequest(); + $result = $this->apiClientHelper->getNlAddress($zipCode, $houseNumber); return [$result]; } @@ -75,7 +91,21 @@ public function validateAddress( ?string $region = null, ?string $streetAndBuilding = null ): array { + $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/Service/CsrfValidator.php b/Service/CsrfValidator.php new file mode 100644 index 0000000..c9fee71 --- /dev/null +++ b/Service/CsrfValidator.php @@ -0,0 +1,29 @@ +_formKeyValidator = $formKeyValidator; + $this->_request = $request; + } + + public function validate(): void + { + if (!$this->_request->isAjax() || !$this->_formKeyValidator->validate($this->_request)) + { + throw new LocalizedException(__('Invalid request')); + } + } +} diff --git a/composer.json b/composer.json index 02494f4..8750e65 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "version": "4.0.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/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,R0lGODlhIAAgAPMAAP///3d3d+Dg4L29vdfX18jIyJOTk6SkpOnp6fDw8Nra2oaGhnl5eQAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHRLYKhKP1oZmADdEAAAh+QQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY/CZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB+A4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6+Ho7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq+B6QDtuetcaBPnW6+O7wDHpIiK9SaVK5GgV543tzjgGcghAgAh+QQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK++G+w48edZPK+M6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE+G+cD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm+FNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk+aV+oJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0/VNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc+XiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30/iI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE/jiuL04RGEBgwWhShRgQExHBAAh+QQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR+ipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY+Yip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd+MFCN6HAAIKgNggY0KtEBAAh+QQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1+vsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d+jYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg+ygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0+bm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h+Kr0SJ8MFihpNbx+4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX+BP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA==); + 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..3da1773 100644 --- a/view/base/web/js/form/components/address-autofill-nl.js +++ b/view/base/web/js/form/components/address-autofill-nl.js @@ -134,6 +134,7 @@ define([ $.get({ url: url, cache: true, + data: {form_key: this.settings.form_key}, dataType: 'json', success: ([response]) => { if (response.error) { 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..a58aea0 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,26 @@ define([ showLogo: viewModel.showLogo ?? true, }); + // Override methods to add form_key. + viewModel.intlAutocompleteInstance.getSuggestions = function (context, term, response) { + context = encodeURIComponent(context); + term = encodeURIComponent(term); + + return this.xhrGet( + `${this.options.autocompleteUrl}/${context}/${term}?form_key=${viewModel.settings.form_key}`, + response + ); + }; + + viewModel.intlAutocompleteInstance.getDetails = function (...args) { + const response = args.pop(); + + return this.xhrGet( + `${this.options.addressDetailsUrl}/${args.join('/')}?form_key=${viewModel.settings.form_key}`, + response + ); + }; + viewModel.inputElement = element; function getAddressDetails(context, callback) { @@ -66,6 +86,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/frontend/web/js/action/customer/address/get-validated-address.js b/view/frontend/web/js/action/customer/address/get-validated-address.js index 2eede4c..85916e8 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 @@ -4,14 +4,16 @@ define([ 'use strict'; function validateAddress(country, streetAndBuilding, postcode, locality) { - const params = [ + const settings = Registry.get('address_autofill').settings, + params = [ 'streetAndBuilding=' + encodeURIComponent(streetAndBuilding ?? ''), 'postcode=' + encodeURIComponent(postcode ?? ''), 'locality=' + encodeURIComponent(locality ?? ''), + 'form_key=' + settings.form_key, ].join('&'), - url = `${Registry.get('address_autofill').settings.api_actions.validate}/${country}?${params}`; + url = `${settings.api_actions.validate}/${country}?${params}`; - return fetch(url).then((response) => { + return fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}}).then((response) => { if (response.ok) { return response.json(); From b7a6a9b6589d0a1bdab41b351e8b812bff687b27 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Wed, 8 Apr 2026 10:24:22 +0200 Subject: [PATCH 02/10] Allow unknown/unvalidated house number addition with Dutch API --- Helper/ApiClientHelper.php | 39 +++++++++++++++++++++++++++++++------- i18n/en_US.csv | 1 + i18n/nl_BE.csv | 2 +- i18n/nl_NL.csv | 2 +- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Helper/ApiClientHelper.php b/Helper/ApiClientHelper.php index eac8b10..5093880 100644 --- a/Helper/ApiClientHelper.php +++ b/Helper/ApiClientHelper.php @@ -303,7 +303,6 @@ public function getNlAddress(string $zipCode, string $houseNumber): array } try { - $client = $this->getApiClient(); $address = $client->dutchAddressByPostcode($zipCode, $houseNumber, $houseNumberAddition); $status = 'valid'; @@ -312,6 +311,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 +322,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 +350,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. * 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" From b9e66c9da0f5fa0adf1eabf319763721482bc042 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Wed, 8 Apr 2026 11:20:04 +0200 Subject: [PATCH 03/10] Prevent making Validate API requests for unsupported countries --- Helper/ApiClientHelper.php | 5 ++++- Service/PostcodeApiClient.php | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Helper/ApiClientHelper.php b/Helper/ApiClientHelper.php index 5093880..0056328 100644 --- a/Helper/ApiClientHelper.php +++ b/Helper/ApiClientHelper.php @@ -378,8 +378,11 @@ private function _formatHouseNumberAdditionOption(int $houseNumber, string $addi */ 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.')]; diff --git a/Service/PostcodeApiClient.php b/Service/PostcodeApiClient.php index ab706b3..b9974d9 100644 --- a/Service/PostcodeApiClient.php +++ b/Service/PostcodeApiClient.php @@ -213,6 +213,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', From 20deec7d18a416dc5e006fd864698f8110f030fc Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Tue, 14 Apr 2026 16:07:37 +0200 Subject: [PATCH 04/10] Accept unknown house number addition in customer address --- .../web/js/form/components/address-autofill-nl.js | 12 ++++++++++-- view/base/web/js/model/address-nl.js | 2 +- .../customer/address/address-autofill-nl.js | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) 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 3da1773..14ae12c 100644 --- a/view/base/web/js/form/components/address-autofill-nl.js +++ b/view/base/web/js/form/components/address-autofill-nl.js @@ -116,7 +116,7 @@ define([ return AddressNlModel.houseNumberRegex.test(this.childHouseNumber().value()); }, - getAddress: function () { + getAddress: function (acceptUnknownAddition = false) { const postcode = encodeURIComponent( AddressNlModel.postcodeRegex.exec(this.childPostcode().value())[0].replace(/\s/g, '') ), @@ -153,7 +153,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/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/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. From bf87d55883ddca46c6276545d14d12c1a9ae136b Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Mon, 4 May 2026 14:43:01 +0200 Subject: [PATCH 05/10] Add controller to handle autocomplete requests from admin * Tweak debug info keys for consistency. --- Controller/Adminhtml/Address/Api.php | 62 +++++++++++++++++++ Helper/ApiClientHelper.php | 10 ++- Helper/StoreConfigHelper.php | 42 ++++++++++--- Model/PostcodeModel.php | 20 +++--- Service/CsrfValidator.php | 19 +++++- .../js/ko/bindings/init-intl-autocomplete.js | 1 + 6 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 Controller/Adminhtml/Address/Api.php diff --git a/Controller/Adminhtml/Address/Api.php b/Controller/Adminhtml/Address/Api.php new file mode 100644 index 0000000..7ad84d5 --- /dev/null +++ b/Controller/Adminhtml/Address/Api.php @@ -0,0 +1,62 @@ +_resultJsonFactory = $resultJsonFactory; + $this->_postcodeModel = $postcodeModel; + $this->_serviceOutputProcessor = $serviceOutputProcessor; + } + + /** + * Call address API methods + * + * @return Json + */ + public function execute(): Json + { + // Get params from path. Slice from index 4 to exclude + // /// + $params = array_slice(explode('/', trim($this->getRequest()->getPathInfo(), '/')), 4); + $params = array_map('rawurldecode', $params); + + switch (array_shift($params)) + { + case 'postcode': + $serviceMethod = 'getNlAddress'; + break; + case 'autocomplete': + $serviceMethod = 'getAddressAutocomplete'; + break; + case 'address_details': + $serviceMethod = count($params) > 1 ? 'getAddressDetailsCountry' : 'getAddressDetails'; + break; + default: + throw new \Exception('Invalid service method'); + } + + $result = $this->_postcodeModel->$serviceMethod(...$params); + $result = $this->_serviceOutputProcessor->process($result, PostcodeModelInterface::class, $serviceMethod); + return $this->_resultJsonFactory->create()->setData($result); + } +} diff --git a/Helper/ApiClientHelper.php b/Helper/ApiClientHelper.php index 0056328..df70523 100644 --- a/Helper/ApiClientHelper.php +++ b/Helper/ApiClientHelper.php @@ -564,18 +564,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/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index 6071449..3a89bf6 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -6,6 +6,8 @@ use Magento\Directory\Model\ResourceModel\Country\CollectionFactory as CountryCollectionFactory; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Backend\Model\UrlInterface as BackendUrlInterface; +use Magento\Framework\App\State as AppState; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Locale\ResolverInterface; @@ -22,6 +24,8 @@ class StoreConfigHelper extends AbstractHelper protected $_countryCollectionFactory; protected $_localeResolver; protected $_formKey; + protected $_appState; + protected $_backendUrl; public const PATH = [ // Status @@ -55,6 +59,8 @@ class StoreConfigHelper extends AbstractHelper * @param CountryCollectionFactory $countryCollectionFactory * @param ResolverInterface $localeResolver * @param FormKey $formKey + * @param AppState $appState + * @param BackendUrlInterface $backendUrl */ public function __construct( Context $context, @@ -64,6 +70,8 @@ public function __construct( CountryCollectionFactory $countryCollectionFactory, ResolverInterface $localeResolver, FormKey $formKey, + AppState $appState, + BackendUrlInterface $backendUrl ) { $this->_storeManager = $storeManager; $this->_developerHelper = $developerHelper; @@ -71,6 +79,8 @@ public function __construct( $this->_countryCollectionFactory = $countryCollectionFactory; $this->_localeResolver = $localeResolver; $this->_formKey = $formKey; + $this->_appState = $appState; + $this->_backendUrl = $backendUrl; parent::__construct($context); } @@ -208,19 +218,37 @@ public function getModuleVersion(): string public function getJsinit(): array { $baseUrl = $this->getCurrentStoreBaseUrl(); - $apiBaseUrl = $baseUrl . 'postcode-eu/V1/'; + $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 . 'postcode', + 'autocomplete' => $apiBaseUrl . 'autocomplete', + 'addressDetails' => $apiBaseUrl . 'address_details', + ]; + } else { + $apiBaseUrl = $baseUrl . 'postcode-eu/V1/'; + $apiActions = [ + 'dutchAddressLookup' => $apiBaseUrl . 'nl/address', + 'autocomplete' => $apiBaseUrl . 'international/autocomplete', + 'addressDetails' => $apiBaseUrl . 'international/address', + 'validate' => $apiBaseUrl . 'international/validate', + ]; + } 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, 'base_url' => $baseUrl, - 'api_actions' => [ - 'dutchAddressLookup' => $apiBaseUrl . 'nl/address', - 'autocomplete' => $apiBaseUrl . 'international/autocomplete', - 'addressDetails' => $apiBaseUrl . 'international/address', - 'validate' => $apiBaseUrl . 'international/validate', - ], + 'api_actions' => $apiActions, 'debug' => $this->isDebugging(), 'change_fields_position' => $this->isSetFlag('change_fields_position'), 'allow_pobox_shipping' => $this->isSetFlag('allow_pobox_shipping'), diff --git a/Model/PostcodeModel.php b/Model/PostcodeModel.php index c091f30..37187e9 100644 --- a/Model/PostcodeModel.php +++ b/Model/PostcodeModel.php @@ -12,15 +12,11 @@ class PostcodeModel implements PostcodeModelInterface { + protected ApiClientHelper $_apiClientHelper; protected CsrfValidator $_csrfValidator; /** - * @var ApiClientHelper - */ - protected $apiClientHelper; - - /** - * __construct function. + * Constructor * * @access public * @param ApiClientHelper $apiClientHelper @@ -31,7 +27,7 @@ public function __construct( CsrfValidator $csrfValidator ) { - $this->apiClientHelper = $apiClientHelper; + $this->_apiClientHelper = $apiClientHelper; $this->_csrfValidator = $csrfValidator; } @@ -42,7 +38,7 @@ public function getAddressAutocomplete(string $context, string $term): Autocompl { $this->_validateRequest(); - $result = $this->apiClientHelper->getAddressAutocomplete($context, $term); + $result = $this->_apiClientHelper->getAddressAutocomplete($context, $term); return new AutocompleteData($result); } @@ -53,7 +49,7 @@ public function getAddressDetails(string $context): array { $this->_validateRequest(); - $result = $this->apiClientHelper->getAddressDetails($context); + $result = $this->_apiClientHelper->getAddressDetails($context); return [$result]; } @@ -64,7 +60,7 @@ public function getAddressDetailsCountry(string $context, string $dispatchCountr { $this->_validateRequest(); - $result = $this->apiClientHelper->getAddressDetails($context, $dispatchCountry); + $result = $this->_apiClientHelper->getAddressDetails($context, $dispatchCountry); return [$result]; } @@ -75,7 +71,7 @@ public function getNlAddress(string $zipCode, string $houseNumber): array { $this->_validateRequest(); - $result = $this->apiClientHelper->getNlAddress($zipCode, $houseNumber); + $result = $this->_apiClientHelper->getNlAddress($zipCode, $houseNumber); return [$result]; } @@ -93,7 +89,7 @@ public function validateAddress( ): array { $this->_validateRequest(); - $result = $this->apiClientHelper->validateAddress(...func_get_args()); + $result = $this->_apiClientHelper->validateAddress(...func_get_args()); return [$result]; } diff --git a/Service/CsrfValidator.php b/Service/CsrfValidator.php index c9fee71..dd1d6fa 100644 --- a/Service/CsrfValidator.php +++ b/Service/CsrfValidator.php @@ -2,7 +2,9 @@ namespace PostcodeEu\AddressValidation\Service; +use Magento\Framework\App\Area; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\State; use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Exception\LocalizedException; @@ -10,19 +12,30 @@ class CsrfValidator { private FormKeyValidator $_formKeyValidator; private HttpRequest $_request; + private State $_appState; public function __construct( FormKeyValidator $formKeyValidator, - HttpRequest $request + HttpRequest $request, + State $appState ) { $this->_formKeyValidator = $formKeyValidator; $this->_request = $request; + $this->_appState = $appState; } public function validate(): void { - if (!$this->_request->isAjax() || !$this->_formKeyValidator->validate($this->_request)) - { + 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/view/base/web/js/ko/bindings/init-intl-autocomplete.js b/view/base/web/js/ko/bindings/init-intl-autocomplete.js index a58aea0..ba34bf3 100644 --- a/view/base/web/js/ko/bindings/init-intl-autocomplete.js +++ b/view/base/web/js/ko/bindings/init-intl-autocomplete.js @@ -27,6 +27,7 @@ define([ term = encodeURIComponent(term); return this.xhrGet( + // See client/helper for language and buildingListMode parameters. `${this.options.autocompleteUrl}/${context}/${term}?form_key=${viewModel.settings.form_key}`, response ); From e0c55112d7ace173847eb28a0c29ffe9d3f41fa3 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Tue, 5 May 2026 11:43:35 +0200 Subject: [PATCH 06/10] Support multiple API accounts --- Block/System/Config/Status.php | 6 +- Cron/UpdateApiData.php | 201 +++++++++++++++--- Helper/Data.php | 34 +-- Helper/StoreConfigHelper.php | 100 +++++++-- Observer/System/Config.php | 107 ++++++---- .../AddAddressAutofillToOrderCreateForm.php | 11 +- 6 files changed, 352 insertions(+), 107 deletions(-) diff --git a/Block/System/Config/Status.php b/Block/System/Config/Status.php index aa2f2b9..d809b7f 100644 --- a/Block/System/Config/Status.php +++ b/Block/System/Config/Status.php @@ -191,13 +191,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/Cron/UpdateApiData.php b/Cron/UpdateApiData.php index 5d3adba..560312d 100644 --- a/Cron/UpdateApiData.php +++ b/Cron/UpdateApiData.php @@ -2,9 +2,15 @@ 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 @@ -13,6 +19,12 @@ class UpdateApiData protected $_apiClientHelper; protected $_configWriter; protected $_storeConfigHelper; + protected $_storeManager; + protected $_configCollectionFactory; + protected $_encryptor; + protected $_cacheTypeList; + protected $_hasChanges = false; + protected $_existingConfig = []; /** * Constructor @@ -22,58 +34,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/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 3a89bf6..803de9e 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -2,11 +2,11 @@ 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\Backend\Model\UrlInterface as BackendUrlInterface; use Magento\Framework\App\State as AppState; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Encryption\EncryptorInterface; @@ -89,11 +89,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); } /** @@ -101,45 +103,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; @@ -213,11 +220,12 @@ 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(); + $baseUrl = $this->getCurrentStoreBaseUrl($storeId); $isAdmin = false; try { @@ -244,9 +252,9 @@ public function getJsinit(): array } 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' => $apiActions, 'debug' => $this->isDebugging(), @@ -261,11 +269,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()]); } @@ -273,10 +282,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 + { + $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 { - return $this->isSetFlag('api_debug') && $this->_developerHelper->isDevAllowed(); + 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) { + // Area code not set, fall through. + } + + return [ScopeInterface::SCOPE_STORES, null]; } } diff --git a/Observer/System/Config.php b/Observer/System/Config.php index fc704a1..a12a67b 100644 --- a/Observer/System/Config.php +++ b/Observer/System/Config.php @@ -10,6 +10,7 @@ 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 @@ -21,6 +22,9 @@ class Config implements ObserverInterface protected $_cacheFrontendPool; protected $_storeConfigHelper; protected $_request; + protected $_messageManager; + protected $_scopeType; + protected $_scopeId; /** * Constructor @@ -33,7 +37,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 +46,8 @@ public function __construct( CacheFrontendPool $cacheFrontendPool, ApiClientHelper $apiClientHelper, StoreConfigHelper $storeConfigHelper, - RequestInterface $request + RequestInterface $request, + ManagerInterface $messageManager ) { $this->_configWriter = $configWriter; $this->_logger = $logger; @@ -51,6 +56,7 @@ public function __construct( $this->_apiClientHelper = $apiClientHelper; $this->_storeConfigHelper = $storeConfigHelper; $this->_request = $request; + $this->_messageManager = $messageManager; } /** @@ -58,63 +64,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 +128,6 @@ public function execute(Observer $observer): void /** * Clean config cache. - * - * @return void */ protected function _cleanConfigCache(): void { @@ -133,13 +136,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', ); From f05f2a811b9c62b6e474716db07d8730f1129810 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Wed, 6 May 2026 13:50:04 +0200 Subject: [PATCH 07/10] Refactor admin API controller, use URL templates --- Controller/Adminhtml/Address/Api.php | 53 +++++++++++-------- Helper/StoreConfigHelper.php | 15 +++--- .../js/form/components/address-autofill-nl.js | 17 +++--- .../js/ko/bindings/init-intl-autocomplete.js | 11 ++-- .../customer/address/get-validated-address.js | 23 ++++---- 5 files changed, 59 insertions(+), 60 deletions(-) diff --git a/Controller/Adminhtml/Address/Api.php b/Controller/Adminhtml/Address/Api.php index 7ad84d5..2e446d7 100644 --- a/Controller/Adminhtml/Address/Api.php +++ b/Controller/Adminhtml/Address/Api.php @@ -10,8 +10,10 @@ use Magento\Framework\Webapi\ServiceOutputProcessor; use PostcodeEu\AddressValidation\Api\PostcodeModelInterface; -Class Api extends Action implements HttpGetActionInterface +class Api extends Action implements HttpGetActionInterface { + const ADMIN_RESOURCE = 'PostcodeEu_AddressValidation::config_postcode_eu'; + protected $_resultJsonFactory; protected $_postcodeModel; protected $_serviceOutputProcessor; @@ -35,28 +37,33 @@ public function __construct( */ public function execute(): Json { - // Get params from path. Slice from index 4 to exclude - // /// - $params = array_slice(explode('/', trim($this->getRequest()->getPathInfo(), '/')), 4); - $params = array_map('rawurldecode', $params); - - switch (array_shift($params)) - { - case 'postcode': - $serviceMethod = 'getNlAddress'; - break; - case 'autocomplete': - $serviceMethod = 'getAddressAutocomplete'; - break; - case 'address_details': - $serviceMethod = count($params) > 1 ? 'getAddressDetailsCountry' : 'getAddressDetails'; - break; - default: - throw new \Exception('Invalid service method'); - } + $resultJson = $this->_resultJsonFactory->create(); + $request = $this->getRequest(); - $result = $this->_postcodeModel->$serviceMethod(...$params); - $result = $this->_serviceOutputProcessor->process($result, PostcodeModelInterface::class, $serviceMethod); - return $this->_resultJsonFactory->create()->setData($result); + 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/Helper/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index 803de9e..c9f0006 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -237,17 +237,18 @@ public function getJsinit($storeId = null): array if ($isAdmin) { $apiBaseUrl = $this->_backendUrl->getUrl('postcode_eu/address/api'); $apiActions = [ - 'dutchAddressLookup' => $apiBaseUrl . 'postcode', - 'autocomplete' => $apiBaseUrl . 'autocomplete', - 'addressDetails' => $apiBaseUrl . 'address_details', + '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', - 'autocomplete' => $apiBaseUrl . 'international/autocomplete', - 'addressDetails' => $apiBaseUrl . 'international/address', - 'validate' => $apiBaseUrl . 'international/validate', + '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, ]; } 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 14ae12c..ad64295 100644 --- a/view/base/web/js/form/components/address-autofill-nl.js +++ b/view/base/web/js/form/components/address-autofill-nl.js @@ -117,13 +117,13 @@ define([ }, getAddress: function (acceptUnknownAddition = false) { - 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}`; + 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,9 +132,8 @@ define([ this.childHouseNumber().error(false); $.get({ - url: url, + url, cache: true, - data: {form_key: this.settings.form_key}, dataType: 'json', success: ([response]) => { if (response.error) { 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 ba34bf3..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,25 +21,20 @@ define([ showLogo: viewModel.showLogo ?? true, }); - // Override methods to add form_key. + // 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}/${context}/${term}?form_key=${viewModel.settings.form_key}`, + this.options.autocompleteUrl.replace('{context}', context).replace('{term}', term), response ); }; viewModel.intlAutocompleteInstance.getDetails = function (...args) { - const response = args.pop(); - - return this.xhrGet( - `${this.options.addressDetailsUrl}/${args.join('/')}?form_key=${viewModel.settings.form_key}`, - response - ); + return this.xhrGet(this.options.addressDetailsUrl.replace('{context}', args[0]), args.at(-1)); }; viewModel.inputElement = element; 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 85916e8..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,19 +3,17 @@ define([ ], function (Registry) { 'use strict'; - function validateAddress(country, streetAndBuilding, postcode, locality) { + function validateAddress(country, streetAndBuilding = '', postcode = '', locality = '') { const settings = Registry.get('address_autofill').settings, - params = [ - 'streetAndBuilding=' + encodeURIComponent(streetAndBuilding ?? ''), - 'postcode=' + encodeURIComponent(postcode ?? ''), - 'locality=' + encodeURIComponent(locality ?? ''), - 'form_key=' + settings.form_key, - ].join('&'), - url = `${settings.api_actions.validate}/${country}?${params}`; + url = new URL(settings.api_actions.validate.replace('{country}', country)), + headers = {'X-Requested-With': 'XMLHttpRequest'}; - return fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}}).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(); } @@ -33,8 +31,7 @@ define([ && !top.status.isAmbiguous && top.status.grade < 'C' && ['Building', 'BuildingPartial'].includes(top.status.validationLevel) - ) - { + ) { return top; } From f0c75653b5aea0988cf564d3faecbb9cb81fbde1 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Wed, 6 May 2026 15:45:35 +0200 Subject: [PATCH 08/10] Move GraphQL support to separate module --- GraphQl/Exception/GraphQlHeaderException.php | 51 ------ Model/Resolver/AddressApiSettings.php | 39 ----- Model/Resolver/DutchAddress.php | 45 ----- Model/Resolver/IntlAddress.php | 46 ----- Model/Resolver/IntlAddress/Details.php | 33 ---- Model/Resolver/IntlAddress/Matches.php | 42 ----- Model/Resolver/ValidatedAddress.php | 54 ------ README.md | 2 +- etc/module.xml | 1 - etc/schema.graphqls | 170 ------------------- 10 files changed, 1 insertion(+), 482 deletions(-) delete mode 100644 GraphQl/Exception/GraphQlHeaderException.php delete mode 100644 Model/Resolver/AddressApiSettings.php delete mode 100644 Model/Resolver/DutchAddress.php delete mode 100644 Model/Resolver/IntlAddress.php delete mode 100644 Model/Resolver/IntlAddress/Details.php delete mode 100644 Model/Resolver/IntlAddress/Matches.php delete mode 100644 Model/Resolver/ValidatedAddress.php delete mode 100644 etc/schema.graphqls 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/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/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/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.") -} From 4b8659350af8e78b7231455d8a74aedb10400e43 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Wed, 6 May 2026 17:02:06 +0200 Subject: [PATCH 09/10] Update version / prepare for release * Reduce code sniffer errors. * Remove unused form_key property. * Fix syntax error in PHP 7.4. --- Api/Data/MagentoDebugInfo.php | 6 ++++++ Api/Data/MagentoDebugInfo/Configuration.php | 2 ++ Api/Data/MagentoDebugInfo/MagentoModule.php | 2 ++ Block/System/Config/Status.php | 13 +++++++++++++ Controller/Adminhtml/Address/Api.php | 3 +++ Cron/NotifyModuleUpdate.php | 3 +++ Cron/UpdateApiData.php | 10 ++++++++++ Helper/ApiClientHelper.php | 18 ++++++++++++++++++ Helper/StoreConfigHelper.php | 13 ++++++++++--- Model/PostcodeModel.php | 12 +++++------- Model/System/Message/RebrandNotice.php | 6 ++---- Model/UpdateNotificationRepository.php | 2 ++ Observer/System/Config.php | 19 +++++++++++++++++++ Plugin/ValidatorPlugin.php | 1 + Service/CsrfValidator.php | 6 ++++-- Service/PostcodeApiClient.php | 3 +++ Setup/Patch/Data/EncryptApiSecrets.php | 4 +++- Setup/Patch/Data/UpdateApiStatusConfig.php | 9 ++++++++- composer.json | 2 +- etc/config.xml | 2 +- 20 files changed, 116 insertions(+), 20 deletions(-) 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 d809b7f..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; /** diff --git a/Controller/Adminhtml/Address/Api.php b/Controller/Adminhtml/Address/Api.php index 2e446d7..bc27138 100644 --- a/Controller/Adminhtml/Address/Api.php +++ b/Controller/Adminhtml/Address/Api.php @@ -14,8 +14,11 @@ class Api extends Action implements HttpGetActionInterface { const ADMIN_RESOURCE = 'PostcodeEu_AddressValidation::config_postcode_eu'; + /** @var JsonFactory */ protected $_resultJsonFactory; + /** @var PostcodeModelInterface */ protected $_postcodeModel; + /** @var ServiceOutputProcessor */ protected $_serviceOutputProcessor; public function __construct( 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 560312d..f08bb61 100644 --- a/Cron/UpdateApiData.php +++ b/Cron/UpdateApiData.php @@ -15,15 +15,25 @@ 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 = []; /** diff --git a/Helper/ApiClientHelper.php b/Helper/ApiClientHelper.php index df70523..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; /** @@ -501,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 { diff --git a/Helper/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index c9f0006..36e719f 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -18,13 +18,21 @@ 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 = [ @@ -262,7 +270,6 @@ public function getJsinit($storeId = null): array 'change_fields_position' => $this->isSetFlag('change_fields_position'), 'allow_pobox_shipping' => $this->isSetFlag('allow_pobox_shipping'), 'split_street_values' => $this->isSetFlag('split_street_values'), - 'form_key' => $this->_formKey->getFormKey(), ]; } @@ -315,8 +322,8 @@ public function getScopeFromRequest(): array /** * Get scope type and code based on request. - * @param int|string|null $storeId * + * @param int|string|null $storeId * @return array - Scope type and id */ private function _getScopeContext($storeId = null): array @@ -332,7 +339,7 @@ private function _getScopeContext($storeId = null): array return [$scope, $scopeId ?: null]; } - } catch (\Magento\Framework\Exception\LocalizedException) { + } catch (\Magento\Framework\Exception\LocalizedException $e) { // Area code not set, fall through. } diff --git a/Model/PostcodeModel.php b/Model/PostcodeModel.php index 37187e9..4245c06 100644 --- a/Model/PostcodeModel.php +++ b/Model/PostcodeModel.php @@ -12,7 +12,9 @@ class PostcodeModel implements PostcodeModelInterface { + /** @var ApiClientHelper */ protected ApiClientHelper $_apiClientHelper; + /** @var CsrfValidator */ protected CsrfValidator $_csrfValidator; /** @@ -25,8 +27,7 @@ class PostcodeModel implements PostcodeModelInterface public function __construct( ApiClientHelper $apiClientHelper, CsrfValidator $csrfValidator - ) - { + ) { $this->_apiClientHelper = $apiClientHelper; $this->_csrfValidator = $csrfValidator; } @@ -95,12 +96,9 @@ public function validateAddress( private function _validateRequest(): void { - try - { + try { $this->_csrfValidator->validate(); - } - catch (LocalizedException $e) - { + } catch (LocalizedException $e) { throw new WebapiException(__($e->getMessage()), 0, WebapiException::HTTP_FORBIDDEN); } } 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 a12a67b..a055fc6 100644 --- a/Observer/System/Config.php +++ b/Observer/System/Config.php @@ -15,15 +15,34 @@ 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; /** 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/Service/CsrfValidator.php b/Service/CsrfValidator.php index dd1d6fa..1b5de34 100644 --- a/Service/CsrfValidator.php +++ b/Service/CsrfValidator.php @@ -10,8 +10,11 @@ class CsrfValidator { + /** @var FormKeyValidator */ private FormKeyValidator $_formKeyValidator; + /** @var HttpRequest */ private HttpRequest $_request; + /** @var State */ private State $_appState; public function __construct( @@ -30,8 +33,7 @@ public function validate(): void if ($this->_appState->getAreaCode() === Area::AREA_ADMINHTML) { return; } - } - catch (LocalizedException $e) { + } catch (LocalizedException $e) { // Area code not set. } diff --git a/Service/PostcodeApiClient.php b/Service/PostcodeApiClient.php index b9974d9..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( 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 8750e65..676d27a 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "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", 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 From 03c2f364f3c64da7632a8af8d18a075975f3b450 Mon Sep 17 00:00:00 2001 From: Jerry Smidt Date: Tue, 12 May 2026 15:52:55 +0200 Subject: [PATCH 10/10] Fix missing storeId arguments --- Helper/StoreConfigHelper.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Helper/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index 36e719f..1abd0ff 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -266,10 +266,10 @@ public function getJsinit($storeId = null): array 'show_hide_address_fields' => $this->getValue('show_hide_address_fields', $storeId) ?? ShowHideAddressFields::SHOW, 'base_url' => $baseUrl, 'api_actions' => $apiActions, - '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'), + '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), ]; }