<?php

/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

use PrestaShop\PrestaShop\Adapter\Configuration as ConfigurationAdapter;
use PrestaShop\PrestaShop\Adapter\ContainerBuilder;
use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever;
use PrestaShop\PrestaShop\Adapter\Presenter\Cart\CartPresenter;
use PrestaShop\PrestaShop\Adapter\Presenter\Object\ObjectPresenter;
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
use PrestaShop\PrestaShop\Core\Security\PasswordPolicyConfiguration;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\IpUtils;

class FrontControllerCore extends Controller
{
    /** @var array Controller warning notifications */
    public $warning = [];

    /** @var array Controller success notifications */
    public $success = [];

    /** @var array Controller info notifications */
    public $info = [];

    /** @var string Language ISO code */
    public $iso;

    /** @var bool If set to true, will redirected user to login page during init function. */
    public $auth = false;

    /**
     * Route of PrestaShop page to redirect to after forced login.
     *
     * @see $auth
     *
     * @var bool|string
     */
    public $authRedirection = false;

    /** @var bool SSL connection flag */
    public $ssl = false;

    /** @var int If Country::GEOLOC_CATALOG_MODE, switches display to restricted country page during init. */
    protected $restrictedCountry = Country::GEOLOC_ALLOWED;

    /** @var bool If true, forces display to maintenance page. */
    protected $maintenance = false;

    /** @var string[] Adds excluded `$_GET` keys for redirection */
    protected $redirectionExtraExcludedKeys = [];

    /**
     * True if controller has already been initialized.
     * Prevents initializing controller more than once.
     *
     * @var bool
     */
    public static $initialized = false;

    /**
     * @var array Holds current customer's groups
     */
    protected static $currentCustomerGroups;

    /**
     * @var ObjectPresenter
     */
    public $objectPresenter;

    /**
     * @var object CartPresenter
     */
    public $cart_presenter;

    /**
     * @var object TemplateFinder
     */
    private $templateFinder;

    /**
     * @var object StylesheetManager
     */
    protected $stylesheetManager;

    /**
     * @var object JavascriptManager
     */
    protected $javascriptManager;

    /**
     * @var object CccReducer
     */
    protected $cccReducer;

    /**
     * @var array Contains the result of getTemplateVarUrls method
     */
    protected $urls;

    /**
     * Set this parameter to false if you don't want cart's invoice address
     * to be set automatically (this behavior is kept for legacy and BC purpose)
     *
     * @var bool automaticallyAllocateInvoiceAddress
     */
    protected $automaticallyAllocateInvoiceAddress = true;

    /**
     * Set this parameter to false if you don't want cart's delivery address
     * to be set automatically (this behavior is kept for legacy and BC purpose)
     *
     * @var bool automaticallyAllocateDeliveryAddress
     */
    protected $automaticallyAllocateDeliveryAddress = true;

    /** @var string Page name */
    public $page_name;

    /**
     * Controller constructor.
     *
     * @global bool $useSSL SSL connection flag
     */
    public function __construct()
    {
        $this->controller_type = 'front';

        global $useSSL;

        parent::__construct();

        if (Configuration::get('PS_SSL_ENABLED')) {
            $this->ssl = true;
        }

        if (isset($useSSL)) {
            $this->ssl = $useSSL;
        } else {
            $useSSL = $this->ssl;
        }

        // Prepare presenters that we will require on every page
        $this->objectPresenter = new ObjectPresenter();
        $this->cart_presenter = new CartPresenter();

        $this->templateFinder = new TemplateFinder($this->context->smarty->getTemplateDir(), '.tpl');
        $this->stylesheetManager = new StylesheetManager(
            [_PS_THEME_URI_, _PS_PARENT_THEME_URI_, __PS_BASE_URI__],
            new ConfigurationAdapter()
        );
        $this->javascriptManager = new JavascriptManager(
            [_PS_THEME_URI_, _PS_PARENT_THEME_URI_, __PS_BASE_URI__],
            new ConfigurationAdapter()
        );
        $this->cccReducer = new CccReducer(
            _PS_THEME_DIR_ . 'assets/cache/',
            new ConfigurationAdapter(),
            new Filesystem()
        );
    }

    /**
     * Check if the controller is available for the current user/visitor.
     *
     * @see Controller::checkAccess()
     *
     * @return bool
     */
    public function checkAccess()
    {
        return true;
    }

    /**
     * Check if the current user/visitor has valid view permissions.
     * Currently not used in any native controller.
     *
     * @see Controller::viewAccess
     *
     * @return bool
     */
    public function viewAccess()
    {
        return true;
    }

    /**
     * Initializes front controller: sets smarty variables,
     * class properties, redirects depending on context, etc.
     *
     * @global bool     $useSSL           SSL connection flag
     *
     * @throws PrestaShopException
     */
    public function init()
    {
        Hook::exec(
            'actionFrontControllerInitBefore',
            [
                'controller' => $this,
            ]
        );
        Hook::exec('action' . $this->getControllerName() . 'InitBefore', ['controller' => $this]);

        /*
         * Globals are DEPRECATED as of version 1.5.0.1
         * Use the Context object to access objects instead.
         * Example: $this->context->cart
         */
        global $useSSL;

        if (self::$initialized) {
            return;
        }

        self::$initialized = true;

        parent::init();

        // If current URL use SSL, set it true (used a lot for module redirect)
        if (Tools::usingSecureMode()) {
            $useSSL = true;
        }

        // Redirect to SSL variant of the page if required and visited in non-ssl mode and not in cli context
        if (!Tools::isPHPCLI()) {
            $this->sslRedirection();
        }

        if ($this->ajax) {
            $this->display_header = false;
            $this->display_footer = false;
        }

        ob_start();

        // Initialize URL provider in context, depending on SSL mode
        $protocol_link = (Configuration::get('PS_SSL_ENABLED') || Tools::usingSecureMode()) ? 'https://' : 'http://';
        $useSSL = ($this->ssl && Configuration::get('PS_SSL_ENABLED')) || Tools::usingSecureMode();
        $protocol_content = ($useSSL) ? 'https://' : 'http://';
        $link = new Link($protocol_link, $protocol_content);
        $this->context->link = $link;

        // Attempt to recover cart, if the user is using recovery link
        // This is used by abandoned cart modules or when sending a prepared order to customer from backoffice
        $this->recoverCart();

        // Redirect user to login page, if the controller requires authentication
        if ($this->auth && !$this->context->customer->isLogged()) {
            Tools::redirect($this->context->link->getPageLink(
                'authentication',
                null,
                null,
                $this->authRedirection ? ['back' => $this->authRedirection] : null
            ));
        }

        // If the theme is missing, we need to throw an Exception
        if (!is_dir(_PS_THEME_DIR_)) {
            throw new PrestaShopException($this->trans('Current theme is unavailable. Please check your theme\'s directory name ("%s") and permissions.', [htmlspecialchars(basename(rtrim(_PS_THEME_DIR_, '/\\')))], 'Admin.Design.Notification'));
        }

        /*
         * The default country is already set in config.inc.php before loading the front controller. This country will be used
         * to display prices, taxes, delivery options and other country specific data.
         *
         * Here, the country can get modified based on geolocation or browser's preferred country.
         *
         * The country may also be changed further down in this method, if we have a cart with an address assigned to it.
         * If there is a cart already, the context country is set to the country of that cart.
         *
         * If you are looking to override the country selection logic, you can use actionFrontControllerInitBefore above
         * and pre-set the things in beforehand or actionFrontControllerInitContextCountryAfter below, if you want to use the built
         * in geolocation logic and just modify the result.
         */
        if (Configuration::get('PS_GEOLOCATION_ENABLED')) {
            if (($new_default = $this->geolocationManagement($this->context->country)) && Validate::isLoadedObject($new_default)) {
                $this->context->country = $new_default;
            }
        } elseif (Configuration::get('PS_DETECT_COUNTRY')) {
            $has_currency = isset($this->context->cookie->id_currency) && (int) $this->context->cookie->id_currency;
            $has_country = isset($this->context->cookie->iso_code_country) && $this->context->cookie->iso_code_country;
            $has_address_type = false;

            if ((int) $this->context->cookie->id_cart) {
                $cart = new Cart((int) $this->context->cookie->id_cart);
                if (Validate::isLoadedObject($cart)) {
                    $has_address_type = isset($cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}) && $cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
                }
            }

            if ((!$has_currency || $has_country) && !$has_address_type) {
                if ($has_country && Validate::isLanguageIsoCode($this->context->cookie->iso_code_country)) {
                    $id_country = (int) Country::getByIso(strtoupper($this->context->cookie->iso_code_country));
                } elseif (Tools::isCountryFromBrowserAvailable()) {
                    $id_country = (int) Country::getByIso(Tools::getCountryIsoCodeFromHeader(), true);
                } else {
                    $id_country = Tools::getCountry();
                }

                $country = new Country($id_country, (int) $this->context->language->id);

                if (!$has_currency && Validate::isLoadedObject($country) && $this->context->country->id !== $country->id) {
                    $this->context->country = $country;
                    $this->context->cookie->id_currency = (int) Currency::getCurrencyInstance($country->id_currency ? (int) $country->id_currency : Currency::getDefaultCurrencyId())->id;
                    $this->context->cookie->iso_code_country = strtoupper($country->iso_code);
                }
            }
        }

        /*
         * Allow modules to override the country selection logic after running geolocation, if needed. Just do whatever you need
         * and assign proper country to context->country.
         */
        Hook::exec(
            'actionFrontControllerDetectContextCountryAfter',
            [
                'controller' => $this,
            ]
        );

        /*
         * Get proper currency from the cookie and $_GET parameters. It will provide us with a requested currency
         * or a default currency, if the requested one is not valid anymore.
         * Assign that currency to the context, so we can immediately use it for calculations.
         */
        $this->context->currency = Tools::setCurrency($this->context->cookie);

        /*
         * Allow modules to override the currency selection logic if needed. Just do whatever you need
         * and assign proper currency to context->currency and context->cookie->id_currency. Useful if you want
         * to implement custom currency selection logic, for example always force a currency based on country.
         */
        Hook::exec(
            'actionFrontControllerInitContextCurrencyAfter',
            [
                'controller' => $this,
            ]
        );

        if (isset($_GET['logout']) || ($this->context->customer->logged && Customer::isBanned($this->context->customer->id))) {
            $this->context->customer->logout();

            Tools::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null);
        } elseif (isset($_GET['mylogout'])) {
            $this->context->customer->mylogout();
            Tools::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null);
        }

        /*
         * If we have an information about some cart in the cookie, we will try to use it, but we need to properly validate it.
         * It can be deleted, order already placed for it and other edge scenarios.
         */
        if ((int) $this->context->cookie->id_cart) {
            if (!isset($cart)) {
                $cart = new Cart((int) $this->context->cookie->id_cart);
            }

            /*
             * Check if cart object is valid and not deleted.
             * Check if there is not an order already placed on a different device or different tab.
             */
            if (!Validate::isLoadedObject($cart) || $cart->orderExists()) {
                PrestaShopLogger::addLog('Frontcontroller::init - Cart cannot be loaded or an order has already been placed using this cart', 1, null, 'Cart', (int) $this->context->cookie->id_cart, true);
                unset($this->context->cookie->id_cart, $cart, $this->context->cookie->checkedTOS);
                $this->context->cookie->check_cgv = false;

            /*
             * If geolocation is enabled and we are not allowed to order from our country, we will delete the cart.
             */
            } elseif (
                (int) Configuration::get('PS_GEOLOCATION_ENABLED')
                && !in_array(strtoupper($this->context->cookie->iso_code_country), explode(';', Configuration::get('PS_ALLOWED_COUNTRIES')))
                && $cart->nbProducts()
                && (int) Configuration::get('PS_GEOLOCATION_NA_BEHAVIOR') != -1
                && !FrontController::isInWhitelistForGeolocation()
                && !in_array($_SERVER['SERVER_NAME'], ['localhost', '127.0.0.1', '::1'])
            ) {
                /* Delete product of cart, if user can't make an order from his country */
                PrestaShopLogger::addLog('Frontcontroller::init - GEOLOCATION is deleting a cart', 1, null, 'Cart', (int) $this->context->cookie->id_cart, true);
                unset($this->context->cookie->id_cart, $cart);

            /*
             * Check if cart data is still matching to what is set in our cookie - currency, language and customer.
             * If not, update it on the cart.
             */
            } elseif (
                $this->context->cookie->id_customer != $cart->id_customer
                || $this->context->language->id != $cart->id_lang
                || $this->context->currency->id != $cart->id_currency
            ) {
                // update cart values
                if ($this->context->cookie->id_customer) {
                    $cart->id_customer = (int) $this->context->cookie->id_customer;
                }
                $cart->id_lang = (int) $this->context->language->id;
                $cart->id_currency = (int) $this->context->currency->id;
                $this->context->cart = $cart;
                $cart->update();
            }

            /*
             * If we don't have any addresses set on the cart and we have a valid customer ID, we will try to automatically
             * assign addresses to that cart. We will do it by taking the first valid address of the customer.
             *
             * If that customer exists but don't have any addresses, it will assign zero and we go on.
             */
            if (
                isset($cart)
                && (!isset($cart->id_address_delivery) || $cart->id_address_delivery == 0 || !isset($cart->id_address_invoice) || $cart->id_address_invoice == 0)
                && $this->context->cookie->id_customer
            ) {
                $to_update = false;
                if ($this->automaticallyAllocateDeliveryAddress && (!isset($cart->id_address_delivery) || $cart->id_address_delivery == 0)) {
                    $to_update = true;
                    $cart->id_address_delivery = (int) Address::getFirstCustomerAddressId($cart->id_customer);
                }
                if ($this->automaticallyAllocateInvoiceAddress && (!isset($cart->id_address_invoice) || $cart->id_address_invoice == 0)) {
                    $to_update = true;
                    $cart->id_address_invoice = (int) Address::getFirstCustomerAddressId($cart->id_customer);
                }
                if ($to_update) {
                    $cart->update();
                }
            }
        }

        /*
         * If the previous logic didn't resolve into any valid cart we can use, we will create a new empty one.
         *
         * It does not have any ID yet. It's just an empty cart object, but modules can use it and ask for it's data
         * without checking a cart exists in a context and all that boring stuff. It will get assigned an ID after
         * first save or update.
         */
        if (!isset($cart) || !$cart->id) {
            $cart = new Cart();
            $cart->id_lang = (int) $this->context->language->id;
            $cart->id_currency = (int) $this->context->currency->id;
            $cart->id_guest = (int) $this->context->cookie->id_guest;
            $cart->id_shop_group = (int) $this->context->shop->id_shop_group;
            $cart->id_shop = $this->context->shop->id;
            if ($this->context->cookie->id_customer) {
                $cart->id_customer = (int) $this->context->cookie->id_customer;
                $cart->id_address_delivery = (int) Address::getFirstCustomerAddressId($cart->id_customer);
                $cart->id_address_invoice = (int) $cart->id_address_delivery;
            } else {
                $cart->id_address_delivery = 0;
                $cart->id_address_invoice = 0;
            }
            $this->context->cart = $cart;
        } else {
            $this->context->cart = $cart;
            $this->context->cart->checkAndUpdateAddresses();
        }

        /*
         * We also need to run automatic cart rule actions.
         * autoAddToCart is required to automatically assigning newly created cart rules with no code (automatic).
         * autoRemoveFromCart is needed to verify, if the cart rules already in a cart are still valid.
         */
        CartRule::autoRemoveFromCart($this->context);
        CartRule::autoAddToCart($this->context);

        $this->context->smarty->assign('request_uri', Tools::safeOutput(urldecode($_SERVER['REQUEST_URI'])));

        // Automatically redirect to the canonical URL if needed
        if (!empty($this->php_self) && !Tools::getValue('ajax') && !Tools::isPHPCLI()) {
            $this->canonicalRedirection($this->context->link->getPageLink($this->php_self, $this->ssl, $this->context->language->id));
        }

        Product::initPricesComputation();

        /*
         * If there is an address assigned to the cart in context, the context->country MUST be set to the country of that address.
         * We use delivery or invoice address, depending on PS_TAX_ADDRESS_TYPE configuration.
         */
        if (isset($cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}) && $cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}) {
            $infos = Address::getCountryAndState((int) $cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
            $this->context->country = new Country((int) $infos['id_country']);
        }

        if (!Tools::isPHPCLI()) {
            $this->displayMaintenancePage();
        }

        if (!Tools::isPHPCLI() && Country::GEOLOC_FORBIDDEN == $this->restrictedCountry) {
            $this->displayRestrictedCountryPage();
        }

        Hook::exec(
            'actionFrontControllerInitAfter',
            [
                'controller' => $this,
            ]
        );
        Hook::exec('action' . $this->getControllerName() . 'InitAfter', ['controller' => $this]);
    }

    /**
     * Method that is executed after init() and checkAccess().
     * Used to process user input.
     *
     * @see Controller::run()
     */
    public function postProcess()
    {
    }

    /**
     * Initializes a set of commonly used variables, available for use in the template.
     *
     * @throws PrestaShopException
     */
    protected function assignGeneralPurposeVariables()
    {
        $templateVars = [];
        $cart = $this->context->cart;

        Hook::exec(
            'actionFrontControllerSetVariablesBefore',
            [
                'templateVars' => &$templateVars,
                'cart' => $cart,
            ]
        );
        Hook::exec('action' . $this->getControllerName() . 'SetVariablesBefore',
            [
                'templateVars' => &$templateVars,
                'cart' => $cart,
            ]
        );

        $templateVars = [
            'cart' => $this->cart_presenter->present($cart, true),
            'currency' => $this->getTemplateVarCurrency(),
            'customer' => $this->getTemplateVarCustomer(),
            'country' => $this->objectPresenter->present($this->context->country),
            'language' => $this->objectPresenter->present($this->context->language),
            'page' => $this->getTemplateVarPage(),
            'shop' => $this->getTemplateVarShop(),
            'core_js_public_path' => $this->getCoreJsPublicPath(),
            'urls' => $this->getTemplateVarUrls(),
            'configuration' => $this->getTemplateVarConfiguration(),
            'field_required' => $this->context->customer->validateFieldsRequiredDatabase(),
            'breadcrumb' => $this->getBreadcrumb(),
            'link' => $this->context->link,
            'time' => time(),
            'static_token' => Tools::getToken(false),
            'token' => Tools::getToken(),
            'debug' => _PS_MODE_DEV_,
        ];

        // An array [module_name => module_output] will be returned
        $modulesVariables = Hook::exec(
            'actionFrontControllerSetVariables',
            [
                'templateVars' => &$templateVars,
            ],
            null,
            true
        );
        $modulesVariables = array_merge(
            $modulesVariables,
            Hook::exec('action' . $this->getControllerName() . 'SetVariables',
                [
                    'templateVars' => &$templateVars,
                ],
                null,
                true
            )
        );

        if (is_array($modulesVariables)) {
            foreach ($modulesVariables as $moduleName => $variables) {
                $templateVars['modules'][$moduleName] = $variables;
            }
        }

        $this->context->smarty->assign($templateVars);

        Media::addJsDef([
            'prestashop' => $this->buildFrontEndObject($templateVars),
        ]);
    }

    /**
     * Builds the "prestashop" javascript object that will be sent to the front end.
     *
     * @param array $object Variables inserted in the template (see FrontController::assignGeneralPurposeVariables)
     *
     * @return array Variables to be inserted in the "prestashop" javascript object
     *
     * @throws PrestaShop\PrestaShop\Core\Filter\FilterException
     * @throws PrestaShopException
     */
    protected function buildFrontEndObject($object)
    {
        $object = $this->get('prestashop.core.filter.front_end_object.main')
            ->filter($object);

        Hook::exec('actionBuildFrontEndObject', [
            'obj' => &$object,
        ]);
        Hook::exec('actionBuild' . $this->getControllerName() . 'FrontEndObject', [
            'obj' => &$object,
        ]);

        return $object;
    }

    /**
     * Initializes common front page content: header, footer and side columns.
     *
     * @throws PrestaShopException
     */
    public function initContent()
    {
        $this->assignGeneralPurposeVariables();
        $this->process();

        $this->context->smarty->assign([
            'HOOK_HEADER' => Hook::exec('displayHeader')
                . Hook::exec('display' . $this->getControllerName() . 'Header'),
        ]);
    }

    public function initFooter()
    {
    }

    /**
     * Initialize the invalid doom page of death.
     */
    public function initCursedPage()
    {
        header('HTTP/1.1 403 Forbidden');

        $this->registerStylesheet('theme-error', '/assets/css/error.css', ['media' => 'all', 'priority' => 50]);
        $this->context->smarty->assign([
            'layout' => 'layouts/layout-error.tpl',
            'urls' => $this->getTemplateVarUrls(),
            'shop' => $this->getTemplateVarShop(),
            'stylesheets' => $this->getStylesheets(),
        ]);
        $this->layout = 'errors/forbidden.tpl';
    }

    /**
     * Called before compiling common page sections (header, footer, columns).
     * Good place to modify smarty variables. Currently not used by any native controller.
     *
     * @see FrontController::initContent()
     */
    public function process()
    {
    }

    /**
     * @return mixed
     */
    public function getStylesheets()
    {
        $cssFileList = $this->stylesheetManager->getList();

        if (Configuration::get('PS_CSS_THEME_CACHE')) {
            $cssFileList = $this->cccReducer->reduceCss($cssFileList);
        }

        return $cssFileList;
    }

    /**
     * @return mixed
     */
    public function getJavascript()
    {
        $jsFileList = $this->javascriptManager->getList();

        if (Configuration::get('PS_JS_THEME_CACHE')) {
            $jsFileList = $this->cccReducer->reduceJs($jsFileList);
        }

        return $jsFileList;
    }

    /**
     * Redirects to redirect_after link.
     *
     * @see $redirect_after
     */
    protected function redirect()
    {
        Tools::redirect($this->redirect_after);
    }

    public function redirectWithNotifications()
    {
        $notifications = json_encode([
            'error' => $this->errors,
            'warning' => $this->warning,
            'success' => $this->success,
            'info' => $this->info,
        ]);

        if (session_status() == PHP_SESSION_ACTIVE) {
            $_SESSION['notifications'] = $notifications;
        } elseif (session_status() == PHP_SESSION_NONE) {
            session_start();
            $_SESSION['notifications'] = $notifications;
        } else {
            setcookie('notifications', $notifications);
        }

        return call_user_func_array(['Tools', 'redirect'], func_get_args());
    }

    /**
     * Renders page content.
     * Used for retrocompatibility with PS 1.4.
     */
    public function displayContent()
    {
    }

    /**
     * Compiles and outputs full page content.
     *
     * @return void
     *
     * @throws Exception
     * @throws SmartyException
     */
    public function display()
    {
        $this->context->smarty->assign([
            'layout' => $this->getLayout(),
            'stylesheets' => $this->getStylesheets(),
            'javascript' => $this->getJavascript(),
            'js_custom_vars' => Media::getJsDef(),
            'notifications' => $this->prepareNotifications(),
        ]);

        $this->smartyOutputContent($this->template);
    }

    protected function smartyOutputContent($content)
    {
        $this->context->cookie->write();

        $html = '';

        $theme = $this->context->shop->theme->getName();

        if (is_array($content)) {
            foreach ($content as $tpl) {
                $html .= $this->context->smarty->fetch($tpl, null, $theme . $this->getLayout());
            }
        } else {
            $html = $this->context->smarty->fetch($content, null, $theme . $this->getLayout());
        }

        Hook::exec('actionOutputHTMLBefore', ['html' => &$html]);
        Hook::exec('actionOutput' . $this->getControllerName() . 'HTMLBefore', ['html' => &$html]);
        echo trim($html);
    }

    protected function prepareNotifications()
    {
        $notifications = [
            'error' => $this->errors,
            'warning' => $this->warning,
            'success' => $this->success,
            'info' => $this->info,
        ];

        if (session_status() == PHP_SESSION_NONE) {
            session_start();
        }

        if (session_status() == PHP_SESSION_ACTIVE && isset($_SESSION['notifications'])) {
            $notifications = array_merge_recursive($notifications, json_decode($_SESSION['notifications'], true));
            unset($_SESSION['notifications']);
        } elseif (isset($_COOKIE['notifications'])) {
            $notifications = array_merge_recursive($notifications, json_decode($_COOKIE['notifications'], true));
            unset($_COOKIE['notifications']);
        }

        return $notifications;
    }

    /**
     * Displays maintenance page if shop is closed.
     */
    protected function displayMaintenancePage()
    {
        if ($this->maintenance == true || !(int) Configuration::get('PS_SHOP_ENABLE')) {
            $this->maintenance = true;

            if (Tools::isAllowedToBypassMaintenance()) {
                return;
            }

            $allowed_ips = array_map('trim', explode(',', Configuration::get('PS_MAINTENANCE_IP')));
            if (!IpUtils::checkIp(Tools::getRemoteAddr(), $allowed_ips)) {
                header('HTTP/1.1 503 Service Unavailable');
                header('Retry-After: 3600');

                $this->setMedia();
                $this->registerStylesheet('theme-error', '/assets/css/error.css', ['media' => 'all', 'priority' => 50]);
                $this->context->smarty->assign([
                    'urls' => $this->getTemplateVarUrls(),
                    'shop' => $this->getTemplateVarShop(),
                    'HOOK_MAINTENANCE' => Hook::exec('displayMaintenance'),
                    'maintenance_text' => Configuration::get('PS_MAINTENANCE_TEXT', (int) $this->context->language->id),
                    'stylesheets' => $this->getStylesheets(),
                ]);
                $this->smartyOutputContent('errors/maintenance.tpl');

                exit;
            }
        }
    }

    /**
     * Displays 'country restricted' page if user's country is not allowed.
     */
    protected function displayRestrictedCountryPage()
    {
        header('HTTP/1.1 403 Forbidden');

        $this->registerStylesheet('theme-error', '/assets/css/error.css', ['media' => 'all', 'priority' => 50]);
        $this->context->smarty->assign([
            'urls' => $this->getTemplateVarUrls(),
            'shop' => $this->getTemplateVarShop(),
            'stylesheets' => $this->getStylesheets(),
        ]);
        $this->smartyOutputContent('errors/restricted-country.tpl');

        exit;
    }

    /**
     * Redirects to correct protocol if settings and request methods don't match.
     */
    protected function sslRedirection()
    {
        // If we call a SSL controller without SSL or a non SSL controller with SSL, we redirect with the right protocol
        if (Configuration::get('PS_SSL_ENABLED') && $_SERVER['REQUEST_METHOD'] != 'POST' && $this->ssl != Tools::usingSecureMode()) {
            $this->context->cookie->disallowWriting();
            header('HTTP/1.1 301 Moved Permanently');
            header('Cache-Control: no-cache');
            if ($this->ssl) {
                header('Location: ' . Tools::getShopDomainSsl(true) . $_SERVER['REQUEST_URI']);
            } else {
                header('Location: ' . Tools::getShopDomain(true) . $_SERVER['REQUEST_URI']);
            }
            exit;
        }
    }

    /**
     * Redirects to canonical URL.
     *
     * @param string $canonical_url
     */
    protected function canonicalRedirection(string $canonical_url = '')
    {
        if (!$canonical_url || !Configuration::get('PS_CANONICAL_REDIRECT') || strtoupper($_SERVER['REQUEST_METHOD']) != 'GET') {
            return;
        }

        $canonical_url = preg_replace('/#.*$/', '', $canonical_url);

        $match_url = rawurldecode(Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
        if (!preg_match('/^' . Tools::pRegexp(rawurldecode($canonical_url), '/') . '([&?].*)?$/', $match_url)) {
            $final_url = $this->sanitizeUrl($canonical_url);

            // Don't send any cookie
            Context::getContext()->cookie->disallowWriting();
            if (defined('_PS_MODE_DEV_') && _PS_MODE_DEV_ && $_SERVER['REQUEST_URI'] != __PS_BASE_URI__) {
                die('[Debug] This page has moved<br />Please use the following URL instead: <a href="' . $final_url . '">' . $final_url . '</a>');
            }

            $redirect_type = Configuration::get('PS_CANONICAL_REDIRECT') == 2 ? '301' : '302';
            header('HTTP/1.0 ' . $redirect_type . ' Moved');
            header('Cache-Control: no-cache');
            Tools::redirect($final_url);
        }
    }

    /**
     * Geolocation management.
     *
     * @param Country $defaultCountry
     *
     * @return Country|false
     */
    protected function geolocationManagement($defaultCountry)
    {
        if (!in_array(Tools::getRemoteAddr(), ['127.0.0.1', '::1']) && !Tools::isPHPCLI()) {
            /* Check if Maxmind Database exists */
            if (@filemtime(_PS_GEOIP_DIR_ . _PS_GEOIP_CITY_FILE_)) {
                if (!isset($this->context->cookie->iso_code_country) || (isset($this->context->cookie->iso_code_country) && !in_array(strtoupper($this->context->cookie->iso_code_country), explode(';', Configuration::get('PS_ALLOWED_COUNTRIES'))))) {
                    $reader = new GeoIp2\Database\Reader(_PS_GEOIP_DIR_ . _PS_GEOIP_CITY_FILE_);

                    try {
                        $record = $reader->city(Tools::getRemoteAddr());
                    } catch (GeoIp2\Exception\AddressNotFoundException $e) {
                        $record = null;
                    }

                    if (is_object($record) && Validate::isLanguageIsoCode($record->country->isoCode) && (int) Country::getByIso(strtoupper($record->country->isoCode)) != 0) {
                        if (!in_array(strtoupper($record->country->isoCode), explode(';', Configuration::get('PS_ALLOWED_COUNTRIES'))) && !FrontController::isInWhitelistForGeolocation()) {
                            if (Configuration::get('PS_GEOLOCATION_BEHAVIOR') == _PS_GEOLOCATION_NO_CATALOG_) {
                                $this->restrictedCountry = Country::GEOLOC_FORBIDDEN;
                            } elseif (Configuration::get('PS_GEOLOCATION_BEHAVIOR') == _PS_GEOLOCATION_NO_ORDER_) {
                                $this->restrictedCountry = Country::GEOLOC_CATALOG_MODE;
                                $this->warning[] = $this->trans('You cannot place a new order from your country (%s).', [htmlspecialchars($record->country->name)], 'Shop.Notifications.Warning');
                            }
                        } else {
                            $hasBeenSet = !isset($this->context->cookie->iso_code_country);
                            $this->context->cookie->iso_code_country = strtoupper($record->country->isoCode);
                        }
                    }
                }

                if (isset($this->context->cookie->iso_code_country) && $this->context->cookie->iso_code_country && !Validate::isLanguageIsoCode($this->context->cookie->iso_code_country)) {
                    $this->context->cookie->iso_code_country = Country::getIsoById((int) Configuration::get('PS_COUNTRY_DEFAULT'));
                }

                if (isset($this->context->cookie->iso_code_country) && ($idCountry = (int) Country::getByIso(strtoupper($this->context->cookie->iso_code_country)))) {
                    /* Update defaultCountry */
                    if ($defaultCountry->iso_code != $this->context->cookie->iso_code_country) {
                        $defaultCountry = new Country($idCountry);
                    }
                    if (isset($hasBeenSet) && $hasBeenSet) {
                        $this->context->cookie->id_currency = (int) ($defaultCountry->id_currency ? (int) $defaultCountry->id_currency : Currency::getDefaultCurrencyId());
                    }

                    return $defaultCountry;
                } elseif (Configuration::get('PS_GEOLOCATION_NA_BEHAVIOR') == _PS_GEOLOCATION_NO_CATALOG_ && !FrontController::isInWhitelistForGeolocation()) {
                    $this->restrictedCountry = Country::GEOLOC_FORBIDDEN;
                } elseif (Configuration::get('PS_GEOLOCATION_NA_BEHAVIOR') == _PS_GEOLOCATION_NO_ORDER_ && !FrontController::isInWhitelistForGeolocation()) {
                    $this->restrictedCountry = Country::GEOLOC_CATALOG_MODE;
                    $countryName = $this->trans('Undefined', [], 'Shop.Theme.Global');
                    if (isset($record->country->name) && $record->country->name) {
                        $countryName = $record->country->name;
                    }
                    $this->warning[] = $this->trans(
                        'You cannot place a new order from your country (%s).',
                        [htmlspecialchars($countryName)],
                        'Shop.Notifications.Warning'
                    );
                }
            }
        }

        return false;
    }

    /**
     * Sets controller CSS and JS files.
     *
     * @return bool
     *
     * @throws PrestaShopException
     */
    public function setMedia()
    {
        $this->registerStylesheet('theme-main', '/assets/css/theme.css', ['media' => 'all', 'priority' => 50]);
        $this->registerStylesheet('theme-custom', '/assets/css/custom.css', ['media' => 'all', 'priority' => 1000]);

        if ($this->context->language->is_rtl) {
            $this->registerStylesheet('theme-rtl', '/assets/css/rtl.css', ['media' => 'all', 'priority' => 900]);
        }

        if ($this->context->shop->theme->requiresCoreScripts()) {
            $this->registerJavascript('corejs', '/themes/core.js', ['position' => 'bottom', 'priority' => 0]);
        }
        $this->registerJavascript('theme-main', '/assets/js/theme.js', ['position' => 'bottom', 'priority' => 50]);
        $this->registerJavascript('theme-custom', '/assets/js/custom.js', ['position' => 'bottom', 'priority' => 1000]);

        $assets = $this->context->shop->theme->getPageSpecificAssets($this->php_self);
        if (!empty($assets)) {
            foreach ($assets['css'] as $css) {
                $this->registerStylesheet($css['id'], $css['path'], $css);
            }
            foreach ($assets['js'] as $js) {
                $this->registerJavascript($js['id'], $js['path'], $js);
            }
        }

        // Execute Hook FrontController SetMedia
        Hook::exec('actionFrontControllerSetMedia');
        Hook::exec('action' . $this->getControllerName() . 'SetMedia');

        return true;
    }

    /**
     * Initializes page header variables.
     */
    public function initHeader()
    {
    }

    /**
     * Sets and returns customer groups that the current customer(visitor) belongs to.
     *
     * @return array
     *
     * @throws PrestaShopDatabaseException
     */
    public static function getCurrentCustomerGroups()
    {
        if (!Group::isFeatureActive()) {
            return [];
        }

        $context = Context::getContext();
        if (!isset($context->customer) || !$context->customer->id) {
            return [];
        }

        if (!is_array(self::$currentCustomerGroups)) {
            self::$currentCustomerGroups = [];
            $result = Db::getInstance()->executeS('SELECT id_group FROM ' . _DB_PREFIX_ . 'customer_group WHERE id_customer = ' . (int) $context->customer->id);
            foreach ($result as $row) {
                self::$currentCustomerGroups[] = $row['id_group'];
            }
        }

        return self::$currentCustomerGroups;
    }

    /**
     * Checks if user's location is whitelisted.
     *
     * @staticvar bool|null $allowed
     *
     * @return bool
     */
    protected static function isInWhitelistForGeolocation()
    {
        static $allowed = null;

        if ($allowed !== null) {
            return $allowed;
        }

        $allowed = false;
        $user_ip = Tools::getRemoteAddr();
        $ips = [];

        // retrocompatibility
        $ips_old = explode(';', Configuration::get('PS_GEOLOCATION_WHITELIST'));
        foreach ($ips_old as $ip) {
            $ips = array_merge($ips, explode("\n", $ip));
        }

        $ips = array_map('trim', $ips);
        foreach ($ips as $ip) {
            if (!empty($ip) && preg_match('/^' . $ip . '.*/', $user_ip)) {
                $allowed = true;
            }
        }

        return $allowed;
    }

    /**
     * Checks if token is valid.
     *
     * @return bool
     */
    public function isTokenValid()
    {
        if (!Configuration::get('PS_TOKEN_ENABLE')) {
            return true;
        }

        return strcasecmp(Tools::getToken(false), Tools::getValue('token')) == 0;
    }

    public function registerStylesheet($id, $relativePath, $params = [])
    {
        if (!is_array($params)) {
            $params = [];
        }

        $default_params = [
            'media' => AbstractAssetManager::DEFAULT_MEDIA,
            'priority' => AbstractAssetManager::DEFAULT_PRIORITY,
            'inline' => false,
            'server' => 'local',
            'version' => null,
            'needRtl' => true,
        ];
        $params = array_merge($default_params, $params);

        if (Tools::hasMediaServer() && !Configuration::get('PS_CSS_THEME_CACHE')) {
            $relativePath = Tools::getCurrentUrlProtocolPrefix() . Tools::getMediaServer($relativePath)
                . ($this->stylesheetManager->getFullPath($relativePath) ?? $relativePath);
            $params['server'] = 'remote';
        }

        $this->stylesheetManager->register($id, $relativePath, $params['media'], $params['priority'], $params['inline'], $params['server'], $params['needRtl'], $params['version']);
    }

    public function unregisterStylesheet($id)
    {
        $this->stylesheetManager->unregisterById($id);
    }

    public function registerJavascript($id, $relativePath, $params = [])
    {
        if (!is_array($params)) {
            $params = [];
        }

        $default_params = [
            'position' => AbstractAssetManager::DEFAULT_JS_POSITION,
            'priority' => AbstractAssetManager::DEFAULT_PRIORITY,
            'inline' => false,
            'attributes' => null,
            'server' => 'local',
            'version' => null,
        ];
        $params = array_merge($default_params, $params);

        if (Tools::hasMediaServer() && !Configuration::get('PS_JS_THEME_CACHE')) {
            $relativePath = Tools::getCurrentUrlProtocolPrefix() . Tools::getMediaServer($relativePath)
                . ($this->javascriptManager->getFullPath($relativePath) ?? $relativePath);
            $params['server'] = 'remote';
        }
        $this->javascriptManager->register($id, $relativePath, $params['position'], $params['priority'], $params['inline'], $params['attributes'], $params['server'], $params['version']);
    }

    public function unregisterJavascript($id)
    {
        $this->javascriptManager->unregisterById($id);
    }

    /**
     * @deprecated 1.7 This function shouldn't be used, use $this->registerStylesheet() instead
     */
    public function addCSS($css_uri, $css_media_type = 'all', $offset = null, $check_path = true)
    {
        /*
        This is deprecated in PrestaShop 1.7 and has no effect in PrestaShop 1.7 theme.
        You should use registerStylesheet($id, $path, $params)
        */

        if (!is_array($css_uri)) {
            $css_uri = (array) $css_uri;
        }

        foreach ($css_uri as $legacy_uri) {
            if ($uri = $this->getAssetUriFromLegacyDeprecatedMethod($legacy_uri)) {
                $this->registerStylesheet(sha1($uri), $uri, ['media' => $css_media_type, 'priority' => 80]);
            }
        }
    }

    /**
     * @deprecated 1.7 This function has no effect in PrestaShop 1.7 theme, use $this->unregisterStylesheet() instead
     */
    public function removeCSS($css_uri, $css_media_type = 'all', $check_path = true)
    {
        /*
        This is deprecated in PrestaShop 1.7 and has no effect in PrestaShop 1.7 theme.
        You should use unregisterStylesheet($id)
        */

        if (!is_array($css_uri)) {
            $css_uri = (array) $css_uri;
        }

        foreach ($css_uri as $legacy_uri) {
            if ($uri = $this->getAssetUriFromLegacyDeprecatedMethod($legacy_uri)) {
                $this->unregisterStylesheet(sha1($uri));
            }
        }
    }

    /**
     * @deprecated 1.7 This function has no effect in PrestaShop 1.7 theme, use $this->registerJavascript() instead
     */
    public function addJS($js_uri, $check_path = true)
    {
        /*
        This is deprecated in PrestaShop 1.7 and has no effect in PrestaShop 1.7 theme.
        You should use registerJavascript($id, $path, $params)
        */

        if (!is_array($js_uri)) {
            $js_uri = (array) $js_uri;
        }

        foreach ($js_uri as $legacy_uri) {
            if ($uri = $this->getAssetUriFromLegacyDeprecatedMethod($legacy_uri)) {
                $this->registerJavascript(sha1($uri), $uri, ['position' => 'bottom', 'priority' => 80]);
            }
        }
    }

    /**
     * @deprecated 1.7 This function has no effect in PrestaShop 1.7 theme, use $this->unregisterJavascript() instead
     */
    public function removeJS($js_uri, $check_path = true)
    {
        /*
        This is deprecated in PrestaShop 1.7 and has no effect in PrestaShop 1.7 theme.
        You should use unregisterJavascript($id)
        */

        if (!is_array($js_uri)) {
            $js_uri = (array) $js_uri;
        }

        foreach ($js_uri as $legacy_uri) {
            if ($uri = $this->getAssetUriFromLegacyDeprecatedMethod($legacy_uri)) {
                $this->unregisterJavascript(sha1($uri));
            }
        }
    }

    /**
     * Adds jQuery UI component(s) to queued JS file list.
     *
     * @param string|array $component
     * @param string $theme
     * @param bool $check_dependencies
     */
    public function addJqueryUI($component, $theme = 'base', $check_dependencies = true)
    {
        if (!is_array($component)) {
            $component = [$component];
        }

        foreach ($component as $ui) {
            // If the component does not start with ui. prefix, we need to add it
            if (!str_starts_with($ui, 'ui.')) {
                $ui = 'ui.' . $ui;
            }

            $ui_path = Media::getJqueryUIPath($ui, $theme, $check_dependencies);
            foreach ($ui_path['css'] as $uiPathCss => $uiMediaCss) {
                $this->registerStylesheet(
                    str_replace(_PS_JS_DIR_ . 'jquery/ui/themes/' . $theme . '/', '', $uiPathCss),
                    str_replace(_PS_JS_DIR_, '/js/', $uiPathCss),
                    ['media' => $uiMediaCss, 'priority' => 95]
                );
            }
            foreach ($ui_path['js'] as $uiPathJs) {
                $this->registerJavascript(
                    str_replace(_PS_JS_DIR_ . 'jquery/ui/', '', $uiPathJs),
                    str_replace(_PS_JS_DIR_, '/js/', $uiPathJs),
                    ['position' => 'bottom', 'priority' => 49]
                );
            }
        }
    }

    /**
     * Add Library not included with classic theme.
     */
    public function requireAssets(array $libraries)
    {
        foreach ($libraries as $library) {
            if ($assets = PrestashopAssetsLibraries::getAssetsLibraries($library)) {
                foreach ($assets as $asset) {
                    $this->{$asset['type']}($library, $asset['path'], $asset['params']);
                }
            }
        }
    }

    /**
     * Adds jQuery plugin(s) to queued JS file list.
     *
     * @param string|array $name
     * @param string|null $folder
     * @param bool $css
     */
    public function addJqueryPlugin($name, $folder = null, $css = true)
    {
        if (!is_array($name)) {
            $name = [$name];
        }

        foreach ($name as $plugin) {
            $plugin_path = Media::getJqueryPluginPath($plugin, $folder);

            if (!empty($plugin_path['js'])) {
                $this->registerJavascript(
                    str_replace(_PS_JS_DIR_ . 'jquery/plugins/', '', $plugin_path['js']),
                    str_replace(_PS_JS_DIR_, 'js/', $plugin_path['js']),
                    ['position' => 'bottom', 'priority' => 100]
                );
            }
            if ($css && !empty($plugin_path['css'])) {
                $this->registerStylesheet(
                    str_replace(_PS_JS_DIR_ . 'jquery/plugins/', '', key($plugin_path['css'])),
                    str_replace(_PS_JS_DIR_, 'js/', key($plugin_path['css'])),
                    ['media' => 'all', 'priority' => 100]
                );
            }
        }
    }

    /**
     * Recovers cart information.
     *
     * @return int|false
     */
    protected function recoverCart()
    {
        if (!Tools::isSubmit('recover_cart')) {
            return false;
        }

        // Get ID cart from URL
        $id_cart = (int) Tools::getValue('recover_cart');

        // Check if token in URL matches, otherwise, ignore it, probably malicious intentions
        if (Tools::getValue('token_cart') != md5(_COOKIE_KEY_ . 'recover_cart_' . $id_cart)) {
            return false;
        }

        // Create cart object and check if it's still valid. It can be deleted by automated cleaners or manually.
        $cart = new Cart($id_cart);
        if (!Validate::isLoadedObject($cart)) {
            $this->errors[] = $this->trans('This cart has expired.', [], 'Shop.Notifications.Error');

            return false;
        }

        // Customer - same scenario. It can be deleted by automated cleaners or manually.
        $customer = new Customer((int) $cart->id_customer);
        if (!Validate::isLoadedObject($customer)) {
            $this->errors[] = $this->trans('This cart has expired.', [], 'Shop.Notifications.Error');

            return false;
        }

        // Check if there is already a finished order with this cart, we notify the customer nicely
        if ($cart->orderExists()) {
            $this->errors[] = $this->trans('This cart was already used in an order and has expired.', [], 'Shop.Notifications.Error');

            return false;
        }

        $this->context->cookie->check_cgv = 1;
        $this->context->cookie->id_cart = $id_cart;

        // Restore customer session and authentication state (cookie + CustomerSession)
        $this->context->updateCustomer($customer);

        // Return the value for backward compatibility
        return $id_cart;
    }

    /**
     * Sets template file for page content output.
     *
     * @param string $template
     */
    public function setTemplate($template, $params = [], $locale = null)
    {
        parent::setTemplate(
            $this->getTemplateFile($template, $params, $locale)
        );
    }

    /**
     * Returns theme directory
     *
     * @return string
     */
    protected function getThemeDir()
    {
        return _PS_THEME_DIR_;
    }

    /**
     * Returns the layout's full path corresponding to the current page by using the override system
     * Ex:
     * On the url: http://localhost/index.php?id_product=1&controller=product, this method will
     * check if the layout exists in the following files (in that order), and return the first found:
     * - /themes/default/override/layout-product-1.tpl
     * - /themes/default/override/layout-product.tpl
     * - /themes/default/layout.tpl.
     *
     * @return bool|string
     */
    public function getLayout()
    {
        // Primary identifier to search for a template is php_self property,
        // For modules, we will use page_name
        $entity = $this->php_self;
        if (empty($entity)) {
            $entity = $this->getPageName();
        }

        // Get layout set in prestashop configuration
        $layout = $this->context->shop->theme->getLayoutNameForPage($entity);

        // Check if we are in content_only mode (used for displaying terms and conditions in a popup for example)
        $content_only = (int) Tools::getValue('content_only');

        // If a module provides its own custom layout, we ignore what is set in configuration
        if ($overridden_layout = Hook::exec(
            'overrideLayoutTemplate',
            [
                'default_layout' => $layout,
                'entity' => $entity,
                'locale' => $this->context->language->locale,
                'controller' => $this,
                'content_only' => $content_only,
            ]
        )) {
            return $overridden_layout;
        }

        // When using content_only, there will be no header, footer and sidebars
        if ($content_only) {
            $layout = 'layout-content-only';
        }

        return $this->context->shop->theme->getLayoutPath($layout);
    }

    /**
     * Returns layout name for the current controller. Used to display layout name in <body> tag.
     *
     * @return string layout name
     */
    protected function getLayoutName()
    {
        return str_replace(['.tpl'], '', basename($this->getLayout()));
    }

    /**
     * Returns template path.
     *
     * @param string $template
     *
     * @return string
     */
    public function getTemplatePath($template)
    {
        return $template;
    }

    public function getTemplateFile($template, $params = [], $locale = null)
    {
        if (!isset($params['entity'])) {
            $params['entity'] = null;
        }
        if (!isset($params['id'])) {
            $params['id'] = null;
        }

        if (null === $locale) {
            $locale = $this->context->language->locale;
        }

        if ($overridden_template = Hook::exec(
            'displayOverrideTemplate',
            [
                'controller' => $this,
                'template_file' => $template,
                'entity' => $params['entity'],
                'id' => $params['id'],
                'locale' => $locale,
            ]
        )) {
            return $overridden_template;
        }

        return $this->getTemplateFinder()->getTemplate(
            $template,
            $params['entity'],
            $params['id'],
            $locale
        );
    }

    /**
     * Initializes a set of commonly used urls of pages, image folders and others, available for use
     * in the template. @see FrontController::assignGeneralPurposeVariables for more information.
     *
     * @return array
     */
    public function getTemplateVarUrls()
    {
        if ($this->urls === null) {
            $http = Tools::getCurrentUrlProtocolPrefix();
            $base_url = $this->context->shop->getBaseURL(true, true);

            $urls = [
                'base_url' => $base_url,
                'current_url' => $this->context->shop->getBaseURL(true, false) . $_SERVER['REQUEST_URI'],
                'shop_domain_url' => $this->context->shop->getBaseURL(true, false),
            ];

            $assign_array = [
                'img_ps_url' => _PS_IMG_,
                'img_cat_url' => _THEME_CAT_DIR_,
                'img_lang_url' => _THEME_LANG_DIR_,
                'img_prod_url' => _THEME_PROD_DIR_,
                'img_manu_url' => _THEME_MANU_DIR_,
                'img_sup_url' => _THEME_SUP_DIR_,
                'img_ship_url' => _THEME_SHIP_DIR_,
                'img_store_url' => _THEME_STORE_DIR_,
                'img_col_url' => _THEME_COL_DIR_,
                'img_url' => _THEME_IMG_DIR_,
                'css_url' => _THEME_CSS_DIR_,
                'js_url' => _THEME_JS_DIR_,
                'pic_url' => _THEME_PROD_PIC_DIR_,
                'theme_assets' => _THEME_DIR_ . 'assets/',
                'theme_dir' => _THEME_DIR_,
            ];

            $themeAssetsConfig = $this->context->shop->theme->get('assets', false);

            if (!empty($themeAssetsConfig['use_parent_assets'])) {
                $assign_array['theme_assets'] = _PS_PARENT_THEME_URI_ . 'assets/';
                $assign_array['img_url'] = $assign_array['theme_assets'] . 'img/';
                $assign_array['css_url'] = $assign_array['theme_assets'] . 'css/';
                $assign_array['js_url'] = $assign_array['theme_assets'] . 'js/';
                $assign_array['child_theme_assets'] = _THEME_DIR_ . 'assets/';
                $assign_array['child_img_url'] = $assign_array['child_theme_assets'] . 'img/';
                $assign_array['child_css_url'] = $assign_array['child_theme_assets'] . 'css/';
                $assign_array['child_js_url'] = $assign_array['child_theme_assets'] . 'js/';
            }

            foreach ($assign_array as $assign_key => $assign_value) {
                $urls[$assign_key] = $http . Tools::getMediaServer($assign_value) . $assign_value;
            }

            $pages = [];
            $p = [
                'address', 'addresses', 'authentication', 'manufacturer', 'cart', 'category', 'cms', 'contact',
                'discount', 'guest-tracking', 'history', 'identity', 'index', 'my-account',
                'order-confirmation', 'order-detail', 'order-follow', 'order', 'order-return',
                'order-slip', 'pagenotfound', 'password', 'pdf-invoice', 'pdf-order-return', 'pdf-order-slip',
                'prices-drop', 'product', 'registration', 'search', 'sitemap', 'stores', 'supplier', 'new-products',
            ];
            foreach ($p as $page_name) {
                $index = str_replace('-', '_', $page_name);
                $pages[$index] = $this->context->link->getPageLink($page_name);
            }
            $pages['brands'] = $pages['manufacturer'];
            $pages['register'] = $this->context->link->getPageLink('registration');
            $pages['order_login'] = $this->context->link->getPageLink('order', null, null, ['login' => '1']);
            $urls['pages'] = $pages;

            $urls['alternative_langs'] = $this->getAlternativeLangsUrl();

            $urls['actions'] = [
                'logout' => $this->context->link->getPageLink('index', null, null, 'mylogout'),
            ];

            $imageRetriever = new ImageRetriever($this->context->link);
            $urls['no_picture_image'] = $imageRetriever->getNoPictureImage($this->context->language);

            $this->urls = $urls;
        }

        return $this->urls;
    }

    /**
     * Initializes a set of commonly used variables of current configuration, available for use
     * in the template. @see FrontController::assignGeneralPurposeVariables for more information.
     *
     * @return array
     */
    public function getTemplateVarConfiguration()
    {
        $quantity_discount_price = Configuration::get('PS_DISPLAY_DISCOUNT_PRICE');

        return [
            'display_taxes_label' => $this->getDisplayTaxesLabel(),
            'display_prices_tax_incl' => (bool) (new TaxConfiguration())->includeTaxes(),
            'taxes_enabled' => (bool) Configuration::get('PS_TAX'),
            'low_quantity_threshold' => (int) Configuration::get('PS_LAST_QTIES'),
            'is_b2b' => (bool) Configuration::get('PS_B2B_ENABLE'),
            'is_catalog' => (bool) Configuration::isCatalogMode(),
            'show_prices' => (bool) Configuration::showPrices(),
            'opt_in' => [
                'partner' => (bool) Configuration::get('PS_CUSTOMER_OPTIN'),
            ],
            'quantity_discount' => [
                'type' => ($quantity_discount_price) ? 'price' : 'discount',
                'label' => ($quantity_discount_price)
                    ? $this->getTranslator()->trans('Unit price', [], 'Shop.Theme.Catalog')
                    : $this->getTranslator()->trans('Unit discount', [], 'Shop.Theme.Catalog'),
            ],
            'voucher_enabled' => (int) CartRule::isFeatureActive(),
            'return_enabled' => (int) Configuration::get('PS_ORDER_RETURN'),
            'number_of_days_for_return' => (int) Configuration::get('PS_ORDER_RETURN_NB_DAYS'),
            'password_policy' => [
                'minimum_length' => (int) Configuration::get(PasswordPolicyConfiguration::CONFIGURATION_MINIMUM_LENGTH),
                'maximum_length' => (int) Configuration::get(PasswordPolicyConfiguration::CONFIGURATION_MAXIMUM_LENGTH),
                'minimum_score' => (int) Configuration::get(PasswordPolicyConfiguration::CONFIGURATION_MINIMUM_SCORE),
            ],
        ];
    }

    protected function getDisplayTaxesLabel()
    {
        return $this->context->country->display_tax_label;
    }

    /**
     * Initializes information about currently used currency, available for use in the template.
     *
     * @see FrontController::assignGeneralPurposeVariables for more information.
     *
     * @return array
     */
    public function getTemplateVarCurrency()
    {
        $curr = [];
        $fields = ['id', 'name', 'iso_code', 'iso_code_num', 'sign'];
        foreach ($fields as $field_name) {
            $curr[$field_name] = $this->context->currency->{$field_name};
        }

        return $curr;
    }

    /**
     * Initializes information about current customer, available for use in the template.
     *
     * @see FrontController::assignGeneralPurposeVariables for more information.
     *
     * @return array
     */
    public function getTemplateVarCustomer($customer = null)
    {
        if (Validate::isLoadedObject($customer)) {
            $cust = $this->objectPresenter->present($customer);
        } else {
            $cust = $this->objectPresenter->present($this->context->customer);
        }

        unset(
            $cust['secure_key'],
            $cust['passwd'],
            $cust['show_public_prices'],
            $cust['deleted'],
            $cust['id_lang']
        );

        $cust['id'] = $this->context->customer->id;
        $cust['is_logged'] = $this->context->customer->isLogged();

        $cust['gender'] = $this->objectPresenter->present(new Gender($cust['id_gender']));
        unset($cust['id_gender']);

        $cust['risk'] = $this->objectPresenter->present(new Risk($cust['id_risk']));
        unset($cust['id_risk']);

        $addresses = $this->context->customer->getSimpleAddresses();
        foreach ($addresses as &$a) {
            $a['formatted'] = AddressFormat::generateAddress(new Address($a['id']), [], '<br>');
        }
        $cust['addresses'] = $addresses;

        return $cust;
    }

    /**
     * Get the shop logo with its dimensions
     *
     * @return array<string, string|int>
     */
    public function getShopLogo(): array
    {
        $logoFileName = Configuration::get('PS_LOGO');
        if (!$logoFileName) {
            return [];
        }

        $logoFileDir = _PS_IMG_DIR_ . $logoFileName;

        if (!file_exists($logoFileDir)) {
            return [];
        }

        list($logoWidth, $logoHeight) = getimagesize($logoFileDir);

        return [
            'src' => ($this->getTemplateVarUrls()['img_ps_url'] ?? _PS_IMG_) . $logoFileName,
            'width' => $logoWidth,
            'height' => $logoHeight,
        ];
    }

    public function getCoreJsPublicPath()
    {
        return $this->context->shop->physical_uri . 'themes/';
    }

    /**
     * Initializes a set of commonly used information about the shop, available for use
     * in the template. @see FrontController::assignGeneralPurposeVariables for more information.
     *
     * @return array
     */
    public function getTemplateVarShop()
    {
        $address = $this->context->shop->getAddress();

        $urls = $this->getTemplateVarUrls();
        $psImageUrl = $urls['img_ps_url'] ?? _PS_IMG_;

        $shop = [
            'id' => $this->context->shop->id,
            'group_id' => $this->context->shop->id_shop_group,
            'name' => Configuration::get('PS_SHOP_NAME'),
            'email' => Configuration::get('PS_SHOP_EMAIL'),
            'registration_number' => Configuration::get('PS_SHOP_DETAILS'),

            'long' => Configuration::get('PS_STORES_CENTER_LONG'),
            'lat' => Configuration::get('PS_STORES_CENTER_LAT'),

            'logo' => self::configuredImageUrl('PS_LOGO', $psImageUrl),
            'logo_details' => $this->getShopLogo(),
            'stores_icon' => self::configuredImageUrl('PS_STORES_ICON', $psImageUrl),
            'favicon' => self::configuredImageUrl('PS_FAVICON', $psImageUrl),
            'favicon_update_time' => Configuration::get('PS_IMG_UPDATE_TIME'),

            'address' => [
                'formatted' => AddressFormat::generateAddress($address, [], '<br>'),
                'address1' => $address->address1,
                'address2' => $address->address2,
                'postcode' => $address->postcode,
                'city' => $address->city,
                'state' => (new State($address->id_state))->name,
                'country' => (new Country($address->id_country))->name[$this->context->language->id],
            ],
            'phone' => Configuration::get('PS_SHOP_PHONE'),
            'fax' => Configuration::get('PS_SHOP_FAX'),
        ];

        return $shop;
    }

    private static function configuredImageUrl(string $configKey, $psImageUrl)
    {
        $image = Configuration::get($configKey);

        return $image ? $psImageUrl . $image : '';
    }

    /**
     * Initializes a set of commonly used variables related to the current page, available for use
     * in the template. @see FrontController::assignGeneralPurposeVariables for more information.
     *
     * @return array
     */
    public function getTemplateVarPage()
    {
        $page_name = $this->getPageName();
        $meta_tags = Meta::getMetaTags($this->context->language->id, $page_name);

        $my_account_controllers = [
            'address',
            'authentication',
            'discount',
            'history',
            'identity',
            'order-follow',
            'order-slip',
            'password',
            'registration',
            'guest-tracking',
        ];

        $body_classes = [
            'lang-' . $this->context->language->iso_code => true,
            'lang-rtl' => (bool) $this->context->language->is_rtl,
            'country-' . $this->context->country->iso_code => true,
            'currency-' . $this->context->currency->iso_code => true,
            $this->getLayoutName() => true,
            'page-' . $this->php_self => true,
            'tax-display-' . ($this->getDisplayTaxesLabel() ? 'enabled' : 'disabled') => true,
            'page-customer-account' => false,
        ];

        $page = [
            'title' => '',
            'canonical' => $this->getCanonicalURL(),
            'meta' => [
                'title' => $meta_tags['meta_title'],
                'description' => $meta_tags['meta_description'],
                'robots' => 'index',
            ],
            'page_name' => $page_name,
            'body_classes' => $body_classes,
            'admin_notifications' => [],
            'password-policy' => [
                'feedbacks' => [
                    0 => $this->getTranslator()->trans('Very weak', [], 'Shop.Theme.Global'),
                    1 => $this->getTranslator()->trans('Weak', [], 'Shop.Theme.Global'),
                    2 => $this->getTranslator()->trans('Average', [], 'Shop.Theme.Global'),
                    3 => $this->getTranslator()->trans('Strong', [], 'Shop.Theme.Global'),
                    4 => $this->getTranslator()->trans('Very strong', [], 'Shop.Theme.Global'),
                    'Straight rows of keys are easy to guess' => $this->getTranslator()->trans('Straight rows of keys are easy to guess', [], 'Shop.Theme.Global'),
                    'Short keyboard patterns are easy to guess' => $this->getTranslator()->trans('Short keyboard patterns are easy to guess', [], 'Shop.Theme.Global'),
                    'Use a longer keyboard pattern with more turns' => $this->getTranslator()->trans('Use a longer keyboard pattern with more turns', [], 'Shop.Theme.Global'),
                    'Repeats like "aaa" are easy to guess' => $this->getTranslator()->trans('Repeats like "aaa" are easy to guess', [], 'Shop.Theme.Global'),
                    'Repeats like "abcabcabc" are only slightly harder to guess than "abc"' => $this->getTranslator()->trans('Repeats like "abcabcabc" are only slightly harder to guess than "abc"', [], 'Shop.Theme.Global'),
                    'Sequences like abc or 6543 are easy to guess' => $this->getTranslator()->trans('Sequences like "abc" or "6543" are easy to guess', [], 'Shop.Theme.Global'),
                    'Recent years are easy to guess' => $this->getTranslator()->trans('Recent years are easy to guess', [], 'Shop.Theme.Global'),
                    'Dates are often easy to guess' => $this->getTranslator()->trans('Dates are often easy to guess', [], 'Shop.Theme.Global'),
                    'This is a top-10 common password' => $this->getTranslator()->trans('This is a top-10 common password', [], 'Shop.Theme.Global'),
                    'This is a top-100 common password' => $this->getTranslator()->trans('This is a top-100 common password', [], 'Shop.Theme.Global'),
                    'This is a very common password' => $this->getTranslator()->trans('This is a very common password', [], 'Shop.Theme.Global'),
                    'This is similar to a commonly used password' => $this->getTranslator()->trans('This is similar to a commonly used password', [], 'Shop.Theme.Global'),
                    'A word by itself is easy to guess' => $this->getTranslator()->trans('A word by itself is easy to guess', [], 'Shop.Theme.Global'),
                    'Names and surnames by themselves are easy to guess' => $this->getTranslator()->trans('Names and surnames by themselves are easy to guess', [], 'Shop.Theme.Global'),
                    'Common names and surnames are easy to guess' => $this->getTranslator()->trans('Common names and surnames are easy to guess', [], 'Shop.Theme.Global'),
                    'Use a few words, avoid common phrases' => $this->getTranslator()->trans('Use a few words, avoid common phrases', [], 'Shop.Theme.Global'),
                    'No need for symbols, digits, or uppercase letters' => $this->getTranslator()->trans('No need for symbols, digits, or uppercase letters', [], 'Shop.Theme.Global'),
                    'Avoid repeated words and characters' => $this->getTranslator()->trans('Avoid repeated words and characters', [], 'Shop.Theme.Global'),
                    'Avoid sequences' => $this->getTranslator()->trans('Avoid sequences', [], 'Shop.Theme.Global'),
                    'Avoid recent years' => $this->getTranslator()->trans('Avoid recent years', [], 'Shop.Theme.Global'),
                    'Avoid years that are associated with you' => $this->getTranslator()->trans('Avoid years that are associated with you', [], 'Shop.Theme.Global'),
                    'Avoid dates and years that are associated with you' => $this->getTranslator()->trans('Avoid dates and years that are associated with you', [], 'Shop.Theme.Global'),
                    'Capitalization doesn\'t help very much' => $this->getTranslator()->trans('Capitalization doesn\'t help very much', [], 'Shop.Theme.Global'),
                    'All-uppercase is almost as easy to guess as all-lowercase' => $this->getTranslator()->trans('All-uppercase is almost as easy to guess as all-lowercase', [], 'Shop.Theme.Global'),
                    'Reversed words aren\'t much harder to guess' => $this->getTranslator()->trans('Reversed words aren\'t much harder to guess', [], 'Shop.Theme.Global'),
                    'Predictable substitutions like \'@\' instead of \'a\' don\'t help very much' => $this->getTranslator()->trans('Predictable substitutions like "@" instead of "a" don\'t help very much', [], 'Shop.Theme.Global'),
                    'Add another word or two. Uncommon words are better.' => $this->getTranslator()->trans('Add another word or two. Uncommon words are better.', [], 'Shop.Theme.Global'),
                ],
            ],
        ];

        if (in_array($this->php_self, $my_account_controllers)) {
            $page['body_classes']['page-customer-account'] = true;
        }

        return $page;
    }

    /**
     * Returns a list of breadcrumbs with their count to show on the current page.
     *
     * @return array
     */
    public function getBreadcrumb()
    {
        $breadcrumb = $this->getBreadcrumbLinks();
        $breadcrumb['count'] = count($breadcrumb['links']);

        return $breadcrumb;
    }

    /**
     * Returns an array of breadcrumbs for current controller. Inheriting controllers should
     * extend this list in most cases.
     *
     * @return array
     */
    protected function getBreadcrumbLinks()
    {
        $breadcrumb = [];

        $breadcrumb['links'][] = [
            'title' => $this->getTranslator()->trans('Home', [], 'Shop.Theme.Global'),
            'url' => $this->context->link->getPageLink('index'),
        ];

        return $breadcrumb;
    }

    protected function getCategoryPath($category)
    {
        if ($category->id_parent != 0 && !$category->is_root_category) {
            return [
                'title' => $category->name,
                'url' => $this->context->link->getCategoryLink($category),
            ];
        }
    }

    /**
     * Returns breadcrumb step for "Your account" page.
     *
     * @return array
     */
    protected function addMyAccountToBreadcrumb()
    {
        return [
            'title' => $this->getTranslator()->trans('Your account', [], 'Shop.Theme.Customeraccount'),
            'url' => $this->context->link->getPageLink('my-account'),
        ];
    }

    /**
     * Generate the canonical URL of the current page
     *
     * Mainly used for ProductController and CategoryController
     * but can be implemented by other classes inheriting from FrontController
     *
     * @return string|void
     */
    public function getCanonicalURL()
    {
    }

    /**
     * @deprecated Since 9.0 and will be removed in the next major.
     */
    protected function updateQueryString(?array $extraParams = null)
    {
        return Tools::updateCurrentQueryString($extraParams);
    }

    protected function getCurrentURL()
    {
        return Tools::getCurrentUrl();
    }

    public function getPageName()
    {
        // Are we in a payment module
        $module_name = '';
        if (Validate::isModuleName(Tools::getValue('module'))) {
            $module_name = Tools::getValue('module');
        }

        if (!empty($this->page_name)) {
            $page_name = $this->page_name;
        } elseif (!empty($this->php_self)) {
            $page_name = $this->php_self;
        } elseif (Tools::getValue('fc') == 'module' && $module_name != '' && (Module::getInstanceByName($module_name) instanceof PaymentModule)) {
            $page_name = 'module-payment-submit';
        } elseif (preg_match('#^' . preg_quote($this->context->shop->physical_uri, '#') . 'modules/([a-zA-Z0-9_-]+?)/(.*)$#', $_SERVER['REQUEST_URI'], $m)) {
            /** @retrocompatibility Are we in a module ? */
            $page_name = 'module-' . $m[1] . '-' . str_replace(['.php', '/'], ['', '-'], $m[2]);
        } else {
            $page_name = Dispatcher::getInstance()->getController();
            $page_name = (preg_match('/^[0-9]/', $page_name) ? 'page_' . $page_name : $page_name);
        }

        return $page_name;
    }

    protected function render($template, array $params = [])
    {
        $templateContent = '';
        $scope = $this->context->smarty->createData(
            $this->context->smarty
        );

        $scope->assign($params);

        try {
            $tpl = $this->context->smarty->createTemplate(
                $this->getTemplateFile($template),
                $scope
            );

            $templateContent = $tpl->fetch();
        } catch (PrestaShopException $e) {
            PrestaShopLogger::addLog($e->getMessage());

            if (defined('_PS_MODE_DEV_') && _PS_MODE_DEV_) {
                $this->warning[] = $e->getMessage();
                $scope->assign(['notifications' => $this->prepareNotifications()]);

                $tpl = $this->context->smarty->createTemplate(
                    $this->getTemplateFile('_partials/notifications'),
                    $scope
                );

                $templateContent = $tpl->fetch();
            }
        }

        return $templateContent;
    }

    protected function getTranslator()
    {
        return $this->translator;
    }

    protected function makeLoginForm()
    {
        $form = new CustomerLoginForm(
            $this->context->smarty,
            $this->context,
            $this->getTranslator(),
            new CustomerLoginFormatter($this->getTranslator()),
            $this->getTemplateVarUrls()
        );

        $form->setAction($this->getCurrentURL());

        return $form;
    }

    protected function makeCustomerFormatter()
    {
        $formatter = new CustomerFormatter(
            $this->getTranslator(),
            $this->context->language
        );

        $customer = new Customer();

        $formatter
            ->setAskForPartnerOptin(Configuration::get('PS_CUSTOMER_OPTIN'))
            ->setAskForBirthdate(Configuration::get('PS_CUSTOMER_BIRTHDATE'))
            ->setPartnerOptinRequired($customer->isFieldRequired('optin'));

        return $formatter;
    }

    protected function makeCustomerForm()
    {
        $guestAllowedCheckout = Configuration::get('PS_GUEST_CHECKOUT_ENABLED');
        $form = new CustomerForm(
            $this->context->smarty,
            $this->context,
            $this->getTranslator(),
            $this->makeCustomerFormatter(),
            new CustomerPersister(
                $this->context,
                $this->get('hashing'),
                $this->getTranslator(),
                $guestAllowedCheckout
            ),
            $this->getTemplateVarUrls()
        );

        $form->setGuestAllowed($guestAllowedCheckout);

        $form->setAction($this->getCurrentURL());

        return $form;
    }

    protected function makeAddressPersister()
    {
        return new CustomerAddressPersister(
            $this->context->customer,
            $this->context->cart,
            Tools::getToken(true, $this->context)
        );
    }

    protected function makeAddressForm()
    {
        if (Configuration::get('PS_RESTRICT_DELIVERED_COUNTRIES')) {
            $availableCountries = Carrier::getDeliveredCountries($this->context->language->id, true, true);
        } else {
            $availableCountries = Country::getCountries($this->context->language->id, true);
        }

        $form = new CustomerAddressForm(
            $this->context->smarty,
            $this->context->language,
            $this->getTranslator(),
            $this->makeAddressPersister(),
            new CustomerAddressFormatter(
                $this->context->country,
                $this->getTranslator(),
                $availableCountries
            )
        );

        $form->setAction($this->getCurrentURL());

        return $form;
    }

    /**
     * Get templateFinder.
     *
     * @return object
     */
    public function getTemplateFinder()
    {
        return $this->templateFinder;
    }

    public function getRestrictedCountry()
    {
        return $this->restrictedCountry;
    }

    public function getAssetUriFromLegacyDeprecatedMethod($legacy_uri)
    {
        $success = preg_match('/modules\/.*/', $legacy_uri, $matches);
        if (!$success) {
            Tools::displayAsDeprecated(
                'Backward compatibility for this method couldn\'t be handled. Use $this->registerJavascript() instead'
            );

            return false;
        } else {
            return $matches[0];
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function buildContainer(): ContainerInterface
    {
        if (!defined('FRONT_LEGACY_CONTEXT') || FRONT_LEGACY_CONTEXT) {
            return ContainerBuilder::getContainer('front', _PS_MODE_DEV_);
        }

        return SymfonyContainer::getInstance();
    }

    /**
     * @return array containing the URLs of the same page but for different languages
     */
    protected function getAlternativeLangsUrl()
    {
        $alternativeLangs = [];
        $languages = Language::getLanguages(true, $this->context->shop->id);

        if (count($languages) < 2) {
            // No need to display alternative lang if there is only one enabled
            return $alternativeLangs;
        }

        foreach ($languages as $lang) {
            $langUrl = $this->context->link->getLanguageLink($lang['id_lang']);
            $alternativeLangs[$lang['language_code']] = $this->sanitizeUrl($langUrl);
        }

        return $alternativeLangs;
    }

    /**
     * Sanitize / Clean params of an URL
     *
     * @param string $url URL to clean
     *
     * @return string cleaned URL
     */
    protected function sanitizeUrl(string $url): string
    {
        $params = [];

        // Extract all parts of the URL
        $url_details = parse_url($url);
        if (!empty($url_details['query'])) {
            parse_str($url_details['query'], $query);
            $params = $this->sanitizeQueryOutput($query);
        }

        // Build a list of parameters we won't be sanitizing
        $excludedKeys = array_merge(
            ['isolang', 'id_lang', 'controller', 'fc', 'id_product', 'id_category', 'id_manufacturer', 'id_supplier', 'id_cms'],
            $this->redirectionExtraExcludedKeys
        );

        // Go through each parameter we got from dispatcher and sanitize it
        foreach ($params as $key => $value) {
            if (
                in_array($key, $excludedKeys)
                || !Validate::isUrl($key)
                || !$this->validateInputAsUrl($value)
            ) {
                continue;
            }

            $params[Tools::safeOutput($key)] = is_array($value) ? array_walk_recursive($value, 'Tools::safeOutput') : Tools::safeOutput($value);
        }

        // Build back the query
        $str_params = http_build_query($params, '', '&');
        $sanitizedUrl = preg_replace('/^([^?]*)?.*$/', '$1', $url) . (!empty($str_params) ? '?' . $str_params : '');

        return $sanitizedUrl;
    }

    /**
     * Recursively sanitize output query
     *
     * @param array $query URL query
     *
     * @return array
     */
    protected function sanitizeQueryOutput(array $query): array
    {
        $params = [];
        foreach ($query as $key => $value) {
            if (is_array($value)) {
                $params[Tools::safeOutput($key)] = $this->sanitizeQueryOutput($value);
            } else {
                $params[Tools::safeOutput($key)] = Tools::safeOutput($value);
            }
        }

        return $params;
    }

    /**
     * Validate data recursively to be sure it's URL compliant
     *
     * @return bool
     */
    protected function validateInputAsUrl($data): bool
    {
        if (is_array($data)) {
            $returnStatement = true;
            foreach ($data as $value) {
                $returnStatement = $returnStatement && $this->validateInputAsUrl($value);
            }

            return $returnStatement;
        }

        return Validate::isUrl($data);
    }
}
