1c3437056SNickeau<?php 2c3437056SNickeau 3c3437056SNickeau 4c3437056SNickeauuse ComboStrap\DatabasePageRow; 511d09b86Sgerardnicouse ComboStrap\DokuwikiId; 604fd306cSNickeauuse ComboStrap\ExceptionBadArgument; 704fd306cSNickeauuse ComboStrap\ExceptionBadSyntax; 804fd306cSNickeauuse ComboStrap\ExceptionCompile; 9*b1aef534SNicouse ComboStrap\ExceptionNotFound; 1004fd306cSNickeauuse ComboStrap\ExceptionSqliteNotAvailable; 1104fd306cSNickeauuse ComboStrap\ExecutionContext; 1204fd306cSNickeauuse ComboStrap\FileSystems; 13c3437056SNickeauuse ComboStrap\HttpResponse; 1404fd306cSNickeauuse ComboStrap\HttpResponseStatus; 15c3437056SNickeauuse ComboStrap\Identity; 16c3437056SNickeauuse ComboStrap\LogUtility; 1704fd306cSNickeauuse ComboStrap\MarkupPath; 1804fd306cSNickeauuse ComboStrap\Meta\Field\AliasType; 19c3437056SNickeauuse ComboStrap\Mime; 20c3437056SNickeauuse ComboStrap\PageId; 21c3437056SNickeauuse ComboStrap\PageRules; 22c3437056SNickeauuse ComboStrap\PageUrlPath; 2382a60d03SNickeauuse ComboStrap\PageUrlType; 2404fd306cSNickeauuse ComboStrap\RouterBestEndPage; 25c3437056SNickeauuse ComboStrap\Site; 2604fd306cSNickeauuse ComboStrap\SiteConfig; 27c3437056SNickeauuse ComboStrap\Sqlite; 2804fd306cSNickeauuse ComboStrap\Web\Url; 2911d09b86Sgerardnicouse ComboStrap\Web\UrlEndpoint; 3054743e42Sgerardnicouse ComboStrap\Web\UrlRewrite; 3104fd306cSNickeauuse ComboStrap\WikiPath; 32c3437056SNickeau 3304fd306cSNickeaurequire_once(__DIR__ . '/../vendor/autoload.php'); 34c3437056SNickeau 35c3437056SNickeau/** 36c3437056SNickeau * Class action_plugin_combo_url 37c3437056SNickeau * 38c3437056SNickeau * The actual URL manager 39c3437056SNickeau * 40c3437056SNickeau * 41c3437056SNickeau */ 42c3437056SNickeauclass action_plugin_combo_router extends DokuWiki_Action_Plugin 43c3437056SNickeau{ 44c3437056SNickeau 45c3437056SNickeau /** 46c3437056SNickeau * @deprecated 47c3437056SNickeau */ 48c3437056SNickeau const URL_MANAGER_ENABLE_CONF = "enableUrlManager"; 49c3437056SNickeau const ROUTER_ENABLE_CONF = "enableRouter"; 50c3437056SNickeau 51c3437056SNickeau // The redirect type 52c3437056SNickeau const REDIRECT_TRANSPARENT_METHOD = 'transparent'; // was (Id) 53c3437056SNickeau // For permanent, see https://developers.google.com/search/docs/advanced/crawling/301-redirects 54c3437056SNickeau const REDIRECT_PERMANENT_METHOD = 'permanent'; // was `Http` (301) 55c3437056SNickeau const REDIRECT_NOTFOUND_METHOD = "notfound"; // 404 (See other) (when best page name is calculated) 56c3437056SNickeau 57c3437056SNickeau public const PERMANENT_REDIRECT_CANONICAL = "permanent:redirect"; 58c3437056SNickeau 59c3437056SNickeau // Where the target id value comes from 60c3437056SNickeau const TARGET_ORIGIN_WELL_KNOWN = 'well-known'; 61c3437056SNickeau const TARGET_ORIGIN_PAGE_RULES = 'pageRules'; 62c3437056SNickeau /** 63c3437056SNickeau * Named Permalink (canonical) 64c3437056SNickeau */ 65c3437056SNickeau const TARGET_ORIGIN_CANONICAL = 'canonical'; 66c3437056SNickeau const TARGET_ORIGIN_ALIAS = 'alias'; 67c3437056SNickeau /** 68c3437056SNickeau * Identifier Permalink (full page id) 69c3437056SNickeau */ 70c3437056SNickeau const TARGET_ORIGIN_PERMALINK = "permalink"; 71c3437056SNickeau /** 72c3437056SNickeau * Extended Permalink (abbreviated page id at the end) 73c3437056SNickeau */ 74c3437056SNickeau const TARGET_ORIGIN_PERMALINK_EXTENDED = "extendedPermalink"; 75c3437056SNickeau const TARGET_ORIGIN_START_PAGE = 'startPage'; 76c3437056SNickeau const TARGET_ORIGIN_BEST_PAGE_NAME = 'bestPageName'; 77c3437056SNickeau const TARGET_ORIGIN_BEST_NAMESPACE = 'bestNamespace'; 78c3437056SNickeau const TARGET_ORIGIN_SEARCH_ENGINE = 'searchEngine'; 79c3437056SNickeau const TARGET_ORIGIN_BEST_END_PAGE_NAME = 'bestEndPageName'; 80c3437056SNickeau const TARGET_ORIGIN_SHADOW_BANNED = "shadowBanned"; 81c3437056SNickeau 82c3437056SNickeau 83c3437056SNickeau // The constant parameters 84c3437056SNickeau const GO_TO_SEARCH_ENGINE = 'GoToSearchEngine'; 85c3437056SNickeau const GO_TO_BEST_NAMESPACE = 'GoToBestNamespace'; 86c3437056SNickeau const GO_TO_BEST_PAGE_NAME = 'GoToBestPageName'; 87c3437056SNickeau const GO_TO_BEST_END_PAGE_NAME = 'GoToBestEndPageName'; 88c3437056SNickeau const GO_TO_NS_START_PAGE = 'GoToNsStartPage'; 89c3437056SNickeau const GO_TO_EDIT_MODE = 'GoToEditMode'; 90c3437056SNickeau const NOTHING = 'Nothing'; 91c3437056SNickeau 92c3437056SNickeau /** @var string - a name used in log and other places */ 93c3437056SNickeau const NAME = 'Url Manager'; 94c3437056SNickeau const CANONICAL = 'router'; 95c3437056SNickeau const PAGE_404 = "<html lang=\"en\"><body></body></html>"; 96c3437056SNickeau const REFRESH_HEADER_NAME = "Refresh"; 97c3437056SNickeau const REFRESH_HEADER_PREFIX = self::REFRESH_HEADER_NAME . ': 0;url='; 9804fd306cSNickeau const LOCATION_HEADER_PREFIX = HttpResponse::LOCATION_HEADER_NAME . ": "; 99c3437056SNickeau public const URL_MANAGER_NAME = "Router"; 100c3437056SNickeau 101c3437056SNickeau 102c3437056SNickeau /** 103c3437056SNickeau * @var PageRules 104c3437056SNickeau */ 105c3437056SNickeau private $pageRules; 106c3437056SNickeau 107c3437056SNickeau 108c3437056SNickeau function __construct() 109c3437056SNickeau { 110c3437056SNickeau // enable direct access to language strings 111c3437056SNickeau // ie $this->lang 112c3437056SNickeau $this->setupLocale(); 113c3437056SNickeau 114c3437056SNickeau } 115c3437056SNickeau 116c3437056SNickeau /** 11704fd306cSNickeau * @param string $refreshHeader 118c3437056SNickeau * @return false|string 119c3437056SNickeau */ 12004fd306cSNickeau public static function getUrlFromRefresh(string $refreshHeader) 121c3437056SNickeau { 122c3437056SNickeau return substr($refreshHeader, strlen(action_plugin_combo_router::REFRESH_HEADER_PREFIX)); 123c3437056SNickeau } 124c3437056SNickeau 125c3437056SNickeau public static function getUrlFromLocation($refreshHeader) 126c3437056SNickeau { 127c3437056SNickeau return substr($refreshHeader, strlen(action_plugin_combo_router::LOCATION_HEADER_PREFIX)); 128c3437056SNickeau } 129c3437056SNickeau 130c3437056SNickeau /** 131ad79af66SNico * @return string|null 132c3437056SNickeau * 13394daa629SNico * Return the original id from the request 13494daa629SNico * ie `howto:how-to-get-started-with-combostrap-m3i8vga8` 13594daa629SNico * if `/howto/how-to-get-started-with-combostrap-m3i8vga8` 13694daa629SNico * 137c3437056SNickeau * Unfortunately, DOKUWIKI_STARTED is not the first event 138c3437056SNickeau * The id may have been changed by 139ad79af66SNico * {@link action_plugin_combo_lang::load_lang()} 140c3437056SNickeau * function, that's why we have this function 141c3437056SNickeau * to get the original requested id 142c3437056SNickeau */ 143ad79af66SNico private static function getOriginalIdFromRequest(): ?string 144c3437056SNickeau { 14594daa629SNico $originalId = $_GET["id"] ?? null; 146f49666c9Sgerardnico if ($originalId === null) { 147f49666c9Sgerardnico return null; 148f49666c9Sgerardnico } 14994daa629SNico // We get a `/` as first character 15094daa629SNico // because we return an id, we need to delete it 15194daa629SNico $originalId = substr($originalId, 1); 15294daa629SNico // transform / to : 15304fd306cSNickeau return str_replace("/", WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $originalId); 154c3437056SNickeau } 155c3437056SNickeau 156c3437056SNickeau /** 157c3437056SNickeau * Determine if the request should be banned based on the id 158c3437056SNickeau * 159c3437056SNickeau * @param string $id 160c3437056SNickeau * @return bool 161c3437056SNickeau * 162c3437056SNickeau * See also {@link https://perishablepress.com/7g-firewall/#features} 163c3437056SNickeau * for blocking rules on http request data such as: 164c3437056SNickeau * * query_string 165c3437056SNickeau * * user_agent, 166c3437056SNickeau * * remote host 167c3437056SNickeau */ 168c3437056SNickeau public static function isShadowBanned(string $id): bool 169c3437056SNickeau { 170c3437056SNickeau /** 171c3437056SNickeau * ie 172c3437056SNickeau * wp-json:api:flutter_woo:config_file 173c3437056SNickeau * wp-content:plugins:wpdiscuz:themes:default:style-rtl.css 174c3437056SNickeau * wp-admin 175c3437056SNickeau * 2020:wp-includes:wlwmanifest.xml 176c3437056SNickeau * wp-content:start 177c3437056SNickeau * wp-admin:css:start 178c3437056SNickeau * sito:wp-includes:wlwmanifest.xml 179c3437056SNickeau * site:wp-includes:wlwmanifest.xml 180c3437056SNickeau * cms:wp-includes:wlwmanifest.xml 181c3437056SNickeau * test:wp-includes:wlwmanifest.xml 182c3437056SNickeau * media:wp-includes:wlwmanifest.xml 183c3437056SNickeau * wp2:wp-includes:wlwmanifest.xml 184c3437056SNickeau * 2019:wp-includes:wlwmanifest.xml 185c3437056SNickeau * shop:wp-includes:wlwmanifest.xml 186c3437056SNickeau * wp1:wp-includes:wlwmanifest.xml 187c3437056SNickeau * news:wp-includes:wlwmanifest.xml 188c3437056SNickeau * 2018:wp-includes:wlwmanifest.xml 189c3437056SNickeau */ 190c3437056SNickeau if (strpos($id, 'wp-') !== false) { 191c3437056SNickeau return true; 192c3437056SNickeau } 193c3437056SNickeau 194c3437056SNickeau /** 195c3437056SNickeau * db:oracle:long_or_1_utl_inaddr.get_host_address_chr_33_chr_126_chr_33_chr_65_chr_66_chr_67_chr_49_chr_52_chr_53_chr_90_chr_81_chr_54_chr_50_chr_68_chr_87_chr_81_chr_65_chr_70_chr_80_chr_79_chr_73_chr_89_chr_67_chr_70_chr_68_chr_33_chr_126_chr_33 196c3437056SNickeau * db:oracle:999999.9:union:all:select_null:from_dual 197c3437056SNickeau * db:oracle:999999.9:union:all:select_null:from_dual_and_0_0 198c3437056SNickeau */ 199c3437056SNickeau if (preg_match('/_chr_|_0_0/', $id) === 1) { 200c3437056SNickeau return true; 201c3437056SNickeau } 202c3437056SNickeau 203c3437056SNickeau 204c3437056SNickeau /** 205c3437056SNickeau * ie 206c3437056SNickeau * git:objects: 207c3437056SNickeau * git:refs:heads:stable 208c3437056SNickeau * git:logs:refs:heads:main 209c3437056SNickeau * git:logs:refs:heads:stable 210c3437056SNickeau * git:hooks:pre-push.sample 211c3437056SNickeau * git:hooks:pre-receive.sample 212c3437056SNickeau */ 213c3437056SNickeau if (strpos($id, "git:") === 0) { 214c3437056SNickeau return true; 215c3437056SNickeau } 216c3437056SNickeau 217c3437056SNickeau return false; 218c3437056SNickeau 219c3437056SNickeau } 220c3437056SNickeau 221c3437056SNickeau /** 222c3437056SNickeau * @param string $id 223c3437056SNickeau * @return bool 224c3437056SNickeau * well-known:traffic-advice = https://github.com/buettner/private-prefetch-proxy/blob/main/traffic-advice.md 225c3437056SNickeau * .well-known/security.txt, id=well-known:security.txt = https://securitytxt.org/ 226c3437056SNickeau * well-known:dnt-policy.txt 227c3437056SNickeau */ 228c3437056SNickeau public static function isWellKnownFile(string $id): bool 229c3437056SNickeau { 230c3437056SNickeau return strpos($id, "well-known") === 0; 231c3437056SNickeau } 232c3437056SNickeau 233c3437056SNickeau 234c3437056SNickeau function register(Doku_Event_Handler $controller) 235c3437056SNickeau { 236c3437056SNickeau 23704fd306cSNickeau if (SiteConfig::getConfValue(self::ROUTER_ENABLE_CONF, 1)) { 23804fd306cSNickeau 239c3437056SNickeau /** 240c3437056SNickeau * This will call the function {@link action_plugin_combo_router::_router()} 241c3437056SNickeau * The event is not DOKUWIKI_STARTED because this is not the first one 242c3437056SNickeau * 243c3437056SNickeau * https://www.dokuwiki.org/devel:event:init_lang_load 244c3437056SNickeau */ 245c3437056SNickeau $controller->register_hook('DOKUWIKI_STARTED', 24604fd306cSNickeau 'BEFORE', 247c3437056SNickeau $this, 248c3437056SNickeau 'router', 249c3437056SNickeau array()); 250c3437056SNickeau 251c3437056SNickeau /** 2525187326aSNico * Bot Ban functionality 253c3437056SNickeau * 2545187326aSNico * Because we make a redirection to the home page, we need to check 2555187326aSNico * if the home is readable, for that, the AUTH plugin needs to be initialized 2565187326aSNico * That's why we wait 2575187326aSNico * https://www.dokuwiki.org/devel:event:dokuwiki_init_done 2585187326aSNico * 2595187326aSNico * and we can't use 260c3437056SNickeau * https://www.dokuwiki.org/devel:event:init_lang_load 2615187326aSNico * because there is no auth setup in {@link auth_aclcheck_cb()} 2625187326aSNico * and the the line `if (!$auth instanceof AuthPlugin) return AUTH_NONE;` return none; 263c3437056SNickeau */ 2645187326aSNico $controller->register_hook('DOKUWIKI_INIT_DONE', 'BEFORE', $this, 'ban', array()); 265c3437056SNickeau 266c3437056SNickeau } 267c3437056SNickeau 268c3437056SNickeau 269c3437056SNickeau } 270c3437056SNickeau 271c3437056SNickeau /** 272c3437056SNickeau * 273c3437056SNickeau * We have created a spacial ban function that is 274c3437056SNickeau * called before the first function 275c3437056SNickeau * {@link action_plugin_combo_metalang::load_lang()} 276c3437056SNickeau * to spare CPU. 277c3437056SNickeau * 278c3437056SNickeau * @param $event 279c3437056SNickeau * @throws Exception 280c3437056SNickeau */ 281c3437056SNickeau function ban(&$event) 282c3437056SNickeau { 283c3437056SNickeau 284c3437056SNickeau $id = self::getOriginalIdFromRequest(); 28506ecf9e7Sgerardnico if ($id === null) { 28606ecf9e7Sgerardnico return; 28706ecf9e7Sgerardnico } 28804fd306cSNickeau $page = MarkupPath::createMarkupFromId($id); 2895187326aSNico if (FileSystems::exists($page)) { 2905187326aSNico return; 2915187326aSNico } 2925187326aSNico 293c3437056SNickeau // Well known 294c3437056SNickeau if (self::isWellKnownFile($id)) { 295c3437056SNickeau $this->logRedirection($id, "", self::TARGET_ORIGIN_WELL_KNOWN, self::REDIRECT_NOTFOUND_METHOD); 29604fd306cSNickeau ExecutionContext::getActualOrCreateFromEnv() 29704fd306cSNickeau ->response() 29804fd306cSNickeau ->setStatus(HttpResponseStatus::NOT_FOUND) 29904fd306cSNickeau ->end(); 300c3437056SNickeau return; 301c3437056SNickeau } 302c3437056SNickeau 303c3437056SNickeau // Shadow banned 304c3437056SNickeau if (self::isShadowBanned($id)) { 30504fd306cSNickeau $webSiteHomePage = Site::getIndexPageName(); 306c3437056SNickeau $this->executeTransparentRedirect($webSiteHomePage, self::TARGET_ORIGIN_SHADOW_BANNED); 307c3437056SNickeau } 3085187326aSNico 309c3437056SNickeau } 310c3437056SNickeau 311c3437056SNickeau /** 312c3437056SNickeau * @param $event Doku_Event 313c3437056SNickeau * @param $param 314c3437056SNickeau * @return void 315c3437056SNickeau * @throws Exception 316c3437056SNickeau */ 317c3437056SNickeau function router(&$event, $param) 318c3437056SNickeau { 319c3437056SNickeau 32004fd306cSNickeau /** 32104fd306cSNickeau * Just the {@link ExecutionContext::SHOW_ACTION} 32204fd306cSNickeau * may be redirected 32304fd306cSNickeau */ 32404fd306cSNickeau $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 32504fd306cSNickeau if ($executionContext->getExecutingAction() !== ExecutionContext::SHOW_ACTION) { 32604fd306cSNickeau return; 32704fd306cSNickeau } 328c3437056SNickeau 32954743e42Sgerardnico $urlRewrite = Site::getUrlRewrite(); 33054743e42Sgerardnico if ($urlRewrite == UrlRewrite::VALUE_DOKU_REWRITE) { 33154743e42Sgerardnico UrlRewrite::sendErrorMessage(); 33254743e42Sgerardnico return; 33354743e42Sgerardnico } 334c3437056SNickeau 335c3437056SNickeau global $ID; 336c3437056SNickeau 337c3437056SNickeau /** 338c3437056SNickeau * Without SQLite, this module does not work further 339c3437056SNickeau */ 34004fd306cSNickeau try { 34104fd306cSNickeau Sqlite::createOrGetSqlite(); 34204fd306cSNickeau } catch (ExceptionSqliteNotAvailable $e) { 343c3437056SNickeau return; 344c3437056SNickeau } 345c3437056SNickeau 34604fd306cSNickeau $this->pageRules = new PageRules(); 34704fd306cSNickeau 34804fd306cSNickeau 349c3437056SNickeau /** 350c3437056SNickeau * Unfortunately, DOKUWIKI_STARTED is not the first event 351c3437056SNickeau * The id may have been changed by 35204fd306cSNickeau * {@link action_plugin_combo_lang::load_lang()} 353c3437056SNickeau * function, that's why we check against the {@link $_REQUEST} 354c3437056SNickeau * and not the global ID 355c3437056SNickeau */ 356c3437056SNickeau $originalId = self::getOriginalIdFromRequest(); 357c3437056SNickeau 358c3437056SNickeau /** 359c3437056SNickeau * Page is an existing id ? 360c3437056SNickeau */ 36104fd306cSNickeau $requestedMarkupPath = MarkupPath::createMarkupFromId($ID); 36204fd306cSNickeau if (FileSystems::exists($requestedMarkupPath)) { 363c3437056SNickeau 364c3437056SNickeau /** 365c3437056SNickeau * If this is not the root home page 36694daa629SNico * and if the canonical id is the not the same (the id has changed) 367c3437056SNickeau * and if this is not a historical page (revision) 368c3437056SNickeau * redirect 369c3437056SNickeau */ 370c3437056SNickeau if ( 37104fd306cSNickeau $originalId !== $requestedMarkupPath->getUrlId() // The id may have been changed 37204fd306cSNickeau && $ID != Site::getIndexPageName() 373c3437056SNickeau && !isset($_REQUEST["rev"]) 374c3437056SNickeau ) { 3754cadd4f8SNickeau /** 3764cadd4f8SNickeau * TODO: When saving for the first time, the page is not stored in the database 3774cadd4f8SNickeau * but that's not the case actually 3784cadd4f8SNickeau */ 37904fd306cSNickeau $databasePageRow = $requestedMarkupPath->getDatabasePage(); 38004fd306cSNickeau if ($databasePageRow->exists()) { 38104fd306cSNickeau /** 38204fd306cSNickeau * A move may leave the database in a bad state, 38304fd306cSNickeau * unfortunately (ie page is not in index, unable to update, ...) 38404fd306cSNickeau * We test therefore if the database page id exists 38504fd306cSNickeau */ 38604fd306cSNickeau $targetPageId = $databasePageRow->getFromRow("id"); 38704fd306cSNickeau $targetPath = WikiPath::createMarkupPathFromId($targetPageId); 38804fd306cSNickeau if (FileSystems::exists($targetPath)) { 389c3437056SNickeau $this->executePermanentRedirect( 39004fd306cSNickeau $requestedMarkupPath->getCanonicalUrl()->toAbsoluteUrlString(), 391c3437056SNickeau self::TARGET_ORIGIN_PERMALINK_EXTENDED 392c3437056SNickeau ); 393c3437056SNickeau } 3944cadd4f8SNickeau } 39504fd306cSNickeau } 396c3437056SNickeau return; 397c3437056SNickeau } 398c3437056SNickeau 399c3437056SNickeau 400c3437056SNickeau $identifier = $ID; 401c3437056SNickeau 402c3437056SNickeau 403c3437056SNickeau /** 4041089b853Sgerardnico * Page Id in the url 405c3437056SNickeau */ 40604fd306cSNickeau $shortPageId = PageUrlPath::getShortEncodedPageIdFromUrlId($requestedMarkupPath->getPathObject()->getLastNameWithoutExtension()); 4071089b853Sgerardnico if ($shortPageId != null) { 408c3437056SNickeau $pageId = PageUrlPath::decodePageId($shortPageId); 4091089b853Sgerardnico } else { 4101089b853Sgerardnico /** 4111089b853Sgerardnico * Permalink with id 4121089b853Sgerardnico */ 4131089b853Sgerardnico $pageId = PageUrlPath::decodePageId($identifier); 4141089b853Sgerardnico } 4151089b853Sgerardnico if ($pageId !== null) { 4161089b853Sgerardnico 4171089b853Sgerardnico if ($requestedMarkupPath->getParent() === null) { 41804fd306cSNickeau $page = DatabasePageRow::createFromPageId($pageId)->getMarkupPath(); 419c3437056SNickeau if ($page !== null && $page->exists()) { 42082a60d03SNickeau $this->executePermanentRedirect( 42104fd306cSNickeau $page->getCanonicalUrl()->toAbsoluteUrlString(), 42282a60d03SNickeau self::TARGET_ORIGIN_PERMALINK 42382a60d03SNickeau ); 4241089b853Sgerardnico return; 425c3437056SNickeau } 426c3437056SNickeau } 427c3437056SNickeau 428c3437056SNickeau /** 429c3437056SNickeau * Page Id Abbr ? 430c3437056SNickeau * {@link PageUrlType::CONF_CANONICAL_URL_TYPE} 431c3437056SNickeau */ 43204fd306cSNickeau $page = DatabasePageRow::createFromPageIdAbbr($pageId)->getMarkupPath(); 433c3437056SNickeau if ($page === null) { 434c3437056SNickeau // or the length of the abbr has changed 43504fd306cSNickeau $canonicalDatabasePage = new DatabasePageRow(); 436*b1aef534SNico try { 43704fd306cSNickeau $row = $canonicalDatabasePage->getDatabaseRowFromAttribute("substr(" . PageId::PROPERTY_NAME . ", 1, " . strlen($pageId) . ")", $pageId); 43804fd306cSNickeau $canonicalDatabasePage->setRow($row); 43904fd306cSNickeau $page = $canonicalDatabasePage->getMarkupPath(); 440*b1aef534SNico } catch (ExceptionNotFound $e) { 441*b1aef534SNico // nothing to do 442c3437056SNickeau } 443c3437056SNickeau } 444c3437056SNickeau if ($page !== null && $page->exists()) { 445c3437056SNickeau /** 446c3437056SNickeau * If the url canonical id has changed, we show it 447c3437056SNickeau * to the writer by performing a permanent redirect 448c3437056SNickeau */ 449c3437056SNickeau if ($identifier != $page->getUrlId()) { 450c3437056SNickeau // Google asks for a redirect 451c3437056SNickeau // https://developers.google.com/search/docs/advanced/crawling/301-redirects 452c3437056SNickeau // People access your site through several different URLs. 453c3437056SNickeau // If, for example, your home page can be reached in multiple ways 454c3437056SNickeau // (for instance, http://example.com/home, http://home.example.com, or http://www.example.com), 455c3437056SNickeau // it's a good idea to pick one of those URLs as your preferred (canonical) destination, 456c3437056SNickeau // and use redirects to send traffic from the other URLs to your preferred URL. 457c3437056SNickeau $this->executePermanentRedirect( 45804fd306cSNickeau $page->getCanonicalUrl()->toAbsoluteUrlString(), 459c3437056SNickeau self::TARGET_ORIGIN_PERMALINK_EXTENDED 460c3437056SNickeau ); 461c3437056SNickeau return; 462c3437056SNickeau } 4634cadd4f8SNickeau 46404fd306cSNickeau $this->executeTransparentRedirect($page->getWikiId(), self::TARGET_ORIGIN_PERMALINK_EXTENDED); 465c3437056SNickeau return; 466c3437056SNickeau 467c3437056SNickeau } 468c3437056SNickeau // permanent url not yet in the database 469c3437056SNickeau // Other permanent such as permanent canonical ? 470c3437056SNickeau // We let the process go with the new identifier 471c3437056SNickeau 472c3437056SNickeau } 473c3437056SNickeau 474c3437056SNickeau // Global variable needed in the process 475c3437056SNickeau global $conf; 476c3437056SNickeau 477c3437056SNickeau /** 478c3437056SNickeau * Identifier is a Canonical ? 479c3437056SNickeau */ 48004fd306cSNickeau $canonicalDatabasePage = DatabasePageRow::createFromCanonical($identifier); 48104fd306cSNickeau $canonicalPage = $canonicalDatabasePage->getMarkupPath(); 48204fd306cSNickeau if ($canonicalPage !== null && $canonicalPage->exists()) { 48382a60d03SNickeau /** 48482a60d03SNickeau * Does the canonical url is canonical name based 48582a60d03SNickeau * ie {@link PageUrlType::CONF_VALUE_CANONICAL_PATH} 48682a60d03SNickeau */ 48704fd306cSNickeau if ($canonicalPage->getUrlId() === $identifier) { 48882a60d03SNickeau $res = $this->executeTransparentRedirect( 48904fd306cSNickeau $canonicalPage->getWikiId(), 49082a60d03SNickeau self::TARGET_ORIGIN_CANONICAL 49182a60d03SNickeau ); 49282a60d03SNickeau } else { 49382a60d03SNickeau $res = $this->executePermanentRedirect( 49404fd306cSNickeau $canonicalPage->getWikiId(), // not the url because, it allows to add url query redirection property 49582a60d03SNickeau self::TARGET_ORIGIN_CANONICAL 49682a60d03SNickeau ); 49782a60d03SNickeau } 498c3437056SNickeau if ($res) { 499c3437056SNickeau return; 500c3437056SNickeau } 501c3437056SNickeau } 502c3437056SNickeau 503c3437056SNickeau /** 504c3437056SNickeau * Identifier is an alias 505c3437056SNickeau */ 50604fd306cSNickeau $aliasRequestedPage = DatabasePageRow::createFromAlias($identifier)->getMarkupPath(); 507c3437056SNickeau if ( 50804fd306cSNickeau $aliasRequestedPage !== null 50904fd306cSNickeau && $aliasRequestedPage->exists() 510c3437056SNickeau // The build alias is the file system metadata alias 511c3437056SNickeau // it may be null if the replication in the database was not successful 51204fd306cSNickeau && $aliasRequestedPage->getBuildAlias() !== null 513c3437056SNickeau ) { 51404fd306cSNickeau $buildAlias = $aliasRequestedPage->getBuildAlias(); 515c3437056SNickeau switch ($buildAlias->getType()) { 516c3437056SNickeau case AliasType::REDIRECT: 51704fd306cSNickeau $res = $this->executePermanentRedirect($aliasRequestedPage->getCanonicalUrl()->toAbsoluteUrlString(), self::TARGET_ORIGIN_ALIAS); 518c3437056SNickeau if ($res) { 519c3437056SNickeau return; 520c3437056SNickeau } 521c3437056SNickeau break; 522c3437056SNickeau case AliasType::SYNONYM: 52304fd306cSNickeau $res = $this->executeTransparentRedirect($aliasRequestedPage->getWikiId(), self::TARGET_ORIGIN_ALIAS); 524c3437056SNickeau if ($res) { 525c3437056SNickeau return; 526c3437056SNickeau } 527c3437056SNickeau break; 528c3437056SNickeau default: 529c3437056SNickeau LogUtility::msg("The alias type ({$buildAlias->getType()}) is unknown. A permanent redirect was performed for the alias $identifier"); 53004fd306cSNickeau $res = $this->executePermanentRedirect($aliasRequestedPage->getCanonicalUrl()->toAbsoluteUrlString(), self::TARGET_ORIGIN_ALIAS); 531c3437056SNickeau if ($res) { 532c3437056SNickeau return; 533c3437056SNickeau } 534c3437056SNickeau break; 535c3437056SNickeau } 536c3437056SNickeau } 537c3437056SNickeau 538c3437056SNickeau 539c3437056SNickeau // If there is a redirection defined in the page rules 540c3437056SNickeau $result = $this->processingPageRules(); 541c3437056SNickeau if ($result) { 542c3437056SNickeau // A redirection has occurred 543c3437056SNickeau // finish the process 544c3437056SNickeau return; 545c3437056SNickeau } 546c3437056SNickeau 547c3437056SNickeau /** 548c3437056SNickeau * 549c3437056SNickeau * There was no redirection found, redirect to edit mode if writer 550c3437056SNickeau * 551c3437056SNickeau */ 552c3437056SNickeau if (Identity::isWriter() && $this->getConf(self::GO_TO_EDIT_MODE) == 1) { 553c3437056SNickeau 554c3437056SNickeau $this->gotToEditMode($event); 555c3437056SNickeau // Stop here 556c3437056SNickeau return; 557c3437056SNickeau 558c3437056SNickeau } 559c3437056SNickeau 56004fd306cSNickeau /** 561c3437056SNickeau * We are still a reader, the redirection does not exist the user is not allowed to edit the page (public of other) 562c3437056SNickeau */ 563c3437056SNickeau if ($this->getConf('ActionReaderFirst') == self::NOTHING) { 564c3437056SNickeau return; 565c3437056SNickeau } 566c3437056SNickeau 567c3437056SNickeau // We are reader and their is no redirection set, we apply the algorithm 568c3437056SNickeau $readerAlgorithms = array(); 569c3437056SNickeau $readerAlgorithms[0] = $this->getConf('ActionReaderFirst'); 570c3437056SNickeau $readerAlgorithms[1] = $this->getConf('ActionReaderSecond'); 571c3437056SNickeau $readerAlgorithms[2] = $this->getConf('ActionReaderThird'); 572c3437056SNickeau 573c3437056SNickeau while ( 574c3437056SNickeau ($algorithm = array_shift($readerAlgorithms)) != null 575c3437056SNickeau ) { 576c3437056SNickeau 577c3437056SNickeau switch ($algorithm) { 578c3437056SNickeau 579c3437056SNickeau case self::NOTHING: 580c3437056SNickeau return; 581c3437056SNickeau 582c3437056SNickeau case self::GO_TO_BEST_END_PAGE_NAME: 583c3437056SNickeau 58404fd306cSNickeau /** 58504fd306cSNickeau * @var MarkupPath $bestEndPage 58604fd306cSNickeau */ 58704fd306cSNickeau list($bestEndPage, $method) = RouterBestEndPage::process($requestedMarkupPath); 588ea801fd8Sgerardnico if ($bestEndPage != null && $bestEndPage->getWikiId() !== $requestedMarkupPath->getWikiId()) { 589c3437056SNickeau $res = false; 590c3437056SNickeau switch ($method) { 591c3437056SNickeau case self::REDIRECT_PERMANENT_METHOD: 59204fd306cSNickeau $res = $this->executePermanentRedirect($bestEndPage->getWikiId(), self::TARGET_ORIGIN_BEST_END_PAGE_NAME); 593c3437056SNickeau break; 594c3437056SNickeau case self::REDIRECT_NOTFOUND_METHOD: 59504fd306cSNickeau $res = $this->performNotFoundRedirect($bestEndPage->getWikiId(), self::TARGET_ORIGIN_BEST_END_PAGE_NAME); 596c3437056SNickeau break; 597c3437056SNickeau default: 598c3437056SNickeau LogUtility::msg("This redirection method ($method) was not expected for the redirection algorithm ($algorithm)"); 599c3437056SNickeau } 600c3437056SNickeau if ($res) { 601c3437056SNickeau // Redirection has succeeded 602c3437056SNickeau return; 603c3437056SNickeau } 604c3437056SNickeau } 605c3437056SNickeau break; 606c3437056SNickeau 607c3437056SNickeau case self::GO_TO_NS_START_PAGE: 608c3437056SNickeau 609c3437056SNickeau // Start page with the conf['start'] parameter 610c3437056SNickeau $startPage = getNS($identifier) . ':' . $conf['start']; 611c3437056SNickeau if (page_exists($startPage)) { 612c3437056SNickeau $res = $this->performNotFoundRedirect($startPage, self::TARGET_ORIGIN_START_PAGE); 613c3437056SNickeau if ($res) { 614c3437056SNickeau return; 615c3437056SNickeau } 616c3437056SNickeau } 617c3437056SNickeau 618c3437056SNickeau // Start page with the same name than the namespace 619c3437056SNickeau $startPage = getNS($identifier) . ':' . curNS($identifier); 620c3437056SNickeau if (page_exists($startPage)) { 621c3437056SNickeau $res = $this->performNotFoundRedirect($startPage, self::TARGET_ORIGIN_START_PAGE); 622c3437056SNickeau if ($res) { 623c3437056SNickeau return; 624c3437056SNickeau } 625c3437056SNickeau } 626c3437056SNickeau break; 627c3437056SNickeau 628c3437056SNickeau case self::GO_TO_BEST_PAGE_NAME: 629c3437056SNickeau 630c3437056SNickeau $bestPageId = null; 631c3437056SNickeau 632c3437056SNickeau $bestPage = $this->getBestPage($identifier); 633c3437056SNickeau $bestPageId = $bestPage['id']; 634c3437056SNickeau $scorePageName = $bestPage['score']; 635c3437056SNickeau 636c3437056SNickeau // Get Score from a Namespace 637c3437056SNickeau $bestNamespace = $this->scoreBestNamespace($identifier); 638c3437056SNickeau $bestNamespaceId = $bestNamespace['namespace']; 639c3437056SNickeau $namespaceScore = $bestNamespace['score']; 640c3437056SNickeau 641c3437056SNickeau // Compare the two score 642c3437056SNickeau if ($scorePageName > 0 or $namespaceScore > 0) { 643c3437056SNickeau if ($scorePageName > $namespaceScore) { 644c3437056SNickeau $this->performNotFoundRedirect($bestPageId, self::TARGET_ORIGIN_BEST_PAGE_NAME); 645c3437056SNickeau } else { 646c3437056SNickeau $this->performNotFoundRedirect($bestNamespaceId, self::TARGET_ORIGIN_BEST_PAGE_NAME); 647c3437056SNickeau } 648c3437056SNickeau return; 649c3437056SNickeau } 650c3437056SNickeau break; 651c3437056SNickeau 652c3437056SNickeau case self::GO_TO_BEST_NAMESPACE: 653c3437056SNickeau 654c3437056SNickeau $scoreNamespace = $this->scoreBestNamespace($identifier); 655c3437056SNickeau $bestNamespaceId = $scoreNamespace['namespace']; 656c3437056SNickeau $score = $scoreNamespace['score']; 657c3437056SNickeau 658c3437056SNickeau if ($score > 0) { 659c3437056SNickeau $this->performNotFoundRedirect($bestNamespaceId, self::TARGET_ORIGIN_BEST_NAMESPACE); 660c3437056SNickeau return; 661c3437056SNickeau } 662c3437056SNickeau break; 663c3437056SNickeau 664c3437056SNickeau case self::GO_TO_SEARCH_ENGINE: 665c3437056SNickeau 666c3437056SNickeau $this->redirectToSearchEngine(); 667c3437056SNickeau 668c3437056SNickeau return; 669c3437056SNickeau 670c3437056SNickeau // End Switch Action 671c3437056SNickeau } 672c3437056SNickeau 673c3437056SNickeau // End While Action 674c3437056SNickeau } 675c3437056SNickeau 676c3437056SNickeau 677c3437056SNickeau } 678c3437056SNickeau 679c3437056SNickeau 680c3437056SNickeau /** 681c3437056SNickeau * getBestNamespace 682c3437056SNickeau * Return a list with 'BestNamespaceId Score' 683c3437056SNickeau * @param $id 684c3437056SNickeau * @return array 685c3437056SNickeau */ 686c3437056SNickeau private 687c3437056SNickeau function scoreBestNamespace($id) 688c3437056SNickeau { 689c3437056SNickeau 690c3437056SNickeau global $conf; 691c3437056SNickeau 692c3437056SNickeau // Parameters 693c3437056SNickeau $pageNameSpace = getNS($id); 694c3437056SNickeau 695c3437056SNickeau // If the page has an existing namespace start page take it, other search other namespace 696c3437056SNickeau $startPageNameSpace = $pageNameSpace . ":"; 697c3437056SNickeau $dateAt = ''; 698c3437056SNickeau // $startPageNameSpace will get a full path (ie with start or the namespace 699c3437056SNickeau resolve_pageid($pageNameSpace, $startPageNameSpace, $exists, $dateAt, true); 700c3437056SNickeau if (page_exists($startPageNameSpace)) { 701c3437056SNickeau $nameSpaces = array($startPageNameSpace); 702c3437056SNickeau } else { 703c3437056SNickeau $nameSpaces = ft_pageLookup($conf['start']); 704c3437056SNickeau } 705c3437056SNickeau 706c3437056SNickeau // Parameters and search the best namespace 707c3437056SNickeau $pathNames = explode(':', $pageNameSpace); 708c3437056SNickeau $bestNbWordFound = 0; 709c3437056SNickeau $bestNamespaceId = ''; 710c3437056SNickeau foreach ($nameSpaces as $nameSpace) { 711c3437056SNickeau 712c3437056SNickeau $nbWordFound = 0; 713c3437056SNickeau foreach ($pathNames as $pathName) { 714c3437056SNickeau if (strlen($pathName) > 2) { 715c3437056SNickeau $nbWordFound = $nbWordFound + substr_count($nameSpace, $pathName); 716c3437056SNickeau } 717c3437056SNickeau } 718c3437056SNickeau if ($nbWordFound > $bestNbWordFound) { 719c3437056SNickeau // Take only the smallest namespace 720c3437056SNickeau if (strlen($nameSpace) < strlen($bestNamespaceId) or $nbWordFound > $bestNbWordFound) { 721c3437056SNickeau $bestNbWordFound = $nbWordFound; 722c3437056SNickeau $bestNamespaceId = $nameSpace; 723c3437056SNickeau } 724c3437056SNickeau } 725c3437056SNickeau } 726c3437056SNickeau 727c3437056SNickeau $startPageFactor = $this->getConf('WeightFactorForStartPage'); 728c3437056SNickeau $nameSpaceFactor = $this->getConf('WeightFactorForSameNamespace'); 729c3437056SNickeau if ($bestNbWordFound > 0) { 730c3437056SNickeau $bestNamespaceScore = $bestNbWordFound * $nameSpaceFactor + $startPageFactor; 731c3437056SNickeau } else { 732c3437056SNickeau $bestNamespaceScore = 0; 733c3437056SNickeau } 734c3437056SNickeau 735c3437056SNickeau 736c3437056SNickeau return array( 737c3437056SNickeau 'namespace' => $bestNamespaceId, 738c3437056SNickeau 'score' => $bestNamespaceScore 739c3437056SNickeau ); 740c3437056SNickeau 741c3437056SNickeau } 742c3437056SNickeau 743c3437056SNickeau /** 744c3437056SNickeau * @param $event 745c3437056SNickeau */ 746c3437056SNickeau private 747c3437056SNickeau function gotToEditMode(&$event) 748c3437056SNickeau { 749c3437056SNickeau global $ACT; 750c3437056SNickeau $ACT = 'edit'; 751c3437056SNickeau 752c3437056SNickeau } 753c3437056SNickeau 754c3437056SNickeau 755c3437056SNickeau /** 756c3437056SNickeau * Redirect to an internal page ie: 757c3437056SNickeau * * on the same domain 758c3437056SNickeau * * no HTTP redirect 759c3437056SNickeau * * id rewrite 760c3437056SNickeau * @param string $targetPageId - target page id 761c3437056SNickeau * @param string $targetOriginId - the source of the target (redirect) 762c3437056SNickeau * @return bool - return true if the user has the permission and that the redirect was done 763c3437056SNickeau * @throws Exception 764c3437056SNickeau */ 765c3437056SNickeau private 766c3437056SNickeau function executeTransparentRedirect(string $targetPageId, string $targetOriginId): bool 767c3437056SNickeau { 768c3437056SNickeau /** 769c3437056SNickeau * Because we set the ID globally for the ID redirect 77004fd306cSNickeau * we make sure that this is not a {@link MarkupPath} 771c3437056SNickeau * object otherwise we got an error in the {@link \ComboStrap\AnalyticsMenuItem} 772c3437056SNickeau * because the constructor takes it {@link \dokuwiki\Menu\Item\AbstractItem} 773c3437056SNickeau */ 774c3437056SNickeau if (is_object($targetPageId)) { 775c3437056SNickeau $class = get_class($targetPageId); 776c3437056SNickeau LogUtility::msg("The parameters targetPageId ($targetPageId) is an object of the class ($class) and it should be a page id"); 777c3437056SNickeau } 778c3437056SNickeau 779c3437056SNickeau if (is_object($targetOriginId)) { 780c3437056SNickeau $class = get_class($targetOriginId); 781c3437056SNickeau LogUtility::msg("The parameters targetOriginId ($targetOriginId) is an object of the class ($class) and it should be a page id"); 782c3437056SNickeau } 783c3437056SNickeau 784c3437056SNickeau // If the user does not have the right to see the target page 785c3437056SNickeau // don't do anything 786c3437056SNickeau if (!(Identity::isReader($targetPageId))) { 787c3437056SNickeau return false; 788c3437056SNickeau } 789c3437056SNickeau 790c3437056SNickeau // Change the id 791c3437056SNickeau global $ID; 792c3437056SNickeau global $INFO; 793c3437056SNickeau $sourceId = $ID; 794c3437056SNickeau $ID = $targetPageId; 79504fd306cSNickeau if (isset($_REQUEST["id"])) { 79604fd306cSNickeau $_REQUEST["id"] = $targetPageId; 79704fd306cSNickeau } 79804fd306cSNickeau if (isset($_GET["id"])) { 79904fd306cSNickeau $_GET["id"] = $targetPageId; 80004fd306cSNickeau } 8014cadd4f8SNickeau 802c3437056SNickeau /** 8034cadd4f8SNickeau * Refresh the $INFO data 8044cadd4f8SNickeau * 8054cadd4f8SNickeau * the info attributes are used elsewhere 8064cadd4f8SNickeau * 'id': for the sidebar 8074cadd4f8SNickeau * 'exist' : for the meta robot = noindex,follow, see {@link tpl_metaheaders()} 8084cadd4f8SNickeau * 'rev' : for the edit button to be sure that the page is still the same 809c3437056SNickeau */ 8104cadd4f8SNickeau $INFO = pageinfo(); 811c3437056SNickeau 812c3437056SNickeau /** 813c3437056SNickeau * Not compatible with 814c3437056SNickeau * https://www.dokuwiki.org/config:send404 is enabled 815c3437056SNickeau * 816c3437056SNickeau * This check happens before that dokuwiki is started 817c3437056SNickeau * and send an header in doku.php 818c3437056SNickeau * 819c3437056SNickeau * We send a warning 820c3437056SNickeau */ 821c3437056SNickeau global $conf; 822c3437056SNickeau if ($conf['send404'] == true) { 823c3437056SNickeau LogUtility::msg("The <a href=\"https://www.dokuwiki.org/config:send404\">dokuwiki send404 configuration</a> is on and should be disabled when using the url manager", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 824c3437056SNickeau } 825c3437056SNickeau 826c3437056SNickeau // Redirection 827c3437056SNickeau $this->logRedirection($sourceId, $targetPageId, $targetOriginId, self::REDIRECT_TRANSPARENT_METHOD); 828c3437056SNickeau 829c3437056SNickeau return true; 830c3437056SNickeau 831c3437056SNickeau } 832c3437056SNickeau 83304fd306cSNickeau private function executePermanentRedirect(string $targetIdOrUrl, $targetOrigin): bool 834c3437056SNickeau { 83504fd306cSNickeau return $this->executeHttpRedirect($targetIdOrUrl, $targetOrigin, self::REDIRECT_PERMANENT_METHOD); 836c3437056SNickeau } 837c3437056SNickeau 838c3437056SNickeau /** 839c3437056SNickeau * The general HTTP Redirect method to an internal page 840c3437056SNickeau * where the redirection method decide which type of redirection 84104fd306cSNickeau * @param string $targetIdOrUrl - a dokuwiki id or an url 842c3437056SNickeau * @param string $targetOrigin - the origin of the target (the algorithm used to get the target origin) 843c3437056SNickeau * @param string $method - the redirection method 844c3437056SNickeau */ 845c3437056SNickeau private 84604fd306cSNickeau function executeHttpRedirect(string $targetIdOrUrl, string $targetOrigin, string $method): bool 847c3437056SNickeau { 848c3437056SNickeau 849c3437056SNickeau global $ID; 850c3437056SNickeau 851c3437056SNickeau 852c3437056SNickeau // Log the redirections 85304fd306cSNickeau $this->logRedirection($ID, $targetIdOrUrl, $targetOrigin, $method); 854c3437056SNickeau 855c3437056SNickeau 85604fd306cSNickeau // An http external url ? 85704fd306cSNickeau try { 8585b0932efSgerardnico $isHttpUrl = Url::createFromString($targetIdOrUrl)->isHttpUrl(); 85904fd306cSNickeau } catch (ExceptionBadSyntax|ExceptionBadArgument $e) { 8605b0932efSgerardnico $isHttpUrl = false; 86104fd306cSNickeau } 86204fd306cSNickeau 86382a60d03SNickeau // If there is a bug in the isValid function for an internal url 86482a60d03SNickeau // We get a loop. 86582a60d03SNickeau // The Url becomes the id, the id is unknown and we do a redirect again 86682a60d03SNickeau // 86782a60d03SNickeau // We check then if the target starts with the base url 86882a60d03SNickeau // if this is the case, it's valid 8695b0932efSgerardnico if (!$isHttpUrl && strpos($targetIdOrUrl, DOKU_URL) === 0) { 8705b0932efSgerardnico $isHttpUrl = true; 87182a60d03SNickeau } 8725b0932efSgerardnico if ($isHttpUrl) { 873c3437056SNickeau 874c3437056SNickeau // defend against HTTP Response Splitting 875c3437056SNickeau // https://owasp.org/www-community/attacks/HTTP_Response_Splitting 87604fd306cSNickeau $targetUrl = stripctl($targetIdOrUrl); 877c3437056SNickeau 878c3437056SNickeau } else { 879c3437056SNickeau 880c3437056SNickeau 881c3437056SNickeau // Explode the page ID and the anchor (#) 88204fd306cSNickeau $link = explode('#', $targetIdOrUrl, 2); 883c3437056SNickeau 88411d09b86Sgerardnico $url = UrlEndpoint::createDokuUrl(); 885c3437056SNickeau 8865b0932efSgerardnico $urlParams = []; 887c3437056SNickeau // if this is search engine redirect 888c3437056SNickeau if ($targetOrigin == self::TARGET_ORIGIN_SEARCH_ENGINE) { 889c3437056SNickeau $replacementPart = array(':', '_', '-'); 890c3437056SNickeau $query = str_replace($replacementPart, ' ', $ID); 89111d09b86Sgerardnico $url->setQueryParameter(ExecutionContext::DO_ATTRIBUTE, ExecutionContext::SEARCH_ACTION); 89211d09b86Sgerardnico $url->setQueryParameter("q", $query); 893c3437056SNickeau } 894c3437056SNickeau 8955b0932efSgerardnico /** 8965b0932efSgerardnico * Doing a permanent redirect with a added query string 8975b0932efSgerardnico * create a new page url on the search engine 8985b0932efSgerardnico * 8995b0932efSgerardnico * ie 9005b0932efSgerardnico * http://host/page 9015b0932efSgerardnico * is not the same 9025b0932efSgerardnico * than 9035b0932efSgerardnico * http://host/page?whatever 9045b0932efSgerardnico * 9055b0932efSgerardnico * We can't pass query string otherwise, we get 90649b8fb24Sgerardnico * the SEO warning / error 9075b0932efSgerardnico * `Alternative page with proper canonical tag` 90811d09b86Sgerardnico * 90911d09b86Sgerardnico * Use HTTP X header for debug 9105b0932efSgerardnico */ 9115b0932efSgerardnico if ($method !== self::REDIRECT_PERMANENT_METHOD) { 91211d09b86Sgerardnico $url->setQueryParameter(action_plugin_combo_routermessage::ORIGIN_PAGE, $ID); 91311d09b86Sgerardnico $url->setQueryParameter(action_plugin_combo_routermessage::ORIGIN_TYPE, $targetOrigin); 9145b0932efSgerardnico } 9155b0932efSgerardnico 91611d09b86Sgerardnico $id = $link[0]; 91711d09b86Sgerardnico $url->setQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $id); 91870bbd7f1Sgerardnico if (array_key_exists(1, $link)) { 91911d09b86Sgerardnico $url->setFragment($link[1]); 920c3437056SNickeau } 92111d09b86Sgerardnico $targetUrl = $url->toAbsoluteUrlString(); 922c3437056SNickeau 923c3437056SNickeau } 924c3437056SNickeau 925c3437056SNickeau /** 926c3437056SNickeau * The dokuwiki function {@link send_redirect()} 927c3437056SNickeau * set the `Location header` and in php, the header function 928c3437056SNickeau * in this case change the status code to 302 Arghhhh. 929c3437056SNickeau * The code below is adapted from this function {@link send_redirect()} 930c3437056SNickeau */ 931c3437056SNickeau global $MSG; // are there any undisplayed messages? keep them in session for display 932c3437056SNickeau if (isset($MSG) && count($MSG) && !defined('NOSESSION')) { 933c3437056SNickeau //reopen session, store data and close session again 934c3437056SNickeau @session_start(); 935c3437056SNickeau $_SESSION[DOKU_COOKIE]['msg'] = $MSG; 936c3437056SNickeau } 937c3437056SNickeau session_write_close(); // always close the session 938c3437056SNickeau 939c3437056SNickeau switch ($method) { 9405b0932efSgerardnico 941c3437056SNickeau case self::REDIRECT_PERMANENT_METHOD: 94204fd306cSNickeau ExecutionContext::getActualOrCreateFromEnv() 94304fd306cSNickeau ->response() 94404fd306cSNickeau ->setStatus(HttpResponseStatus::PERMANENT_REDIRECT) 945c3437056SNickeau ->addHeader(self::LOCATION_HEADER_PREFIX . $targetUrl) 94604fd306cSNickeau ->end(); 947c3437056SNickeau return true; 9485b0932efSgerardnico 949c3437056SNickeau case self::REDIRECT_NOTFOUND_METHOD: 950c3437056SNickeau 9515b0932efSgerardnico 952c3437056SNickeau // Empty 404 body to not get the standard 404 page of the browser 953c3437056SNickeau // but a blank page to avoid a sort of FOUC. 954c3437056SNickeau // ie the user see a page briefly 95504fd306cSNickeau ExecutionContext::getActualOrCreateFromEnv() 95604fd306cSNickeau ->response() 95704fd306cSNickeau ->setStatus(HttpResponseStatus::NOT_FOUND) 958c3437056SNickeau ->addHeader(self::REFRESH_HEADER_PREFIX . $targetUrl) 95904fd306cSNickeau ->setBody(self::PAGE_404, Mime::getHtml()) 96004fd306cSNickeau ->end(); 961c3437056SNickeau return true; 962c3437056SNickeau 963c3437056SNickeau default: 964c3437056SNickeau LogUtility::msg("The method ($method) is not an http redirection"); 965c3437056SNickeau return false; 966c3437056SNickeau } 967c3437056SNickeau 968c3437056SNickeau 969c3437056SNickeau } 970c3437056SNickeau 971c3437056SNickeau /** 972c3437056SNickeau * @param $id 973c3437056SNickeau * @return array 974c3437056SNickeau */ 975c3437056SNickeau private 976c3437056SNickeau function getBestPage($id): array 977c3437056SNickeau { 978c3437056SNickeau 979c3437056SNickeau // The return parameters 980c3437056SNickeau $bestPageId = null; 981c3437056SNickeau $scorePageName = null; 982c3437056SNickeau 983c3437056SNickeau // Get Score from a page 984c3437056SNickeau $pageName = noNS($id); 985c3437056SNickeau $pagesWithSameName = ft_pageLookup($pageName); 986c3437056SNickeau if (count($pagesWithSameName) > 0) { 987c3437056SNickeau 988c3437056SNickeau // Search same namespace in the page found than in the Id page asked. 989c3437056SNickeau $bestNbWordFound = 0; 990c3437056SNickeau 991c3437056SNickeau 992c3437056SNickeau $wordsInPageSourceId = explode(':', $id); 993c3437056SNickeau foreach ($pagesWithSameName as $targetPageId => $title) { 994c3437056SNickeau 995c3437056SNickeau // Nb of word found in the target page id 996c3437056SNickeau // that are in the source page id 997c3437056SNickeau $nbWordFound = 0; 998c3437056SNickeau foreach ($wordsInPageSourceId as $word) { 999c3437056SNickeau $nbWordFound = $nbWordFound + substr_count($targetPageId, $word); 1000c3437056SNickeau } 1001c3437056SNickeau 1002c3437056SNickeau if ($bestPageId == null) { 1003c3437056SNickeau 1004c3437056SNickeau $bestNbWordFound = $nbWordFound; 1005c3437056SNickeau $bestPageId = $targetPageId; 1006c3437056SNickeau 1007c3437056SNickeau } else { 1008c3437056SNickeau 1009c3437056SNickeau if ($nbWordFound >= $bestNbWordFound && strlen($bestPageId) > strlen($targetPageId)) { 1010c3437056SNickeau 1011c3437056SNickeau $bestNbWordFound = $nbWordFound; 1012c3437056SNickeau $bestPageId = $targetPageId; 1013c3437056SNickeau 1014c3437056SNickeau } 1015c3437056SNickeau 1016c3437056SNickeau } 1017c3437056SNickeau 1018c3437056SNickeau } 1019c3437056SNickeau $scorePageName = $this->getConf('WeightFactorForSamePageName') + ($bestNbWordFound - 1) * $this->getConf('WeightFactorForSameNamespace'); 1020c3437056SNickeau return array( 1021c3437056SNickeau 'id' => $bestPageId, 1022c3437056SNickeau 'score' => $scorePageName); 1023c3437056SNickeau } 1024c3437056SNickeau return array( 1025c3437056SNickeau 'id' => $bestPageId, 1026c3437056SNickeau 'score' => $scorePageName 1027c3437056SNickeau ); 1028c3437056SNickeau 1029c3437056SNickeau } 1030c3437056SNickeau 1031c3437056SNickeau 1032c3437056SNickeau /** 1033c3437056SNickeau * Redirect to the search engine 1034c3437056SNickeau */ 1035c3437056SNickeau private 1036c3437056SNickeau function redirectToSearchEngine() 1037c3437056SNickeau { 1038c3437056SNickeau 1039c3437056SNickeau global $ID; 1040c3437056SNickeau $this->performNotFoundRedirect($ID, self::TARGET_ORIGIN_SEARCH_ENGINE); 1041c3437056SNickeau 1042c3437056SNickeau } 1043c3437056SNickeau 1044c3437056SNickeau 1045c3437056SNickeau /** 1046c3437056SNickeau * 1047c3437056SNickeau * * For a conf file, it will update the Redirection Action Data as Referrer, Count Of Redirection, Redirection Date 1048c3437056SNickeau * * For a SQlite database, it will add a row into the log 1049c3437056SNickeau * 1050c3437056SNickeau * @param string $sourcePageId 1051c3437056SNickeau * @param $targetPageId 1052c3437056SNickeau * @param $algorithmic 1053c3437056SNickeau * @param $method - http or rewrite 1054c3437056SNickeau */ 1055c3437056SNickeau function logRedirection(string $sourcePageId, $targetPageId, $algorithmic, $method) 1056c3437056SNickeau { 1057c3437056SNickeau 1058c3437056SNickeau $row = array( 1059c3437056SNickeau "TIMESTAMP" => date("c"), 1060c3437056SNickeau "SOURCE" => $sourcePageId, 1061c3437056SNickeau "TARGET" => $targetPageId, 106270bbd7f1Sgerardnico "REFERRER" => $_SERVER['HTTP_REFERER'] ?? null, 1063c3437056SNickeau "TYPE" => $algorithmic, 1064c3437056SNickeau "METHOD" => $method 1065c3437056SNickeau ); 1066c3437056SNickeau $request = Sqlite::createOrGetBackendSqlite() 1067c3437056SNickeau ->createRequest() 1068c3437056SNickeau ->setTableRow('redirections_log', $row); 1069c3437056SNickeau try { 1070c3437056SNickeau $request 1071c3437056SNickeau ->execute(); 107204fd306cSNickeau } catch (ExceptionCompile $e) { 1073c3437056SNickeau LogUtility::msg("Redirection Log Insert Error. {$e->getMessage()}"); 1074c3437056SNickeau } finally { 1075c3437056SNickeau $request->close(); 1076c3437056SNickeau } 1077c3437056SNickeau 1078c3437056SNickeau 1079c3437056SNickeau } 1080c3437056SNickeau 1081c3437056SNickeau /** 1082c3437056SNickeau * This function check if there is a redirection declared 1083c3437056SNickeau * in the redirection table 1084c3437056SNickeau * @return bool - true if a rewrite or redirection occurs 1085c3437056SNickeau * @throws Exception 1086c3437056SNickeau */ 1087c3437056SNickeau private function processingPageRules(): bool 1088c3437056SNickeau { 1089c3437056SNickeau global $ID; 1090c3437056SNickeau 1091c3437056SNickeau $calculatedTarget = null; 1092c3437056SNickeau $ruleMatcher = null; // Used in a warning message if the target page does not exist 1093c3437056SNickeau // Known redirection in the table 1094c3437056SNickeau // Get the page from redirection data 1095c3437056SNickeau $rules = $this->pageRules->getRules(); 1096c3437056SNickeau foreach ($rules as $rule) { 1097c3437056SNickeau 1098c3437056SNickeau $ruleMatcher = strtolower($rule[PageRules::MATCHER_NAME]); 1099c3437056SNickeau $ruleTarget = $rule[PageRules::TARGET_NAME]; 1100c3437056SNickeau 1101c3437056SNickeau // Glob to Rexgexp 110204fd306cSNickeau $regexpPattern = '/' . str_replace("*", "(.*)", $ruleMatcher) . '/i'; 1103c3437056SNickeau 1104c3437056SNickeau // Match ? 1105c3437056SNickeau // https://www.php.net/manual/en/function.preg-match.php 110686867397Sgerardnico $pregMatchResult = @preg_match($regexpPattern, $ID, $matches); 110786867397Sgerardnico if ($pregMatchResult === false) { 110886867397Sgerardnico // The `if` to take into account this problem 110986867397Sgerardnico // PHP Warning: preg_match(): Unknown modifier 'd' in /opt/www/datacadamia.com/lib/plugins/combo/action/router.php on line 972 111086867397Sgerardnico LogUtility::log2file("processing Page Rules An error occurred with the pattern ($regexpPattern)", LogUtility::LVL_MSG_WARNING); 111186867397Sgerardnico return false; 111286867397Sgerardnico } 111386867397Sgerardnico if ($pregMatchResult) { 1114c3437056SNickeau $calculatedTarget = $ruleTarget; 1115c3437056SNickeau foreach ($matches as $key => $match) { 1116c3437056SNickeau if ($key == 0) { 1117c3437056SNickeau continue; 1118c3437056SNickeau } else { 1119c3437056SNickeau $calculatedTarget = str_replace('$' . $key, $match, $calculatedTarget); 1120c3437056SNickeau } 1121c3437056SNickeau } 1122c3437056SNickeau break; 1123c3437056SNickeau } 1124c3437056SNickeau } 1125c3437056SNickeau 1126c3437056SNickeau if ($calculatedTarget == null) { 1127c3437056SNickeau return false; 1128c3437056SNickeau } 1129c3437056SNickeau 1130c3437056SNickeau // If this is an external redirect (other domain) 113104fd306cSNickeau try { 113204fd306cSNickeau $isHttpUrl = Url::createFromString($calculatedTarget)->isHttpUrl(); 113304fd306cSNickeau } catch (ExceptionBadSyntax $e) { 113404fd306cSNickeau $isHttpUrl = false; 113504fd306cSNickeau } 113604fd306cSNickeau if ($isHttpUrl) { 1137c3437056SNickeau $this->executeHttpRedirect($calculatedTarget, self::TARGET_ORIGIN_PAGE_RULES, self::REDIRECT_PERMANENT_METHOD); 1138c3437056SNickeau return true; 1139c3437056SNickeau } 1140c3437056SNickeau 1141c3437056SNickeau // If the page exist 1142c3437056SNickeau if (page_exists($calculatedTarget)) { 1143c3437056SNickeau 1144c3437056SNickeau // This is DokuWiki Id and should always be lowercase 1145c3437056SNickeau // The page rule may have change that 1146c3437056SNickeau $calculatedTarget = strtolower($calculatedTarget); 1147d0abe7fcSgerardnico $res = $this->executeHttpRedirect($calculatedTarget, self::TARGET_ORIGIN_PAGE_RULES, self::REDIRECT_PERMANENT_METHOD); 1148c3437056SNickeau if ($res) { 1149c3437056SNickeau return true; 1150c3437056SNickeau } else { 1151c3437056SNickeau return false; 1152c3437056SNickeau } 1153c3437056SNickeau 1154c3437056SNickeau } else { 1155c3437056SNickeau 1156c3437056SNickeau LogUtility::msg("The calculated target page ($calculatedTarget) (for the non-existing page `$ID` with the matcher `$ruleMatcher`) does not exist", LogUtility::LVL_MSG_ERROR); 1157c3437056SNickeau return false; 1158c3437056SNickeau 1159c3437056SNickeau } 1160c3437056SNickeau 1161c3437056SNickeau } 1162c3437056SNickeau 1163c3437056SNickeau private function performNotFoundRedirect(string $targetId, string $origin): bool 1164c3437056SNickeau { 1165c3437056SNickeau return $this->executeHttpRedirect($targetId, $origin, self::REDIRECT_NOTFOUND_METHOD); 1166c3437056SNickeau } 1167c3437056SNickeau 1168c3437056SNickeau 1169c3437056SNickeau} 1170