File Manager

Current Directory: /home/astoriaah/www/administrator/components/com_sh404sef/classes
Viewing File: /home/astoriaah/www/administrator/components/com_sh404sef/classes/router.php
<?php /** * sh404SEF - SEO extension for Joomla! * * @package sh404SEF * @copyright (c) Yannick Gaultier - Weeblr llc - 2022 * @author Yannick Gaultier * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL * @version 4.24.1.4308 * @date 2022-05-31 */ use Weeblr\Wblib\V_SH4_4308\Factory; use Joomla\CMS\Router; // no direct access defined('_JEXEC') or die; /** * Class to create and parse routes for the site application */ class Sh404sefClassRouterInternal extends JRouterSite { const ROUTER_MODE_SH404SEF = 999; // flag, to make sure we know when we are parsing the request // parsing can be called upon several times by Joomla // however some operations associated with parsing (mostly // automated redirects) should be done only once public static $requestParsed = false; public static $parsedWithJoomlaRouter = false; public $joomlaErrorHandler = null; public $joomlaExceptionHandler = null; protected $_shId = 'sh404SEF for Joomla 1.6'; protected $_originalBuildUri = null; protected $_guessedLanguageCode = ''; protected $bypassSh404sefRouting = false; /** * Perform startup operations such as detecting environment * and checking automated redirections */ public function startup(&$uri) { // check some SEO related redirects $this->_checkSeoRedirects($uri); // check www vs non-www and other domain related stuff // may redirect to another url $this->_checkDomain($uri); // load common language strings $language = JFactory::getLanguage(); $language->load('com_sh404sef.sys', JPATH_ADMINISTRATOR); if (JFactory::getApplication()->isSite()) { $language->load('com_sh404sef'); } // hookup to Joomla error page handling $this->_setupNotFoundErrorHandling(); } protected function _checkSeoRedirects($uri) { // facebook: it may happen that FB cause an URL that has been liked or otherise shared // to be linked to and thus indexed with an added query parameter. This will cause // duplicate content issue, so if it happens, we want to 301 redirect to the same URL // without that parameter $fb_xd_bust = isset($_GET['fb_xd_bust']); $fb_xd_fragment = isset($_GET['fb_xd_fragment']); if (!empty($fb_xd_bust) || !empty($fb_xd_fragment)) { // sanity checks, like don't redirect if there is some POST data if (!$this->_canRedirectFrom($uri)) { return; } // need to redirect, let's kill the faulty params $uri->delVar('fb_xd_bust'); $uri->delVar('fb_xd_fragment'); // finally redirect $target = $uri->toString(); ShlSystem_Log::debug('sh404sef', 'Performing seo redirect to:' . $target); shRedirect($target); } } /** * Check various conditions on a request to * decide whether it is safe to allow a redirection * to another page * Does NOT check configuration settings, only look * at the passed uri and method parameters * * @param JURI object $uri object describing the current request, from which we want to redirect * @param string $method current request method * * @return bool */ protected function _canRedirectFrom($uri, $method = '', $url = '') { // use framework if no method passed if (empty($method)) { $method = $this->app->input->getMethod(); } // get the requested url $url = empty($url) ? $this->getFullUrl($uri) : $url; // start with hope $canRedirect = !self::$requestParsed; $canRedirect = $canRedirect && !empty($url); $canRedirect = $canRedirect && strpos($url, 'index2.php') === false; $canRedirect = $canRedirect && strpos($url, 'tmpl=component') === false; $canRedirect = $canRedirect && strpos($url, 'no_html=1') === false; $canRedirect = $canRedirect && strpos($url, 'sh404sef_splash=1') === false; $canRedirect = $canRedirect && (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest')); $canRedirect = $canRedirect && empty($_POST); $canRedirect = $canRedirect && $method != 'POST'; return $canRedirect; } /** * Put back the base path that was removed by Joomla router prior to sending * uri to parserule * * @param JURI $uri * * @return string */ protected function getFullUrl($uri) { $clone = clone $uri; $basePath = $clone->base(true); if (!empty($basePath)) { $currentPath = $clone->getPath(); $clone->setPath($basePath . '/' . $currentPath); } $fullUrl = $clone->toString(); return $fullUrl; } /** * * Performs some checks, and possibly some redirects based * on the current request domain * * Typically, ensure access through either www or non-www version of domain * * In the future, will also allow for language determination based on * domain or sub domain * * @param JURI object $uri * * @throws Exception */ protected function _checkDomain($originalUri) { $app = JFactory::getApplication(); if ($app->isAdmin()) { return; } // break reference $uri = clone $originalUri; // get configuration $sefConfig = Sh404sefFactory::getConfig(); $pageInfo = &Sh404sefFactory::getPageInfo(); // get request infos: $rootUrl = $pageInfo->getDefaultFrontLiveSite(); $host = $uri->getHost(); $canRedirect = $app->isSite() && $this->_canRedirectFrom($uri) && !empty($host) && $host != 'localhost' && $host != 'web.local'; $targetUrl = ''; // first check if there are some settings for per language domain, and apply them // search for live site in our list of per language root url foreach ($sefConfig->liveSites as $language => $langRootUrl) { if (!empty($langRootUrl) && $langRootUrl == $rootUrl) { // TODO: switch to that language // stop break; } } // if we have not already $shouldRedirect = false; if (empty($targetUrl) && $canRedirect && $sefConfig->shAutoRedirectWww != shSEFConfig::DONT_ENFORCE_WWW) { if (substr($host, 0, 4) != 'www.' && $sefConfig->shAutoRedirectWww == shSEFConfig::ENFORCE_WWW) { ShlSystem_Log::debug('sh404sef', 'Redirecting from non www to wwww'); $uri->setHost('www.' . $host); $shouldRedirect = true; } if (substr($host, 0, 4) == 'www.' && $sefConfig->shAutoRedirectWww == shSEFConfig::ENFORCE_NO_WWW) { ShlSystem_Log::debug('sh404sef', 'Redirecting from www to non wwww'); $uri->setHost(wbLTrim($host, 'www.')); $shouldRedirect = true; } } if ($shouldRedirect) { // redirect to SSL in one hop if ($app->get('force_ssl') == 2) { $uri->setScheme('https'); } $targetUrl = $uri->toString(); } // Redirect if needed if ($shouldRedirect && !empty($targetUrl)) { shRedirect($targetUrl); } } /** * Optionnally overrides Joomla 404 error page * * const ERROR_404_HANDLE = 0; * const ERROR_404_OVERRIDE_JOOMLA = 1; * const ERROR_404_USE_JOOMLA = 2; */ protected function _setupNotFoundErrorHandling() { // we store current handler, so as to pass-thru to it if needed $this->joomlaErrorHandler = JError::getErrorHandling(E_ERROR); // then override Joomla! handler JError::setErrorHandling(E_ERROR, 'callback', array($this, 'sh404sefErrorPage')); $this->joomlaExceptionHandler = set_exception_handler(array($this, 'sh404sefExceptionHandler')); } /** * Circumvent the language filter to allow checking for aliases and shurls * * Only executed after J 3.4 (needs "processing stages in router") * * @param $router * @param $uri * * @throws Exception */ public function preprocessParseRule(&$router, &$uri) { if ($this->bypassSh404sefRouting) { return; } $originalUrl = Sh404sefHelperUrl::getOriginalUrlFromUri($uri); $originalPath = trim(Sh404sefHelperUrl::getOriginalPathFromUri($uri, JUri::base()), '/'); $originalUri = new JUri($originalUrl); $originalUri->setPath($originalPath); // check requests rejection rules if (JFactory::getApplication()->isSite()) { Sh404sefFactory::getWaf($uri) ->filter(); } $this->_checkAliases($uri); $this->_checkShurls($uri); } /** * Method attached to J! main router object * and processed as a parseRule * * @param JRouter $jRouter object router object reference * @param JUri $uri object provided by Joomla. Will represent request upon * first call of this method, but any uri upon subsequent calls * * @return array $vars list of query variables as decoded by us, if any * @throws Exception */ public function parseRule(&$jRouter, &$uri) { static $_vars = array(); if ($this->bypassSh404sefRouting) { return; } /** * Filter the incoming URI just before the sh404SEF parsing starts. * * @api * @package sh404SEF\filter\router * @var sh404sef_before_parse_rule * * @param JRouter $jRouter The Joomla router instance. * @param JUri $uri The incoming URI. * * @return array * @since 4.15.3 * */ $uri = ShlHook::filter( 'sh404sef_before_parse_rule', $uri, $jRouter ); $app = JFactory::getApplication(); // rebuild an URI object, bypassing any change made by the language filter plugin $originalUrl = Sh404sefHelperUrl::getOriginalUrlFromUri($uri); $originalPath = trim(Sh404sefHelperUrl::getOriginalPathFromUri($uri, JUri::base()), '/'); // by pass routing when extensions incorrectly POST data // (usually with ajax) to a relative URL if ($this->_relativeUrlBypass($originalPath)) { return array(); } $originalUri = new JUri($originalUrl); $originalUri->setPath($originalPath); // check if url is accessed thru /index.php/ while site is // set up to use url rewriting $this->_checkAccessThruIndexphp($originalUri); // check if user has set any alias for this - possibly non-sef - url if (version_compare(JVERSION, '3.4', '<')) { // only do this on J < 3.4 // if > 3.4, we do this check at the PROCESS_BEFORE stage of routing // Only way to have proper aliases and shURLs when language filter is on $this->_checkAliases($originalUri); } // check SEO redirects, such as index.php on home page $this->_checkHomepageRedirects($originalUri); // calculate signature of uri, avoiding multiple parsing of same uri $signature = md5($originalUrl); // don't parse twice, this would break J! 1.6 Router mode handling if (isset($_vars[$signature])) { if (!version_compare(JVERSION, '3.8', 'lt') || !self::$parsedWithJoomlaRouter) { $uri->setPath(''); $uri->setQuery($_vars[$signature]); } return $_vars[$signature]; } // debug info ShlSystem_Log::debug('sh404sef', 'Starting to parse %s', print_r($uri, true)); // fix the path before checking url. Joomla unfortunately remove // the trailing slash from the path, thus we're not working on the actual requested URL if (!self::$requestParsed) { $path = $uri->getPath(); if (!empty($path)) { if ($this->_hasTrailingSlash() & JString::substr($path, -1) != '/') { // if trailing slash on original request, but not on path // put it back $uri->setPath($path . '/'); } } /* * 1 - We don't know what the language filter may or may not have done * with the path. It may already have returned a lang var to the router * but we don't know that either * * Actually we do: if a language was detected, $app->input->get('language') * will have the language code (ie en-GB) * If that is set, the path has been truncated, ie the leading en/ code * has been removed (and no query var set to compensate!) * * 2 - What we can do: compare the original url with the path * in $uri. If we do have a language in the original but not * in the incoming $uri, then language filter already has done lang * detection and removed that from the path (including when there's no * language code at all: that's default language) * Then we can: * - figure out what the language is and stick it back in $uri * - parse through sh404sef * - remove the leading lang code we put in $uri * - unset any 'lang' we may have calculated ourselves so as * not to override what the languge filter has done * EDIT: to still be able to switch back to default language (when no lang code is used * on default language, we must use trick (ie set a cookie) * */ $appLang = $app->input->get('language', ''); $sefLangCode = ''; if (!empty($appLang)) { $languages = JLanguageHelper::getLanguages('lang_code'); $sefLangCode = empty($languages[$appLang]) ? '' : $languages[$appLang]->sef; } $currentPath = ''; if (!empty($sefLangCode) && $originalPath != 'index.php' && ( $appLang != Sh404sefHelperLanguage::getDefaultLanguageTag() || ( $appLang == Sh404sefHelperLanguage::getDefaultLanguageTag() && Sh404sefHelperLanguage::getInsertLangCodeInDefaultLanguage() && (!empty($originalPath)) ) ) ) { // stick back the path in the url $currentPath = $uri->getPath(); $uri->setPath($sefLangCode . '/' . (empty($currentPath) ? '' : $currentPath)); $this->_guessedLanguageCode = $sefLangCode; } // do the parsing $_vars[$signature] = $this->_parseSefRoute($uri); if (!empty($_vars[$signature])) { // merge our decoded vars with those that could be passed as query string $_vars[$signature] = array_merge($_vars[$signature], $uri->getQuery($asArray = true)); if (!version_compare(JVERSION, '3.8', 'lt') || !self::$parsedWithJoomlaRouter) { $uri->setQuery($_vars[$signature]); } else { // $uri is not set when Joomla! router is used to parse, so // build reference non-sef url otherwise $query = urldecode(http_build_query($_vars[$signature], '', '&')); $query = empty($query) ? '' : '?' . $query; $nonSefUrl = 'index.php' . $query . $uri->toString( array( 'fragment' ) ); } // kill the path, so that J! router don't try to keep parsing // the sef url if (!version_compare(JVERSION, '3.8', 'lt') || !self::$parsedWithJoomlaRouter) { $uri->setPath(''); // apply user-set rules for Itemid if (!empty($_vars[$signature]['option'])) { $config = Sh404sefFactory::getConfig(); $noComOption = wbLTrim($_vars[$signature]['option'], 'com_'); if (!empty($noComOption)) { if ( // if set to always override Itemid in_array($noComOption, $config->itemidOverridesAlways) || // or to override if none (in_array($noComOption, $config->itemidOverridesIfMissing) && empty($_vars[$signature]['Itemid'])) ) { // add/ replace current Itemid with the one set // we may have to override the Itemid if (!empty($config->itemidOverridesValues[$noComOption])) { $_vars[$signature]['Itemid'] = $config->itemidOverridesValues[$noComOption]; } } } } // when J! will try parse this as RAW route, for some reasons it // tries to get the Itemid from the request, so we have to fake having one if (!empty($_vars[$signature]['Itemid'])) { $app->input->set('Itemid', (int)$_vars[$signature]['Itemid']); } // unset the Itemid var if it's empty: some extensions (or user-created custom SEF URLs) // have "&Itemid=&id=..., which currently caused the Itemid to be set to nothing // instead of Joomla default behavior if (empty($_vars[$signature]['Itemid']) && isset($_vars[$signature]['Itemid'])) { unset($_vars[$signature]['Itemid']); } // big bad hack for Joomla 3.x // removed 4.14.1 //$this->_mailtoHack($_vars[$signature]); } // now store the decoded current non-sef url $nonSefUrl = empty($nonSefUrl) ? 'index.php' . $uri->toString( array( 'query', 'fragment' ) ) : $nonSefUrl; Sh404sefFactory::getPageInfo()->currentNonSefUrl = Sh404sefHelperUrl::stripTrackingVarsFromNonSef($nonSefUrl); } else if (!empty($appLang)) { // revert the changes we made to $uri $uri->setPath($currentPath); } } // mark request as parsed, so that we don't try to do // redirects and such if the router parse method is // called several times self::$requestParsed = true; // a few per-extension hacks $this->_extensionsParseHacks($jRouter, $uri, $_vars[$signature]); // store page language information if (!empty($appLang)) { Sh404sefFactory::getPageInfo()->setCurrentLanguage($appLang); } // Send that back to J! router to put everything together if (version_compare(JVERSION, '3.8', 'lt') && self::$parsedWithJoomlaRouter) { $parsedVars = array(); } else { $parsedVars = empty($_vars[$signature]) ? array() : $_vars[$signature]; } /** * Filter the list of query variables as parsed by sh404SEF router. * * @api * @package sh404SEF\filter\router * @var sh404sef_after_router_parse_vars * * @param array $parsedVars Associative array of query vars. * @param JRouter $jRouter The Joomla router instance. * @param JUri $uri The incoming URI. * * @return array * @since 4.8 * */ $parsedVars = ShlHook::filter( 'sh404sef_after_router_parse_vars', $parsedVars, $jRouter, $uri ); /** * Filter the Itemid as parsed by sh404SEF router. Return null to keep Itemid unchanged, * or an Itemid value to be set. * * @api * @package sh404SEF\filter\router * @var sh404sef_after_router_parse_vars_set_itemid * * @param int $Itemid The current Itemid if any. * @param array $parsedVars Associative array of query vars. * @param JRouter $jRouter The Joomla router instance. * @param JUri $uri The incoming URI. * * @return int | null * @since 4.12.0 * */ $Itemid = wbArrayGet($parsedVars, 'Itemid', null); $Itemid = ShlHook::filter( 'sh404sef_after_router_parse_vars_set_itemid', $Itemid, $parsedVars, $jRouter, $uri ); if (!is_null($Itemid)) { $parsedVars['Itemid'] = $Itemid; $uri->setVar('Itemid', $Itemid); JFactory::getApplication()->input->set('Itemid', $Itemid); } return $parsedVars; } /** * Detect POSTing of data to index.php and accept * it even if the URL is invalid (a 404). * * Prevents errors when extensions javascript send * ajax requests to relative URLs. * * @param string $originalPath * * @return bool * @throws Exception */ private function _relativeUrlBypass($originalPath) { $method = JFactory::getApplication()->input->getMethod(); if ($method != 'POST') { return false; } $bits = explode('/', $originalPath); if (empty($bits)) { return false; } $lastBit = array_pop($bits); if ($lastBit != 'index.php') { return false; } return true; } /** * Detects if requests access an existing page using /index.php/ * while url rewriting is activated, and redirect if so * * @param JURI $uri */ protected function _checkAccessThruIndexphp($uri) { if (self::$requestParsed || !empty(Sh404sefFactory::getConfig()->shRewriteMode)) { return; } // is the url being accessed with index.php? $pageInfo = Sh404sefFactory::getPageInfo(); $originalUrl = Sh404sefHelperUrl::getOriginalUrlFromUri($uri); $originalPathAndQuery = wbLTrim($originalUrl, JUri::base()); if (strpos($originalPathAndQuery, 'index.php/') === 0) { if (!$this->_canRedirectFrom($uri)) { return; } $targetSefUrl = str_replace('/index.php/', '/', $pageInfo->currentSefUrl); ShlSystem_Log::debug('sh404sef', 'Redirecting to same url without /index.php: ' . $targetSefUrl); shRedirect($targetSefUrl); } } /** * Uses a redirector model to apply aliases redirection rules, if any. * * @param JUri $uri */ protected function _checkAliases($uri) { if (self::$requestParsed) { return; } // initial creation of the redirector Sh404sefFactory::getRedirector($uri) ->redirectFromAlias(); } /** * * Performs various seo redirect checks, in case where the request is * for the home page. A home page request only means the request path is empty; * such request may have query vars - ie site.com/index.php?option=com_content&id=12&view=article * is still a home page request * * Include redirecting site.com/index.php to site.com and, in the future * site.com/index.php?lang=xx to site.com/xx or the correct sef url for that language * * @param JURI object $uri */ protected function _checkHomepageRedirects($uri) { if (self::$requestParsed || !$this->_canRedirectFromNonSef($uri)) { return; } // check if we already did all the redirections we can $pageInfo = Sh404sefFactory::getPageInfo(); // basic data $sefConfig = Sh404sefFactory::getConfig(); $path = $uri->getPath(); $url = $this->getFullUrl($uri); $vars = $uri->getQuery(true); // 0 - check forced homepage, in case of index.html splash page (!!!) if (!empty($sefConfig->shForcedHomePage) && ($sefConfig->shForcedHomePage == $pageInfo->originalUri || $sefConfig->shForcedHomePage . '?sh404sef_splash=1' == $pageInfo->originalUri) ) { return; } // 1 - check index.php on home page $indexString = str_replace($pageInfo->getDefaultFrontLiveSite(), '', $pageInfo->currentSefUrl); $indexString = explode('?', $indexString); $indexString = JString::substr($indexString[0], -9); // IIS sometimes adds index.php to uri, even if user did not request it. $IIS = !empty($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false; if (sh404SEF_REDIRECT_IF_INDEX_PHP && !$IIS && (empty($path) || $path == 'index.php') && empty($vars) && $indexString == 'index.php' ) { // redirect to home page $targetUrl = $pageInfo->getDefaultFrontLiveSite(); ShlSystem_Log::debug('sh404sef', 'Redirecting home page request with index.php to home page: ' . $targetUrl); shRedirect($targetUrl); } // 2 - Home page, some query vars, but we don't have an option var, ie dunno what component to go to // just drop the index.php if (empty($path) && !empty($vars) && empty($vars['option']) && empty($vars['lang']) && $indexString == 'index.php' ) { $query = $uri->getQuery(); $targetUrl = $uri->base() . (empty($query) ? '' : '?' . $query); ShlSystem_Log::debug( 'sh404sef', 'Redirecting home page request with index.php and query vars to home page: ' . $targetUrl ); shRedirect($targetUrl); } // 3 - request for home page with a language element if (empty($path) && !empty($vars) && empty($vars['option']) && !empty($vars['lang']) && $indexString == 'index.php' ) { $query = $uri->getQuery(); $targetUrl = $uri->base() . (empty($query) ? '' : '?' . $query); ShlSystem_Log::debug( 'sh404sef', 'Redirecting home page request to non-default language home page: ' . $targetUrl ); shRedirect($targetUrl); } // 4 - Still home page, ie empty path, but some query vars, lookup db to find it if (empty($path) && !empty($vars) && !empty($vars['option'])) { // rebuild the non-sef url requested $nonSefUrl = 'index.php' . $uri->toString( array( 'query' ) ); $sefUrl = ''; // try to get it from our url store $urlType = shGetSefURLFromCacheOrDB($nonSefUrl, $sefUrl); if ($urlType == sh404SEF_URLTYPE_AUTO || $urlType == sh404SEF_URLTYPE_CUSTOM) { // found a match in database $sefUrl = $uri->base() . ltrim($sefConfig->shRewriteStrings[$sefConfig->shRewriteMode], '/') . $sefUrl; ShlSystem_Log::debug('sh404sef', 'redirecting non-sef to existing SEF : ' . $sefUrl); shRedirect($sefUrl); } // 5 - no success yet, we'll try SEF-y the non-sef url if ($sefConfig->shRedirectNonSefToSef && !empty($nonSefUrl) && (empty($pageInfo->isMultilingual) || (($pageInfo->isMultilingual !== false) && Sh404sefHelperLanguage::getDefaultLanguageTag() == $pageInfo->currentLanguageTag)) ) { $sefUrl = JRoute::_($nonSefUrl, $xhtml = false); $s = WbLTrim($sefUrl, JURI::base(true)); if (!empty($s) && $s != '/') { $sefUrl = $uri->toString( array( 'scheme', 'host', 'port' ) ) . $sefUrl; if (!shIsHomepage($sefUrl) && strpos($sefUrl, 'option=com') === false && $sefUrl != $url) { ShlSystem_Log::debug( 'sh404sef', 'Homepage redirect to newly created SEF : ' . $sefUrl . ' from ' . $url ); shRedirect($sefUrl); } } } } } /** * * Check a number of conditions, both global and * relative to a provided source page uri * to decide whether a redirect to another page * can take place * Will also check configuration settings * * @param object $uri * * @return bool */ protected function _canRedirectFromNonSef($uri, $method = '') { // if not parsing the initial request, no way we can redirect if (self::$requestParsed) { return false; } $pageInfo = Sh404sefFactory::getPageInfo(); // what's the requested url? $url = $pageInfo->currentSefUrl; $request = wbLTrim($url, $pageInfo->getDefaultFrontLiveSite()); if ($request != '/index.php' && strpos($url, 'index.php?') === false) { return false; } if ($request != '/index.php' && strpos($url, 'sh404sef_splash=1') !== false) { return false; } // use framework if no method passed if (empty($method)) { $method = $this->app->input->getMethod(); } // get config $sefConfig = Sh404sefFactory::getConfig(); // get/set data $vars = $uri->getQuery(true); $canRedirect = true; // first condition: component should not be set to "skip" if (!empty($vars['option'])) { $shOption = wbLTrim($vars['option'], 'com_'); if (!empty($shOption) && in_array($shOption, $sefConfig->skip)) { $canRedirect = false; } } // redirect only well formed component name $canRedirect = $canRedirect && (empty($vars['option']) || strpos($vars['option'], 'com_') === 0); // additional redirects checks on the full URL $canRedirect = $canRedirect && $sefConfig->shRedirectNonSefToSef && $this->_canRedirectFrom($uri, $method); return $canRedirect; } /** * * Checks if the path of a request has a trailing slash * * @param JURI object $uri * * @return boolean true if has trailing slash */ protected function _hasTrailingSlash($uri = null) { if (is_null($uri)) { $url = Sh404sefFactory::getPageInfo()->currentSefUrl; } else { $url = wbGetProtectedProperty('Juri', 'uri', $uri); } $rawPath = explode('?', $url); $rawPath = $rawPath[0]; // query string removed, if any $rawPath = explode('#', $rawPath); $rawPath = $rawPath[0]; // fragment removed, if any $trailingSlash = JString::substr($rawPath, -1) == '/'; return $trailingSlash; } /** * As of Joomla 3.2.x, mailto component incorrectly uses JRequest::getVar() to fetch * the mailto link (it uses the 'method' param, which is ok on J 2.5, but bad on 3.x, as * the router does not set the request after routing, but instead sets $app->input) * This hack makes sure link is passed as a GET var * * @param array $vars * * @throws Exception */ private function _mailtoHack($vars) { if (!version_compare(JVERSION, '3.0', 'ge') || empty($vars['option']) || empty($vars['link'])) { return; } if ($vars['option'] == 'com_mailto' && empty($vars['view'])) { JFactory::getApplication()->input->set('link', $vars['link']); } } /** * Some extensions do unusual things we might have to accomodate * * @param JRouter $jRouter * @param JURI $uri * @param array $vars * * @throws Exception */ private function _extensionsParseHacks(&$jRouter, &$uri, &$vars) { $app = JFactory::getApplication(); $hasJRequest = class_exists('JRequest'); // Some extensions have not updated to J 3.0 API // or use hacks to delay updating $method = strtolower($app->input->getMethod()); if ('get' == $method && !empty($vars['option']) && strpos(Sh404sefFactory::getConfig()->extensionToExtractGetVars, $vars['option']) !== false ) { foreach ($vars as $key => $value) { $app->input->set($key, $value); // Extensions such as Mijoshop can't live without that. if ($hasJRequest) { JRequest::setVar($key, $value); } } } // com_config: front end editing: on multilingual site, with J! 3.6.x, the language filter // has a bug that causes a redirect when there should not be any (PR pending) // As the redirect is done to homepage, the home page vars are merged with the com_config // ones, which may result in wrong view and/or layout. // here we make sure the com_config view and layout are used if ('get' == $method && !empty($vars['option']) && $vars['option'] == 'com_config') { unset($vars['view']); unset($vars['layout']); unset($vars['Itemid']); $uri->delVar('view'); $uri->delVar('layout'); $uri->delVar('Itemid'); } // Kunena - sometimes - uses an internal $current variable // to build urls, notably pagination links. That variable // is set inside the parse function of its router.php KunenaParseRoute function if ('get' == $method && !empty($vars['option']) && $vars['option'] == 'com_kunena' && class_exists('KunenaRoute') && isset(KunenaRoute::$current)) { foreach ($vars as $key => $value) { if ($key != 'start') { KunenaRoute::$current->setVar($key, $value); } } } // Community Builder: uses directly $_GET, which is not set // by the Joomla router if ('get' == $method && !empty($vars['option']) && $vars['option'] == 'com_comprofiler') { foreach ($vars as $key => $value) { if ($key == 'limitstart') { $app->input->get->set($key, $value); } } } // mijoshop still using J2 api. Also, always require the "route" var // extracted into $_GET, even on POST request if (!empty($vars['option']) && $vars['option'] == 'com_mijoshop') { if (!empty($vars['route'])) { $app->input->get->set('route', $vars['route']); JRequest::setVar('route', $vars['route']); } } } /** * Method attached to J! main router object * and processed as a buildRule * * @param JRouter $jrouter router object reference * @param JUri $uri object provided by Joomla. We must set the path of this * object, and adjust its query vars list, removing those that are represented * in the path and thus don't need anymore to be specified as query vars */ public function buildRule(&$jRouter, &$uri) { if ($this->bypassSh404sefRouting) { return; } // if superadmin, display non-sef URL, for testing/setting up purposes if (sh404SEF_NON_SEF_IF_SUPERADMIN) { $user = JFactory::getUser(); if ($user->get('isRoot')) { ShlSystem_Log::debug('sh404sef', 'Returning non-sef because superadmin said so.'); return; } } // hook $uri = ShlHook::filter('sh404sef_before_plugin_build', $uri); // keep a copy of Joomla original URI, which has article names in it (ie: 43:article-title) $this->_originalBuildUri = clone($uri); // build the path $this->_buildSefRoute($uri); // a few per-extension hacks $this->_extensionsBuildHacks($jRouter, $uri); // record URL source information Sh404sefHelperUrl::storeUrlSource(); } /** * Some extensions do unusual things we might have to accomodate * * @param JRouter $jRouter * @param JURI $uri */ private function _extensionsBuildHacks(&$jRouter, &$uri) { // From J! 3.4.3 language filter is broken, not reading cookie language when // set to "Not remove language code on default language". // This is best workaround to ensure users can actually switch language // to default language at the expense of duplicating the whole site // Adding a canonical to alleviate that. $targetJoomlaVersions = Sh404sefFactory::getPConfig()->jVersionForceHomeLangCode; if (version_compare(JVERSION, $targetJoomlaVersions['min'], 'ge') && version_compare(JVERSION, $targetJoomlaVersions['max'], '<') && Sh404sefFactory::getPageInfo()->isMultilingual ) { $path = $uri->getPath(); $query = $uri->getQuery(); if (empty($path) && empty($query)) { $sef = Sh404sefHelperLanguage::getDefaultLanguageSef(); $path = 'index.php' . (empty($sef) ? '' : '/' . $sef . '/'); $uri->setPath($path); Sh404sefHelperUrl::$buildingSef = $path; } } } /** * Public wrapper around encode route segments * from J! router method * * @param array $segments An array of route segments */ public function encodeSegments($segments) { sh404sefHelperUrl::encodeSegments($segments); } protected function _buildSefRouteInternal(&$uri) { // J! 4.3+ hack for fixing language filter plugin modifying language query variable on the fly $this->_fixUriLanguageVar($uri); // kill Joomla suffix, so that it doesn't add or remove it in the parsing/building process JFactory::$config->set('sef_suffix', 0); $pageInfo = Sh404sefFactory::getPageInfo(); $sefConfig = Sh404sefFactory::getConfig(); $app = JFactory::getApplication(); $menu = $app->getMenu('site'); // Store Itemid, as Joomla may have updated it - change in J! 3.7.x $originalItemid = (int)$uri->getvar('Itemid', 0); // restore uri as it will have been "damaged" by languagefilter plugin if ($app->getLanguageFilter()) { $originalUrl = Sh404sefHelperUrl::getOriginalUrlFromUri($uri); $uri = new JUri($originalUrl); $this->_fixUriLanguageVar($uri); $option = $uri->getVar('option', ''); if (empty($option)) { $Itemid = $uri->getVar('Itemid', $originalItemid); if (!empty($Itemid)) { $menuItem = $menu->getItem($Itemid); if (!empty($menuItem)) { $tmpVars = array(); parse_str(wbLTrim($menuItem->link, 'index.php?'), $tmpVars); if (!empty($tmpVars['option'])) { $uri->setVar('option', $tmpVars['option']); } } } } } // Be sure to use the Joomla generated Itemid if (!empty($originalItemid)) { $uri->setVar('Itemid', $originalItemid); } // keep a copy of almost-original uri $originalUri = clone($uri); // put non-sef in a standard form, includes restoring full url // from menu item, when only an Itemid is specified shNormalizeNonSefUri($uri, $menu); // do our job! $query = $uri->getQuery(false); // shortcut for components that must be left non-sef or use Joomla! router $option = Sh404sefHelperUrl::getUrlVar($query, 'option', ''); $option = wbLTrim($option, 'com_'); if (!empty($option)) { $alwaysSkip = Sh404sefFactory::getPConfig()->alwaysNonSefComponents; $toSkip = array_merge($alwaysSkip, $sefConfig->skip); if (in_array($option, $toSkip)) { $sefUrl = $uri->toString( array( 'path', 'query', 'fragment' ) ); $uri->setPath($sefUrl); $uri->setQuery(array()); return; } // use J! router if (in_array($option, $sefConfig->useJoomlaRouter)) { // restore original suffix setting, so that Joomla adds suffix // where it's due JFactory::$config->set('sef_suffix', $pageInfo->joomlaSuffixSetting); $uri = $this->_originalBuildUri; return; } } // no shortcut, normal route Sh404sefHelperUrl::$buildingNonSef = 'index.php?' . $query; $route = shSefRelToAbs(Sh404sefHelperUrl::$buildingNonSef, null, $uri, $originalUri); $route = ltrim(wbLTrim($route, $pageInfo->getDefaultFrontLiveSite()), '/'); $route = $route == '/' ? '' : $route; // check against forced home page, ie using a splash page $forcedHomePage = empty($sefConfig->shForcedHomePage) ? '' : wbLTrim($sefConfig->shForcedHomePage, $pageInfo->getDefaultFrontLiveSite()); if ($route == 'index.php' && !empty($forcedHomePage)) { $route = $forcedHomePage . '?sh404sef_splash=1'; } $this->protectAgainstDocumentTypeError( $uri, $originalUri ); // find path $nonSefVars = $uri->getQuery($asArray = true); if ( !empty($nonSefVars) && wbContains($route, '?') ) { $parts = explode('?', $route); // there are some query vars, just use the path $path = $parts[0]; } else { $path = $route; } $uri->setPath($path); Sh404sefHelperUrl::$buildingSef = $path; } /** * Optional protection against rogue plugins (system usually) that * make a call to Factory::getDocument() BEFORE the onAfterRoute * event, thus forcing the document type to be HTML, as the incoming * URL has not been decoded yet and the actual document type is unknown. * * @param \Joomla\CMS\Uri\Uri $uri * @param \Joomla\CMS\Uri\Uri $originalUri */ private function protectAgainstDocumentTypeError(&$uri, $originalUri) { $protectAgainstBadPlugins = false; // legacy setting if ( defined('sh404SEF_PROTECT_AGAINST_DOCUMENT_TYPE_ERROR') && sh404SEF_PROTECT_AGAINST_DOCUMENT_TYPE_ERROR != 0 ) { $protectAgainstBadPlugins = true; } /** * Whether to enable protection against rogue plugins that force * document type to be HTML by calling Factory::getDocument() too early. * * @api * @package sh404SEF\filter\router * @var sh404sef_should_protect_against_document_type_error * * @param bool $protectAgainstBadPlugins If true, leave document format as query var * @param Uri $originalUri Uri object we are building a SEF URL for. * * @return array * @since 4.20.0 * */ $protectAgainstBadPlugins = Factory::get()->getThe('hook')->filter( 'sh404sef_should_protect_against_document_type_error', $protectAgainstBadPlugins, $originalUri ); if (!$protectAgainstBadPlugins) { return; } $format = $originalUri->getVar('format', ''); if (!empty($format) && $format != 'html') { $uri->setVar( 'format', $format ); } } /** * Process "lang" var in a JURI object to make sure it's a 2 letters lang code * instead of the possibly 4 letters code that can be injected by the language filter plugin * * @param $uri * @param $languageCode */ private function _fixUriLanguageVar(&$uri) { $languageCode = $uri->getVar('lang', ''); if (!empty($languageCode)) { $isValid = Sh404sefHelperLanguage::validateSefLanguageCode($languageCode); if (!$isValid) { // this may not be a valid sef language code, but instead a full language code // (happens when user selected, for instance /tc/ as sef lang code, for language zh-TW, // but still use zh-TW in non-sef URL $fixedLang = Sh404sefHelperLanguage::getUrlCodeFromTag($languageCode, $default = false); if ($fixedLang != $languageCode) { // we found a match, let's use that $uri->setVar('lang', $fixedLang); $isValid = true; } } // if not valid, but looks like a full language code as can be added by language filter plugin // preprocess rules post J! 3.4, we can turn that back into a valid sef-code // just remove the extra characters // trying to invalidate completely is a problem for many sites // where default language does not exist as a "Content" language if (!$isValid && strpos($languageCode, '-') !== false) { $fixedLang = explode('-', $languageCode); $fixedLang = empty($fixedLang) ? '' : $fixedLang[0]; if (!empty($fixedLang)) { $uri->setVar('lang', $fixedLang); } } elseif (!$isValid) { // destroy invalid language $uri->delVar('lang'); } } } /** * Actual method performing parsing of a request as described * by an JURI object * * Also performs additional duties, like checking on aliases, * searching alternates syntax (add/remove trailing slash), * auto-redirect from non-sef to sef, etc * Possibly trigger a 404 if url can't be parsed to a valid * non sef url * * @see JRouter::_parseSefRoute() */ protected function _parseSefRouteInternal(&$uri) { $app = JFactory::getApplication(); // will hold query vars parsed $vars = array(); // general config $sefConfig = Sh404sefFactory::getConfig(); $pageInfo = Sh404sefFactory::getPageInfo(); // get request path and try to decode it $path = $uri->getPath(); $method = JFactory::getApplication()->input->getMethod(); // home page or // /xx or /xx/ where xx is the default language code, on a multilingual site. $isHome = empty($path) || ( $app->getLanguageFilter() && JString::rtrim($path, '/') == Sh404sefHelperLanguage::getDefaultLanguageSef() ); if ($isHome) { $path = ''; // check more redirects: from non sef to sef $this->_checkNonSefToSefRedirects($uri); // now if no query vars, this is really home page $uriVars = $uri->getQuery(true); $uriVars = Sh404sefHelperUrl::stripTrackingVarsFromArray($uriVars); if ($method !== 'POST' && empty($uriVars)) { // Create the query array. $homeLink = $pageInfo->homeLink; $homeLink = wbLTrim($homeLink, 'index.php?'); $homeLink = str_replace('&amp;', '&', $homeLink); parse_str($homeLink, $vars); // Get its Itemid $vars['Itemid'] = $pageInfo->homeItemid; } else { // if some query vars, just pass them thru $vars = $uriVars; } } // non home page if (!empty($path)) { // lookup db for this sef $lookUp = $this->_lookupSef($uri, $vars, $path); // store the fetched data, if any $vars = $lookUp->vars; // and take additional actions based on the result of the lookup switch ($lookUp->urlType) { // existing matching url case sh404SEF_URLTYPE_AUTO: case sh404SEF_URLTYPE_CUSTOM: // record a hit on that URL if (!empty($lookUp->urlRecord)) { $hits = $lookUp->urlRecord->cpt + 1; ShlDbHelper::update( '#__sh404sef_urls', array( 'cpt' => $hits, 'last_hit' => ShlSystem_Date::getUTCNow() ), array('id' => $lookUp->urlRecord->id, 'rank' => 0) ); } break; // 404 or some kind of redirect default: // maybe there are some manually created redirects // in Joomla Redirect component? $this->_checkJoomlaRedirects(); // try find similar urls to redirect to: with or without trailing slash $this->_checkTrailingSlash($uri); // there might be an alias we're supposed to redirect current request to $this->_checkAliases($uri); // check if this is a short url $this->_checkShurls($uri); // try using J! router // if at least one extension uses Joomla! router, we must first try to use that // also handles optional automatic redirect from Joomla SEF to sh404SEF URLs. $joomlaRouterVars = $this->parseWithJoomlaRouterAndMaybeRedirect( $uri, $path ); if (self::$parsedWithJoomlaRouter) { $vars = $joomlaRouterVars; break; } // try to auto redirect to similar URL if ($sefConfig->autoRedirect404) { $this->_autoRedirect404($uri); } // if no alternative found, issue a 404 $vars = $this->_do404(); break; } } // Set the menu item as active, if we found any // this would normally be done by Joomla own _parseSefRoute() method // except it's not gonna be run if (!self::$requestParsed && ($pageInfo->isMultilingual === false || $pageInfo->isMultilingual == 'joomla')) { if (!empty($vars['Itemid'])) { $app->getMenu('site')->setActive($vars['Itemid']); } } // do security checks after decoding url $secLayerFile = JPATH_ROOT . '/components/com_sh404sef/shSec.php'; if ($sefConfig->shSecEnableSecurity) { include_once($secLayerFile); // do security checks shDoSecurityChecks($this->getFullUrl($uri), false); // check this newly created URL } return $vars; } /** * Use the Joomla router to parse an incoming request and optionally redirect to its sh404SEF counterpart. * * @param \Joomla\Uri\Uri $uri * @param string $path * * @return array */ private function parseWithJoomlaRouterAndMaybeRedirect($uri, $path) { $vars = array(); $config = Sh404sefFactory::getConfig(); if ( empty($config->useJoomlaRouter) && empty($config->shRedirectJoomlaSefToSef) ) { return $vars; } // on ML sites, Joomla router doesn't expect the language code in the path, // we have to remove it if ( !empty($this->_guessedLanguageCode) && wbStartsWith( $path, $this->_guessedLanguageCode . '/' ) ) { $path = wbLTrim( $path, $this->_guessedLanguageCode . '/' ); $uri->setPath( $path ); } // Joomla always strip a trailing slash in URLs. Let's do that too, to reduce chances of // hitting routing bugs in some extensions (DOCMan comes to mind) $uri->setPath( JString::rtrim( $uri->getPath(), '/' ) ); // use parent parser $this->bypassSh404sefRouting = true; $parsedVars = $this->parseSefRoute($uri); $this->bypassSh404sefRouting = false; // collect vars that may have been stored by J! such as Itemid $routerVars = Sh404sefFactory::getPageInfo()->router->getVars(); // Joomla component router may not find an Itemid but it still sets the // Itemid var in the $vars array. That causes a mismatch when trying to rebuild // the SEF based on those parsed vars. if (array_key_exists('Itemid', $routerVars) && empty($routerVars['Itemid'])) { unset($routerVars['Itemid']); } // merge both $parsedVars = array_merge( $routerVars, $parsedVars ); if ( empty($parsedVars['Itemid']) && empty($parsedVars['option']) ) { // cannot do without at least either a component or a menu item return $vars; } $option = wbArrayGet($parsedVars, 'option'); // Joomla router will return incoherent data when the URL does not exist (ie: site.com/xxxx/yyyy when parsed // will return some variables, Itemid and option) as long as xxx is the alias of a menu item or a category // or something. // Try to catch some of that non-sense, version 2 : // we ask the J! router to re-build the SEF URL based on what it parsed. The rebuild URL should match the // just parsed SEF or else the parsing is garbage $joomlaRouterStrictMode = in_array( $option, array( 'com_banners', 'com_content', 'com_newsfeeds', //'com_tags', // nope either //'com_users', // nope ) ); /** * Whether sh404SEF should try to match parsed vars when using Joomla router * to the rebuilt URL. * * @api * @package sh404SEF\filter\router * @var sh404sef_match_rebuilt_sef_when_using_joomla_router * * @param bool $isOnExclusionList If false, do not check rebuilt URL * @param string $option * @param \Joomla\CMS\Uri\Uri $uri The requested uri * @param array $parsedVars the non-sef vars parsed by Joomla router * * @return array * @since 4.20.3 * */ $joomlaRouterStrictMode = ShlHook::filter( 'sh404sef_joomla_router_use_strict_mode', $joomlaRouterStrictMode, $option, $uri, $parsedVars ); if ($joomlaRouterStrictMode) { $this->bypassSh404sefRouting = true; $this->setMode(JROUTER_MODE_SEF); $rebuiltUri = $this->build( Sh404sefHelperUrl::buildUrl( $parsedVars, $option ) ); $this->bypassSh404sefRouting = false; $this->setMode(JROUTER_MODE_RAW); $rebuiltSef = $rebuiltUri->getPath(); $rebuiltSef = wbLTrim( $rebuiltSef, $rebuiltUri->base(true) ); $rebuiltSef = wbLTrim( $rebuiltSef, '/' ); if (!empty($config->shRewriteMode)) { $rebuiltSef = wbLTrim( $rebuiltSef, 'index.php/' ); } if ($rebuiltSef != $path) { return array(); } } // is this component using Joomla router? if (in_array( wbLTrim($option, 'com_'), $config->useJoomlaRouter )) { // if URL was properly decoded with Joomla router // raise a flag and return the parsed query vars. self::$parsedWithJoomlaRouter = true; $vars = $parsedVars; } // check if redirect Joomla SEF to our SEF, and perform a redirect if suitable $this->_checkJoomlaSefToSefRedirects($uri, $parsedVars); return $vars; } /** * Redirect a request that should be a 404 Not found * to a similar one, taken from the URL database * * @param $uri */ protected function _autoRedirect404($uri) { if (function_exists('shFindSimilarUrls')) { $plugin = JPluginHelper::getPlugin('sh404sefcore', 'sh404sefsimilarurls'); // get params from plugin $pluginParams = new JRegistry; $pluginParams->loadString($plugin->params); // search for similar URLs $path = $uri->getPath(); /** * Whether 404 auto-redirect should be attempted for the current request. * * @api * @package sh404SEF\filter\router * @var sh404sef_should_try_404_auto_redirect * * @param bool $shouldAutoredirect If false, do not attempt to find similar URLs. * @param string $path The requested path. * @param JUri $uri The incoming URI. * * @return array * @since 4.17.1 * */ $shouldAutoredirect = ShlHook::filter( 'sh404sef_should_try_404_auto_redirect', true, $path, $uri ); if (!$shouldAutoredirect) { return; } $urlList = shFindSimilarUrls($path, $pluginParams); if (!empty($urlList)) { $target = array_shift($urlList); if (!empty($target->oldurl) && $path != $target->oldurl && $this->_canRedirectFrom($uri, $method = '', $path)) { // still store the 404 $this->_store404Page(); $config = Sh404sefFactory::getConfig(); // load language strings for info message $message = ''; if ($config->autoRedirect404WithMessage) { JFactory::getLanguage()->load('plg_sh404sefcore_sh404sefsimilarurls', JPATH_ADMINISTRATOR); $message = JText::sprintf('COM_SH404SEF_404_AUTO_REDIRECT', htmlspecialchars('/' . $path, ENT_COMPAT, 'UTF-8')); if (!empty($urlList)) { $alternativesHtml = JText::_('COM_SH404SEF_404_AUTO_REDIRECT_ALTERNATIVES'); if (strpos($alternativesHtml, '{sh404sefSimilarUrls}') !== false) { $message = $message . ' ' . str_replace('{sh404sefSimilarUrls}', shFormatSimilarUrls($urlList), $alternativesHtml); } } } // redirect to similar URL $targetUrl = JUri::base() . ltrim($config->shRewriteStrings[$config->shRewriteMode], '/') . JString::ltrim($target->oldurl, '/'); ShlSystem_Log::debug('sh404sef', 'Redirecting 404 to similar URL : ' . $targetUrl . ' from ' . $path); $target = array( 'url' => $targetUrl, 'message' => $message ); /** * Filter the target URL and message before performing a * 404 autoredirect. * * @api * @package sh404SEF\filter\router * @var sh404sef_404_auto_redirect_target * * @param array $target An array with 'url' and 'message' items. * @param string $path The requested path. * @param JUri $uri The incoming URI. * * @return array * @since 4.17.1 * */ $filteredTarget = ShlHook::filter( 'sh404sef_404_auto_redirect_target', $target, $path, $uri ); $message = wbArrayGet( $filteredTarget, 'message', '' ); if (!empty($message)) { JFactory::getSession()->set( 'wb_sh404sef_404_redirect_message', array( 'content' => $message, 'title' => JText::_('COM_SH404SEF_404_AUTO_REDIRECT_TITLE'), 'rawUrlList' => $urlList, 'color' => $config->error404MsgColor, 'background_color' => $config->error404MsgBackgroundColor, // #7FBA00 'opcacity' => $config->error404MsgOpacity ) ); } $targetUrl = wbArrayGet( $filteredTarget, 'url', '' ); if (!empty($targetUrl)) { shRedirect($targetUrl); } } } } } /** * * Redirects a non-sef request to its SEF equivalent * if any can be found or built * * @param JUri $uri */ protected function _checkNonSefToSefRedirects($uri) { // don't redirect if this is simply J! trying to parse a URL // or if this is an ajax call or similar if (!$this->_canRedirectFromNonSef($uri)) { return; } // check this is really a non-sef request $path = $uri->getPath(); if (!empty($path)) { // we only try to redirect fully non sef requests, too hard otherwise return; } // prevent homepage loops $query = $uri->getQuery(); if (empty($query)) { // empty path and empty query, this is just root url return; } // break linkage $newUri = clone $uri; // collect languages information $languages = JLanguageHelper::getLanguages('lang_code'); // make sure we have a language code $langCode = $newUri->getVar('lang'); if (empty($langCode)) { $currentLang = JFactory::getLanguage()->getTag(); $newUri ->setVar( 'lang', empty($languages[$currentLang]) || empty($languages[$currentLang]->sef) ? 'en' : $languages[$currentLang]->sef ); } //redirect $this->_redirectNonSef($newUri); } protected function _redirectNonSef($uri) { if (!$this->_canRedirectFromNonSef($uri)) { return; } // search cache and db for a sef url $nonSefUrl = Sh404sefHelperUrl::sortUrl('index.php?' . $uri->getQuery()); $targetSefUrl = ''; $urlType = ShlMvcModel_Base::getInstance('Sefurls', 'Sh404sefModel') ->getSefURLFromCacheOrDB($nonSefUrl, $targetSefUrl); $pageInfo = Sh404sefFactory::getPageInfo(); // found a match : redirect if ($urlType == sh404SEF_URLTYPE_AUTO || $urlType == sh404SEF_URLTYPE_CUSTOM) { $tmpUri = new JURI($uri->base() . $targetSefUrl); $targetSefUrl = $tmpUri->toString(); if ($targetSefUrl != $pageInfo->currentSefUrl) { ShlSystem_Log::debug('sh404sef', 'redirecting non-sef to existing SEF : ' . $targetSefUrl); shRedirect($targetSefUrl); } } // haven't found a SEF in the cache or DB, maybe we can just create it on the fly ? if (!empty($nonSefUrl) && (empty($pageInfo->isMultilingual) || $pageInfo->isMultilingual == 'joomla')) { // $currentLanguageTag is still deafult lang, as language has not been discovered yet $GLOBALS['mosConfig_defaultLang'] = Sh404sefFactory::getPageInfo()->currentLanguageTag; // create new sef url $targetSefUrl = JRoute::_($nonSefUrl, $xhtml = false); $s = wbLTrim($targetSefUrl, JURI::base(true)); if (!empty($s) && $s != '/' && $targetSefUrl != $pageInfo->currentSefUrl) { $targetSefUrl = $uri->toString( array( 'scheme', 'host', 'port' ) ) . $targetSefUrl; $sourceUrl = $this->getFullUrl($uri); if (strpos($targetSefUrl, 'option=com') === false && $targetSefUrl != $sourceUrl) { ShlSystem_Log::debug( 'sh404sef', 'Redirecting non-sef to newly created SEF : ' . $targetSefUrl . ' from ' . $nonSefUrl ); shRedirect($targetSefUrl); } } } } /** * * Lookup a SEF url in the cache and database, searching * for a non-sef associated url * Returns a record holding query vars and a status code * If no non-sef is found, incoming query vars are returned untouched * If a non-sef is found, a query var array is built, merged with incoming vars * and returned instead of the incoming one * * @param $uri * @param array $vars key/value pairs of query vars, usually empty * @param string $sefUrl the sef url to search for * * @return stdClass * @throws Exception */ protected function _lookupSef($uri, $vars, $sefUrl) { // object to hold result $result = new stdClass(); // identify Sh404sefClassBaseextplugin::TYPE_SIMPLE URLs, ie simple encoding, DB bypass $isSimpleUrl = $this->_isSimpleEncodingSef($sefUrl); if ($isSimpleUrl) { // handle manual decoding $vars = $this->_parseSimpleUrls($sefUrl); $urlType = sh404SEF_URLTYPE_AUTO; $urlRecord = null; } if (!$isSimpleUrl) { // get a model and check if we've seen this request before $nonSefUrl = ''; $urlRecord = ShlMvcModel_Base::getInstance('Sefurls', 'Sh404sefModel') ->getNonSefUrlRecordFromDatabase($sefUrl, $nonSefUrl); $urlType = $urlRecord['status']; switch ($urlType) { // existing matching url case sh404SEF_URLTYPE_AUTO: case sh404SEF_URLTYPE_CUSTOM: // our db lookup is case insensitive, which allows // doing a 301 to the correct url case, avoiding duplicate content $this->_checkRedirectToCorrectCase($uri, $sefUrl); // collect the query vars, using a JURI instance $newUri = new JURI($nonSefUrl); $vars = array_merge($vars, $newUri->getQuery(true)); break; // 404 or some kind or error default: break; } } // store result $result->vars = $vars; $result->urlType = $urlType; $result->urlRecord = $urlRecord['url']; return $result; } /** * Recognize if a given (usually incoming) url has been * encoded as a simple-type encoding * Such urls start with component/com_ or lang_code/component/com_ * * @param string $url * * @return bool * @throws Exception */ protected function _isSimpleEncodingSef($url) { $isSimpleEncodingSef = $this->_getSimpleSefLanguageSefCode($url) !== false; return $isSimpleEncodingSef; } /** * Figure out whether an incoming url has been encoded as SEF * using the 'simple' scheme, and at the same time * identify if a language code was encoded * Format is: xx/component/com_xxxx... or component/com_xxxx... * * Return true if simple scheme, a sef lang string if simple scheme and a language * has been recognized or an empty string if default language and language filter * plugin is set to remove lang code from URL on default langague or this is * a monolingual site * * @param string $url * * @return string|boolean false if not simple-scheme SEF, empty string if default language or sef lang code * otherwise * @throws Exception */ protected function _getSimpleSefLanguageSefCode($url) { static $prefixes = null; $app = JFactory::getApplication(); if (is_null($prefixes)) { $prefixes = array(); // build up prefix list $languages = JLanguageHelper::getLanguages('sef'); $default = Sh404sefHelperLanguage::getDefaultLanguageSef(); foreach ($languages as $sefCode => $language) { if ($sefCode == $default) { $prefixes[$sefCode] = $app->getLanguageFilter() && Sh404sefHelperLanguage::getInsertLangCodeInDefaultLanguage() ? $sefCode . '/component/com_' : 'component/com_'; } else { $prefixes[$sefCode] = $sefCode . '/component/com_'; } } } foreach ($prefixes as $sefCode => $prefix) { if ($prefix == JString::substr($url, 0, Jstring::strlen($prefix))) { if ($app->getLanguageFilter() && !Sh404sefHelperLanguage::getInsertLangCodeInDefaultLanguage() && $sefCode == Sh404sefHelperLanguage::getDefaultLanguageSef() ) { return ''; } else { // return no language code if not a multilingual site return $app->getLanguageFilter() ? $sefCode : ''; } } } return false; } /** * * Parse sef urls built using the TYPE_SIMPLE method * ie: simply combining each argument, comma separated * * @param string $sefUrl * * @return array * @throws Exception */ protected function _parseSimpleUrls($sefUrl) { $vars = array(); // create an array $urlArray = explode('/', $sefUrl); $urlArray = array_filter($urlArray); $urlArray = array_values($urlArray); $sefLangCode = $this->_getSimpleSefLanguageSefCode($sefUrl); if (!empty($sefLangCode)) { // remove language information $vars['lang'] = $sefLangCode; array_shift($urlArray); } // remove 'component' array_shift($urlArray); // start by extracting option, which is always first $option = array_shift($urlArray); // sanitize $vars['option'] = JFilterInput::getInstance()->clean($option, 'cmd'); // process remaining vars, if any if (!empty($urlArray)) { foreach ($urlArray as $segment) { $tmp = explode(',', $segment, 2); if (count($tmp) == 2) { $vars[$tmp[0]] = $tmp[1]; } } } return $vars; } protected function _checkRedirectToCorrectCase($uri, $path) { if (!$this->_canRedirectFrom($uri)) { return; } // get config object $sefConfig = Sh404sefFactory::getConfig(); if ($sefConfig->redirectToCorrectCaseUrl) { // if initial query exactly matches oldurl found in db, then case is correct // else we redirect to the url found in db, but we also need to append query string to it ! $originalPath = $uri->getPath(); // now the only difference between the two can be the case if ($originalPath != $path) { // can only be different from case, change case in uri, and add rewrite mode prefix, if any $uri->setPath($path); $targetUrl = $uri->base() . ltrim($sefConfig->shRewriteStrings[$sefConfig->shRewriteMode], '/') . $uri->toString( array( 'path', 'query', 'fragment' ) ); // perform redirect ShlSystem_Log::debug( 'sh404sef', 'Redirecting to correct url case : from ' . $this->getFullUrl($uri) . ' to ' . $targetUrl ); shRedirect($targetUrl); } } } /** * Checks Joomla's redirections table for * existing redirects */ protected function _checkJoomlaRedirects() { if (self::$requestParsed) { return; } $fullUrl = Sh404sefFactory::getPageInfo()->currentSefUrl; $redirectUrl = ShlDbHelper::selectObject( '#__redirect_links', '*', array( 'old_url' => $fullUrl, 'published' => 1 ) ); if (!empty($redirectUrl->new_url)) { ShlSystem_Log::debug( 'sh404sef', 'Redirecting 404 to SEF from Joomla! #__redirect_links table : ' . $redirectUrl->new_url . ' from ' . $fullUrl ); shRedirect($redirectUrl->new_url); } } /** * Called in case of failure to parse an incoming URL but the Joomla * router was able to parse it. Builds and redirects to the corresponding * sh404SEF URL. * Start by asking the Joomla router to build the Joomla SEF URL based on the vars * it just parsed. Needed as Joomla router will basically always parsed something out * of any URL so we check that the URL built using the parsed vars is the same * as the parsed URL. * * @param \Joomla\CMS\Uri\Uri $uri Incoming request URI object * @param array $vars */ protected function _checkJoomlaSefToSefRedirects($uri, $vars) { $config = Sh404sefFactory::getConfig(); if ( self::$requestParsed || !$config->shRedirectJoomlaSefToSef ) { return; } // try to build a target URL, we have the vars $option = wbArrayGet($vars, 'option'); if (empty($option)) { return; } // only redirect if not already "use Joomla router" type of component if ( !empty($config->useJoomlaRouter) && in_array( wbLTrim($option, 'com_'), Sh404sefFactory::getConfig()->useJoomlaRouter ) ) { return; } $dest = Sh404sefHelperUrl::buildUrl($vars, $option); $target = $this->checkRedirect($dest, Sh404sefFactory::getPageInfo()->currentSefUrl); if (!empty($target)) { // do redirect ShlSystem_Log::debug('sh404sef', 'Redirecting to ' . $target . ' from Joomla SEF ' . $uri->toString()); shRedirect($target); } } protected function _checkTrailingSlash($uri) { if (!$this->_canRedirectFrom($uri)) { return; } // get config object $sefConfig = Sh404sefFactory::getConfig(); // get the url path and try add or remove a trailing slash $path = $uri->getPath(); // if path ends with current suffix, stop here if (wbEndsWith($path, $sefConfig->suffix)) { return; } // same with optional index file if (wbEndsWith($path, $sefConfig->addFile)) { return; } // now add or remove trailing slash. Must check existence of trailing slash // on the $uri->_uri, as J! always remove it from $uri->_path $trailingSlash = $this->_hasTrailingSlash($uri); if ($trailingSlash) { $path = JString::rtrim($path, '/'); } else { $path .= '/'; } // and check db again $vars = array(); // dummy, we don't care about the actual vars retrieved $lookUp = $this->_lookupSef($uri, $vars, $path); // if url exists with slash added or removed, 301 to that valid url if ($lookUp->urlType == sh404SEF_URLTYPE_AUTO || $lookUp->urlType == sh404SEF_URLTYPE_CUSTOM) { $query = $uri->getQuery(); $targetSefUrl = $uri->base() . $path . (empty($query) ? '' : '?' . $query); ShlSystem_Log::debug('sh404sef', 'Redirecting to same with trailing slash added: ' . $targetSefUrl); shRedirect($targetSefUrl); } } protected function _checkShurls($uri) { if (self::$requestParsed) { return; } // sanitize $path = $uri->getPath(); if (empty($path)) { // no path in request, no possible short url return; } // our configuration object $config = Sh404sefFactory::getConfig(); // check short url based on request path if ($config->enablePageId) { try { $shurlRecord = ShlDbHelper::selectObject( '#__sh404sef_pageids', '*', array( 'pageid' => $path ) ); // checks on shurl target: if empty, prevent loop, plus stitch back query string, if any if (!empty($shurlRecord)) { $queryString = $uri->getQuery(); if (!empty($queryString)) { $shurlRecord->newurl .= JString::strpos($shurlRecord->newurl, '?') !== false ? '&' . $queryString : '?' . $queryString; } $target = $this->checkRedirect($shurlRecord->newurl, $this->getFullUrl($uri)); if (!empty($target)) { // update alias hits counter ShlDbHelper::update('#__sh404sef_pageids', array('hits' => $shurlRecord->hits + 1), array('id' => $shurlRecord->id)); // record a detailed log of the 404, if set to if ($config->logShurlsHits) { $recorder = Sh404sefModelReqrecorder::getInstance(Sh404sefModelReqrecorder::REQUEST_SHURL); $recorder->record($path, $target); } // do redirect ShlSystem_Log::debug('sh404sef', 'Redirecting to ' . $target . ' from shurl ' . $path); shRedirect($target); } } } catch (Sh404sefException $e) { // if error, just log ShlSystem_Log::error( 'sh404sef', '%s::%d: %s', __METHOD__, __LINE__, ' Database error reading shurl: ' . $e->getMessage() ); } } } protected function checkRedirect($dest, $incomingUrl) { $config = Sh404sefFactory::getConfig(); if (!empty($dest) && $dest != $incomingUrl) { // redirect to alias if ($dest == sh404SEF_HOMEPAGE_CODE) { if (!empty($config->shForcedHomePage)) { $dest = shFinalizeURL($config->shForcedHomePage); } else { $dest = shFinalizeURL(Sh404sefFactory::getPageInfo()->getDefaultFrontLiveSite()); } } else { $shUri = new JURI($dest); $shOriginalUri = clone($shUri); $dest = shSefRelToAbs($dest, '', $shUri, $shOriginalUri) . $shUri->toString(array('query')); } if ($this->_canRedirectFrom(null, '', $incomingUrl) && $dest != $incomingUrl) { return $dest; } } return false; } /** * Make Joomla display our 404 page * * @param $uri * * @return mixed */ protected function _do404($error = null) { if (self::$requestParsed) { return; } // Create an error object if none supplied if (empty($error)) { $error = new \Exception(JText::_('JERROR_PAGE_NOT_FOUND'), 404); } // render document self::sh404sefErrorPage($error); // and stop there exit(); } /** * Proxy method for handling exception and passing them * on to the regular method sh404sefErrorPage * Avoids errors on PHP7, due to sh404sefErrorPage requires * a reference (B/C with previous Joomla handlers) * * @param $exception */ public function sh404sefExceptionHandler($exception) { $this->sh404sefErrorPage($exception); if (!empty($this->joomlaExceptionHandler)) { call_user_func_array( $this->joomlaExceptionHandler, array($exception) ); } } /** * Overrride for Joomla! default error page handler * * @param \Exception $error */ public function sh404sefErrorPage($error) { // only handle 404 errors, defer to previous handler otherwise $code = is_callable(array($error, 'getCode')) ? $error->getCode() : 500; if (404 != $code && !empty($this->joomlaErrorHandler) && ($this->joomlaErrorHandler['mode'] != 'callback' || !is_callable($this->joomlaErrorHandler['options']))) { JError::customErrorPage($error); return; } if (404 != $code) { // do not handle other, non-404 exceptions // Proxy to the previous exception handler if available, otherwise just render the error page if ($this->joomlaExceptionHandler) { call_user_func_array($this->joomlaExceptionHandler, array($error)); } else { JErrorPage::render($error); } return; } // Give a chance to others to process /** * Announce a 404 error. * * @api * @package sh404SEF\filter\routing * @var sh404sef_before_404_handling * @since 4.22.2 * * @param \Exception $error * */ \ShlHook::run( 'sh404sef_before_404_handling', $error ); $setting = Sh404sefFactory::getConfig()->notFoundErrorHandling; if (Sh404sefHelperGeneral::ERROR_404_HANDLE == $setting) { // if we should only handle URL parsing errors // adjust switch value according to current situation if (self::$requestParsed) { // Joomla error, use Joomla handling $setting = Sh404sefHelperGeneral::ERROR_404_USE_JOOMLA; } else { // URL parsing error, use sh404SEF handling $setting = Sh404sefHelperGeneral::ERROR_404_OVERRIDE_JOOMLA; } } // request is considered parsed now self::$requestParsed = true; switch ($setting) { case Sh404sefHelperGeneral::ERROR_404_USE_JOOMLA: $this->_store404Page(); $configItemid = Sh404sefFactory::getConfig()->shPageNotFoundItemid; if (!empty($configItemid)) { Sh404sefHelperError::setItemid($configItemid); } // we forward the error to the original Joomla handler if (!empty($this->joomlaErrorHandler) && $this->joomlaErrorHandler['mode'] == 'callback' && is_callable($this->joomlaErrorHandler['options'])) { call_user_func_array($this->joomlaErrorHandler['options'], array(&$error)); } else { // This is required to prevent a very unhelpful white-screen-of-death jexit('<strong>An error just happened: </strong>' . $error->getMessage() . '<br /><small>and there was another one displaying this message (invalid error handler).</small>'); } break; case Sh404sefHelperGeneral::ERROR_404_OVERRIDE_JOOMLA: // we use our own handling $this->_store404Page(); Sh404sefHelperError::render404ErrorDocument($error); exit(); break; default: break; } } protected function _store404Page($uri = null) { $uri = empty($uri) ? JUri::getInstance() : $uri; // optionnally log the 404 details $reqPath = $uri->getPath(); $basePath = JUri::base(true); if ( // site is in root empty($basePath) || // site in subfolder (!empty($basePath) && strpos($reqPath, $basePath) === 0) ) { $reqPath = substr($reqPath, JString::strlen($basePath) + 1); } $storer = Sh404sefModelNotfoundstore::getInstance(); $storer->store($reqPath, Sh404sefFactory::getConfig()); } /** * Redirect user, on first visit, to its own language * as detected in browser * Happens only on home page, as otherwise we wouldn't know * on which page to redirect him/her * * @param JURI $uri */ protected function _languageRedirect($uri, $currentLanguageCode) { if (!$this->_canRedirectFrom($uri)) { return; } $pageInfo = Sh404sefFactory::getPageInfo(); // decide whether this is homepage request $path = JString::trim( wbLTrim($pageInfo->currentSefUrl, $pageInfo->getDefaultFrontLiveSite() . '/'), '/' ); $possibleUrls = array(); foreach ($pageInfo->homeLinks as $language => $link) { $possibleUrls[] = Sh404sefHelperLanguage::getUrlCodeFromTag($language); } if (!empty($path) && !in_array($path, $possibleUrls)) { return; } $cookieTag = JFactory::getApplication()->input->getString(JApplication::getHash('language'), null, 'cookie'); // no cookie - try autoredirect to user language if (empty($cookieTag)) { if (Sh404sefHelperLanguage::getLanguageFilterPluginParam('detect_browser', 1)) { $userLanguage = JLanguageHelper::detectLanguage(); if (!empty($userLanguage) && $userLanguage != $currentLanguageCode) { ShlSystem_Log::debug( 'sh404sef', 'User lang: ' . $userLanguage . ' langcode: ' . $currentLanguageCode ); // user has a specific language, let's redirect to the home page in that language $target = JRoute::_($pageInfo->homeLinks[$userLanguage]); shRedirect($target); } } } } } if (version_compare(JVERSION, '3.2', 'ge')) { class Sh404sefClassRouter extends Sh404sefClassRouterInternal { protected function _parseSefRoute(&$uri) { return $this->_parseSefRouteInternal($uri); } protected function _buildSefRoute(&$uri) { if ($this->bypassSh404sefRouting) { parent::_buildSefRoute($uri); } else { $this->_buildSefRouteInternal($uri); } } } } else if (version_compare(JVERSION, '3.0', 'ge')) { class Sh404sefClassRouter extends Sh404sefClassRouterInternal { protected function _parseSefRoute($uri) { return $this->_parseSefRouteInternal($uri); } protected function _buildSefRoute($uri) { $this->_buildSefRouteInternal($uri); } } } else { class Sh404sefClassRouter extends Sh404sefClassRouterInternal { protected function _parseSefRoute(&$uri) { return $this->_parseSefRouteInternal($uri); } protected function _buildSefRoute(&$uri) { $this->_buildSefRouteInternal($uri); } } }