Source of file MisdirectionService.php
Size: 11,777 Bytes - Last Modified: 2021-12-23T10:06:11+00:00
/var/www/docs.ssmods.com/process/src/src/services/MisdirectionService.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 | <?php namespace nglasl\misdirection; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\HTTP; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; use SilverStripe\ORM\ArrayList; use SilverStripe\SiteConfig\SiteConfig; use Symbiote\Multisites\Multisites; /** * Handles the link mapping recursion to return the eventual result, while providing any additional functionality required by the module. * @author Nathan Glasl <nathan@symbiote.com.au> */ class MisdirectionService { /** * Unifies a URL so link mappings are predictable. * * @parameter <{URL}> string * @return string */ public static function unify_URL($URL) { return strtolower(trim($URL, ' ?/')); } /** * Use third party validation to determine an external URL (https://gist.github.com/dperini/729294 and http://mathiasbynens.be/demo/url-regex). * * @parameter <{URL}> string * @return boolean */ public static function is_external_URL($URL) { $URL = trim($URL, '/?!"#$%&\'()*+,-.@:;<=>[\\]^_`{|}~'); return preg_match('%^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@|\d{1,3}(?:\.\d{1,3}){3}|(?:(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)(?:\.(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)*(?:\.[a-z\x{00a1}-\x{ffff}]{2,6}))(?::\d+)?(?:[^\s]*)?$%iu', $URL); } /** * Retrieve the appropriate link mapping for a request, with the ability to enable testing and return the recursion stack. * * @parameter <{REQUEST}> http request * @parameter <{RETURN_STACK}> boolean * @return link mapping */ public function getMappingByRequest($request, $testing = false) { // Make sure a URL comes through correctly. $link = str_replace(array( ':/', ' ' ), array( '://', '%20' ), $request->getURL(true)); $host = $request->getHeader('Host'); // Retrieve the appropriate link mapping. $map = $this->getMapping($link, $host); // Traverse the link mapping chain and return the eventual result, preventing multiple redirections. return $map ? $this->getRecursiveMapping($map, $host, $testing) : null; } /** * Retrieve the appropriate link mapping for a URL. * * @parameter <{URL}> string * @parameter <{HOSTNAME}> string * @return link mapping */ public function getMapping($URL, $host = null) { $URL = self::is_external_URL($URL) ? parse_url($URL, PHP_URL_PATH) : Director::makeRelative($URL); $URL = self::unify_URL($URL); $parts = explode('?', $URL); // Instantiate the link mapping query. $matches = LinkMapping::get(); // Enforce any hostname restriction that may have been defined. if(is_null($host) && Controller::has_curr() && ($controller = Controller::curr())) { $host = $controller->getRequest()->getHeader('Host'); } $temporary = $host; $host = Convert::raw2sql($host); $matches = $matches->where("(HostnameRestriction IS NULL) OR (HostnameRestriction = '{$host}')"); $regex = clone $matches; // Determine the simple matching from the database. $base = Convert::raw2sql($parts[0]); $matches = $matches->where("(LinkType = 'Simple') AND (((IncludesHostname = 0) AND ((MappedLink = '{$base}') OR (MappedLink LIKE '{$base}?%'))) OR ((IncludesHostname = 1) AND ((MappedLink = '{$host}/{$base}') OR (MappedLink LIKE '{$host}/{$base}?%'))))"); $host = $temporary; // Determine the remaining regular expression matching, as this is inconsistent from the database. $regex = $regex->filter('LinkType', 'Regular Expression'); $filtered = ArrayList::create(); foreach($regex as $match) { if((!$match->IncludesHostname && preg_match("%{$match->MappedLink}%", $URL)) || ($match->IncludesHostname && preg_match("%{$match->MappedLink}%", "{$host}/{$URL}"))) { $filtered->push($match); } } $filtered->merge($matches); $matches = $filtered; // Make sure the link mappings are ordered by priority and specificity. $matches = $matches->sort(array( 'Priority' => 'DESC', 'LinkType' => 'DESC', 'MappedLink' => 'DESC', 'ID' => Config::inst()->get(LinkMapping::class, 'priority') )); // Determine which link mapping should be returned, based on the sort order. $queryParameters = array(); if(isset($parts[1])) { parse_str($parts[1], $queryParameters); } foreach($matches as $match) { // Make sure the link mapping is live on the current stage. if($match->isLive() !== 'false') { // Ignore GET parameter matching for regular expressions, considering the special characters. $matchParts = explode('?', $match->MappedLink); if(($match->LinkType === 'Simple') && isset($matchParts[1])) { // Make sure the GET parameters match in any order. $matchParameters = array(); parse_str($matchParts[1], $matchParameters); if($matchParameters == $queryParameters) { return $match; } } else { // Return the first link mapping when GET parameters aren't present. $match->setMatchedURL($match->IncludesHostname ? "{$host}/{$URL}" : $URL); return $match; } } } // No mapping has been found. return null; } /** * Traverse the link mapping chain and return the eventual result, preventing multiple redirections. * * @parameter <{LINK_MAPPING}> link mapping * @parameter <{HOSTNAME}> string * @parameter <{RETURN_STACK}> boolean * @return link mapping/array */ public function getRecursiveMapping($map, $host = null, $testing = false) { // Keep track of the link mapping recursion. $counter = 1; $redirect = $map->getLink(); $chain = array( array_merge($map->toMap(), array( 'Counter' => $counter, 'RedirectLink' => $map->getLinkSummary(), 'LinkMapping' => $map )) ); // Determine the subsequent host. if($map->getLinkHost()) { $host = $map->getLinkHost(); } // Determine the next link mapping, immediately redirecting towards an external URL. while((($map->RedirectType === 'Page') || !self::is_external_URL($redirect)) && ($next = $this->getMapping($redirect, $host))) { // Enforce a maximum number of redirects, preventing infinite recursion and inefficient link mappings. if($counter === Config::inst()->get(MisDirectionRequestProcessor::class, 'maximum_requests')) { $chain[] = array( 'ResponseCode' => 404 ); // Return the call stack when testing has been enabled. return $testing ? $chain : null; } $redirect = $next->getLink(); $chain[] = array_merge($next->toMap(), array( 'Counter' => ++$counter, 'RedirectLink' => $next->getLinkSummary(), 'LinkMapping' => $next )); // Determine the subsequent host. if($next->getLinkHost()) { $host = $next->getLinkHost(); } $map = $next; } // Return either the call stack when testing has been enabled, or the eventual link mapping result. return $testing ? $chain : $map; } /** * Determine the fallback for a URL when the CMS module is present. * * @parameter <{URL}> string * @return array(string, integer) */ public function determineFallback($URL) { // Make sure the CMS module is present. if(ClassInfo::exists(SiteTree::class) && $URL) { // Instantiate the required variables. $segments = explode('/', self::unify_URL($URL)); $applicableRule = null; $nearestParent = null; $thisPage = null; $toURL = null; $responseCode = 303; // This prevents a page not found from redirecting back to the same page. array_pop($segments); // Retrieve the default site configuration fallback. $config = SiteConfig::current_site_config(); if($config && $config->Fallback) { $applicableRule = $config->Fallback; $nearestParent = $thisPage = Director::baseURL(); $toURL = $config->FallbackLink; $responseCode = $config->FallbackResponseCode; } // This is required to support multiple sites. if(ClassInfo::exists(Multisites::class) && ($parent = Multisites::inst()->getCurrentSite())) { $parentID = $parent->ID; if($parent->Fallback) { $applicableRule = $parent->Fallback; $nearestParent = $thisPage = Director::baseURL(); $toURL = $parent->FallbackLink; $responseCode = $parent->FallbackResponseCode; } } else { $parentID = 0; } // Determine the page specific fallback. for($iteration = 0; $iteration < count($segments); $iteration++) { $page = SiteTree::get()->filter(array( 'URLSegment' => $segments[$iteration], 'ParentID' => $parentID ))->first(); if($page) { // Determine the home page URL when appropriate. $link = ($page->Link() === Director::baseURL()) ? Controller::join_links(Director::baseURL(), 'home/') : $page->Link(); $nearestParent = $link; // Keep track of the current page fallback. if($page->Fallback) { $applicableRule = $page->Fallback; $thisPage = $link; $toURL = $page->FallbackLink; $responseCode = $page->FallbackResponseCode; } $parentID = $page->ID; } else { // The bottom of the chain has been reached. break; } } // Determine the applicable fallback. if($applicableRule) { $link = null; switch($applicableRule) { // Bypass the request filter. case 'Nearest': $link = '/' . HTTP::setGetVar('misdirected', true, $nearestParent); break; case 'This': $link = '/' . HTTP::setGetVar('misdirected', true, $thisPage); break; case 'URL': // When appropriate, prepend the base URL to match a page redirection. $link = self::is_external_URL($toURL) ? (ClassInfo::exists(Multisites::class) ? HTTP::setGetVar('misdirected', true, $toURL) : $toURL) : ('/' . HTTP::setGetVar('misdirected', true, Controller::join_links(Director::baseURL(), $toURL))); break; } if($link) { return array( 'link' => $link, 'code' => (int)$responseCode ); } } } // No fallback has been found. return null; } /** * Instantiate a new link mapping, redirecting a URL towards a page. * * @parameter <{MAPPING_URL}> string * @parameter <{MAPPING_PAGE_ID}> integer * @parameter <{MAPPING_PRIORITY}> integer * @return link mapping */ public function createPageMapping($URL, $redirectID, $priority = 1) { // Retrieve an already existing link mapping if one exists. $existing = LinkMapping::get()->filter(array( 'MappedLink' => $URL, 'RedirectType' => 'Page', 'RedirectPageID' => $redirectID ))->first(); if($existing) { return $existing; } // Instantiate the new link mapping with appropriate default values. $mapping = LinkMapping::create(); $mapping->MappedLink = $URL; $mapping->RedirectType = 'Page'; $mapping->RedirectPageID = (int)$redirectID; $mapping->Priority = (int)$priority; $mapping->write(); return $mapping; } /** * Instantiate a new link mapping, redirecting a URL towards another URL. * * @parameter <{MAPPING_URL}> string * @parameter <{MAPPING_REDIRECT_URL}> string * @parameter <{MAPPING_PRIORITY}> integer * @return link mapping */ public function createURLMapping($URL, $redirectURL, $priority = 1) { // Retrieve an already existing link mapping if one exists. $existing = LinkMapping::get()->filter(array( 'MappedLink' => $URL, 'RedirectType' => 'Link', 'RedirectLink' => $redirectURL ))->first(); if($existing) { return $existing; } // Instantiate the new link mapping with appropriate default values. $mapping = LinkMapping::create(); $mapping->MappedLink = $URL; $mapping->RedirectType = 'Link'; $mapping->RedirectLink = $redirectURL; $mapping->Priority = (int)$priority; $mapping->write(); return $mapping; } } |