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('&', '&', $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);
}
}
}