xref: /plugin/combo/action/router.php (revision b1aef5347afbf2a2acc6b03406098007d8ba91ef)
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