* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ use Symfony\Component\HttpFoundation\Request as SymfonyRequest; /** * @since 1.5.0 */ class DispatcherCore { /** * List of available front controllers types. */ public const FC_FRONT = 1; public const FC_ADMIN = 2; public const FC_MODULE = 3; public const REWRITE_PATTERN = '[_a-zA-Z0-9\x{0600}-\x{06FF}\pL\pS-]*?'; /** * @var Dispatcher|null */ public static $instance = null; /** * @var SymfonyRequest */ private $request; /** * @var array List of default routes */ public $default_routes = [ 'upload' => [ 'controller' => 'upload', 'rule' => 'upload/{file}', 'keywords' => [ 'file' => ['regexp' => '.+', 'param' => 'file'], ], ], 'category_rule' => [ 'controller' => 'category', 'rule' => '{id}-{rewrite}', 'keywords' => [ 'id' => ['regexp' => '[0-9]+', 'param' => 'id_category'], 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], ], ], 'supplier_rule' => [ 'controller' => 'supplier', 'rule' => 'supplier/{id}-{rewrite}', 'keywords' => [ 'id' => ['regexp' => '[0-9]+', 'param' => 'id_supplier'], 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], ], ], 'manufacturer_rule' => [ 'controller' => 'manufacturer', 'rule' => 'brand/{id}-{rewrite}', 'keywords' => [ 'id' => ['regexp' => '[0-9]+', 'param' => 'id_manufacturer'], 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], ], ], 'cms_rule' => [ 'controller' => 'cms', 'rule' => 'content/{id}-{rewrite}', 'keywords' => [ 'id' => ['regexp' => '[0-9]+', 'param' => 'id_cms'], 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], ], ], 'cms_category_rule' => [ 'controller' => 'cms', 'rule' => 'content/category/{id}-{rewrite}', 'keywords' => [ 'id' => ['regexp' => '[0-9]+', 'param' => 'id_cms_category'], 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], ], ], 'module' => [ 'controller' => null, 'rule' => 'module/{module}{/:controller}', 'keywords' => [ 'module' => ['regexp' => '[_a-zA-Z0-9_-]+', 'param' => 'module'], 'controller' => ['regexp' => '[_a-zA-Z0-9_-]+', 'param' => 'controller'], ], 'params' => [ 'fc' => 'module', ], ], 'product_rule' => [ 'controller' => 'product', 'rule' => '{category:/}{id}{-:id_product_attribute}-{rewrite}{-:ean13}.html', 'keywords' => [ 'id' => ['regexp' => '[0-9]+', 'param' => 'id_product'], 'id_product_attribute' => ['regexp' => '[0-9]*+', 'param' => 'id_product_attribute'], 'rewrite' => ['regexp' => self::REWRITE_PATTERN, 'param' => 'rewrite'], 'ean13' => ['regexp' => '[0-9\pL]*'], 'category' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'categories' => ['regexp' => '[/_a-zA-Z0-9-\pL]*'], 'reference' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'manufacturer' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'supplier' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 'price' => ['regexp' => '[0-9\.,]*'], 'tags' => ['regexp' => '[a-zA-Z0-9-\pL]*'], ], ], ]; /** * @var bool If true, use routes to build URL (mod rewrite must be activated) */ protected $use_routes = false; protected $multilang_activated = false; /** * @var array List of loaded routes */ protected $routes = []; /** * @var string Current controller name */ protected $controller; /** * @var string Current request uri */ protected $request_uri; /** * @var array Store empty route (a route with an empty rule) */ protected $empty_route; /** * @var string Set default controller, which will be used if http parameter 'controller' is empty */ protected $default_controller; protected $use_default_controller = false; /** * @var string Controller to use if found controller doesn't exist */ protected $controller_not_found = 'pagenotfound'; /** * @var int Front controller to use */ protected $front_controller = self::FC_FRONT; /** * Get current instance of dispatcher (singleton). * * @return Dispatcher * * @throws PrestaShopException */ public static function getInstance(SymfonyRequest $request = null) { if (!self::$instance) { if (null === $request) { $request = SymfonyRequest::createFromGlobals(); } self::$instance = new Dispatcher($request); } return self::$instance; } /** * Needs to be instantiated from getInstance() method. * * @param SymfonyRequest|null $request * * @throws PrestaShopException */ protected function __construct(SymfonyRequest $request = null) { $this->setRequest($request); $this->use_routes = (bool) Configuration::get('PS_REWRITING_SETTINGS'); // Select right front controller if (defined('_PS_ADMIN_DIR_')) { $this->front_controller = self::FC_ADMIN; $this->controller_not_found = 'adminnotfound'; } elseif (Tools::getValue('fc') == 'module') { $this->front_controller = self::FC_MODULE; $this->controller_not_found = 'pagenotfound'; } else { $this->front_controller = self::FC_FRONT; $this->controller_not_found = 'pagenotfound'; } $this->setRequestUri(); // Switch language if needed (only on front) if (in_array($this->front_controller, [self::FC_FRONT, self::FC_MODULE])) { Tools::switchLanguage(); } if (Language::isMultiLanguageActivated()) { $this->multilang_activated = true; } $this->loadRoutes(); } /** * Either sets a given request or a new one. * * @param SymfonyRequest|null $request */ private function setRequest(SymfonyRequest $request = null) { if (null === $request) { $request = SymfonyRequest::createFromGlobals(); } $this->request = $request; } /** * Returns the request property. * * @return SymfonyRequest */ private function getRequest() { return $this->request; } /** * Sets and returns the default controller. * * @param int $frontControllerType The front controller type * @param Employee|null $employee The current employee * * @return string */ private function getDefaultController($frontControllerType, Employee $employee = null) { switch ($frontControllerType) { case self::FC_ADMIN: // Default $defaultController = 'AdminDashboard'; // If there is an employee with a default tab set if (null !== $employee) { $tabClassName = $employee->getDefaultTabClassName(); if (null !== $tabClassName) { $tabProfileAccess = Profile::getProfileAccess($employee->id_profile, Tab::getIdFromClassName($tabClassName)); if (is_array($tabProfileAccess) && isset($tabProfileAccess['view']) && $tabProfileAccess['view'] === '1') { $defaultController = $tabClassName; } } } break; case self::FC_MODULE: $defaultController = 'default'; break; default: $defaultController = 'index'; } $this->setDefaultController($defaultController); return $defaultController; } /** * Sets the default controller. * * @param string $defaultController */ private function setDefaultController($defaultController) { $this->default_controller = $defaultController; } /** * Sets use_default_controller to true, sets and returns the default controller. * * @return string */ public function useDefaultController() { $this->use_default_controller = true; // If it was already set just return it if (null !== $this->default_controller) { return $this->default_controller; } $employee = Context::getContext()->employee; return $this->getDefaultController($this->front_controller, $employee); } /** * Find the controller and instantiate it. */ public function dispatch() { $controller_class = ''; // Get current controller $this->getController(); if (!$this->controller) { $this->controller = $this->useDefaultController(); } // Execute hook dispatcher before Hook::exec('actionDispatcherBefore', ['controller_type' => $this->front_controller]); // Dispatch with right front controller switch ($this->front_controller) { // Dispatch front office controller case self::FC_FRONT: $controllers = Dispatcher::getControllers([ _PS_FRONT_CONTROLLER_DIR_, _PS_OVERRIDE_DIR_ . 'controllers/front/', ]); $controllers['index'] = 'IndexController'; if (isset($controllers['auth'])) { $controllers['authentication'] = $controllers['auth']; } if (isset($controllers['contact'])) { $controllers['contactform'] = $controllers['contact']; } if (!isset($controllers[strtolower($this->controller)])) { $this->controller = $this->controller_not_found; } $controller_class = $controllers[strtolower($this->controller)]; $params_hook_action_dispatcher = [ 'controller_type' => self::FC_FRONT, 'controller_class' => $controller_class, 'is_module' => 0, ]; break; // Dispatch module controller for front office case self::FC_MODULE: $module_name = Validate::isModuleName(Tools::getValue('module')) ? Tools::getValue('module') : ''; $module = Module::getInstanceByName($module_name); $controller_class = 'PageNotFoundController'; if (Validate::isLoadedObject($module) && $module->active) { $controllers = Dispatcher::getControllers(_PS_MODULE_DIR_ . "$module_name/controllers/front/"); if (isset($controllers[strtolower($this->controller)])) { include_once _PS_MODULE_DIR_ . "$module_name/controllers/front/{$this->controller}.php"; if (file_exists( _PS_OVERRIDE_DIR_ . "modules/$module_name/controllers/front/{$this->controller}.php" )) { include_once _PS_OVERRIDE_DIR_ . "modules/$module_name/controllers/front/{$this->controller}.php"; $controller_class = $module_name . $this->controller . 'ModuleFrontControllerOverride'; } else { $controller_class = $module_name . $this->controller . 'ModuleFrontController'; } } } $params_hook_action_dispatcher = [ 'controller_type' => self::FC_FRONT, 'controller_class' => $controller_class, 'is_module' => 1, ]; break; // Dispatch back office controller + module back office controller case self::FC_ADMIN: if ($this->use_default_controller && !Tools::getValue('token') && Validate::isLoadedObject(Context::getContext()->employee) && Context::getContext()->employee->isLoggedBack() ) { Tools::redirectAdmin( "index.php?controller={$this->controller}&token=" . Tools::getAdminTokenLite($this->controller) ); } $tab = Tab::getInstanceFromClassName($this->controller, (int) Configuration::get('PS_LANG_DEFAULT')); $retrocompatibility_admin_tab = null; if ($tab->module) { if (file_exists(_PS_MODULE_DIR_ . "{$tab->module}/{$tab->class_name}.php")) { $retrocompatibility_admin_tab = _PS_MODULE_DIR_ . "{$tab->module}/{$tab->class_name}.php"; } else { $controllers = Dispatcher::getControllers(_PS_MODULE_DIR_ . $tab->module . '/controllers/admin/'); if (!isset($controllers[strtolower($this->controller)])) { $this->controller = $this->controller_not_found; $controller_class = 'AdminNotFoundController'; } else { $controller_name = $controllers[strtolower($this->controller)]; // Controllers in modules can be named AdminXXX.php or AdminXXXController.php include_once _PS_MODULE_DIR_ . "{$tab->module}/controllers/admin/$controller_name.php"; if (file_exists( _PS_OVERRIDE_DIR_ . "modules/{$tab->module}/controllers/admin/$controller_name.php" )) { include_once _PS_OVERRIDE_DIR_ . "modules/{$tab->module}/controllers/admin/$controller_name.php"; $controller_class = $controller_name . ( strpos($controller_name, 'Controller') ? 'Override' : 'ControllerOverride' ); } else { $controller_class = $controller_name . ( strpos($controller_name, 'Controller') ? '' : 'Controller' ); } } } $params_hook_action_dispatcher = [ 'controller_type' => self::FC_ADMIN, 'controller_class' => $controller_class, 'is_module' => 1, ]; } else { $controllers = Dispatcher::getControllers( [ _PS_ADMIN_DIR_ . '/tabs/', _PS_ADMIN_CONTROLLER_DIR_, _PS_OVERRIDE_DIR_ . 'controllers/admin/', ] ); if (!isset($controllers[strtolower($this->controller)])) { // If this is a parent tab, load the first child if (Validate::isLoadedObject($tab) && $tab->id_parent == 0 && ($tabs = Tab::getTabs(Context::getContext()->language->id, $tab->id)) && isset($tabs[0]) ) { Tools::redirectAdmin(Context::getContext()->link->getAdminLink($tabs[0]['class_name'])); } $this->controller = $this->controller_not_found; } $controller_class = $controllers[strtolower($this->controller)]; $params_hook_action_dispatcher = [ 'controller_type' => self::FC_ADMIN, 'controller_class' => $controller_class, 'is_module' => 0, ]; if (file_exists(_PS_ADMIN_DIR_ . '/tabs/' . $controller_class . '.php')) { $retrocompatibility_admin_tab = _PS_ADMIN_DIR_ . '/tabs/' . $controller_class . '.php'; } } // @retrocompatibility with admin/tabs/ old system if ($retrocompatibility_admin_tab) { include_once $retrocompatibility_admin_tab; include_once _PS_ADMIN_DIR_ . '/functions.php'; runAdminTab($this->controller, !empty($_REQUEST['ajaxMode'])); return; } break; default: throw new PrestaShopException('Bad front controller chosen'); } // Instantiate controller try { // Loading controller $controller = Controller::getController($controller_class); // Execute hook dispatcher Hook::exec('actionDispatcher', $params_hook_action_dispatcher); // Running controller $controller->run(); // Execute hook dispatcher after Hook::exec('actionDispatcherAfter', $params_hook_action_dispatcher); } catch (PrestaShopException $e) { $e->displayMessage(); } } /** * Sets request uri and if necessary $_GET['isolang']. */ protected function setRequestUri() { $shop = Context::getContext()->shop; if (!Validate::isLoadedObject($shop)) { $shop = null; } $this->request_uri = $this->buildRequestUri( $this->getRequest()->getRequestUri(), Language::isMultiLanguageActivated(), $shop ); } /** * Builds request URI and if necessary sets $_GET['isolang']. * * @param string $requestUri To retrieve the request URI from it * @param bool $isMultiLanguageActivated * @param Shop $shop * * @return string */ private function buildRequestUri($requestUri, $isMultiLanguageActivated, Shop $shop = null) { // Decode raw request URI $requestUri = rawurldecode($requestUri); // Remove the shop base URI part from the request URI if (null !== $shop) { $requestUri = preg_replace( '#^' . preg_quote($shop->getBaseURI(), '#') . '#i', '/', $requestUri ); } // If there are several languages, set $_GET['isolang'] and remove the language part from the request URI if ( $this->use_routes && $isMultiLanguageActivated && preg_match('#^/([a-z]{2})(?:/.*)?$#', $requestUri, $matches) ) { $_GET['isolang'] = $matches[1]; $requestUri = substr($requestUri, 3); } return $requestUri; } /** * Load default routes group by languages. * * @param int $id_shop */ protected function loadRoutes($id_shop = null) { // Initialize shop context if not provided $context = Context::getContext(); if (isset($context->shop) && $id_shop === null) { $id_shop = (int) $context->shop->id; } // Initialize language list we will be building our routes in $language_ids = Language::getIDs(); if (isset($context->language) && !in_array($context->language->id, $language_ids)) { $language_ids[] = (int) $context->language->id; } /* * Step 1 - We have some default hardcoded routes initialized in $this->default_routes, these will * be used as a base. */ /* * Step 2 - Module routes * * Loads custom routes from modules for given shop. Beware that these routes are not multilanguage, * passed routes will be the same for each language of the shop. * * Module routes can overwrite those set in $this->default_routes, if their name matches. * An array [module_name => module_output] will be returned * Hook call is ignoring exceptions set in the backoffice */ $modules_routes = Hook::exec('moduleRoutes', ['id_shop' => $id_shop], null, true, false); if (is_array($modules_routes) && count($modules_routes)) { foreach ($modules_routes as $module_route) { if (is_array($module_route) && count($module_route)) { foreach ($module_route as $route => $route_details) { if (array_key_exists('controller', $route_details) && array_key_exists('rule', $route_details) && array_key_exists('keywords', $route_details) && array_key_exists('params', $route_details) ) { if (!isset($this->default_routes[$route])) { $this->default_routes[$route] = []; } $this->default_routes[$route] = array_merge($this->default_routes[$route], $route_details); } } } } } /* * Step 3 - Initialize default routes into $this->routes that will get used. * * This takes each default route we have until now and calls computeRoute upon each route. * This enriches the route by a final regex and strips not needed keywords. Then, we add it * to route list of each language. */ foreach ($this->default_routes as $id => $route) { $route = $this->computeRoute( $route['rule'], $route['controller'], $route['keywords'], isset($route['params']) ? $route['params'] : [] ); foreach ($language_ids as $id_lang) { $this->routes[$id_shop][$id_lang][$id] = $route; } } if ($this->use_routes) { /* * Step 4 - Load multilanguage routes from meta table. These are static routes for pages like /bestsellers that configurable * in SEO & URL section in the backoffice and don't use any parameters or keywords. */ $sql = 'SELECT m.page, ml.url_rewrite, ml.id_lang FROM `' . _DB_PREFIX_ . 'meta` m LEFT JOIN `' . _DB_PREFIX_ . 'meta_lang` ml ON (m.id_meta = ml.id_meta' . Shop::addSqlRestrictionOnLang('ml', (int) $id_shop) . ') ORDER BY LENGTH(ml.url_rewrite) DESC'; if ($results = Db::getInstance()->executeS($sql)) { foreach ($results as $row) { if ($row['url_rewrite']) { $this->addRoute( $row['page'], $row['url_rewrite'], $row['page'], $row['id_lang'], [], [], $id_shop ); } } } // Set default empty route if no empty route (that's weird I know). // Should probably be set as default value in the constructor in 9.0.0. if (!$this->empty_route) { $this->empty_route = [ 'routeID' => 'index', 'rule' => '', 'controller' => 'index', ]; } /* * Step 5 - Custom routes set in ps_configurations. Those are configured product, category, * cms etc. rules that you can configure in SEO & URL section in the backoffice. * * Beware that these routes are not multilanguage, they will be the same for each language of the shop. * It probably would not be difficult to make them multilanguage, if route was stored in configuration * for each language. */ foreach ($this->default_routes as $route_id => $route_data) { if ($custom_route = Configuration::get('PS_ROUTE_' . $route_id, null, null, $id_shop)) { $route = $this->computeRoute( $custom_route, $route_data['controller'], $route_data['keywords'], isset($route_data['params']) ? $route_data['params'] : [] ); foreach ($language_ids as $id_lang) { $this->routes[$id_shop][$id_lang][$route_id] = $route; } } } } /* * Step 6 - Allow modules to modify routes in any way or add their own multilanguage routes. * * Use getRoutes, addRoute, removeRoute methods for this purpose. */ Hook::exec('actionAfterLoadRoutes', ['dispatcher' => $this, 'id_shop' => $id_shop]); } /** * Create the route array, by computing the final regex & keywords. * * @param string $rule Url rule * @param string $controller Controller to call if request uri match the rule * @param array $keywords keywords associated with the route * @param array $params optional params of the route * * @return array */ public function computeRoute($rule, $controller, array $keywords = [], array $params = []) { $regexp = preg_quote($rule, '#'); if ($keywords) { $transform_keywords = []; preg_match_all( '#\\\{(([^{}]*)\\\:)?(' . implode('|', array_keys($keywords)) . ')(\\\:([^{}]*))?\\\}#', $regexp, $m ); for ($i = 0, $total = count($m[0]); $i < $total; ++$i) { $prepend = $m[2][$i]; $keyword = $m[3][$i]; $append = $m[5][$i]; $transform_keywords[$keyword] = [ 'required' => isset($keywords[$keyword]['param']), 'prepend' => stripslashes($prepend), 'append' => stripslashes($append), ]; $prepend_regexp = $append_regexp = ''; if ($prepend || $append) { $prepend_regexp = '(' . $prepend; $append_regexp = $append . ')?'; } if (isset($keywords[$keyword]['param'])) { $regexp = str_replace( $m[0][$i], $prepend_regexp . '(?P<' . $keywords[$keyword]['param'] . '>' . $keywords[$keyword]['regexp'] . ')' . $append_regexp, $regexp ); } else { $regexp = str_replace( $m[0][$i], $prepend_regexp . '(' . $keywords[$keyword]['regexp'] . ')' . $append_regexp, $regexp ); } } $keywords = $transform_keywords; } $regexp = '#^/' . $regexp . '$#u'; return [ 'rule' => $rule, 'regexp' => $regexp, 'controller' => $controller, 'keywords' => $keywords, 'params' => $params, ]; } /** * Adds a new route to the list of routes. If it already exists, it will override the existing one. * * @param string $route_id Name of the route * @param string $rule Url rule * @param string $controller Controller to call if request uri match the rule * @param int $id_lang * @param array $keywords * @param array $params * @param int $id_shop */ public function addRoute( $route_id, $rule, $controller, $id_lang = null, array $keywords = [], array $params = [], $id_shop = null ) { $context = Context::getContext(); if (isset($context->language) && $id_lang === null) { $id_lang = (int) $context->language->id; } if (isset($context->shop) && $id_shop === null) { $id_shop = (int) $context->shop->id; } $route = $this->computeRoute($rule, $controller, $keywords, $params); if (!isset($this->routes[$id_shop])) { $this->routes[$id_shop] = []; } if (!isset($this->routes[$id_shop][$id_lang])) { $this->routes[$id_shop][$id_lang] = []; } $this->routes[$id_shop][$id_lang][$route_id] = $route; } /** * Returns a list of processed routes getting used. * * @return array List of routes */ public function getRoutes() { return $this->routes; } /** * Removes a route from a list of processed routes. * * @param string $route_id Name of the route * @param int $id_lang * @param int $id_shop */ public function removeRoute($route_id, $id_lang = null, $id_shop = null) { $context = Context::getContext(); if (isset($context->language) && $id_lang === null) { $id_lang = (int) $context->language->id; } if (isset($context->shop) && $id_shop === null) { $id_shop = (int) $context->shop->id; } if (isset($this->routes[$id_shop][$id_lang][$route_id])) { unset($this->routes[$id_shop][$id_lang][$route_id]); } } /** * Check if a route exists. * * @param string $route_id * @param int $id_lang * @param int $id_shop * * @return bool */ public function hasRoute($route_id, $id_lang = null, $id_shop = null) { if (isset(Context::getContext()->language) && $id_lang === null) { $id_lang = (int) Context::getContext()->language->id; } if (isset(Context::getContext()->shop) && $id_shop === null) { $id_shop = (int) Context::getContext()->shop->id; } if (!isset($this->routes[$id_shop])) { $this->loadRoutes($id_shop); } return isset($this->routes[$id_shop][$id_lang][$route_id]); } /** * Check if a keyword is written in a route rule. * * @param string $route_id * @param int $id_lang * @param string $keyword * @param int $id_shop * * @return bool */ public function hasKeyword($route_id, $id_lang, $keyword, $id_shop = null) { if ($id_shop === null) { $id_shop = (int) Context::getContext()->shop->id; } if (!isset($this->routes[$id_shop])) { $this->loadRoutes($id_shop); } if (!isset($this->routes[$id_shop]) || !isset($this->routes[$id_shop][$id_lang]) || !isset($this->routes[$id_shop][$id_lang][$route_id])) { return false; } return preg_match('#\{([^{}]*:)?' . preg_quote($keyword, '#') . '(:[^{}]*)?\}#', $this->routes[$id_shop][$id_lang][$route_id]['rule']); } /** * Check if a route rule contain all required keywords of default route definition. * * @param string $route_id * @param string $rule Rule to verify * @param array $errors List of missing keywords * * @return bool */ public function validateRoute($route_id, $rule, &$errors = []) { $errors = []; if (!isset($this->default_routes[$route_id])) { return false; } foreach ($this->default_routes[$route_id]['keywords'] as $keyword => $data) { if (isset($data['param']) && !preg_match('#\{([^{}]*:)?' . $keyword . '(:[^{}]*)?\}#', $rule)) { $errors[] = $keyword; } } return (count($errors)) ? false : true; } /** * Create an url from. * * @param string $route_id Name the route * @param int $id_lang * @param array $params * @param bool $force_routes * @param string $anchor Optional anchor to add at the end of this url * @param null $id_shop * * @return string * * @throws PrestaShopException */ public function createUrl( $route_id, $id_lang = null, array $params = [], $force_routes = false, $anchor = '', $id_shop = null ) { if ($id_lang === null) { $id_lang = (int) Context::getContext()->language->id; } if ($id_shop === null) { $id_shop = (int) Context::getContext()->shop->id; } if (!isset($this->routes[$id_shop])) { $this->loadRoutes($id_shop); } if (!isset($this->routes[$id_shop][$id_lang][$route_id])) { $query = http_build_query($params, '', '&'); $index_link = $this->use_routes ? '' : 'index.php'; return ($route_id == 'index') ? $index_link . (($query) ? '?' . $query : '') : ((trim($route_id) == '') ? '' : 'index.php?controller=' . $route_id) . (($query) ? '&' . $query : '') . $anchor; } $route = $this->routes[$id_shop][$id_lang][$route_id]; // Check required fields $query_params = isset($route['params']) ? $route['params'] : []; foreach ($route['keywords'] as $key => $data) { if (!$data['required']) { continue; } if (!array_key_exists($key, $params)) { throw new PrestaShopException('Dispatcher::createUrl() miss required parameter "' . $key . '" for route "' . $route_id . '"'); } if (isset($this->default_routes[$route_id])) { $query_params[$this->default_routes[$route_id]['keywords'][$key]['param']] = $params[$key]; } } // Build an url which match a route if ($this->use_routes || $force_routes) { $url = $route['rule']; $add_param = []; foreach ($params as $key => $value) { if (!isset($route['keywords'][$key])) { if (!isset($this->default_routes[$route_id]['keywords'][$key])) { $add_param[$key] = $value; } } else { if ($params[$key]) { $parameter = $params[$key]; if (is_array($parameter)) { if (array_key_exists($id_lang, $parameter)) { $parameter = $parameter[$id_lang]; } else { // made the choice to return the first element of the array $parameter = reset($parameter); } } $replace = $route['keywords'][$key]['prepend'] . $parameter . $route['keywords'][$key]['append']; } else { $replace = ''; } $url = preg_replace('#\{([^{}]*:)?' . $key . '(:[^{}]*)?\}#', $replace, $url); } } $url = preg_replace('#\{([^{}]*:)?[a-z0-9_]+?(:[^{}]*)?\}#', '', $url); if (count($add_param)) { $url .= '?' . http_build_query($add_param, '', '&'); } } else { // Build a classic url index.php?controller=foo&... $add_params = []; foreach ($params as $key => $value) { if (!isset($route['keywords'][$key]) && !isset($this->default_routes[$route_id]['keywords'][$key])) { $add_params[$key] = $value; } } // Add controller to parameters if not present if (!empty($route['controller'])) { $query_params['controller'] = $route['controller']; } // Build final parameters, add language if needed $urlParams = array_merge($add_params, $query_params); // If multilanguage is activated, we add proper language ID, overwriting // the previous one if it was provided if ($this->multilang_activated) { $urlParams['id_lang'] = (int) $id_lang; } // Build the final URL $url = 'index.php?' . http_build_query($urlParams, '', '&'); } return $url . $anchor; } /** * Retrieve the controller from url or request uri if routes are activated. * * @param int $id_shop * * @return string */ public function getController($id_shop = null) { if (defined('_PS_ADMIN_DIR_')) { $_GET['controllerUri'] = Tools::getValue('controller'); } if ($this->controller) { $_GET['controller'] = $this->controller; return $this->controller; } if (isset(Context::getContext()->shop) && $id_shop === null) { $id_shop = (int) Context::getContext()->shop->id; } $controller = Tools::getValue('controller'); if (isset($controller) && is_string($controller) && preg_match('/^([0-9a-z_-]+)\?(.*)=(.*)$/Ui', $controller, $m) ) { $controller = $m[1]; if (isset($_GET['controller'])) { $_GET[$m[2]] = $m[3]; } elseif (isset($_POST['controller'])) { $_POST[$m[2]] = $m[3]; } } if (!Validate::isControllerName($controller)) { $controller = false; } // Use routes ? (for url rewriting) if ($this->use_routes && !$controller && !defined('_PS_ADMIN_DIR_')) { if (!$this->request_uri) { return strtolower($this->controller_not_found); } $controller = $this->controller_not_found; $test_request_uri = preg_replace('/(=http:\/\/)/', '=', $this->request_uri); // If the request_uri matches a static file, unless it's in the upload folder, // then there is no need to check the routes, we keep // "controller_not_found" (a static file should not go through the dispatcher) if ( !preg_match('/\.(gif|jpe?g|png|css|js|ico)$/i', parse_url($test_request_uri, PHP_URL_PATH)) || preg_match('/^\/upload/', parse_url($test_request_uri, PHP_URL_PATH))) { // Add empty route as last route to prevent this greedy regexp to match request uri before right time if ($this->empty_route) { $this->addRoute( $this->empty_route['routeID'], $this->empty_route['rule'], $this->empty_route['controller'], Context::getContext()->language->id, [], [], $id_shop ); } list($uri) = explode('?', $this->request_uri); if (isset($this->routes[$id_shop][Context::getContext()->language->id])) { foreach ($this->routes[$id_shop][Context::getContext()->language->id] as $route) { if (preg_match($route['regexp'], $uri, $m)) { // Route found ! Now fill $_GET with parameters of uri foreach ($m as $k => $v) { if (!is_numeric($k)) { $_GET[$k] = $v; } } $controller = $route['controller'] ? $route['controller'] : $_GET['controller']; if (!empty($route['params'])) { foreach ($route['params'] as $k => $v) { $_GET[$k] = $v; } } // A patch for module friendly urls if (preg_match('#module-([a-z0-9_-]+)-([a-z0-9_]+)$#i', $controller, $m)) { $_GET['module'] = $m[1]; $_GET['fc'] = 'module'; $controller = $m[2]; } if (isset($_GET['fc']) && $_GET['fc'] == 'module') { $this->front_controller = self::FC_MODULE; } break; } } } } if ($controller == 'index' || preg_match('/^\/index.php(?:\?.*)?$/', $this->request_uri)) { $controller = $this->useDefaultController(); } } $this->controller = str_replace('-', '', $controller); $_GET['controller'] = $this->controller; return $this->controller; } /** * Get list of all available FO controllers. * * @param mixed $dirs * * @return array */ public static function getControllers($dirs) { if (!is_array($dirs)) { $dirs = [$dirs]; } $controllers = []; foreach ($dirs as $dir) { $controllers = array_merge($controllers, Dispatcher::getControllersInDirectory($dir)); } return $controllers; } /** * Get list of all available Module Front controllers. * * @param string $type * @param string|array|null $module * * @return array */ public static function getModuleControllers($type = 'all', $module = null) { $modules_controllers = []; if (null === $module) { $modules = Module::getModulesOnDisk(true); } elseif (!is_array($module)) { $modules = [Module::getInstanceByName($module)]; } else { $modules = []; foreach ($module as $_mod) { $modules[] = Module::getInstanceByName($_mod); } } foreach ($modules as $mod) { foreach (Dispatcher::getControllersInDirectory(_PS_MODULE_DIR_ . $mod->name . '/controllers/') as $controller) { if ($type == 'admin') { if (strpos($controller, 'Admin') !== false) { $modules_controllers[$mod->name][] = $controller; } } elseif ($type == 'front') { if (strpos($controller, 'Admin') === false) { $modules_controllers[$mod->name][] = $controller; } } else { $modules_controllers[$mod->name][] = $controller; } } } return $modules_controllers; } /** * Get list of available controllers from the specified dir. * * @param string $dir Directory to scan (recursively) * * @return array */ public static function getControllersInDirectory($dir) { if (!is_dir($dir)) { return []; } $controllers = []; $controller_files = scandir($dir, SCANDIR_SORT_NONE); foreach ($controller_files as $controller_filename) { if ($controller_filename[0] != '.') { if (!strpos($controller_filename, '.php') && is_dir($dir . $controller_filename)) { $controllers += Dispatcher::getControllersInDirectory( $dir . $controller_filename . DIRECTORY_SEPARATOR ); } elseif ($controller_filename != 'index.php') { $key = str_replace(['controller.php', '.php'], '', strtolower($controller_filename)); $controllers[$key] = basename($controller_filename, '.php'); } } } return $controllers; } /** * Get the default php_self value of a controller. * * @param string $controller The controller class name * * @return string|null */ public static function getControllerPhpself(string $controller) { if (!class_exists($controller)) { return null; } $reflectionClass = new ReflectionClass($controller); $controllerDefaultProperties = $reflectionClass->getDefaultProperties(); return $controllerDefaultProperties['php_self'] ?? null; } /** * Get list of all php_self property values of each available controller in the specified dir. * * @param string $dir Directory to scan (recursively) * @param bool $base_name_otherwise Return the controller base name if no php_self is found * * @return array */ public static function getControllersPhpselfList(string $dir, bool $base_name_otherwise = true) { $controllers = Dispatcher::getControllers($dir); $controllersPhpself = []; foreach ($controllers as $controllerBaseName => $controllerClassName) { $controllerPhpself = Dispatcher::getControllerPhpself($controllerClassName); if ($base_name_otherwise) { $controllerPhpself = $controllerPhpself ?? $controllerBaseName; } if ($controllerPhpself) { $controllersPhpself[] = $controllerPhpself; } } return $controllersPhpself; } }