1<?php 2/** 3 * Copyright since 2007 PrestaShop SA and Contributors 4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA 5 * 6 * NOTICE OF LICENSE 7 * 8 * This source file is subject to the Open Software License (OSL 3.0) 9 * that is bundled with this package in the file LICENSE.md. 10 * It is also available through the world-wide-web at this URL: 11 * https://opensource.org/licenses/OSL-3.0 12 * If you did not receive a copy of the license and are unable to 13 * obtain it through the world-wide-web, please send an email 14 * to license@prestashop.com so we can send you a copy immediately. 15 * 16 * DISCLAIMER 17 * 18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 19 * versions in the future. If you wish to customize PrestaShop for your 20 * needs please refer to https://devdocs.prestashop.com/ for more information. 21 * 22 * @author PrestaShop SA and Contributors <contact@prestashop.com> 23 * @copyright Since 2007 PrestaShop SA and Contributors 24 * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) 25 */ 26use Symfony\Component\HttpFoundation\Request as SymfonyRequest; 27 28/** 29 * @since 1.5.0 30 */ 31class DispatcherCore 32{ 33 /** 34 * List of available front controllers types. 35 */ 36 const FC_FRONT = 1; 37 const FC_ADMIN = 2; 38 const FC_MODULE = 3; 39 40 const REWRITE_PATTERN = '[_a-zA-Z0-9\x{0600}-\x{06FF}\pL\pS-]*?'; 41 42 /** 43 * @var Dispatcher 44 */ 45 public static $instance = null; 46 47 /** 48 * @var SymfonyRequest 49 */ 50 private $request; 51 52 /** 53 * @var array List of default routes 54 */ 55 public $default_routes = [ 56 'category_rule' => [ 57 'controller' => 'category', 58 'rule' => '{id}-{rewrite}', 59 'keywords' => [ 60 'id' => ['regexp' => '[0-9]+', 'param' => 'id_category'], 61 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 62 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 63 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 64 ], 65 ], 66 'supplier_rule' => [ 67 'controller' => 'supplier', 68 'rule' => 'supplier/{id}-{rewrite}', 69 'keywords' => [ 70 'id' => ['regexp' => '[0-9]+', 'param' => 'id_supplier'], 71 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 72 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 73 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 74 ], 75 ], 76 'manufacturer_rule' => [ 77 'controller' => 'manufacturer', 78 'rule' => 'brand/{id}-{rewrite}', 79 'keywords' => [ 80 'id' => ['regexp' => '[0-9]+', 'param' => 'id_manufacturer'], 81 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 82 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 83 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 84 ], 85 ], 86 'cms_rule' => [ 87 'controller' => 'cms', 88 'rule' => 'content/{id}-{rewrite}', 89 'keywords' => [ 90 'id' => ['regexp' => '[0-9]+', 'param' => 'id_cms'], 91 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 92 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 93 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 94 ], 95 ], 96 'cms_category_rule' => [ 97 'controller' => 'cms', 98 'rule' => 'content/category/{id}-{rewrite}', 99 'keywords' => [ 100 'id' => ['regexp' => '[0-9]+', 'param' => 'id_cms_category'], 101 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 102 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 103 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 104 ], 105 ], 106 'module' => [ 107 'controller' => null, 108 'rule' => 'module/{module}{/:controller}', 109 'keywords' => [ 110 'module' => ['regexp' => '[_a-zA-Z0-9_-]+', 'param' => 'module'], 111 'controller' => ['regexp' => '[_a-zA-Z0-9_-]+', 'param' => 'controller'], 112 ], 113 'params' => [ 114 'fc' => 'module', 115 ], 116 ], 117 'product_rule' => [ 118 'controller' => 'product', 119 'rule' => '{category:/}{id}{-:id_product_attribute}-{rewrite}{-:ean13}.html', 120 'keywords' => [ 121 'id' => ['regexp' => '[0-9]+', 'param' => 'id_product'], 122 'id_product_attribute' => ['regexp' => '[0-9]+', 'param' => 'id_product_attribute'], 123 'rewrite' => ['regexp' => self::REWRITE_PATTERN, 'param' => 'rewrite'], 124 'ean13' => ['regexp' => '[0-9\pL]*'], 125 'category' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 126 'categories' => ['regexp' => '[/_a-zA-Z0-9-\pL]*'], 127 'reference' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 128 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 129 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 130 'manufacturer' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 131 'supplier' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 132 'price' => ['regexp' => '[0-9\.,]*'], 133 'tags' => ['regexp' => '[a-zA-Z0-9-\pL]*'], 134 ], 135 ], 136 /* Must be after the product and category rules in order to avoid conflict */ 137 'layered_rule' => [ 138 'controller' => 'category', 139 'rule' => '{id}-{rewrite}{/:selected_filters}', 140 'keywords' => [ 141 'id' => ['regexp' => '[0-9]+', 'param' => 'id_category'], 142 /* Selected filters is used by the module blocklayered */ 143 'selected_filters' => ['regexp' => '.*', 'param' => 'selected_filters'], 144 'rewrite' => ['regexp' => self::REWRITE_PATTERN], 145 'meta_keywords' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 146 'meta_title' => ['regexp' => '[_a-zA-Z0-9-\pL]*'], 147 ], 148 ], 149 ]; 150 151 /** 152 * @var bool If true, use routes to build URL (mod rewrite must be activated) 153 */ 154 protected $use_routes = false; 155 156 protected $multilang_activated = false; 157 158 /** 159 * @var array List of loaded routes 160 */ 161 protected $routes = []; 162 163 /** 164 * @var string Current controller name 165 */ 166 protected $controller; 167 168 /** 169 * @var string Current request uri 170 */ 171 protected $request_uri; 172 173 /** 174 * @var array Store empty route (a route with an empty rule) 175 */ 176 protected $empty_route; 177 178 /** 179 * @var string Set default controller, which will be used if http parameter 'controller' is empty 180 */ 181 protected $default_controller; 182 protected $use_default_controller = false; 183 184 /** 185 * @var string Controller to use if found controller doesn't exist 186 */ 187 protected $controller_not_found = 'pagenotfound'; 188 189 /** 190 * @var string Front controller to use 191 */ 192 protected $front_controller = self::FC_FRONT; 193 194 /** 195 * Get current instance of dispatcher (singleton). 196 * 197 * @return Dispatcher 198 * 199 * @throws PrestaShopException 200 */ 201 public static function getInstance(SymfonyRequest $request = null) 202 { 203 if (!self::$instance) { 204 if (null === $request) { 205 $request = SymfonyRequest::createFromGlobals(); 206 } 207 self::$instance = new Dispatcher($request); 208 } 209 210 return self::$instance; 211 } 212 213 /** 214 * Needs to be instantiated from getInstance() method. 215 * 216 * @param SymfonyRequest|null $request 217 * 218 * @throws PrestaShopException 219 */ 220 protected function __construct(SymfonyRequest $request = null) 221 { 222 $this->setRequest($request); 223 224 $this->use_routes = (bool) Configuration::get('PS_REWRITING_SETTINGS'); 225 226 // Select right front controller 227 if (defined('_PS_ADMIN_DIR_')) { 228 $this->front_controller = self::FC_ADMIN; 229 $this->controller_not_found = 'adminnotfound'; 230 } elseif (Tools::getValue('fc') == 'module') { 231 $this->front_controller = self::FC_MODULE; 232 $this->controller_not_found = 'pagenotfound'; 233 } else { 234 $this->front_controller = self::FC_FRONT; 235 $this->controller_not_found = 'pagenotfound'; 236 } 237 238 $this->setRequestUri(); 239 240 // Switch language if needed (only on front) 241 if (in_array($this->front_controller, [self::FC_FRONT, self::FC_MODULE])) { 242 Tools::switchLanguage(); 243 } 244 245 if (Language::isMultiLanguageActivated()) { 246 $this->multilang_activated = true; 247 } 248 249 $this->loadRoutes(); 250 } 251 252 /** 253 * Either sets a given request or a new one. 254 * 255 * @param SymfonyRequest|null $request 256 */ 257 private function setRequest(SymfonyRequest $request = null) 258 { 259 if (null === $request) { 260 $request = SymfonyRequest::createFromGlobals(); 261 } 262 263 $this->request = $request; 264 } 265 266 /** 267 * Returns the request property. 268 * 269 * @return SymfonyRequest 270 */ 271 private function getRequest() 272 { 273 return $this->request; 274 } 275 276 /** 277 * Sets and returns the default controller. 278 * 279 * @param int $frontControllerType The front controller type 280 * @param Employee|null $employee The current employee 281 * 282 * @return string 283 */ 284 private function getDefaultController($frontControllerType, Employee $employee = null) 285 { 286 switch ($frontControllerType) { 287 case self::FC_ADMIN: 288 // Default 289 $defaultController = 'AdminDashboard'; 290 // If there is an employee with a default tab set 291 if (null !== $employee) { 292 $tabClassName = $employee->getDefaultTabClassName(); 293 if (null !== $tabClassName) { 294 $tabProfileAccess = Profile::getProfileAccess($employee->id_profile, Tab::getIdFromClassName($tabClassName)); 295 if (is_array($tabProfileAccess) && isset($tabProfileAccess['view']) && $tabProfileAccess['view'] === '1') { 296 $defaultController = $tabClassName; 297 } 298 } 299 } 300 301 break; 302 case self::FC_MODULE: 303 $defaultController = 'default'; 304 305 break; 306 default: 307 $defaultController = 'index'; 308 } 309 310 $this->setDefaultController($defaultController); 311 312 return $defaultController; 313 } 314 315 /** 316 * Sets the default controller. 317 * 318 * @param string $defaultController 319 */ 320 private function setDefaultController($defaultController) 321 { 322 $this->default_controller = $defaultController; 323 } 324 325 /** 326 * Sets use_default_controller to true, sets and returns the default controller. 327 * 328 * @return string 329 */ 330 public function useDefaultController() 331 { 332 $this->use_default_controller = true; 333 334 // If it was already set just return it 335 if (null !== $this->default_controller) { 336 return $this->default_controller; 337 } 338 339 $employee = Context::getContext()->employee; 340 341 return $this->getDefaultController($this->front_controller, $employee); 342 } 343 344 /** 345 * Find the controller and instantiate it. 346 */ 347 public function dispatch() 348 { 349 $controller_class = ''; 350 351 // Get current controller 352 $this->getController(); 353 if (!$this->controller) { 354 $this->controller = $this->useDefaultController(); 355 } 356 // Execute hook dispatcher before 357 Hook::exec('actionDispatcherBefore', ['controller_type' => $this->front_controller]); 358 359 // Dispatch with right front controller 360 switch ($this->front_controller) { 361 // Dispatch front office controller 362 case self::FC_FRONT: 363 $controllers = Dispatcher::getControllers([ 364 _PS_FRONT_CONTROLLER_DIR_, 365 _PS_OVERRIDE_DIR_ . 'controllers/front/', 366 ]); 367 $controllers['index'] = 'IndexController'; 368 if (isset($controllers['auth'])) { 369 $controllers['authentication'] = $controllers['auth']; 370 } 371 if (isset($controllers['contact'])) { 372 $controllers['contactform'] = $controllers['contact']; 373 } 374 375 if (!isset($controllers[strtolower($this->controller)])) { 376 $this->controller = $this->controller_not_found; 377 } 378 $controller_class = $controllers[strtolower($this->controller)]; 379 $params_hook_action_dispatcher = [ 380 'controller_type' => self::FC_FRONT, 381 'controller_class' => $controller_class, 382 'is_module' => 0, 383 ]; 384 385 break; 386 387 // Dispatch module controller for front office 388 case self::FC_MODULE: 389 $module_name = Validate::isModuleName(Tools::getValue('module')) ? Tools::getValue('module') : ''; 390 $module = Module::getInstanceByName($module_name); 391 $controller_class = 'PageNotFoundController'; 392 if (Validate::isLoadedObject($module) && $module->active) { 393 $controllers = Dispatcher::getControllers(_PS_MODULE_DIR_ . "$module_name/controllers/front/"); 394 if (isset($controllers[strtolower($this->controller)])) { 395 include_once _PS_MODULE_DIR_ . "$module_name/controllers/front/{$this->controller}.php"; 396 if (file_exists( 397 _PS_OVERRIDE_DIR_ . "modules/$module_name/controllers/front/{$this->controller}.php" 398 )) { 399 include_once _PS_OVERRIDE_DIR_ . "modules/$module_name/controllers/front/{$this->controller}.php"; 400 $controller_class = $module_name . $this->controller . 'ModuleFrontControllerOverride'; 401 } else { 402 $controller_class = $module_name . $this->controller . 'ModuleFrontController'; 403 } 404 } 405 } 406 $params_hook_action_dispatcher = [ 407 'controller_type' => self::FC_FRONT, 408 'controller_class' => $controller_class, 409 'is_module' => 1, 410 ]; 411 412 break; 413 414 // Dispatch back office controller + module back office controller 415 case self::FC_ADMIN: 416 if ($this->use_default_controller 417 && !Tools::getValue('token') 418 && Validate::isLoadedObject(Context::getContext()->employee) 419 && Context::getContext()->employee->isLoggedBack() 420 ) { 421 Tools::redirectAdmin( 422 "index.php?controller={$this->controller}&token=" . Tools::getAdminTokenLite($this->controller) 423 ); 424 } 425 426 $tab = Tab::getInstanceFromClassName($this->controller, Configuration::get('PS_LANG_DEFAULT')); 427 $retrocompatibility_admin_tab = null; 428 429 if ($tab->module) { 430 if (file_exists(_PS_MODULE_DIR_ . "{$tab->module}/{$tab->class_name}.php")) { 431 $retrocompatibility_admin_tab = _PS_MODULE_DIR_ . "{$tab->module}/{$tab->class_name}.php"; 432 } else { 433 $controllers = Dispatcher::getControllers(_PS_MODULE_DIR_ . $tab->module . '/controllers/admin/'); 434 if (!isset($controllers[strtolower($this->controller)])) { 435 $this->controller = $this->controller_not_found; 436 $controller_class = 'AdminNotFoundController'; 437 } else { 438 $controller_name = $controllers[strtolower($this->controller)]; 439 // Controllers in modules can be named AdminXXX.php or AdminXXXController.php 440 include_once _PS_MODULE_DIR_ . "{$tab->module}/controllers/admin/$controller_name.php"; 441 if (file_exists( 442 _PS_OVERRIDE_DIR_ . "modules/{$tab->module}/controllers/admin/$controller_name.php" 443 )) { 444 include_once _PS_OVERRIDE_DIR_ . "modules/{$tab->module}/controllers/admin/$controller_name.php"; 445 $controller_class = $controller_name . ( 446 strpos($controller_name, 'Controller') ? 'Override' : 'ControllerOverride' 447 ); 448 } else { 449 $controller_class = $controller_name . ( 450 strpos($controller_name, 'Controller') ? '' : 'Controller' 451 ); 452 } 453 } 454 } 455 $params_hook_action_dispatcher = [ 456 'controller_type' => self::FC_ADMIN, 457 'controller_class' => $controller_class, 458 'is_module' => 1, 459 ]; 460 } else { 461 $controllers = Dispatcher::getControllers( 462 [ 463 _PS_ADMIN_DIR_ . '/tabs/', 464 _PS_ADMIN_CONTROLLER_DIR_, 465 _PS_OVERRIDE_DIR_ . 'controllers/admin/', 466 ] 467 ); 468 if (!isset($controllers[strtolower($this->controller)])) { 469 // If this is a parent tab, load the first child 470 if (Validate::isLoadedObject($tab) 471 && $tab->id_parent == 0 472 && ($tabs = Tab::getTabs(Context::getContext()->language->id, $tab->id)) 473 && isset($tabs[0]) 474 ) { 475 Tools::redirectAdmin(Context::getContext()->link->getAdminLink($tabs[0]['class_name'])); 476 } 477 $this->controller = $this->controller_not_found; 478 } 479 480 $controller_class = $controllers[strtolower($this->controller)]; 481 $params_hook_action_dispatcher = [ 482 'controller_type' => self::FC_ADMIN, 483 'controller_class' => $controller_class, 484 'is_module' => 0, 485 ]; 486 487 if (file_exists(_PS_ADMIN_DIR_ . '/tabs/' . $controller_class . '.php')) { 488 $retrocompatibility_admin_tab = _PS_ADMIN_DIR_ . '/tabs/' . $controller_class . '.php'; 489 } 490 } 491 492 // @retrocompatibility with admin/tabs/ old system 493 if ($retrocompatibility_admin_tab) { 494 include_once $retrocompatibility_admin_tab; 495 include_once _PS_ADMIN_DIR_ . '/functions.php'; 496 runAdminTab($this->controller, !empty($_REQUEST['ajaxMode'])); 497 498 return; 499 } 500 501 break; 502 503 default: 504 throw new PrestaShopException('Bad front controller chosen'); 505 } 506 507 // Instantiate controller 508 try { 509 // Loading controller 510 $controller = Controller::getController($controller_class); 511 512 // Execute hook dispatcher 513 if (isset($params_hook_action_dispatcher)) { 514 Hook::exec('actionDispatcher', $params_hook_action_dispatcher); 515 } 516 517 // Running controller 518 $controller->run(); 519 520 // Execute hook dispatcher after 521 if (isset($params_hook_action_dispatcher)) { 522 Hook::exec('actionDispatcherAfter', $params_hook_action_dispatcher); 523 } 524 } catch (PrestaShopException $e) { 525 $e->displayMessage(); 526 } 527 } 528 529 /** 530 * Sets request uri and if necessary $_GET['isolang']. 531 */ 532 protected function setRequestUri() 533 { 534 $shop = Context::getContext()->shop; 535 if (!Validate::isLoadedObject($shop)) { 536 $shop = null; 537 } 538 539 $this->request_uri = $this->buildRequestUri( 540 $this->getRequest()->getRequestUri(), 541 Language::isMultiLanguageActivated(), 542 $shop 543 ); 544 } 545 546 /** 547 * Builds request URI and if necessary sets $_GET['isolang']. 548 * 549 * @param string $requestUri To retrieve the request URI from it 550 * @param bool $isMultiLanguageActivated 551 * @param Shop $shop 552 * 553 * @return string 554 */ 555 private function buildRequestUri($requestUri, $isMultiLanguageActivated, Shop $shop = null) 556 { 557 // Decode raw request URI 558 $requestUri = rawurldecode($requestUri); 559 560 // Remove the shop base URI part from the request URI 561 if (null !== $shop) { 562 $requestUri = preg_replace( 563 '#^' . preg_quote($shop->getBaseURI(), '#') . '#i', 564 '/', 565 $requestUri 566 ); 567 } 568 569 // If there are several languages, set $_GET['isolang'] and remove the language part from the request URI 570 if ( 571 $this->use_routes && 572 $isMultiLanguageActivated && 573 preg_match('#^/([a-z]{2})(?:/.*)?$#', $requestUri, $matches) 574 ) { 575 $_GET['isolang'] = $matches[1]; 576 $requestUri = substr($requestUri, 3); 577 } 578 579 return $requestUri; 580 } 581 582 /** 583 * Load default routes group by languages. 584 * 585 * @param int $id_shop 586 */ 587 protected function loadRoutes($id_shop = null) 588 { 589 $context = Context::getContext(); 590 591 if (isset($context->shop) && $id_shop === null) { 592 $id_shop = (int) $context->shop->id; 593 } 594 595 // Load custom routes from modules 596 $modules_routes = Hook::exec('moduleRoutes', ['id_shop' => $id_shop], null, true, false); 597 if (is_array($modules_routes) && count($modules_routes)) { 598 foreach ($modules_routes as $module_route) { 599 if (is_array($module_route) && count($module_route)) { 600 foreach ($module_route as $route => $route_details) { 601 if (array_key_exists('controller', $route_details) 602 && array_key_exists('rule', $route_details) 603 && array_key_exists('keywords', $route_details) 604 && array_key_exists('params', $route_details) 605 ) { 606 if (!isset($this->default_routes[$route])) { 607 $this->default_routes[$route] = []; 608 } 609 $this->default_routes[$route] = array_merge($this->default_routes[$route], $route_details); 610 } 611 } 612 } 613 } 614 } 615 616 $language_ids = Language::getIDs(); 617 618 if (isset($context->language) && !in_array($context->language->id, $language_ids)) { 619 $language_ids[] = (int) $context->language->id; 620 } 621 622 // Set default routes 623 foreach ($this->default_routes as $id => $route) { 624 $route = $this->computeRoute( 625 $route['rule'], 626 $route['controller'], 627 $route['keywords'], 628 isset($route['params']) ? $route['params'] : [] 629 ); 630 foreach ($language_ids as $id_lang) { 631 // the default routes are the same, whatever the language 632 $this->routes[$id_shop][$id_lang][$id] = $route; 633 } 634 } 635 636 // Load the custom routes prior the defaults to avoid infinite loops 637 if ($this->use_routes) { 638 // Load routes from meta table 639 $sql = 'SELECT m.page, ml.url_rewrite, ml.id_lang 640 FROM `' . _DB_PREFIX_ . 'meta` m 641 LEFT JOIN `' . _DB_PREFIX_ . 'meta_lang` ml ON (m.id_meta = ml.id_meta' . Shop::addSqlRestrictionOnLang('ml', (int) $id_shop) . ') 642 ORDER BY LENGTH(ml.url_rewrite) DESC'; 643 if ($results = Db::getInstance()->executeS($sql)) { 644 foreach ($results as $row) { 645 if ($row['url_rewrite']) { 646 $this->addRoute( 647 $row['page'], 648 $row['url_rewrite'], 649 $row['page'], 650 $row['id_lang'], 651 [], 652 [], 653 $id_shop 654 ); 655 } 656 } 657 } 658 659 // Set default empty route if no empty route (that's weird I know) 660 if (!$this->empty_route) { 661 $this->empty_route = [ 662 'routeID' => 'index', 663 'rule' => '', 664 'controller' => 'index', 665 ]; 666 } 667 668 // Load custom routes 669 foreach ($this->default_routes as $route_id => $route_data) { 670 if ($custom_route = Configuration::get('PS_ROUTE_' . $route_id, null, null, $id_shop)) { 671 if (isset($context->language) && !in_array($context->language->id, $language_ids)) { 672 $language_ids[] = (int) $context->language->id; 673 } 674 675 $route = $this->computeRoute( 676 $custom_route, 677 $route_data['controller'], 678 $route_data['keywords'], 679 isset($route_data['params']) ? $route_data['params'] : [] 680 ); 681 foreach ($language_ids as $id_lang) { 682 // those routes are the same, whatever the language 683 $this->routes[$id_shop][$id_lang][$route_id] = $route; 684 } 685 } 686 } 687 } 688 } 689 690 /** 691 * Create the route array, by computing the final regex & keywords. 692 * 693 * @param string $rule Url rule 694 * @param string $controller Controller to call if request uri match the rule 695 * @param array $keywords keywords associated with the route 696 * @param array $params optional params of the route 697 * 698 * @return array 699 */ 700 public function computeRoute($rule, $controller, array $keywords = [], array $params = []) 701 { 702 $regexp = preg_quote($rule, '#'); 703 if ($keywords) { 704 $transform_keywords = []; 705 preg_match_all( 706 '#\\\{(([^{}]*)\\\:)?(' . 707 implode('|', array_keys($keywords)) . ')(\\\:([^{}]*))?\\\}#', 708 $regexp, 709 $m 710 ); 711 for ($i = 0, $total = count($m[0]); $i < $total; ++$i) { 712 $prepend = $m[2][$i]; 713 $keyword = $m[3][$i]; 714 $append = $m[5][$i]; 715 $transform_keywords[$keyword] = [ 716 'required' => isset($keywords[$keyword]['param']), 717 'prepend' => stripslashes($prepend), 718 'append' => stripslashes($append), 719 ]; 720 721 $prepend_regexp = $append_regexp = ''; 722 if ($prepend || $append) { 723 $prepend_regexp = '(' . $prepend; 724 $append_regexp = $append . ')?'; 725 } 726 727 if (isset($keywords[$keyword]['param'])) { 728 $regexp = str_replace( 729 $m[0][$i], 730 $prepend_regexp . 731 '(?P<' . $keywords[$keyword]['param'] . '>' . $keywords[$keyword]['regexp'] . ')' . 732 $append_regexp, 733 $regexp 734 ); 735 } else { 736 $regexp = str_replace( 737 $m[0][$i], 738 $prepend_regexp . 739 '(' . $keywords[$keyword]['regexp'] . ')' . 740 $append_regexp, 741 $regexp 742 ); 743 } 744 } 745 $keywords = $transform_keywords; 746 } 747 748 $regexp = '#^/' . $regexp . '$#u'; 749 750 return [ 751 'rule' => $rule, 752 'regexp' => $regexp, 753 'controller' => $controller, 754 'keywords' => $keywords, 755 'params' => $params, 756 ]; 757 } 758 759 /** 760 * @param string $route_id Name of the route (need to be uniq,a second route with same name will override the first) 761 * @param string $rule Url rule 762 * @param string $controller Controller to call if request uri match the rule 763 * @param int $id_lang 764 * @param array $keywords 765 * @param array $params 766 * @param int $id_shop 767 */ 768 public function addRoute( 769 $route_id, 770 $rule, 771 $controller, 772 $id_lang = null, 773 array $keywords = [], 774 array $params = [], 775 $id_shop = null 776 ) { 777 $context = Context::getContext(); 778 779 if (isset($context->language) && $id_lang === null) { 780 $id_lang = (int) $context->language->id; 781 } 782 783 if (isset($context->shop) && $id_shop === null) { 784 $id_shop = (int) $context->shop->id; 785 } 786 787 $route = $this->computeRoute($rule, $controller, $keywords, $params); 788 789 if (!isset($this->routes[$id_shop])) { 790 $this->routes[$id_shop] = []; 791 } 792 if (!isset($this->routes[$id_shop][$id_lang])) { 793 $this->routes[$id_shop][$id_lang] = []; 794 } 795 796 $this->routes[$id_shop][$id_lang][$route_id] = $route; 797 } 798 799 /** 800 * Check if a route exists. 801 * 802 * @param string $route_id 803 * @param int $id_lang 804 * @param int $id_shop 805 * 806 * @return bool 807 */ 808 public function hasRoute($route_id, $id_lang = null, $id_shop = null) 809 { 810 if (isset(Context::getContext()->language) && $id_lang === null) { 811 $id_lang = (int) Context::getContext()->language->id; 812 } 813 if (isset(Context::getContext()->shop) && $id_shop === null) { 814 $id_shop = (int) Context::getContext()->shop->id; 815 } 816 817 return isset($this->routes[$id_shop][$id_lang][$route_id]); 818 } 819 820 /** 821 * Check if a keyword is written in a route rule. 822 * 823 * @param string $route_id 824 * @param int $id_lang 825 * @param string $keyword 826 * @param int $id_shop 827 * 828 * @return bool 829 */ 830 public function hasKeyword($route_id, $id_lang, $keyword, $id_shop = null) 831 { 832 if ($id_shop === null) { 833 $id_shop = (int) Context::getContext()->shop->id; 834 } 835 836 if (!isset($this->routes[$id_shop])) { 837 $this->loadRoutes($id_shop); 838 } 839 840 if (!isset($this->routes[$id_shop]) || !isset($this->routes[$id_shop][$id_lang]) 841 || !isset($this->routes[$id_shop][$id_lang][$route_id])) { 842 return false; 843 } 844 845 return preg_match('#\{([^{}]*:)?' . preg_quote($keyword, '#') . 846 '(:[^{}]*)?\}#', $this->routes[$id_shop][$id_lang][$route_id]['rule']); 847 } 848 849 /** 850 * Check if a route rule contain all required keywords of default route definition. 851 * 852 * @param string $route_id 853 * @param string $rule Rule to verify 854 * @param array $errors List of missing keywords 855 * 856 * @return bool 857 */ 858 public function validateRoute($route_id, $rule, &$errors = []) 859 { 860 $errors = []; 861 if (!isset($this->default_routes[$route_id])) { 862 return false; 863 } 864 865 foreach ($this->default_routes[$route_id]['keywords'] as $keyword => $data) { 866 if (isset($data['param']) && !preg_match('#\{([^{}]*:)?' . $keyword . '(:[^{}]*)?\}#', $rule)) { 867 $errors[] = $keyword; 868 } 869 } 870 871 return (count($errors)) ? false : true; 872 } 873 874 /** 875 * Create an url from. 876 * 877 * @param string $route_id Name the route 878 * @param int $id_lang 879 * @param array $params 880 * @param bool $force_routes 881 * @param string $anchor Optional anchor to add at the end of this url 882 * @param null $id_shop 883 * 884 * @return string 885 * 886 * @throws PrestaShopException 887 */ 888 public function createUrl( 889 $route_id, 890 $id_lang = null, 891 array $params = [], 892 $force_routes = false, 893 $anchor = '', 894 $id_shop = null 895 ) { 896 if ($id_lang === null) { 897 $id_lang = (int) Context::getContext()->language->id; 898 } 899 if ($id_shop === null) { 900 $id_shop = (int) Context::getContext()->shop->id; 901 } 902 903 if (!isset($this->routes[$id_shop])) { 904 $this->loadRoutes($id_shop); 905 } 906 907 if (!isset($this->routes[$id_shop][$id_lang][$route_id])) { 908 $query = http_build_query($params, '', '&'); 909 $index_link = $this->use_routes ? '' : 'index.php'; 910 911 return ($route_id == 'index') ? $index_link . (($query) ? '?' . $query : '') : 912 ((trim($route_id) == '') ? '' : 'index.php?controller=' . $route_id) . (($query) ? '&' . $query : '') . $anchor; 913 } 914 $route = $this->routes[$id_shop][$id_lang][$route_id]; 915 // Check required fields 916 $query_params = isset($route['params']) ? $route['params'] : []; 917 foreach ($route['keywords'] as $key => $data) { 918 if (!$data['required']) { 919 continue; 920 } 921 922 if (!array_key_exists($key, $params)) { 923 throw new PrestaShopException('Dispatcher::createUrl() miss required parameter "' . $key . '" for route "' . $route_id . '"'); 924 } 925 if (isset($this->default_routes[$route_id])) { 926 $query_params[$this->default_routes[$route_id]['keywords'][$key]['param']] = $params[$key]; 927 } 928 } 929 930 // Build an url which match a route 931 if ($this->use_routes || $force_routes) { 932 $url = $route['rule']; 933 $add_param = []; 934 935 foreach ($params as $key => $value) { 936 if (!isset($route['keywords'][$key])) { 937 if (!isset($this->default_routes[$route_id]['keywords'][$key])) { 938 $add_param[$key] = $value; 939 } 940 } else { 941 if ($params[$key]) { 942 $parameter = $params[$key]; 943 if (is_array($parameter)) { 944 if (array_key_exists($id_lang, $parameter)) { 945 $parameter = $parameter[$id_lang]; 946 } else { 947 // made the choice to return the first element of the array 948 $parameter = reset($parameter); 949 } 950 } 951 $replace = $route['keywords'][$key]['prepend'] . $parameter . $route['keywords'][$key]['append']; 952 } else { 953 $replace = ''; 954 } 955 $url = preg_replace('#\{([^{}]*:)?' . $key . '(:[^{}]*)?\}#', $replace, $url); 956 } 957 } 958 $url = preg_replace('#\{([^{}]*:)?[a-z0-9_]+?(:[^{}]*)?\}#', '', $url); 959 if (count($add_param)) { 960 $url .= '?' . http_build_query($add_param, '', '&'); 961 } 962 } else { 963 // Build a classic url index.php?controller=foo&... 964 $add_params = []; 965 foreach ($params as $key => $value) { 966 if (!isset($route['keywords'][$key]) && !isset($this->default_routes[$route_id]['keywords'][$key])) { 967 $add_params[$key] = $value; 968 } 969 } 970 971 if (!empty($route['controller'])) { 972 $query_params['controller'] = $route['controller']; 973 } 974 $query = http_build_query(array_merge($add_params, $query_params), '', '&'); 975 if ($this->multilang_activated) { 976 $query .= (!empty($query) ? '&' : '') . 'id_lang=' . (int) $id_lang; 977 } 978 $url = 'index.php?' . $query; 979 } 980 981 return $url . $anchor; 982 } 983 984 /** 985 * Retrieve the controller from url or request uri if routes are activated. 986 * 987 * @param int $id_shop 988 * 989 * @return string 990 */ 991 public function getController($id_shop = null) 992 { 993 if (defined('_PS_ADMIN_DIR_')) { 994 $_GET['controllerUri'] = Tools::getValue('controller'); 995 } 996 if ($this->controller) { 997 $_GET['controller'] = $this->controller; 998 999 return $this->controller; 1000 } 1001 1002 if (isset(Context::getContext()->shop) && $id_shop === null) { 1003 $id_shop = (int) Context::getContext()->shop->id; 1004 } 1005 1006 $controller = Tools::getValue('controller'); 1007 1008 if (isset($controller) 1009 && is_string($controller) 1010 && preg_match('/^([0-9a-z_-]+)\?(.*)=(.*)$/Ui', $controller, $m) 1011 ) { 1012 $controller = $m[1]; 1013 if (isset($_GET['controller'])) { 1014 $_GET[$m[2]] = $m[3]; 1015 } elseif (isset($_POST['controller'])) { 1016 $_POST[$m[2]] = $m[3]; 1017 } 1018 } 1019 1020 if (!Validate::isControllerName($controller)) { 1021 $controller = false; 1022 } 1023 1024 // Use routes ? (for url rewriting) 1025 if ($this->use_routes && !$controller && !defined('_PS_ADMIN_DIR_')) { 1026 if (!$this->request_uri) { 1027 return strtolower($this->controller_not_found); 1028 } 1029 $controller = $this->controller_not_found; 1030 $test_request_uri = preg_replace('/(=http:\/\/)/', '=', $this->request_uri); 1031 1032 // If the request_uri matches a static file, then there is no need to check the routes, we keep 1033 // "controller_not_found" (a static file should not go through the dispatcher) 1034 if (!preg_match( 1035 '/\.(gif|jpe?g|png|css|js|ico)$/i', 1036 parse_url($test_request_uri, PHP_URL_PATH) 1037 )) { 1038 // Add empty route as last route to prevent this greedy regexp to match request uri before right time 1039 if ($this->empty_route) { 1040 $this->addRoute( 1041 $this->empty_route['routeID'], 1042 $this->empty_route['rule'], 1043 $this->empty_route['controller'], 1044 Context::getContext()->language->id, 1045 [], 1046 [], 1047 $id_shop 1048 ); 1049 } 1050 1051 list($uri) = explode('?', $this->request_uri); 1052 1053 if (isset($this->routes[$id_shop][Context::getContext()->language->id])) { 1054 foreach ($this->routes[$id_shop][Context::getContext()->language->id] as $route) { 1055 if (preg_match($route['regexp'], $uri, $m)) { 1056 // Route found ! Now fill $_GET with parameters of uri 1057 foreach ($m as $k => $v) { 1058 if (!is_numeric($k)) { 1059 $_GET[$k] = $v; 1060 } 1061 } 1062 1063 $controller = $route['controller'] ? $route['controller'] : $_GET['controller']; 1064 if (!empty($route['params'])) { 1065 foreach ($route['params'] as $k => $v) { 1066 $_GET[$k] = $v; 1067 } 1068 } 1069 1070 // A patch for module friendly urls 1071 if (preg_match('#module-([a-z0-9_-]+)-([a-z0-9_]+)$#i', $controller, $m)) { 1072 $_GET['module'] = $m[1]; 1073 $_GET['fc'] = 'module'; 1074 $controller = $m[2]; 1075 } 1076 1077 if (isset($_GET['fc']) && $_GET['fc'] == 'module') { 1078 $this->front_controller = self::FC_MODULE; 1079 } 1080 1081 break; 1082 } 1083 } 1084 } 1085 } 1086 1087 if ($controller == 'index' || preg_match('/^\/index.php(?:\?.*)?$/', $this->request_uri)) { 1088 $controller = $this->useDefaultController(); 1089 } 1090 } 1091 1092 $this->controller = str_replace('-', '', $controller); 1093 $_GET['controller'] = $this->controller; 1094 1095 return $this->controller; 1096 } 1097 1098 /** 1099 * Get list of all available FO controllers. 1100 * 1101 * @var mixed 1102 * 1103 * @return array 1104 */ 1105 public static function getControllers($dirs) 1106 { 1107 if (!is_array($dirs)) { 1108 $dirs = [$dirs]; 1109 } 1110 1111 $controllers = []; 1112 foreach ($dirs as $dir) { 1113 $controllers = array_merge($controllers, Dispatcher::getControllersInDirectory($dir)); 1114 } 1115 1116 return $controllers; 1117 } 1118 1119 /** 1120 * Get list of all available Module Front controllers. 1121 * 1122 * @param string $type 1123 * @param string $module 1124 * 1125 * @return array 1126 */ 1127 public static function getModuleControllers($type = 'all', $module = null) 1128 { 1129 $modules_controllers = []; 1130 if (null === $module) { 1131 $modules = Module::getModulesOnDisk(true); 1132 } elseif (!is_array($module)) { 1133 $modules = [Module::getInstanceByName($module)]; 1134 } else { 1135 $modules = []; 1136 foreach ($module as $_mod) { 1137 $modules[] = Module::getInstanceByName($_mod); 1138 } 1139 } 1140 1141 foreach ($modules as $mod) { 1142 foreach (Dispatcher::getControllersInDirectory(_PS_MODULE_DIR_ . $mod->name . '/controllers/') as $controller) { 1143 if ($type == 'admin') { 1144 if (strpos($controller, 'Admin') !== false) { 1145 $modules_controllers[$mod->name][] = $controller; 1146 } 1147 } elseif ($type == 'front') { 1148 if (strpos($controller, 'Admin') === false) { 1149 $modules_controllers[$mod->name][] = $controller; 1150 } 1151 } else { 1152 $modules_controllers[$mod->name][] = $controller; 1153 } 1154 } 1155 } 1156 1157 return $modules_controllers; 1158 } 1159 1160 /** 1161 * Get list of available controllers from the specified dir. 1162 * 1163 * @param string $dir Directory to scan (recursively) 1164 * 1165 * @return array 1166 */ 1167 public static function getControllersInDirectory($dir) 1168 { 1169 if (!is_dir($dir)) { 1170 return []; 1171 } 1172 1173 $controllers = []; 1174 $controller_files = scandir($dir, SCANDIR_SORT_NONE); 1175 foreach ($controller_files as $controller_filename) { 1176 if ($controller_filename[0] != '.') { 1177 if (!strpos($controller_filename, '.php') && is_dir($dir . $controller_filename)) { 1178 $controllers += Dispatcher::getControllersInDirectory( 1179 $dir . $controller_filename . DIRECTORY_SEPARATOR 1180 ); 1181 } elseif ($controller_filename != 'index.php') { 1182 $key = str_replace(['controller.php', '.php'], '', strtolower($controller_filename)); 1183 $controllers[$key] = basename($controller_filename, '.php'); 1184 } 1185 } 1186 } 1187 1188 return $controllers; 1189 } 1190 1191 /** 1192 * Get the default php_self value of a controller. 1193 * 1194 * @param string $controller The controller class name 1195 * 1196 * @return string|null 1197 */ 1198 public static function getControllerPhpself(string $controller) 1199 { 1200 if (!class_exists($controller)) { 1201 return; 1202 } 1203 1204 $reflectionClass = new ReflectionClass($controller); 1205 $controllerDefaultProperties = $reflectionClass->getDefaultProperties(); 1206 1207 return $controllerDefaultProperties['php_self'] ?? null; 1208 } 1209 1210 /** 1211 * Get list of all php_self property values of each available controller in the specified dir. 1212 * 1213 * @param string $dir Directory to scan (recursively) 1214 * @param bool $base_name_otherwise Return the controller base name if no php_self is found 1215 * 1216 * @return array 1217 */ 1218 public static function getControllersPhpselfList(string $dir, bool $base_name_otherwise = true) 1219 { 1220 $controllers = Dispatcher::getControllers($dir); 1221 1222 $controllersPhpself = []; 1223 1224 foreach ($controllers as $controllerBaseName => $controllerClassName) { 1225 $controllerPhpself = Dispatcher::getControllerPhpself($controllerClassName); 1226 1227 if ($base_name_otherwise) { 1228 $controllerPhpself = $controllerPhpself ?? $controllerBaseName; 1229 } 1230 1231 if ($controllerPhpself) { 1232 $controllersPhpself[] = $controllerPhpself; 1233 } 1234 } 1235 1236 return $controllersPhpself; 1237 } 1238} 1239