* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Router; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Application\SiteApplication; use Joomla\CMS\Component\Router\RouterInterface; use Joomla\CMS\Component\Router\RouterLegacy; use Joomla\CMS\Component\Router\RouterServiceInterface; use Joomla\CMS\Factory; use Joomla\CMS\Menu\AbstractMenu; use Joomla\CMS\Uri\Uri; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Class to create and parse routes for the site application * * @since 1.5 */ class SiteRouter extends Router { /** * Component-router objects * * @var array * * @since 3.3 */ protected $componentRouters = []; /** * @var CMSApplication * * @since 3.4 */ protected $app; /** * Current Menu-Object * * @var AbstractMenu * * @since 3.4 */ protected $menu; /** * Class constructor * * @param CMSApplication $app Application Object * @param AbstractMenu $menu Menu object * * @since 3.4 */ public function __construct(CMSApplication $app = null, AbstractMenu $menu = null) { $this->app = $app ?: Factory::getContainer()->get(SiteApplication::class); $this->menu = $menu ?: $this->app->getMenu(); // Add core rules if ($this->app->get('force_ssl') === 2) { $this->attachParseRule(array($this, 'parseCheckSSL'), self::PROCESS_BEFORE); } $this->attachParseRule(array($this, 'parseInit'), self::PROCESS_BEFORE); $this->attachBuildRule(array($this, 'buildInit'), self::PROCESS_BEFORE); $this->attachBuildRule(array($this, 'buildComponentPreprocess'), self::PROCESS_BEFORE); if ($this->app->get('sef', 1)) { if ($this->app->get('sef_suffix')) { $this->attachParseRule(array($this, 'parseFormat'), self::PROCESS_BEFORE); $this->attachBuildRule(array($this, 'buildFormat'), self::PROCESS_AFTER); } $this->attachParseRule(array($this, 'parseSefRoute'), self::PROCESS_DURING); $this->attachBuildRule(array($this, 'buildSefRoute'), self::PROCESS_DURING); $this->attachParseRule(array($this, 'parsePaginationData'), self::PROCESS_AFTER); $this->attachBuildRule(array($this, 'buildPaginationData'), self::PROCESS_AFTER); if ($this->app->get('sef_rewrite')) { $this->attachBuildRule(array($this, 'buildRewrite'), self::PROCESS_AFTER); } } $this->attachParseRule(array($this, 'parseRawRoute'), self::PROCESS_DURING); $this->attachBuildRule(array($this, 'buildBase'), self::PROCESS_AFTER); } /** * Force to SSL * * @param Router &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function parseCheckSSL(&$router, &$uri) { if (strtolower($uri->getScheme()) !== 'https') { // Forward to https $uri->setScheme('https'); $this->app->redirect((string) $uri, 301); } } /** * Do some initial cleanup before parsing the URL * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function parseInit(&$router, &$uri) { // Get the path // Decode URL to convert percent-encoding to unicode so that strings match when routing. $path = urldecode($uri->getPath()); /** * In some environments (e.g. CLI we can't form a valid base URL). In this case we catch the exception thrown * by URI and set an empty base URI for further work. * @todo: This should probably be handled better */ try { $baseUri = Uri::base(true); } catch (\RuntimeException $e) { $baseUri = ''; } // Remove the base URI path. $path = substr_replace($path, '', 0, \strlen($baseUri)); // Check to see if a request to a specific entry point has been made. if (preg_match("#.*?\.php#u", $path, $matches)) { // Get the current entry point path relative to the site path. $scriptPath = realpath($_SERVER['SCRIPT_FILENAME'] ?: str_replace('\\\\', '\\', $_SERVER['PATH_TRANSLATED'])); $relativeScriptPath = str_replace('\\', '/', str_replace(JPATH_SITE, '', $scriptPath)); // If a php file has been found in the request path, check to see if it is a valid file. // Also verify that it represents the same file from the server variable for entry script. if (is_file(JPATH_SITE . $matches[0]) && ($matches[0] === $relativeScriptPath)) { // Remove the entry point segments from the request path for proper routing. $path = str_replace($matches[0], '', $path); } } // Set the route $uri->setPath(trim($path, '/')); } /** * Parse the format of the request * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function parseFormat(&$router, &$uri) { $route = $uri->getPath(); // Identify format if (!(substr($route, -9) === 'index.php' || substr($route, -1) === '/') && $suffix = pathinfo($route, PATHINFO_EXTENSION)) { $uri->setVar('format', $suffix); $route = str_replace('.' . $suffix, '', $route); $uri->setPath($route); } } /** * Convert a sef route to an internal URI * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function parseSefRoute(&$router, &$uri) { $route = $uri->getPath(); // If the URL is empty, we handle this in the non-SEF parse URL if (empty($route)) { return; } // Parse the application route $segments = explode('/', $route); if (\count($segments) > 1 && $segments[0] === 'component') { $uri->setVar('option', 'com_' . $segments[1]); $uri->setVar('Itemid', null); $route = implode('/', \array_slice($segments, 2)); } else { // Get menu items. $items = $this->menu->getItems(['parent_id', 'access'], [1, null]); $lang_tag = $this->app->getLanguage()->getTag(); $found = null; foreach ($segments as $segment) { $matched = false; foreach ($items as $item) { if ( $item->alias == $segment && (!$this->app->getLanguageFilter() || ($item->language === '*' || $item->language === $lang_tag)) ) { $found = $item; $matched = true; $items = $item->getChildren(); break; } } if (!$matched) { break; } } // Menu links are not valid URLs. Find the first parent that isn't a menulink if ($found && $found->type === 'menulink') { while ($found->hasParent() && $found->type === 'menulink') { $found = $found->getParent(); } if ($found->type === 'menulink') { $found = null; } } if (!$found) { $found = $this->menu->getDefault($lang_tag); } else { $route = trim(substr($route, \strlen($found->route)), '/'); } if ($found) { if ($found->type === 'alias') { $newItem = $this->menu->getItem($found->getParams()->get('aliasoptions')); if ($newItem) { $found->query = array_merge($found->query, $newItem->query); $found->component = $newItem->component; } } $uri->setVar('Itemid', $found->id); $uri->setVar('option', $found->component); } } // Set the active menu item if ($uri->getVar('Itemid')) { $this->menu->setActive($uri->getVar('Itemid')); } // Parse the component route if (!empty($route) && $uri->getVar('option')) { $segments = explode('/', $route); if (\count($segments)) { // Handle component route $component = preg_replace('/[^A-Z0-9_\.-]/i', '', $uri->getVar('option')); $crouter = $this->getComponentRouter($component); $uri->setQuery(array_merge($uri->getQuery(true), $crouter->parse($segments))); } $route = implode('/', $segments); } $uri->setPath($route); } /** * Convert a raw route to an internal URI * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function parseRawRoute(&$router, &$uri) { if ($uri->getVar('Itemid')) { $item = $this->menu->getItem($uri->getVar('Itemid')); } else { $item = $this->menu->getDefault($this->app->getLanguage()->getTag()); } if ($item && $item->type === 'alias') { $newItem = $this->menu->getItem($item->getParams()->get('aliasoptions')); if ($newItem) { $item->query = array_merge($item->query, $newItem->query); $item->component = $newItem->component; } } if (\is_object($item)) { // Set the active menu item $this->menu->setActive($item->id); $uri->setVar('Itemid', $item->id); $uri->setQuery(array_merge($item->query, $uri->getQuery(true))); } } /** * Convert limits for pagination * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function parsePaginationData(&$router, &$uri) { // Process the pagination support $start = $uri->getVar('start'); if ($start !== null) { $uri->setVar('limitstart', $uri->getVar('start')); $uri->delVar('start'); } } /** * Do some initial processing for building a URL * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildInit(&$router, &$uri) { $itemid = $uri->getVar('Itemid'); // If no Itemid and option given, merge in the current requests data if (!$itemid && !$uri->getVar('option')) { $uri->setQuery(array_merge($this->getVars(), $uri->getQuery(true))); } // If Itemid is given, but no option, set the option from the menu item if ($itemid && !$uri->getVar('option')) { if ($item = $this->menu->getItem($itemid)) { $uri->setVar('option', $item->component); } } } /** * Run the component preprocess method * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildComponentPreprocess(&$router, &$uri) { // Get the query data $query = $uri->getQuery(true); if (!isset($query['option'])) { return; } $component = preg_replace('/[^A-Z0-9_\.-]/i', '', $query['option']); $crouter = $this->getComponentRouter($component); $query = $crouter->preprocess($query); // Make sure any menu vars are used if no others are specified if ( isset($query['Itemid']) && (\count($query) === 2 || (\count($query) === 3 && isset($query['lang']))) ) { // Get the active menu item $item = $this->menu->getItem($query['Itemid']); $query = array_merge($item->query, $query); } $uri->setQuery($query); } /** * Build the SEF route * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildSefRoute(&$router, &$uri) { // Get the query data $query = $uri->getQuery(true); if (!isset($query['option'])) { return; } // Get Menu Item $item = empty($query['Itemid']) ? null : $this->menu->getItem($query['Itemid']); // Build the component route $component = preg_replace('/[^A-Z0-9_\.-]/i', '', $query['option']); $crouter = $this->getComponentRouter($component); $parts = $crouter->build($query); $tmp = trim(implode('/', $parts)); // Build the application route if ($item !== null && $query['option'] === $item->component) { if (!$item->home) { $tmp = $tmp ? $item->route . '/' . $tmp : $item->route; } unset($query['Itemid']); } else { $tmp = 'component/' . substr($query['option'], 4) . '/' . $tmp; } // Get the route if ($tmp) { $uri->setPath($uri->getPath() . '/' . $tmp); } // Unset unneeded query information unset($query['option']); // Set query again in the URI $uri->setQuery($query); } /** * Convert limits for pagination * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildPaginationData(&$router, &$uri) { $limitstart = $uri->getVar('limitstart'); if ($limitstart !== null) { $uri->setVar('start', (int) $uri->getVar('limitstart')); $uri->delVar('limitstart'); } } /** * Build the format of the request * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildFormat(&$router, &$uri) { $route = $uri->getPath(); // Identify format if (!(substr($route, -9) === 'index.php' || substr($route, -1) === '/') && $format = $uri->getVar('format', 'html')) { $route .= '.' . $format; $uri->setPath($route); $uri->delVar('format'); } } /** * Create a uri based on a full or partial URL string * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildRewrite(&$router, &$uri) { // Get the path data $route = $uri->getPath(); // Transform the route if ($route === 'index.php') { $route = ''; } else { $route = str_replace('index.php/', '', $route); } $uri->setPath($route); } /** * Add the basepath to the URI * * @param SiteRouter &$router Router object * @param Uri &$uri URI object to process * * @return void * * @since 4.0.0 */ public function buildBase(&$router, &$uri) { // Add frontend basepath to the uri $uri->setPath(Uri::root(true) . '/' . $uri->getPath()); } /** * Get component router * * @param string $component Name of the component including com_ prefix * * @return RouterInterface Component router * * @since 3.3 */ public function getComponentRouter($component) { if (!isset($this->componentRouters[$component])) { $componentInstance = $this->app->bootComponent($component); if ($componentInstance instanceof RouterServiceInterface) { $this->componentRouters[$component] = $componentInstance->createRouter($this->app, $this->menu); } if (!isset($this->componentRouters[$component])) { $this->componentRouters[$component] = new RouterLegacy(ucfirst(substr($component, 4))); } } return $this->componentRouters[$component]; } /** * Set a router for a component * * @param string $component Component name with com_ prefix * @param object $router Component router * * @return boolean True if the router was accepted, false if not * * @since 3.3 */ public function setComponentRouter($component, $router) { $reflection = new \ReflectionClass($router); if (\in_array('Joomla\\CMS\\Component\\Router\\RouterInterface', $reflection->getInterfaceNames())) { $this->componentRouters[$component] = $router; return true; } else { return false; } } }