File Manager

Current Directory: /home/astoriaah/www/administrator/components/com_sh404sef/models
Viewing File: /home/astoriaah/www/administrator/components/com_sh404sef/models/redirector.php
<?php /** * sh404SEF - SEO extension for Joomla! * * @author Yannick Gaultier * @copyright (c) Yannick Gaultier - Weeblr llc - 2022 * @package sh404SEF * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL * @version 4.24.1.4308 * @date 2022-05-31 */ // Security check to ensure this file is being included by a parent file. if (!defined('_JEXEC')) { die(); } class Sh404sefModelRedirector { /** * Whether to redirect or insert a canonical when using an alias. */ const TARGET_TYPE_REDIRECT = 0; const TARGET_TYPE_CANONICAL = 1; const TARGET_TYPE_INTERNAL_REWRITE = 2; /** * If there are less placeholders in alias target than in alias source, in which direction * replacement should take place? */ const REPLACEMENT_DIRECTION_END_TO_START = 0; const REPLACEMENT_DIRECTION_START_TO_END = 1; /** * @var object stores sh404SEF config. */ private $config; /** * @var JRegistry Stores Joomla configuration. */ private $jconfig; /** * @var JUri Stores the current request URI. */ private $uri; /** * @var array Stores the wildcard alias rules set by user. */ private $wildcardAliases; /** * @var array Stores the rules that matched the current request. */ private $matchedRules = array(); /** * @var array Stores whether a URL was triggered, per target type. */ private $aliasFound = array(); /** * @var array The alias rule that should be executed. */ private $aliasToExecute = null; /** * @var array Stores the wildcard strings that can be used in a wildcard alias. */ static public $aliasesWildcardsChars = array('{*}', '{?}'); /** * Stores external information. * * @param $uri * @param $config * @param $jconfig */ public function __construct(&$uri, $config, $jconfig) { $this->uri = $uri; $this->config = $config; $this->jconfig = $jconfig; $this->loadMatchRules(); } /** * Loads all aliases rules that match the current request. * * @return $this */ private function loadMatchRules() { // requestPathAndQuery HAS the query string in it $requestPathAndQuery = str_replace( JURI::root() . $this->getRewritePrefix(), '', $this->getCurrentRequestUrl() ); if (empty($requestPathAndQuery)) { $requestPathAndQuery = '/'; } $parts = explode('?', $requestPathAndQuery, 2); $path = empty($parts) ? '' : array_shift($parts); $path = rawurldecode($path); $queryString = empty($parts) ? '' : array_shift($parts); try { $this->findHardcodedMatchedRules($requestPathAndQuery, $path, $queryString) ->findWildcardMatchedRules($requestPathAndQuery, $path, $queryString); } catch (\Exception $e) { // if error, just log ShlSystem_Log::error( 'sh404sef', '%s::%d: %s', __METHOD__, __LINE__, 'Error checking redirect aliases: ' . $e->getMessage() ); } return $this; } /** * Checks whether any kind of alias matches the passed request, * and perform the redirect if any. * */ public function redirectFromAlias() { // shortcut: no redirect on POST if ('POST' == JFactory::getApplication()->input->getMethod()) { return; } try { $this->redirectFromAliases('hardcoded') ->redirectFromAliases('wildcard'); } catch (\Exception $e) { // if error, just log ShlSystem_Log::error( 'sh404sef', '%s::%d: %s', __METHOD__, __LINE__, 'Error checking redirect aliases: ' . $e->getMessage() ); } } /** * Finds and store all the hardcoded aliases redirect rules that match the current URI. * Those rules target is a non-sef URL that has an sh404SEF SEF record. * * @param string $requestPathAndQuery * @param string $path * @param string $queryString * * @return $this */ private function findHardcodedMatchedRules($requestPathAndQuery, $path, $queryString) { if (!isset($this->matchedRules['hardcoded'])) { $this->matchedRules['hardcoded'] = array(); // build sql query, we may check both path and full query $sql = 'SELECT * FROM ?? WHERE ?? = 1 and (?? = ?'; $nameQuoted = array( '#__sh404sef_aliases', 'state', 'alias' ); $quoted = array( $requestPathAndQuery ); $isNonSefRequest = wbStartsWith( $requestPathAndQuery, array('index.php?', '?') ); if ($isNonSefRequest) { // a non-sef, let's try with the router version $sef = JRoute::_($requestPathAndQuery); $sef = wbLTrim($sef, JUri::base(true)); $sef = JString::ltrim($sef, '/'); $sql .= ' or ?? = ?'; $nameQuoted[] = 'alias'; $quoted[] = $sef; } // path different from full url requested, means there is a query string else if (!empty($path) && $path != $requestPathAndQuery) { $sql .= ' or ?? = ?'; $nameQuoted[] = 'alias'; $quoted[] = $path; if (JString::substr($path, -1) != '/') { // getPath will trim trailing / so we must try also with it $sql .= ' or ?? = ?'; $nameQuoted[] = 'alias'; $quoted[] = $path . '/'; } } $sql .= ')'; // finally order by user selected ordering $sql .= ' order by ?? asc'; $nameQuoted[] = 'ordering'; $aliasRecord = ShlDbHelper::quoteQuery($sql, $nameQuoted, $quoted)->loadObject(); if (!empty($aliasRecord)) { $trimmedAlias = JString::trim($aliasRecord->alias); if ($trimmedAlias == 'index.php' || $trimmedAlias == '/' || empty($trimmedAlias)) { $aliasRecord = null; } } // do the redirect, after checking a few conditions if (!empty($aliasRecord)) { // if match occured on full requested URL, or this is a non-sef or this is a canonical, not a redirect: no need to re-append the query string // to the target URL. $queryString = $aliasRecord->target_type == self::TARGET_TYPE_CANONICAL || $isNonSefRequest || ($aliasRecord->target_type == self::TARGET_TYPE_REDIRECT && $aliasRecord->alias == $requestPathAndQuery) ? '' : $queryString; $this->matchedRules['hardcoded'][] = array( 'rule' => $aliasRecord, 'request_path' => $requestPathAndQuery, 'matches' => array(), 'query_string' => $queryString ); } } return $this; } /** * Finds and store all the wildcard aliases redirect rules that match the current URI. * * @param string $requestPathAndQuery * @param string $path * @param string $queryString * * @return $this */ private function findWildcardMatchedRules($requestPathAndQuery, $path, $queryString) { if (!isset($this->matchedRules['wildcard'])) { $this->matchedRules['wildcard'] = array(); $isNonSefRequest = wbStartsWith( $requestPathAndQuery, array('index.php?', '?') ); if (is_null($this->wildcardAliases)) { $this->wildcardAliases = ShlDbHelper::selectObjectList( '#__sh404sef_aliases', '*', 'state = 1 and (type = ? or type = ?)', array( Sh404sefTableAliases::URLTYPE_ALIAS_WILDCARD, Sh404sefTableAliases::URLTYPE_ALIAS_CUSTOM ) ); $this->wildcardAliases = empty($this->wildcardAliases) ? array() : $this->wildcardAliases; } foreach ($this->wildcardAliases as $wildcardsAliasRecord) { // test the full URL $matches = ShlSystem_Route::urlRuleMatch( $wildcardsAliasRecord->alias, $requestPathAndQuery, $wildChar = '{*}', $singleChar = '{?}', $regexpChar = '~' ); if (!empty($matches[0])) { $this->matchedRules['wildcard'][] = array( 'rule' => $wildcardsAliasRecord, 'request_path' => $requestPathAndQuery, 'matches' => $matches, 'query_string' => '' ); } // test the path only $matches = ShlSystem_Route::urlRuleMatch( $wildcardsAliasRecord->alias, $path, $wildChar = '{*}', $singleChar = '{?}', $regexpChar = '~' ); if (!empty($matches[0])) { // if match occured on full requested URL, or this is a non-sef or this is a canonical, not a redirect: no need to re-append the query string // to the target URL. $queryString = $wildcardsAliasRecord->target_type == self::TARGET_TYPE_CANONICAL || $isNonSefRequest || $wildcardsAliasRecord->newurl == $requestPathAndQuery ? '' : $queryString; $this->matchedRules['wildcard'][] = array( 'rule' => $wildcardsAliasRecord, 'request_path' => $requestPathAndQuery, 'matches' => $matches, 'query_string' => $queryString ); } } } return $this; } /** * Checks whether any wildcard alias match the request. * Used after the simple, hardcoded aliases have been checked. * * @param string $requestPath * @param string $path * @param string $queryString * * @return $this */ private function redirectFromAliases($type) { if (empty($this->matchedRules[$type])) { return $this; } // Iterate over all matched rules. We execute the first // rule found for each target type. That way user can set // a rule for both an internal rewrite and a canonical for instance. foreach ($this->matchedRules[$type] as $matchedRule) { $alreadyFound = wbArrayGet( $this->aliasFound, $matchedRule['rule']->target_type, false ); if (!$alreadyFound) { $this->executeAlias( $matchedRule ); } } return $this; } /** * Performs the actual redirect to an alias. * * @param array $matchedRule Definition of the rule that matched to trigger this alias. */ protected function executeAlias($matchedRule) { $aliasRecord = wbArrayGet($matchedRule, 'rule'); if (!empty($aliasRecord)) { $destUrl = $this->buildAliasTarget( $aliasRecord, $this->getCurrentRequestUrl(), $matchedRule ); /** * Filter the target destination just before sh404SEF execute an alias rule (redirect or canonical insertion). * Returning an empty string will prevent the alias rule to be executed. * @api * @package sh404SEF\filter\routing * @var sh404sef_redirect_alias * @since 4.12.0 * * @param string $destUrl The computed target URL. * @param string $requestPath The path requested. * @param string $queryString The query string requested. * @param stdClass $aliasRecord The alias definition that triggered the redirect/canonical * * @return array */ $destUrl = ShlHook::filter( 'sh404sef_redirect_alias', $destUrl, wbArrayGet($matchedRule, 'request_path'), wbArrayGet($matchedRule, 'query_string'), $aliasRecord ); if (!empty($destUrl)) { // log alias execution (only for redirects) $this->logAliasExecution( wbArrayGet($matchedRule, 'request_path'), $aliasRecord, $destUrl ); // do redirect ShlSystem_Log::debug('sh404sef', 'Aliasing to ' . $destUrl . ' from ' . wbArrayGet($matchedRule, 'request_path') . ' with a ' . ($aliasRecord->target_type == self::TARGET_TYPE_CANONICAL ? 'canonical' : 'redirect')); switch ($aliasRecord->target_type) { case self::TARGET_TYPE_REDIRECT: ShlSystem_Http::redirectPermanent($destUrl); break; case self::TARGET_TYPE_CANONICAL: $this->aliasFound[self::TARGET_TYPE_CANONICAL] = true; $this->aliasToExecute = ShlSystem_Route::absolutify( $destUrl, $forceDomain = true ); break; case self::TARGET_TYPE_INTERNAL_REWRITE: $this->aliasFound[self::TARGET_TYPE_INTERNAL_REWRITE] = true; $newUri = JURI::getInstance( $destUrl ); $newPath = wbLTrim( $newUri->getPath(), $newUri->base($pathOnly = true) ); $newPath = wbLTrim( $newPath, '/' ); $this->uri->setPath( $newPath ); $this->uri->setQuery( $newUri->getQuery() ); break; } } } } /** * Getter for the alias to execute, if any. * * @return array */ public function getAliasToExecute() { return $this->aliasToExecute; } /** * Logs an alias execution, just before using it. Only applies to redirects. * * @param string $requestPath * @param stdClass $aliasRecord * @param string $destUrl */ private function logAliasExecution($requestPath, $aliasRecord, $destUrl) { if ($aliasRecord->target_type == self::TARGET_TYPE_REDIRECT) { // update alias hits counter ShlDbHelper::update( '#__sh404sef_aliases', array('hits' => $aliasRecord->hits + 1), array('id' => $aliasRecord->id) ); // record a detailed log of the alias, if set to if ($this->config->logAliasesHits) { $recorder = Sh404sefModelReqrecorder::getInstance( Sh404sefModelReqrecorder::REQUEST_ALIAS ); $recorder->record($requestPath, $destUrl); } } } /** * Run a few tests to decide whether a redirect should be allowed, * and build the target URL. * * @param stdClass $aliasRecord * @param string $incomingUrl * @param array $matchedRule * * @return bool|string */ private function buildAliasTarget($aliasRecord, $incomingUrl, $matchedRule) { if ('POST' == JFactory::getApplication()->input->getMethod()) { return false; } $matches = wbArrayGet($matchedRule, 'matches'); $queryString = wbArrayGet($matchedRule, 'query_string'); $redirectTarget = $this->getExpandedAliasTarget($aliasRecord, $matches, $matchedRule); if (empty($redirectTarget) || ($redirectTarget == $incomingUrl && $aliasRecord->target_type != self::TARGET_TYPE_CANONICAL)) { return false; } switch (true) { // redirect to home page case $redirectTarget == sh404SEF_HOMEPAGE_CODE: if (!empty($this->config->shForcedHomePage)) { $redirectTarget = $this->config->shForcedHomePage; } else { $redirectTarget = $this->getHomepageUrl(); } break; // redirect target is a non-sef url, route it case wbStartsWith($redirectTarget, 'index.php'): $shUri = new JURI($redirectTarget); $shOriginalUri = clone($shUri); $redirectTarget = shSefRelToAbs($redirectTarget, '', $shUri, $shOriginalUri) . $shUri->toString(array('query')); break; // directly use the redirect target default: break; } // re-append query string if it was the path that matched, not the actual full request URL if (!empty($redirectTarget) && !empty($queryString)) { $redirectTarget .= wbContains($redirectTarget, '?') ? '&' . $queryString : '?' . $queryString; } $redirectTarget = $this->prepareUrlForRedirect($redirectTarget); if ( $redirectTarget != $incomingUrl || $aliasRecord->target_type == self::TARGET_TYPE_CANONICAL ) { return $redirectTarget; } return false; } /** * Expand the alias record newurl that can have wildcard into its final form. * * sample-{*}-more-{*}-end -> sample-{*}-new-{*}-end * * @param stdClass $aliasRecord * @param array $matches * @param string $matchedRule * * @return string */ private function getExpandedAliasTarget($aliasRecord, $matches, $matchedRule) { // for most aliases, redirect target is directly the stored one $redirectTarget = $aliasRecord->newurl; /** * Filter replacement direction of wildcard charecters if there are less placeholders in alias target than in alias source. * * @api * @package sh404SEF\filter\routing * @var sh404sef_redirect_alias_replacement_direction * @since 4.12.0 * * @param string $replacementDirection The direction to use when doing wildcard replacement. * @param stdClass $aliasRecord The alias definition that triggered the redirect/canonical * * @return array */ $replacementDirection = ShlHook::filter( 'sh404sef_redirect_alias_replacement_direction', self::REPLACEMENT_DIRECTION_END_TO_START, $aliasRecord ); switch ($aliasRecord->type) { case Sh404sefHelperGeneral::COM_SH404SEF_URLTYPE_ALIAS_CUSTOM: // targets are {$1}, {$2} if ( wbStartsWith($aliasRecord->alias, '~') && preg_match('~\{\$[0-9]+\}~', $redirectTarget) ) { array_shift($matches); if (!empty($matches)) { foreach ($matches as $index => $match) { $redirectTarget = str_replace( '{$' . ($index + 1) . '}', $match, $redirectTarget ); } } return $redirectTarget; } // if there are wildcards, and the incoming request URL have some matches // we can inject them in the redirect target if ( wbContains($redirectTarget, self::$aliasesWildcardsChars) && count($matches) > 1 ) { // insert back, starting from the end if (!empty($matches)) { switch ($replacementDirection) { case self::REPLACEMENT_DIRECTION_START_TO_END: // inject back the matching elements in the same order // until none is available array_shift($matches); if (!empty($matches)) { $redirectTarget = preg_replace_callback( '~\{([?|*])\}~', function ($targetMatches) use (&$matches) { $value = empty($matches) ? '' : array_shift($matches); return $value; }, $redirectTarget ); } break; default: // inject back matching elements, in reverse order, starting from the end of the URL. $redirectTarget = JString::strrev($redirectTarget); $matches = array_reverse($matches); $redirectTarget = preg_replace_callback( '~\}([?|*])\{~', function ($targetMatches) use (&$matches) { $value = empty($matches) ? '' : array_shift($matches); $value = JString::strrev($value); return $value; }, $redirectTarget ); $redirectTarget = JString::strrev($redirectTarget); break; } } } break; default: break; } return $redirectTarget; } /** * Minor touch up to the target URL. * * @param string $url * * @return string */ private function prepareUrlForRedirect($url) { $url = str_replace('&amp;', '&', $url); $url = ShlSystem_Route::absolutify($url); $url = wbRTrim($url, '//'); return $url; } /** * Computes the full URL of the site home page. * * @return string */ private function getHomepageUrl() { static $home = null; if (is_null($home)) { $home = JString::rtrim( JUri::getInstance()->base(), '/' ); $home = wbRTrim($home, '/administrator'); } return $home; } private function getRewritePrefix() { static $prefix = null; if (is_null($prefix)) { if (!$this->jconfig->get('sef_rewrite', 0)) { $prefix .= 'index.php/'; } } return $prefix; } private function getCurrentRequestUrl() { static $currentRequestUrl = null; if (is_null($currentRequestUrl)) { $currentRequestUrl = wbGetProtectedProperty('Juri', 'uri', $this->uri); } return $currentRequestUrl; } }