xref: /plugin/combo/action/router.php (revision c3437056399326d621a01da73b649707fbb0ae69)
1*c3437056SNickeau<?php
2*c3437056SNickeau
3*c3437056SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
4*c3437056SNickeau
5*c3437056SNickeau
6*c3437056SNickeauuse ComboStrap\Alias;
7*c3437056SNickeauuse ComboStrap\AliasType;
8*c3437056SNickeauuse ComboStrap\DatabasePageRow;
9*c3437056SNickeauuse ComboStrap\DokuPath;
10*c3437056SNickeauuse ComboStrap\ExceptionCombo;
11*c3437056SNickeauuse ComboStrap\HttpResponse;
12*c3437056SNickeauuse ComboStrap\Identity;
13*c3437056SNickeauuse ComboStrap\LogUtility;
14*c3437056SNickeauuse ComboStrap\Mime;
15*c3437056SNickeauuse ComboStrap\Page;
16*c3437056SNickeauuse ComboStrap\PageId;
17*c3437056SNickeauuse ComboStrap\PageRules;
18*c3437056SNickeauuse ComboStrap\PageUrlPath;
19*c3437056SNickeauuse ComboStrap\PluginUtility;
20*c3437056SNickeauuse ComboStrap\Site;
21*c3437056SNickeauuse ComboStrap\Sqlite;
22*c3437056SNickeauuse ComboStrap\Url;
23*c3437056SNickeauuse ComboStrap\UrlManagerBestEndPage;
24*c3437056SNickeauuse ComboStrap\PageUrlType;
25*c3437056SNickeau
26*c3437056SNickeau
27*c3437056SNickeau/**
28*c3437056SNickeau * Class action_plugin_combo_url
29*c3437056SNickeau *
30*c3437056SNickeau * The actual URL manager
31*c3437056SNickeau *
32*c3437056SNickeau *
33*c3437056SNickeau */
34*c3437056SNickeauclass action_plugin_combo_router extends DokuWiki_Action_Plugin
35*c3437056SNickeau{
36*c3437056SNickeau
37*c3437056SNickeau    /**
38*c3437056SNickeau     * @deprecated
39*c3437056SNickeau     */
40*c3437056SNickeau    const URL_MANAGER_ENABLE_CONF = "enableUrlManager";
41*c3437056SNickeau    const ROUTER_ENABLE_CONF = "enableRouter";
42*c3437056SNickeau
43*c3437056SNickeau    // The redirect type
44*c3437056SNickeau    const REDIRECT_TRANSPARENT_METHOD = 'transparent'; // was (Id)
45*c3437056SNickeau    // For permanent, see https://developers.google.com/search/docs/advanced/crawling/301-redirects
46*c3437056SNickeau    const REDIRECT_PERMANENT_METHOD = 'permanent'; // was `Http` (301)
47*c3437056SNickeau    const REDIRECT_NOTFOUND_METHOD = "notfound"; // 404 (See other) (when best page name is calculated)
48*c3437056SNickeau
49*c3437056SNickeau    public const PERMANENT_REDIRECT_CANONICAL = "permanent:redirect";
50*c3437056SNickeau
51*c3437056SNickeau    // Where the target id value comes from
52*c3437056SNickeau    const TARGET_ORIGIN_WELL_KNOWN = 'well-known';
53*c3437056SNickeau    const TARGET_ORIGIN_PAGE_RULES = 'pageRules';
54*c3437056SNickeau    /**
55*c3437056SNickeau     * Named Permalink (canonical)
56*c3437056SNickeau     */
57*c3437056SNickeau    const TARGET_ORIGIN_CANONICAL = 'canonical';
58*c3437056SNickeau    const TARGET_ORIGIN_ALIAS = 'alias';
59*c3437056SNickeau    /**
60*c3437056SNickeau     * Identifier Permalink (full page id)
61*c3437056SNickeau     */
62*c3437056SNickeau    const TARGET_ORIGIN_PERMALINK = "permalink";
63*c3437056SNickeau    /**
64*c3437056SNickeau     * Extended Permalink (abbreviated page id at the end)
65*c3437056SNickeau     */
66*c3437056SNickeau    const TARGET_ORIGIN_PERMALINK_EXTENDED = "extendedPermalink";
67*c3437056SNickeau    const TARGET_ORIGIN_START_PAGE = 'startPage';
68*c3437056SNickeau    const TARGET_ORIGIN_BEST_PAGE_NAME = 'bestPageName';
69*c3437056SNickeau    const TARGET_ORIGIN_BEST_NAMESPACE = 'bestNamespace';
70*c3437056SNickeau    const TARGET_ORIGIN_SEARCH_ENGINE = 'searchEngine';
71*c3437056SNickeau    const TARGET_ORIGIN_BEST_END_PAGE_NAME = 'bestEndPageName';
72*c3437056SNickeau    const TARGET_ORIGIN_SHADOW_BANNED = "shadowBanned";
73*c3437056SNickeau
74*c3437056SNickeau
75*c3437056SNickeau    // The constant parameters
76*c3437056SNickeau    const GO_TO_SEARCH_ENGINE = 'GoToSearchEngine';
77*c3437056SNickeau    const GO_TO_BEST_NAMESPACE = 'GoToBestNamespace';
78*c3437056SNickeau    const GO_TO_BEST_PAGE_NAME = 'GoToBestPageName';
79*c3437056SNickeau    const GO_TO_BEST_END_PAGE_NAME = 'GoToBestEndPageName';
80*c3437056SNickeau    const GO_TO_NS_START_PAGE = 'GoToNsStartPage';
81*c3437056SNickeau    const GO_TO_EDIT_MODE = 'GoToEditMode';
82*c3437056SNickeau    const NOTHING = 'Nothing';
83*c3437056SNickeau
84*c3437056SNickeau    /** @var string - a name used in log and other places */
85*c3437056SNickeau    const NAME = 'Url Manager';
86*c3437056SNickeau    const CANONICAL = 'router';
87*c3437056SNickeau    const PAGE_404 = "<html lang=\"en\"><body></body></html>";
88*c3437056SNickeau    const REFRESH_HEADER_NAME = "Refresh";
89*c3437056SNickeau    const REFRESH_HEADER_PREFIX = self::REFRESH_HEADER_NAME . ': 0;url=';
90*c3437056SNickeau    const LOCATION_HEADER_NAME = "Location";
91*c3437056SNickeau    const LOCATION_HEADER_PREFIX = self::LOCATION_HEADER_NAME . ": ";
92*c3437056SNickeau    public const URL_MANAGER_NAME = "Router";
93*c3437056SNickeau
94*c3437056SNickeau
95*c3437056SNickeau    /**
96*c3437056SNickeau     * @var PageRules
97*c3437056SNickeau     */
98*c3437056SNickeau    private $pageRules;
99*c3437056SNickeau
100*c3437056SNickeau
101*c3437056SNickeau    function __construct()
102*c3437056SNickeau    {
103*c3437056SNickeau        // enable direct access to language strings
104*c3437056SNickeau        // ie $this->lang
105*c3437056SNickeau        $this->setupLocale();
106*c3437056SNickeau
107*c3437056SNickeau    }
108*c3437056SNickeau
109*c3437056SNickeau    /**
110*c3437056SNickeau     * @param $refreshHeader
111*c3437056SNickeau     * @return false|string
112*c3437056SNickeau     */
113*c3437056SNickeau    public static function getUrlFromRefresh($refreshHeader)
114*c3437056SNickeau    {
115*c3437056SNickeau        return substr($refreshHeader, strlen(action_plugin_combo_router::REFRESH_HEADER_PREFIX));
116*c3437056SNickeau    }
117*c3437056SNickeau
118*c3437056SNickeau    public static function getUrlFromLocation($refreshHeader)
119*c3437056SNickeau    {
120*c3437056SNickeau        return substr($refreshHeader, strlen(action_plugin_combo_router::LOCATION_HEADER_PREFIX));
121*c3437056SNickeau    }
122*c3437056SNickeau
123*c3437056SNickeau    /**
124*c3437056SNickeau     * @return array|mixed|string|string[]
125*c3437056SNickeau     *
126*c3437056SNickeau     * Unfortunately, DOKUWIKI_STARTED is not the first event
127*c3437056SNickeau     * The id may have been changed by
128*c3437056SNickeau     * {@link action_plugin_combo_metalang::load_lang()}
129*c3437056SNickeau     * function, that's why we have this function
130*c3437056SNickeau     * to get the original requested id
131*c3437056SNickeau     */
132*c3437056SNickeau    private static function getOriginalIdFromRequest()
133*c3437056SNickeau    {
134*c3437056SNickeau        $originalId = $_GET["id"];
135*c3437056SNickeau        return str_replace("/", DokuPath::PATH_SEPARATOR, $originalId);
136*c3437056SNickeau    }
137*c3437056SNickeau
138*c3437056SNickeau    /**
139*c3437056SNickeau     * Determine if the request should be banned based on the id
140*c3437056SNickeau     *
141*c3437056SNickeau     * @param string $id
142*c3437056SNickeau     * @return bool
143*c3437056SNickeau     *
144*c3437056SNickeau     * See also {@link https://perishablepress.com/7g-firewall/#features}
145*c3437056SNickeau     * for blocking rules on http request data such as:
146*c3437056SNickeau     *   * query_string
147*c3437056SNickeau     *   * user_agent,
148*c3437056SNickeau     *   * remote host
149*c3437056SNickeau     */
150*c3437056SNickeau    public static function isShadowBanned(string $id): bool
151*c3437056SNickeau    {
152*c3437056SNickeau        /**
153*c3437056SNickeau         * ie
154*c3437056SNickeau         * wp-json:api:flutter_woo:config_file
155*c3437056SNickeau         * wp-content:plugins:wpdiscuz:themes:default:style-rtl.css
156*c3437056SNickeau         * wp-admin
157*c3437056SNickeau         * 2020:wp-includes:wlwmanifest.xml
158*c3437056SNickeau         * wp-content:start
159*c3437056SNickeau         * wp-admin:css:start
160*c3437056SNickeau         * sito:wp-includes:wlwmanifest.xml
161*c3437056SNickeau         * site:wp-includes:wlwmanifest.xml
162*c3437056SNickeau         * cms:wp-includes:wlwmanifest.xml
163*c3437056SNickeau         * test:wp-includes:wlwmanifest.xml
164*c3437056SNickeau         * media:wp-includes:wlwmanifest.xml
165*c3437056SNickeau         * wp2:wp-includes:wlwmanifest.xml
166*c3437056SNickeau         * 2019:wp-includes:wlwmanifest.xml
167*c3437056SNickeau         * shop:wp-includes:wlwmanifest.xml
168*c3437056SNickeau         * wp1:wp-includes:wlwmanifest.xml
169*c3437056SNickeau         * news:wp-includes:wlwmanifest.xml
170*c3437056SNickeau         * 2018:wp-includes:wlwmanifest.xml
171*c3437056SNickeau         */
172*c3437056SNickeau        if (strpos($id, 'wp-') !== false) {
173*c3437056SNickeau            return true;
174*c3437056SNickeau        }
175*c3437056SNickeau
176*c3437056SNickeau        /**
177*c3437056SNickeau         * 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
178*c3437056SNickeau         * db:oracle:999999.9:union:all:select_null:from_dual
179*c3437056SNickeau         * db:oracle:999999.9:union:all:select_null:from_dual_and_0_0
180*c3437056SNickeau         */
181*c3437056SNickeau        if (preg_match('/_chr_|_0_0/', $id) === 1) {
182*c3437056SNickeau            return true;
183*c3437056SNickeau        }
184*c3437056SNickeau
185*c3437056SNickeau
186*c3437056SNickeau        /**
187*c3437056SNickeau         * ie
188*c3437056SNickeau         * git:objects:
189*c3437056SNickeau         * git:refs:heads:stable
190*c3437056SNickeau         * git:logs:refs:heads:main
191*c3437056SNickeau         * git:logs:refs:heads:stable
192*c3437056SNickeau         * git:hooks:pre-push.sample
193*c3437056SNickeau         * git:hooks:pre-receive.sample
194*c3437056SNickeau         */
195*c3437056SNickeau        if (strpos($id, "git:") === 0) {
196*c3437056SNickeau            return true;
197*c3437056SNickeau        }
198*c3437056SNickeau
199*c3437056SNickeau        return false;
200*c3437056SNickeau
201*c3437056SNickeau    }
202*c3437056SNickeau
203*c3437056SNickeau    /**
204*c3437056SNickeau     * @param string $id
205*c3437056SNickeau     * @return bool
206*c3437056SNickeau     * well-known:traffic-advice = https://github.com/buettner/private-prefetch-proxy/blob/main/traffic-advice.md
207*c3437056SNickeau     * .well-known/security.txt, id=well-known:security.txt = https://securitytxt.org/
208*c3437056SNickeau     * well-known:dnt-policy.txt
209*c3437056SNickeau     */
210*c3437056SNickeau    public static function isWellKnownFile(string $id): bool
211*c3437056SNickeau    {
212*c3437056SNickeau        return strpos($id, "well-known") === 0;
213*c3437056SNickeau    }
214*c3437056SNickeau
215*c3437056SNickeau
216*c3437056SNickeau    function register(Doku_Event_Handler $controller)
217*c3437056SNickeau    {
218*c3437056SNickeau
219*c3437056SNickeau        if (PluginUtility::getConfValue(self::ROUTER_ENABLE_CONF, 1)) {
220*c3437056SNickeau            /**
221*c3437056SNickeau             * This will call the function {@link action_plugin_combo_router::_router()}
222*c3437056SNickeau             * The event is not DOKUWIKI_STARTED because this is not the first one
223*c3437056SNickeau             *
224*c3437056SNickeau             * https://www.dokuwiki.org/devel:event:init_lang_load
225*c3437056SNickeau             */
226*c3437056SNickeau            $controller->register_hook('DOKUWIKI_STARTED',
227*c3437056SNickeau                'AFTER',
228*c3437056SNickeau                $this,
229*c3437056SNickeau                'router',
230*c3437056SNickeau                array());
231*c3437056SNickeau
232*c3437056SNickeau            /**
233*c3437056SNickeau             * This is the real first call of Dokuwiki
234*c3437056SNickeau             * Unfortunately, it does not create the environment
235*c3437056SNickeau             * We just ban to spare server resources
236*c3437056SNickeau             *
237*c3437056SNickeau             * https://www.dokuwiki.org/devel:event:init_lang_load
238*c3437056SNickeau             */
239*c3437056SNickeau            $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'ban', array());
240*c3437056SNickeau
241*c3437056SNickeau        }
242*c3437056SNickeau
243*c3437056SNickeau
244*c3437056SNickeau    }
245*c3437056SNickeau
246*c3437056SNickeau    /**
247*c3437056SNickeau     *
248*c3437056SNickeau     * We have created a spacial ban function that is
249*c3437056SNickeau     * called before the first function
250*c3437056SNickeau     * {@link action_plugin_combo_metalang::load_lang()}
251*c3437056SNickeau     * to spare CPU.
252*c3437056SNickeau     *
253*c3437056SNickeau     * @param $event
254*c3437056SNickeau     * @throws Exception
255*c3437056SNickeau     */
256*c3437056SNickeau    function ban(&$event)
257*c3437056SNickeau    {
258*c3437056SNickeau
259*c3437056SNickeau        $id = self::getOriginalIdFromRequest();
260*c3437056SNickeau        $page = Page::createPageFromId($id);
261*c3437056SNickeau        if (!$page->exists()) {
262*c3437056SNickeau            // Well known
263*c3437056SNickeau            if (self::isWellKnownFile($id)) {
264*c3437056SNickeau                $this->logRedirection($id, "", self::TARGET_ORIGIN_WELL_KNOWN, self::REDIRECT_NOTFOUND_METHOD);
265*c3437056SNickeau                HttpResponse::create(HttpResponse::STATUS_NOT_FOUND)
266*c3437056SNickeau                    ->send();
267*c3437056SNickeau                return;
268*c3437056SNickeau            }
269*c3437056SNickeau
270*c3437056SNickeau            // Shadow banned
271*c3437056SNickeau            if (self::isShadowBanned($id)) {
272*c3437056SNickeau                $webSiteHomePage = Site::getHomePageName();
273*c3437056SNickeau                $this->executeTransparentRedirect($webSiteHomePage, self::TARGET_ORIGIN_SHADOW_BANNED);
274*c3437056SNickeau            }
275*c3437056SNickeau        }
276*c3437056SNickeau    }
277*c3437056SNickeau
278*c3437056SNickeau    /**
279*c3437056SNickeau     * @param $event Doku_Event
280*c3437056SNickeau     * @param $param
281*c3437056SNickeau     * @return void
282*c3437056SNickeau     * @throws Exception
283*c3437056SNickeau     */
284*c3437056SNickeau    function router(&$event, $param)
285*c3437056SNickeau    {
286*c3437056SNickeau
287*c3437056SNickeau        global $ACT;
288*c3437056SNickeau        if ($ACT !== 'show') return;
289*c3437056SNickeau
290*c3437056SNickeau
291*c3437056SNickeau        global $ID;
292*c3437056SNickeau
293*c3437056SNickeau        /**
294*c3437056SNickeau         * Without SQLite, this module does not work further
295*c3437056SNickeau         */
296*c3437056SNickeau        $sqlite = Sqlite::createOrGetSqlite();
297*c3437056SNickeau        if ($sqlite == null) {
298*c3437056SNickeau            return;
299*c3437056SNickeau        } else {
300*c3437056SNickeau            $this->pageRules = new PageRules();
301*c3437056SNickeau        }
302*c3437056SNickeau
303*c3437056SNickeau        /**
304*c3437056SNickeau         * Unfortunately, DOKUWIKI_STARTED is not the first event
305*c3437056SNickeau         * The id may have been changed by
306*c3437056SNickeau         * {@link action_plugin_combo_metalang::load_lang()}
307*c3437056SNickeau         * function, that's why we check against the {@link $_REQUEST}
308*c3437056SNickeau         * and not the global ID
309*c3437056SNickeau         */
310*c3437056SNickeau        $originalId = self::getOriginalIdFromRequest();
311*c3437056SNickeau
312*c3437056SNickeau        /**
313*c3437056SNickeau         * Page is an existing id ?
314*c3437056SNickeau         */
315*c3437056SNickeau        $targetPage = Page::createPageFromId($ID);
316*c3437056SNickeau        if ($targetPage->exists()) {
317*c3437056SNickeau
318*c3437056SNickeau            /**
319*c3437056SNickeau             * If this is not the root home page
320*c3437056SNickeau             * and if the canonical id is the not the same,
321*c3437056SNickeau             * and if this is not a historical page (revision)
322*c3437056SNickeau             * redirect
323*c3437056SNickeau             */
324*c3437056SNickeau            if (
325*c3437056SNickeau                $originalId !== $targetPage->getUrlId() // The id may have been changed
326*c3437056SNickeau                && $ID != Site::getHomePageName()
327*c3437056SNickeau                && !isset($_REQUEST["rev"])
328*c3437056SNickeau            ) {
329*c3437056SNickeau                $this->executePermanentRedirect(
330*c3437056SNickeau                    $targetPage->getCanonicalUrl([], true),
331*c3437056SNickeau                    self::TARGET_ORIGIN_PERMALINK_EXTENDED
332*c3437056SNickeau                );
333*c3437056SNickeau            }
334*c3437056SNickeau            return;
335*c3437056SNickeau        }
336*c3437056SNickeau
337*c3437056SNickeau
338*c3437056SNickeau        $identifier = $ID;
339*c3437056SNickeau
340*c3437056SNickeau
341*c3437056SNickeau        /**
342*c3437056SNickeau         * Page Id Website / root Permalink ?
343*c3437056SNickeau         */
344*c3437056SNickeau        $shortPageId = PageUrlPath::getShortEncodedPageIdFromUrlId($targetPage->getPath()->getLastName());
345*c3437056SNickeau        if ($shortPageId !== null) {
346*c3437056SNickeau            $pageId = PageUrlPath::decodePageId($shortPageId);
347*c3437056SNickeau            if ($targetPage->getParentPage() === null && $pageId !== null) {
348*c3437056SNickeau                $page = DatabasePageRow::createFromPageId($pageId)->getPage();
349*c3437056SNickeau                if ($page !== null && $page->exists()) {
350*c3437056SNickeau                    $this->executePermanentRedirect($page->getCanonicalUrl(), self::TARGET_ORIGIN_PERMALINK);
351*c3437056SNickeau                }
352*c3437056SNickeau            }
353*c3437056SNickeau
354*c3437056SNickeau            /**
355*c3437056SNickeau             * Page Id Abbr ?
356*c3437056SNickeau             * {@link PageUrlType::CONF_CANONICAL_URL_TYPE}
357*c3437056SNickeau             */
358*c3437056SNickeau            if (
359*c3437056SNickeau                $pageId !== null
360*c3437056SNickeau            ) {
361*c3437056SNickeau                $page = DatabasePageRow::createFromPageIdAbbr($pageId)->getPage();
362*c3437056SNickeau                if ($page === null) {
363*c3437056SNickeau                    // or the length of the abbr has changed
364*c3437056SNickeau                    $databasePage = new DatabasePageRow();
365*c3437056SNickeau                    $row = $databasePage->getDatabaseRowFromAttribute("substr(" . PageId::PROPERTY_NAME . ", 1, " . strlen($pageId) . ")", $pageId);
366*c3437056SNickeau                    if ($row != null) {
367*c3437056SNickeau                        $databasePage->setRow($row);
368*c3437056SNickeau                        $page = $databasePage->getPage();
369*c3437056SNickeau                    }
370*c3437056SNickeau                }
371*c3437056SNickeau                if ($page !== null && $page->exists()) {
372*c3437056SNickeau                    /**
373*c3437056SNickeau                     * If the url canonical id has changed, we show it
374*c3437056SNickeau                     * to the writer by performing a permanent redirect
375*c3437056SNickeau                     */
376*c3437056SNickeau                    if ($identifier != $page->getUrlId()) {
377*c3437056SNickeau                        // Google asks for a redirect
378*c3437056SNickeau                        // https://developers.google.com/search/docs/advanced/crawling/301-redirects
379*c3437056SNickeau                        // People access your site through several different URLs.
380*c3437056SNickeau                        // If, for example, your home page can be reached in multiple ways
381*c3437056SNickeau                        // (for instance, http://example.com/home, http://home.example.com, or http://www.example.com),
382*c3437056SNickeau                        // it's a good idea to pick one of those URLs as your preferred (canonical) destination,
383*c3437056SNickeau                        // and use redirects to send traffic from the other URLs to your preferred URL.
384*c3437056SNickeau                        $this->executePermanentRedirect(
385*c3437056SNickeau                            $page->getCanonicalUrl([], true),
386*c3437056SNickeau                            self::TARGET_ORIGIN_PERMALINK_EXTENDED
387*c3437056SNickeau                        );
388*c3437056SNickeau                        return;
389*c3437056SNickeau                    }
390*c3437056SNickeau                    $this->executeTransparentRedirect($page->getDokuwikiId(), self::TARGET_ORIGIN_PERMALINK_EXTENDED);
391*c3437056SNickeau                    return;
392*c3437056SNickeau
393*c3437056SNickeau                }
394*c3437056SNickeau                // permanent url not yet in the database
395*c3437056SNickeau                // Other permanent such as permanent canonical ?
396*c3437056SNickeau                // We let the process go with the new identifier
397*c3437056SNickeau
398*c3437056SNickeau            }
399*c3437056SNickeau
400*c3437056SNickeau        }
401*c3437056SNickeau
402*c3437056SNickeau        // Global variable needed in the process
403*c3437056SNickeau        global $conf;
404*c3437056SNickeau
405*c3437056SNickeau        /**
406*c3437056SNickeau         * Identifier is a Canonical ?
407*c3437056SNickeau         */
408*c3437056SNickeau        $databasePage = DatabasePageRow::createFromCanonical($identifier);
409*c3437056SNickeau        $targetPage = $databasePage->getPage();
410*c3437056SNickeau        if ($targetPage !== null && $targetPage->exists()) {
411*c3437056SNickeau            $res = $this->executePermanentRedirect($targetPage->getDokuwikiId(), self::TARGET_ORIGIN_CANONICAL);
412*c3437056SNickeau            if ($res) {
413*c3437056SNickeau                return;
414*c3437056SNickeau            }
415*c3437056SNickeau        }
416*c3437056SNickeau
417*c3437056SNickeau        /**
418*c3437056SNickeau         * Identifier is an alias
419*c3437056SNickeau         */
420*c3437056SNickeau        $targetPage = DatabasePageRow::createFromAlias($identifier)->getPage();
421*c3437056SNickeau        if (
422*c3437056SNickeau            $targetPage !== null
423*c3437056SNickeau            && $targetPage->exists()
424*c3437056SNickeau            // The build alias is the file system metadata alias
425*c3437056SNickeau            // it may be null if the replication in the database was not successful
426*c3437056SNickeau            && $targetPage->getBuildAlias() !== null
427*c3437056SNickeau        ) {
428*c3437056SNickeau            $buildAlias = $targetPage->getBuildAlias();
429*c3437056SNickeau            switch ($buildAlias->getType()) {
430*c3437056SNickeau                case AliasType::REDIRECT:
431*c3437056SNickeau                    $res = $this->executePermanentRedirect($targetPage->getCanonicalUrl(), self::TARGET_ORIGIN_ALIAS);
432*c3437056SNickeau                    if ($res) {
433*c3437056SNickeau                        return;
434*c3437056SNickeau                    }
435*c3437056SNickeau                    break;
436*c3437056SNickeau                case AliasType::SYNONYM:
437*c3437056SNickeau                    $res = $this->executeTransparentRedirect($targetPage->getDokuwikiId(), self::TARGET_ORIGIN_ALIAS);
438*c3437056SNickeau                    if ($res) {
439*c3437056SNickeau                        return;
440*c3437056SNickeau                    }
441*c3437056SNickeau                    break;
442*c3437056SNickeau                default:
443*c3437056SNickeau                    LogUtility::msg("The alias type ({$buildAlias->getType()}) is unknown. A permanent redirect was performed for the alias $identifier");
444*c3437056SNickeau                    $res = $this->executePermanentRedirect($targetPage->getCanonicalUrl(), self::TARGET_ORIGIN_ALIAS);
445*c3437056SNickeau                    if ($res) {
446*c3437056SNickeau                        return;
447*c3437056SNickeau                    }
448*c3437056SNickeau                    break;
449*c3437056SNickeau            }
450*c3437056SNickeau        }
451*c3437056SNickeau
452*c3437056SNickeau
453*c3437056SNickeau        // If there is a redirection defined in the page rules
454*c3437056SNickeau        $result = $this->processingPageRules();
455*c3437056SNickeau        if ($result) {
456*c3437056SNickeau            // A redirection has occurred
457*c3437056SNickeau            // finish the process
458*c3437056SNickeau            return;
459*c3437056SNickeau        }
460*c3437056SNickeau
461*c3437056SNickeau        /**
462*c3437056SNickeau         *
463*c3437056SNickeau         * There was no redirection found, redirect to edit mode if writer
464*c3437056SNickeau         *
465*c3437056SNickeau         */
466*c3437056SNickeau        if (Identity::isWriter() && $this->getConf(self::GO_TO_EDIT_MODE) == 1) {
467*c3437056SNickeau
468*c3437056SNickeau            $this->gotToEditMode($event);
469*c3437056SNickeau            // Stop here
470*c3437056SNickeau            return;
471*c3437056SNickeau
472*c3437056SNickeau        }
473*c3437056SNickeau
474*c3437056SNickeau        /*
475*c3437056SNickeau         *  We are still a reader, the redirection does not exist the user is not allowed to edit the page (public of other)
476*c3437056SNickeau         */
477*c3437056SNickeau        if ($this->getConf('ActionReaderFirst') == self::NOTHING) {
478*c3437056SNickeau            return;
479*c3437056SNickeau        }
480*c3437056SNickeau
481*c3437056SNickeau        // We are reader and their is no redirection set, we apply the algorithm
482*c3437056SNickeau        $readerAlgorithms = array();
483*c3437056SNickeau        $readerAlgorithms[0] = $this->getConf('ActionReaderFirst');
484*c3437056SNickeau        $readerAlgorithms[1] = $this->getConf('ActionReaderSecond');
485*c3437056SNickeau        $readerAlgorithms[2] = $this->getConf('ActionReaderThird');
486*c3437056SNickeau
487*c3437056SNickeau        while (
488*c3437056SNickeau            ($algorithm = array_shift($readerAlgorithms)) != null
489*c3437056SNickeau        ) {
490*c3437056SNickeau
491*c3437056SNickeau            switch ($algorithm) {
492*c3437056SNickeau
493*c3437056SNickeau                case self::NOTHING:
494*c3437056SNickeau                    return;
495*c3437056SNickeau
496*c3437056SNickeau                case self::GO_TO_BEST_END_PAGE_NAME:
497*c3437056SNickeau
498*c3437056SNickeau                    list($targetPage, $method) = UrlManagerBestEndPage::process($identifier);
499*c3437056SNickeau                    if ($targetPage != null) {
500*c3437056SNickeau                        $res = false;
501*c3437056SNickeau                        switch ($method) {
502*c3437056SNickeau                            case self::REDIRECT_PERMANENT_METHOD:
503*c3437056SNickeau                                $res = $this->executePermanentRedirect($targetPage, self::TARGET_ORIGIN_BEST_END_PAGE_NAME);
504*c3437056SNickeau                                break;
505*c3437056SNickeau                            case self::REDIRECT_NOTFOUND_METHOD:
506*c3437056SNickeau                                $res = $this->performNotFoundRedirect($targetPage, self::TARGET_ORIGIN_BEST_END_PAGE_NAME);
507*c3437056SNickeau                                break;
508*c3437056SNickeau                            default:
509*c3437056SNickeau                                LogUtility::msg("This redirection method ($method) was not expected for the redirection algorithm ($algorithm)");
510*c3437056SNickeau                        }
511*c3437056SNickeau                        if ($res) {
512*c3437056SNickeau                            // Redirection has succeeded
513*c3437056SNickeau                            return;
514*c3437056SNickeau                        }
515*c3437056SNickeau                    }
516*c3437056SNickeau                    break;
517*c3437056SNickeau
518*c3437056SNickeau                case self::GO_TO_NS_START_PAGE:
519*c3437056SNickeau
520*c3437056SNickeau                    // Start page with the conf['start'] parameter
521*c3437056SNickeau                    $startPage = getNS($identifier) . ':' . $conf['start'];
522*c3437056SNickeau                    if (page_exists($startPage)) {
523*c3437056SNickeau                        $res = $this->performNotFoundRedirect($startPage, self::TARGET_ORIGIN_START_PAGE);
524*c3437056SNickeau                        if ($res) {
525*c3437056SNickeau                            return;
526*c3437056SNickeau                        }
527*c3437056SNickeau                    }
528*c3437056SNickeau
529*c3437056SNickeau                    // Start page with the same name than the namespace
530*c3437056SNickeau                    $startPage = getNS($identifier) . ':' . curNS($identifier);
531*c3437056SNickeau                    if (page_exists($startPage)) {
532*c3437056SNickeau                        $res = $this->performNotFoundRedirect($startPage, self::TARGET_ORIGIN_START_PAGE);
533*c3437056SNickeau                        if ($res) {
534*c3437056SNickeau                            return;
535*c3437056SNickeau                        }
536*c3437056SNickeau                    }
537*c3437056SNickeau                    break;
538*c3437056SNickeau
539*c3437056SNickeau                case self::GO_TO_BEST_PAGE_NAME:
540*c3437056SNickeau
541*c3437056SNickeau                    $bestPageId = null;
542*c3437056SNickeau
543*c3437056SNickeau                    $bestPage = $this->getBestPage($identifier);
544*c3437056SNickeau                    $bestPageId = $bestPage['id'];
545*c3437056SNickeau                    $scorePageName = $bestPage['score'];
546*c3437056SNickeau
547*c3437056SNickeau                    // Get Score from a Namespace
548*c3437056SNickeau                    $bestNamespace = $this->scoreBestNamespace($identifier);
549*c3437056SNickeau                    $bestNamespaceId = $bestNamespace['namespace'];
550*c3437056SNickeau                    $namespaceScore = $bestNamespace['score'];
551*c3437056SNickeau
552*c3437056SNickeau                    // Compare the two score
553*c3437056SNickeau                    if ($scorePageName > 0 or $namespaceScore > 0) {
554*c3437056SNickeau                        if ($scorePageName > $namespaceScore) {
555*c3437056SNickeau                            $this->performNotFoundRedirect($bestPageId, self::TARGET_ORIGIN_BEST_PAGE_NAME);
556*c3437056SNickeau                        } else {
557*c3437056SNickeau                            $this->performNotFoundRedirect($bestNamespaceId, self::TARGET_ORIGIN_BEST_PAGE_NAME);
558*c3437056SNickeau                        }
559*c3437056SNickeau                        return;
560*c3437056SNickeau                    }
561*c3437056SNickeau                    break;
562*c3437056SNickeau
563*c3437056SNickeau                case self::GO_TO_BEST_NAMESPACE:
564*c3437056SNickeau
565*c3437056SNickeau                    $scoreNamespace = $this->scoreBestNamespace($identifier);
566*c3437056SNickeau                    $bestNamespaceId = $scoreNamespace['namespace'];
567*c3437056SNickeau                    $score = $scoreNamespace['score'];
568*c3437056SNickeau
569*c3437056SNickeau                    if ($score > 0) {
570*c3437056SNickeau                        $this->performNotFoundRedirect($bestNamespaceId, self::TARGET_ORIGIN_BEST_NAMESPACE);
571*c3437056SNickeau                        return;
572*c3437056SNickeau                    }
573*c3437056SNickeau                    break;
574*c3437056SNickeau
575*c3437056SNickeau                case self::GO_TO_SEARCH_ENGINE:
576*c3437056SNickeau
577*c3437056SNickeau                    $this->redirectToSearchEngine();
578*c3437056SNickeau
579*c3437056SNickeau                    return;
580*c3437056SNickeau                    break;
581*c3437056SNickeau
582*c3437056SNickeau                // End Switch Action
583*c3437056SNickeau            }
584*c3437056SNickeau
585*c3437056SNickeau            // End While Action
586*c3437056SNickeau        }
587*c3437056SNickeau
588*c3437056SNickeau
589*c3437056SNickeau    }
590*c3437056SNickeau
591*c3437056SNickeau
592*c3437056SNickeau    /**
593*c3437056SNickeau     * getBestNamespace
594*c3437056SNickeau     * Return a list with 'BestNamespaceId Score'
595*c3437056SNickeau     * @param $id
596*c3437056SNickeau     * @return array
597*c3437056SNickeau     */
598*c3437056SNickeau    private
599*c3437056SNickeau    function scoreBestNamespace($id)
600*c3437056SNickeau    {
601*c3437056SNickeau
602*c3437056SNickeau        global $conf;
603*c3437056SNickeau
604*c3437056SNickeau        // Parameters
605*c3437056SNickeau        $pageNameSpace = getNS($id);
606*c3437056SNickeau
607*c3437056SNickeau        // If the page has an existing namespace start page take it, other search other namespace
608*c3437056SNickeau        $startPageNameSpace = $pageNameSpace . ":";
609*c3437056SNickeau        $dateAt = '';
610*c3437056SNickeau        // $startPageNameSpace will get a full path (ie with start or the namespace
611*c3437056SNickeau        resolve_pageid($pageNameSpace, $startPageNameSpace, $exists, $dateAt, true);
612*c3437056SNickeau        if (page_exists($startPageNameSpace)) {
613*c3437056SNickeau            $nameSpaces = array($startPageNameSpace);
614*c3437056SNickeau        } else {
615*c3437056SNickeau            $nameSpaces = ft_pageLookup($conf['start']);
616*c3437056SNickeau        }
617*c3437056SNickeau
618*c3437056SNickeau        // Parameters and search the best namespace
619*c3437056SNickeau        $pathNames = explode(':', $pageNameSpace);
620*c3437056SNickeau        $bestNbWordFound = 0;
621*c3437056SNickeau        $bestNamespaceId = '';
622*c3437056SNickeau        foreach ($nameSpaces as $nameSpace) {
623*c3437056SNickeau
624*c3437056SNickeau            $nbWordFound = 0;
625*c3437056SNickeau            foreach ($pathNames as $pathName) {
626*c3437056SNickeau                if (strlen($pathName) > 2) {
627*c3437056SNickeau                    $nbWordFound = $nbWordFound + substr_count($nameSpace, $pathName);
628*c3437056SNickeau                }
629*c3437056SNickeau            }
630*c3437056SNickeau            if ($nbWordFound > $bestNbWordFound) {
631*c3437056SNickeau                // Take only the smallest namespace
632*c3437056SNickeau                if (strlen($nameSpace) < strlen($bestNamespaceId) or $nbWordFound > $bestNbWordFound) {
633*c3437056SNickeau                    $bestNbWordFound = $nbWordFound;
634*c3437056SNickeau                    $bestNamespaceId = $nameSpace;
635*c3437056SNickeau                }
636*c3437056SNickeau            }
637*c3437056SNickeau        }
638*c3437056SNickeau
639*c3437056SNickeau        $startPageFactor = $this->getConf('WeightFactorForStartPage');
640*c3437056SNickeau        $nameSpaceFactor = $this->getConf('WeightFactorForSameNamespace');
641*c3437056SNickeau        if ($bestNbWordFound > 0) {
642*c3437056SNickeau            $bestNamespaceScore = $bestNbWordFound * $nameSpaceFactor + $startPageFactor;
643*c3437056SNickeau        } else {
644*c3437056SNickeau            $bestNamespaceScore = 0;
645*c3437056SNickeau        }
646*c3437056SNickeau
647*c3437056SNickeau
648*c3437056SNickeau        return array(
649*c3437056SNickeau            'namespace' => $bestNamespaceId,
650*c3437056SNickeau            'score' => $bestNamespaceScore
651*c3437056SNickeau        );
652*c3437056SNickeau
653*c3437056SNickeau    }
654*c3437056SNickeau
655*c3437056SNickeau    /**
656*c3437056SNickeau     * @param $event
657*c3437056SNickeau     */
658*c3437056SNickeau    private
659*c3437056SNickeau    function gotToEditMode(&$event)
660*c3437056SNickeau    {
661*c3437056SNickeau        global $ACT;
662*c3437056SNickeau        $ACT = 'edit';
663*c3437056SNickeau
664*c3437056SNickeau    }
665*c3437056SNickeau
666*c3437056SNickeau
667*c3437056SNickeau    /**
668*c3437056SNickeau     * Redirect to an internal page ie:
669*c3437056SNickeau     *   * on the same domain
670*c3437056SNickeau     *   * no HTTP redirect
671*c3437056SNickeau     *   * id rewrite
672*c3437056SNickeau     * @param string $targetPageId - target page id
673*c3437056SNickeau     * @param string $targetOriginId - the source of the target (redirect)
674*c3437056SNickeau     * @return bool - return true if the user has the permission and that the redirect was done
675*c3437056SNickeau     * @throws Exception
676*c3437056SNickeau     */
677*c3437056SNickeau    private
678*c3437056SNickeau    function executeTransparentRedirect(string $targetPageId, string $targetOriginId): bool
679*c3437056SNickeau    {
680*c3437056SNickeau        /**
681*c3437056SNickeau         * Because we set the ID globally for the ID redirect
682*c3437056SNickeau         * we make sure that this is not a {@link Page}
683*c3437056SNickeau         * object otherwise we got an error in the {@link \ComboStrap\AnalyticsMenuItem}
684*c3437056SNickeau         * because the constructor takes it {@link \dokuwiki\Menu\Item\AbstractItem}
685*c3437056SNickeau         */
686*c3437056SNickeau        if (is_object($targetPageId)) {
687*c3437056SNickeau            $class = get_class($targetPageId);
688*c3437056SNickeau            LogUtility::msg("The parameters targetPageId ($targetPageId) is an object of the class ($class) and it should be a page id");
689*c3437056SNickeau        }
690*c3437056SNickeau
691*c3437056SNickeau        if (is_object($targetOriginId)) {
692*c3437056SNickeau            $class = get_class($targetOriginId);
693*c3437056SNickeau            LogUtility::msg("The parameters targetOriginId ($targetOriginId) is an object of the class ($class) and it should be a page id");
694*c3437056SNickeau        }
695*c3437056SNickeau
696*c3437056SNickeau        // If the user does not have the right to see the target page
697*c3437056SNickeau        // don't do anything
698*c3437056SNickeau        if (!(Identity::isReader($targetPageId))) {
699*c3437056SNickeau            return false;
700*c3437056SNickeau        }
701*c3437056SNickeau
702*c3437056SNickeau        // Change the id
703*c3437056SNickeau        global $ID;
704*c3437056SNickeau        global $INFO;
705*c3437056SNickeau        $sourceId = $ID;
706*c3437056SNickeau        $ID = $targetPageId;
707*c3437056SNickeau        // Change the info id for the sidebar
708*c3437056SNickeau        $INFO['id'] = $targetPageId;
709*c3437056SNickeau        /**
710*c3437056SNickeau         * otherwise there is:
711*c3437056SNickeau         *   * a meta robot = noindex,follow
712*c3437056SNickeau         * See {@link tpl_metaheaders()}
713*c3437056SNickeau         */
714*c3437056SNickeau        $INFO['exists'] = true;
715*c3437056SNickeau
716*c3437056SNickeau        /**
717*c3437056SNickeau         * Not compatible with
718*c3437056SNickeau         * https://www.dokuwiki.org/config:send404 is enabled
719*c3437056SNickeau         *
720*c3437056SNickeau         * This check happens before that dokuwiki is started
721*c3437056SNickeau         * and send an header in doku.php
722*c3437056SNickeau         *
723*c3437056SNickeau         * We send a warning
724*c3437056SNickeau         */
725*c3437056SNickeau        global $conf;
726*c3437056SNickeau        if ($conf['send404'] == true) {
727*c3437056SNickeau            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);
728*c3437056SNickeau        }
729*c3437056SNickeau
730*c3437056SNickeau        // Redirection
731*c3437056SNickeau        $this->logRedirection($sourceId, $targetPageId, $targetOriginId, self::REDIRECT_TRANSPARENT_METHOD);
732*c3437056SNickeau
733*c3437056SNickeau        return true;
734*c3437056SNickeau
735*c3437056SNickeau    }
736*c3437056SNickeau
737*c3437056SNickeau    private function executePermanentRedirect(string $target, $targetOrigin): bool
738*c3437056SNickeau    {
739*c3437056SNickeau        return $this->executeHttpRedirect($target, $targetOrigin, self::REDIRECT_PERMANENT_METHOD);
740*c3437056SNickeau    }
741*c3437056SNickeau
742*c3437056SNickeau    /**
743*c3437056SNickeau     * The general HTTP Redirect method to an internal page
744*c3437056SNickeau     * where the redirection method decide which type of redirection
745*c3437056SNickeau     * @param string $target - a dokuwiki id or an url
746*c3437056SNickeau     * @param string $targetOrigin - the origin of the target (the algorithm used to get the target origin)
747*c3437056SNickeau     * @param string $method - the redirection method
748*c3437056SNickeau     */
749*c3437056SNickeau    private
750*c3437056SNickeau    function executeHttpRedirect(string $target, string $targetOrigin, string $method): bool
751*c3437056SNickeau    {
752*c3437056SNickeau
753*c3437056SNickeau        global $ID;
754*c3437056SNickeau
755*c3437056SNickeau
756*c3437056SNickeau        // Log the redirections
757*c3437056SNickeau        $this->logRedirection($ID, $target, $targetOrigin, $method);
758*c3437056SNickeau
759*c3437056SNickeau
760*c3437056SNickeau        // An external url ?
761*c3437056SNickeau        if (Url::isValidURL($target)) {
762*c3437056SNickeau
763*c3437056SNickeau            // defend against HTTP Response Splitting
764*c3437056SNickeau            // https://owasp.org/www-community/attacks/HTTP_Response_Splitting
765*c3437056SNickeau            $targetUrl = stripctl($target);
766*c3437056SNickeau
767*c3437056SNickeau        } else {
768*c3437056SNickeau
769*c3437056SNickeau
770*c3437056SNickeau            // Explode the page ID and the anchor (#)
771*c3437056SNickeau            $link = explode('#', $target, 2);
772*c3437056SNickeau
773*c3437056SNickeau            // Query String to pass the message
774*c3437056SNickeau            $urlParams = [];
775*c3437056SNickeau            if ($targetOrigin != self::TARGET_ORIGIN_PERMALINK) {
776*c3437056SNickeau                $urlParams = array(
777*c3437056SNickeau                    action_plugin_combo_routermessage::ORIGIN_PAGE => $ID,
778*c3437056SNickeau                    action_plugin_combo_routermessage::ORIGIN_TYPE => $targetOrigin
779*c3437056SNickeau                );
780*c3437056SNickeau            }
781*c3437056SNickeau
782*c3437056SNickeau            // if this is search engine redirect
783*c3437056SNickeau            if ($targetOrigin == self::TARGET_ORIGIN_SEARCH_ENGINE) {
784*c3437056SNickeau                $replacementPart = array(':', '_', '-');
785*c3437056SNickeau                $query = str_replace($replacementPart, ' ', $ID);
786*c3437056SNickeau                $urlParams["do"] = "search";
787*c3437056SNickeau                $urlParams["q"] = $query;
788*c3437056SNickeau            }
789*c3437056SNickeau
790*c3437056SNickeau            $targetUrl = wl($link[0], $urlParams, true, '&');
791*c3437056SNickeau            // %3A back to :
792*c3437056SNickeau            $targetUrl = str_replace("%3A", ":", $targetUrl);
793*c3437056SNickeau            if ($link[1]) {
794*c3437056SNickeau                $targetUrl .= '#' . rawurlencode($link[1]);
795*c3437056SNickeau            }
796*c3437056SNickeau
797*c3437056SNickeau        }
798*c3437056SNickeau
799*c3437056SNickeau        /**
800*c3437056SNickeau         * The dokuwiki function {@link send_redirect()}
801*c3437056SNickeau         * set the `Location header` and in php, the header function
802*c3437056SNickeau         * in this case change the status code to 302 Arghhhh.
803*c3437056SNickeau         * The code below is adapted from this function {@link send_redirect()}
804*c3437056SNickeau         */
805*c3437056SNickeau        global $MSG; // are there any undisplayed messages? keep them in session for display
806*c3437056SNickeau        if (isset($MSG) && count($MSG) && !defined('NOSESSION')) {
807*c3437056SNickeau            //reopen session, store data and close session again
808*c3437056SNickeau            @session_start();
809*c3437056SNickeau            $_SESSION[DOKU_COOKIE]['msg'] = $MSG;
810*c3437056SNickeau        }
811*c3437056SNickeau        session_write_close(); // always close the session
812*c3437056SNickeau
813*c3437056SNickeau        switch ($method) {
814*c3437056SNickeau            case self::REDIRECT_PERMANENT_METHOD:
815*c3437056SNickeau                HttpResponse::create(HttpResponse::STATUS_PERMANENT_REDIRECT)
816*c3437056SNickeau                    ->addHeader(self::LOCATION_HEADER_PREFIX . $targetUrl)
817*c3437056SNickeau                    ->send();
818*c3437056SNickeau                return true;
819*c3437056SNickeau            case self::REDIRECT_NOTFOUND_METHOD:
820*c3437056SNickeau
821*c3437056SNickeau                // Empty 404 body to not get the standard 404 page of the browser
822*c3437056SNickeau                // but a blank page to avoid a sort of FOUC.
823*c3437056SNickeau                // ie the user see a page briefly
824*c3437056SNickeau                HttpResponse::create(HttpResponse::STATUS_NOT_FOUND)
825*c3437056SNickeau                    ->addHeader(self::REFRESH_HEADER_PREFIX . $targetUrl)
826*c3437056SNickeau                    ->send(self::PAGE_404, Mime::HTML);
827*c3437056SNickeau                return true;
828*c3437056SNickeau
829*c3437056SNickeau            default:
830*c3437056SNickeau                LogUtility::msg("The method ($method) is not an http redirection");
831*c3437056SNickeau                return false;
832*c3437056SNickeau        }
833*c3437056SNickeau
834*c3437056SNickeau
835*c3437056SNickeau    }
836*c3437056SNickeau
837*c3437056SNickeau    /**
838*c3437056SNickeau     * @param $id
839*c3437056SNickeau     * @return array
840*c3437056SNickeau     */
841*c3437056SNickeau    private
842*c3437056SNickeau    function getBestPage($id): array
843*c3437056SNickeau    {
844*c3437056SNickeau
845*c3437056SNickeau        // The return parameters
846*c3437056SNickeau        $bestPageId = null;
847*c3437056SNickeau        $scorePageName = null;
848*c3437056SNickeau
849*c3437056SNickeau        // Get Score from a page
850*c3437056SNickeau        $pageName = noNS($id);
851*c3437056SNickeau        $pagesWithSameName = ft_pageLookup($pageName);
852*c3437056SNickeau        if (count($pagesWithSameName) > 0) {
853*c3437056SNickeau
854*c3437056SNickeau            // Search same namespace in the page found than in the Id page asked.
855*c3437056SNickeau            $bestNbWordFound = 0;
856*c3437056SNickeau
857*c3437056SNickeau
858*c3437056SNickeau            $wordsInPageSourceId = explode(':', $id);
859*c3437056SNickeau            foreach ($pagesWithSameName as $targetPageId => $title) {
860*c3437056SNickeau
861*c3437056SNickeau                // Nb of word found in the target page id
862*c3437056SNickeau                // that are in the source page id
863*c3437056SNickeau                $nbWordFound = 0;
864*c3437056SNickeau                foreach ($wordsInPageSourceId as $word) {
865*c3437056SNickeau                    $nbWordFound = $nbWordFound + substr_count($targetPageId, $word);
866*c3437056SNickeau                }
867*c3437056SNickeau
868*c3437056SNickeau                if ($bestPageId == null) {
869*c3437056SNickeau
870*c3437056SNickeau                    $bestNbWordFound = $nbWordFound;
871*c3437056SNickeau                    $bestPageId = $targetPageId;
872*c3437056SNickeau
873*c3437056SNickeau                } else {
874*c3437056SNickeau
875*c3437056SNickeau                    if ($nbWordFound >= $bestNbWordFound && strlen($bestPageId) > strlen($targetPageId)) {
876*c3437056SNickeau
877*c3437056SNickeau                        $bestNbWordFound = $nbWordFound;
878*c3437056SNickeau                        $bestPageId = $targetPageId;
879*c3437056SNickeau
880*c3437056SNickeau                    }
881*c3437056SNickeau
882*c3437056SNickeau                }
883*c3437056SNickeau
884*c3437056SNickeau            }
885*c3437056SNickeau            $scorePageName = $this->getConf('WeightFactorForSamePageName') + ($bestNbWordFound - 1) * $this->getConf('WeightFactorForSameNamespace');
886*c3437056SNickeau            return array(
887*c3437056SNickeau                'id' => $bestPageId,
888*c3437056SNickeau                'score' => $scorePageName);
889*c3437056SNickeau        }
890*c3437056SNickeau        return array(
891*c3437056SNickeau            'id' => $bestPageId,
892*c3437056SNickeau            'score' => $scorePageName
893*c3437056SNickeau        );
894*c3437056SNickeau
895*c3437056SNickeau    }
896*c3437056SNickeau
897*c3437056SNickeau
898*c3437056SNickeau    /**
899*c3437056SNickeau     * Redirect to the search engine
900*c3437056SNickeau     */
901*c3437056SNickeau    private
902*c3437056SNickeau    function redirectToSearchEngine()
903*c3437056SNickeau    {
904*c3437056SNickeau
905*c3437056SNickeau        global $ID;
906*c3437056SNickeau        $this->performNotFoundRedirect($ID, self::TARGET_ORIGIN_SEARCH_ENGINE);
907*c3437056SNickeau
908*c3437056SNickeau    }
909*c3437056SNickeau
910*c3437056SNickeau
911*c3437056SNickeau    /**
912*c3437056SNickeau     *
913*c3437056SNickeau     *   * For a conf file, it will update the Redirection Action Data as Referrer, Count Of Redirection, Redirection Date
914*c3437056SNickeau     *   * For a SQlite database, it will add a row into the log
915*c3437056SNickeau     *
916*c3437056SNickeau     * @param string $sourcePageId
917*c3437056SNickeau     * @param $targetPageId
918*c3437056SNickeau     * @param $algorithmic
919*c3437056SNickeau     * @param $method - http or rewrite
920*c3437056SNickeau     */
921*c3437056SNickeau    function logRedirection(string $sourcePageId, $targetPageId, $algorithmic, $method)
922*c3437056SNickeau    {
923*c3437056SNickeau
924*c3437056SNickeau        $row = array(
925*c3437056SNickeau            "TIMESTAMP" => date("c"),
926*c3437056SNickeau            "SOURCE" => $sourcePageId,
927*c3437056SNickeau            "TARGET" => $targetPageId,
928*c3437056SNickeau            "REFERRER" => $_SERVER['HTTP_REFERER'],
929*c3437056SNickeau            "TYPE" => $algorithmic,
930*c3437056SNickeau            "METHOD" => $method
931*c3437056SNickeau        );
932*c3437056SNickeau        $request = Sqlite::createOrGetBackendSqlite()
933*c3437056SNickeau            ->createRequest()
934*c3437056SNickeau            ->setTableRow('redirections_log', $row);
935*c3437056SNickeau        try {
936*c3437056SNickeau            $request
937*c3437056SNickeau                ->execute();
938*c3437056SNickeau        } catch (ExceptionCombo $e) {
939*c3437056SNickeau            LogUtility::msg("Redirection Log Insert Error. {$e->getMessage()}");
940*c3437056SNickeau        } finally {
941*c3437056SNickeau            $request->close();
942*c3437056SNickeau        }
943*c3437056SNickeau
944*c3437056SNickeau
945*c3437056SNickeau    }
946*c3437056SNickeau
947*c3437056SNickeau    /**
948*c3437056SNickeau     * This function check if there is a redirection declared
949*c3437056SNickeau     * in the redirection table
950*c3437056SNickeau     * @return bool - true if a rewrite or redirection occurs
951*c3437056SNickeau     * @throws Exception
952*c3437056SNickeau     */
953*c3437056SNickeau    private function processingPageRules(): bool
954*c3437056SNickeau    {
955*c3437056SNickeau        global $ID;
956*c3437056SNickeau
957*c3437056SNickeau        $calculatedTarget = null;
958*c3437056SNickeau        $ruleMatcher = null; // Used in a warning message if the target page does not exist
959*c3437056SNickeau        // Known redirection in the table
960*c3437056SNickeau        // Get the page from redirection data
961*c3437056SNickeau        $rules = $this->pageRules->getRules();
962*c3437056SNickeau        foreach ($rules as $rule) {
963*c3437056SNickeau
964*c3437056SNickeau            $ruleMatcher = strtolower($rule[PageRules::MATCHER_NAME]);
965*c3437056SNickeau            $ruleTarget = $rule[PageRules::TARGET_NAME];
966*c3437056SNickeau
967*c3437056SNickeau            // Glob to Rexgexp
968*c3437056SNickeau            $regexpPattern = '/' . str_replace("*", "(.*)", $ruleMatcher) . '/';
969*c3437056SNickeau
970*c3437056SNickeau            // Match ?
971*c3437056SNickeau            // https://www.php.net/manual/en/function.preg-match.php
972*c3437056SNickeau            if (preg_match($regexpPattern, $ID, $matches)) {
973*c3437056SNickeau                $calculatedTarget = $ruleTarget;
974*c3437056SNickeau                foreach ($matches as $key => $match) {
975*c3437056SNickeau                    if ($key == 0) {
976*c3437056SNickeau                        continue;
977*c3437056SNickeau                    } else {
978*c3437056SNickeau                        $calculatedTarget = str_replace('$' . $key, $match, $calculatedTarget);
979*c3437056SNickeau                    }
980*c3437056SNickeau                }
981*c3437056SNickeau                break;
982*c3437056SNickeau            }
983*c3437056SNickeau        }
984*c3437056SNickeau
985*c3437056SNickeau        if ($calculatedTarget == null) {
986*c3437056SNickeau            return false;
987*c3437056SNickeau        }
988*c3437056SNickeau
989*c3437056SNickeau        // If this is an external redirect (other domain)
990*c3437056SNickeau        if (Url::isValidURL($calculatedTarget)) {
991*c3437056SNickeau
992*c3437056SNickeau            $this->executeHttpRedirect($calculatedTarget, self::TARGET_ORIGIN_PAGE_RULES, self::REDIRECT_PERMANENT_METHOD);
993*c3437056SNickeau            return true;
994*c3437056SNickeau
995*c3437056SNickeau        }
996*c3437056SNickeau
997*c3437056SNickeau        // If the page exist
998*c3437056SNickeau        if (page_exists($calculatedTarget)) {
999*c3437056SNickeau
1000*c3437056SNickeau            // This is DokuWiki Id and should always be lowercase
1001*c3437056SNickeau            // The page rule may have change that
1002*c3437056SNickeau            $calculatedTarget = strtolower($calculatedTarget);
1003*c3437056SNickeau            $res = $this->executeTransparentRedirect($calculatedTarget, self::TARGET_ORIGIN_PAGE_RULES);
1004*c3437056SNickeau            if ($res) {
1005*c3437056SNickeau                return true;
1006*c3437056SNickeau            } else {
1007*c3437056SNickeau                return false;
1008*c3437056SNickeau            }
1009*c3437056SNickeau
1010*c3437056SNickeau        } else {
1011*c3437056SNickeau
1012*c3437056SNickeau            LogUtility::msg("The calculated target page ($calculatedTarget) (for the non-existing page `$ID` with the matcher `$ruleMatcher`) does not exist", LogUtility::LVL_MSG_ERROR);
1013*c3437056SNickeau            return false;
1014*c3437056SNickeau
1015*c3437056SNickeau        }
1016*c3437056SNickeau
1017*c3437056SNickeau    }
1018*c3437056SNickeau
1019*c3437056SNickeau    private function performNotFoundRedirect(string $targetId, string $origin): bool
1020*c3437056SNickeau    {
1021*c3437056SNickeau        return $this->executeHttpRedirect($targetId, $origin, self::REDIRECT_NOTFOUND_METHOD);
1022*c3437056SNickeau    }
1023*c3437056SNickeau
1024*c3437056SNickeau
1025*c3437056SNickeau}
1026