1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\EventHandler;
5use dokuwiki\Extension\Event;
6
7/**
8 * Redirect2 - DokuWiki Redirect Manager
9 *
10 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
11 * @author     Satoshi Sahara <sahara.satoshi@gmail.com>
12 */
13class action_plugin_redirect2 extends ActionPlugin
14{
15    protected $LogFile;       // log file, see function _log_redirection
16    protected $debug = false; // enabled if DEBUG file exists in this plugin directory
17
18    /**
19     * Register event handlers
20     */
21    public function register(EventHandler $controller)
22    {
23        $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE',    $this, 'handleReplacedBy');
24        $controller->register_hook('ACTION_HEADERS_SEND', 'BEFORE', $this, 'redirectPage');
25        $controller->register_hook('FETCH_MEDIA_STATUS', 'BEFORE',  $this, 'redirectMedia');
26        $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'errorDocument404');
27    }
28
29    public function __construct()
30    {
31        global $conf;
32        $this->LogFile  = $conf['cachedir'].'/redirection.log';
33        if (@file_exists(dirname(__FILE__).'/DEBUG')) $this->debug = true;
34    }
35
36
37    /**
38     * ErrorDocument404 - not found response
39     * show 404 wiki page instead of inc/lang/<iso>/newpage.txt
40     * TPL_CONTENT_DISPLAY:BEFORE event handler
41     *
42     * The code adopted from dokuwiki-plugin-notfound
43     * https://www.dokuwiki.org/plugin:notfound
44     * @author     Andreas Gohr <andi@splitbrain.org>
45     */
46     public function errorDocument404(Event $event, $param)
47     {
48        global $ACT, $ID, $INFO;
49
50         if ( $INFO['exists'] || ($ACT != 'show') ) return false;
51         $page = $this->getConf('404page');
52         if (empty($page)) return false;
53
54         $event->stopPropagation();
55         $event->preventDefault();
56
57         $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
58         $this->_log_redirection(404, $ID, $referer);
59         echo p_wiki_xhtml($this->getConf('404page'), false);
60         return true;
61     }
62
63
64    /**
65     * Get redirect destination URL
66     *
67     * @param int $status  redirect status, 301 or 302
68     * @param string $dest redirect destination, absolute id or external url
69     * @return mixed       url of the destination page/media, or false
70     */
71    protected function getRedirectURL($status = 302, $dest)
72    {
73        global $ID, $INFO;
74
75        if (preg_match('@^(https?://|/)@', $dest)) {
76            $url = $dest; // external url
77        } else {
78            list($ext, $mime) = mimetype($dest);
79            if ($ext) {   // media
80                $url = ml($dest);
81            } else {      // page
82                list($page, $section) = explode('#', $dest, 2);
83
84                // check whether visit again using breadcrums trace
85                // Note: this does not completely eliminate redirect loop.
86                if ($this->_foundInBreadcrumbs($page) && $INFO['exists']) {
87                    $this->_show_message('redirect_halt', $ID, $page);
88                    return false;
89                }
90
91                $url = wl($page);
92                if (!empty($section)) $url.= '#'.rawurlencode($section);
93
94                // output message, to be shown at destination page (after redirect)
95                $this->_show_message('redirected_from', $ID, $dest, $status);
96            }
97        }
98        return $url;
99    }
100
101    /**
102     * Check if the page found in breadcrumbs (session cookie)
103     * to prevent infinite redirection loop
104     *
105     * @param string $id  absolute page name (id)
106     * @return bool  true if id (of the page) found in breadcrumbs
107     */
108    private function _foundInBreadcrumbs ($id)
109    {
110        list($page, $section) = explode('#', $id, 2);
111
112        if (isset($_SESSION[DOKU_COOKIE]['bc']) &&
113            array_key_exists($page, $_SESSION[DOKU_COOKIE]['bc'])) {
114            if ($this->debug) {
115                $hist = $_SESSION[DOKU_COOKIE]['bc'];
116                error_log('redirect to page['.$page.'] must stop due to prevent loop '."\n".
117                          'found in breadcrumbs = '.var_export($hist, true));
118            }
119            return true;
120        }
121        return false;
122    }
123
124
125    /**
126     * Page redirection based on metadata 'relation isreplacedby'
127     * that is set by syntax component
128     * DOKUWIKI_STARTED:BEFORE event handler
129     */
130    function handleReplacedBy(Event $event, $param)
131    {
132        global $ID, $ACT, $REV, $INPUT;
133
134        if (($ACT != 'show' && $ACT != '') || $REV) return;
135        if (!plugin_isdisabled('pageredirect')) return;
136
137        // return if no redirection data
138        $id = p_get_metadata($ID,'relation isreplacedby');
139        if (empty($id)) return;
140
141        // check whether redirection is temporarily disabled by url paramter
142        if (is_null($INPUT->str('redirect', NULL))) {
143            // Redirect current page
144            $dest = $id;
145            $status = 301;
146            $url = $this->getRedirectURL($status, $dest);
147            if ($url !== false) {
148                $this->_log_redirection($status, $ID, $dest);
149                http_status($status);
150                send_redirect($url);
151                exit;
152            }
153        }
154        return;
155    }
156
157
158    /**
159     * Redirection of pages based on redirect.conf file
160     * ACTION_HEADERS_SEND:BEFORE event handler
161     */
162    function redirectPage(Event $event, $param)
163    {
164        global $ACT, $ID, $INPUT;
165
166        if (!($ACT == 'show' || (!is_array($ACT) && substr($ACT, 0, 7) == 'export_')) ) return;
167
168        // return if redirection is temporarily disabled by url paramter
169        if ($INPUT->str('redirect',NULL) == 'no') return;
170
171        // read redirect map
172        $map = $this->loadHelper($this->getPluginName());
173        if (empty($map)) return false;
174
175        /*
176         * Redirect based on simple prefix match of the current page
177         * (Redirect Directives)
178         */
179        $leaf = '';  // rest of checkID ($ID = $checkID + $leaf)
180        $checkID = $ID;
181        do {
182            if (isset($map->pattern[$checkID])) {
183                $dest = $map->pattern[$checkID]['destination'];
184                list($ns, $section) = explode('#', $dest, 2);
185                $dest = $ns.$leaf;
186                $dest.= (!empty($section)) ? '#'.rawurlencode($section) : '';
187
188                $status = $map->pattern[$checkID]['status'];
189                $url = $this->getRedirectURL($status, $dest);
190                if ($url !== false) {
191                    $this->_log_redirection($status, $ID, $dest);
192                    http_status($status);
193                    send_redirect($url);
194                    exit;
195                }
196            }
197            // check hierarchic namespace replacement
198            $leaf = noNS(rtrim($checkID,':')).$leaf;
199            $checkID = ($checkID == ':') ? false : getNS(rtrim($checkID,':')).':';
200        } while ($checkID != false);
201
202        /*
203         * Redirect based on a regular expression match against the current page
204         * (RedirectMatch Directives)
205         */
206        if ($this->getConf('useRedirectMatch')) {
207            $redirect = $this->_RedirectMatch($ID, $map);
208            if ($redirect !== false) {
209                $dest = $redirect['destination'];
210                $status = $redirect['status'];
211                $url = $this->getRedirectURL($status, $dest);
212                if ($url !== false) {
213                    $this->_log_redirection($status, $ID, $dest);
214                    http_status($status);
215                    send_redirect($url);
216                    exit;
217                }
218            }
219        }
220        return true;
221    }
222
223
224    /**
225     * Redirect of media based on redirect.conf file
226     * FETCH_MEDIA_STATUS event handler
227     * @see also https://www.dokuwiki.org/devel:event:fetch_media_status
228     */
229    function redirectMedia(Event $event, $param)
230    {
231        // read redirect map
232        $map = $this->loadHelper($this->getPluginName());
233        if (empty($map)) return false;
234
235        /*
236         * Redirect based on simple prefix match of the current media
237         * (Redirect Directives)
238         */
239        $leaf = '';
240        // for media, $checkID need to be clean with ':' prefixed
241        $checkID = ':'.ltrim($event->data['media'],':');
242        do {
243            if (isset($map->pattern[$checkID])) {
244                $dest = $map->pattern[$checkID]['destination'];
245                list($ns, $section) = explode('#', $dest, 2);
246                $dest = $ns.$leaf;
247                $dest.= (!empty($section)) ? '#'.rawurlencode($section) : '';
248
249                $status = $map->pattern[$checkID]['status'];
250                $url = $this->getRedirectURL($status, $dest);
251                if ($url !== false) {
252                    $this->_log_redirection($status, $event->data['media'], $dest);
253                    $event->data['status'] = $status;
254                    $event->data['statusmessage'] = $url;
255                    return; // Redirect will happen at lib/exe/fetch.php
256                }
257            }
258            // check hierarchic namespace replacement
259            $leaf = noNS(rtrim($checkID,':')).$leaf;
260            $checkID = ($checkID == '::') ? false : ':'.getNS(trim($checkID,':')).':';
261        } while ($checkID != false);
262
263        /*
264         * Redirect based on a regular expression match against the current media
265         * (RedirectMatch Directives)
266         */
267        if ($this->getConf('useRedirectMatch')) {
268            $checkID = ':'.ltrim($event->data['media'],':');
269            $redirect = $this->_RedirectMatch($checkID, $map);
270            if ($redirect !== false) {
271                $dest = $redirect['destination'];
272                $status = $redirect['status'];
273                $url = $this->getRedirectURL($status, $dest);
274                if ($url !== false) {
275                    $this->_log_redirection($status, $event->data['media'], $dest);
276                    $event->data['status'] = $status;
277                    $event->data['statusmessage'] = $url;
278                    return; // Redirect will happen at lib/exe/fetch.php
279                }
280            }
281        }
282        return true;
283    }
284
285
286    /**
287     * Resolve destination page/media id by regular expression match
288     * using rediraction pattern map config file
289     *
290     * @param string $checkID  full and cleaned name of page or media
291     *                         for the page, it must be clean id
292     *                         for media, it must be clean with ':' prefixed
293     * @param array $map       redirect map
294     * @return array of status and destination (id), or false if no matched
295     */
296    protected function _RedirectMatch( $checkID, $map )
297    {
298        foreach ($map->pattern as $pattern => $data) {
299            if (preg_match('/^%.*%$/', $pattern) !== 1) continue;
300            $destID = preg_replace( $pattern, $data['destination'], $checkID, -1, $count);
301            if ($count > 0) {
302                $status = $data['status'];
303                return array('status' => $status, 'destination' => $destID);
304                break;
305            }
306        }
307        return false;
308    }
309
310
311    /**
312     * Show message to inform user redirection
313     *
314     * @param string $format   key name for message string
315     * @param string $orig     page id of redirect origin
316     * @param string $dest     page id of redirect destination
317     * @param int    $status   http status of the redirection
318     */
319    protected function _show_message($format, $orig=NULL, $dest=NULL, $status=302)
320    {
321        global $ID, $INFO, $INPUT;
322
323        // check who can see the message
324        $show = ( ($INFO['isadmin'] && ($this->getConf('msg_target') >= 0))
325               || ($INFO['ismanager'] && ($this->getConf('msg_target') >= 1))
326               || ($INPUT->server->has('REMOTE_USER') && ($this->getConf('msg_target') >= 2))
327               || ($this->getConf('msg_target') >= 3) );
328        if (!$show) return;
329        // make links used in message
330        $link = array();
331        foreach (array($orig, $dest) as $id) {
332            $title = hsc(p_get_metadata($id, 'title'));
333            if (empty($title)) {
334                $title = hsc(useHeading('navigation') ? p_get_first_heading($id) : $id);
335            }
336            resolve_pageid(':', $id, $exists); // absolute pagename
337            $class = ($exists) ? 'wikilink1' : 'wikilink2';
338            $link[$id] = '<a href="'.wl($id, array('redirect' => 'no')).'" rel="nofollow"'.
339                         ' class="'.$class.'" title="'.$id.'">'.$title.'</a>';
340        }
341
342        switch ($format) {
343            case 'redirect_halt':
344                // "Halted redirection from %1$s to %2$s due to prevent loop."
345                msg(sprintf($this->getLang($format), $link[$orig], $link[$dest]), -1);
346                break;
347
348            case 'redirected_from':
349                // "You were redirected here (%2$s) from %1$s."
350                if ( ($this->getConf('show_msg') == 0) ||
351                    (($this->getConf('show_msg') == 1) && ($status != 301)) ) {
352                    break; // no need to show message
353                }
354                msg(sprintf($this->getLang($format), $link[$orig], $link[$dest]), 0);
355                break;
356
357        } // end switch
358
359    }
360
361
362    /**
363     * Logging of redirection
364     */
365    protected function _log_redirection($status, $orig, $dest='')
366    {
367        if (!$this->getConf('logging')) return;
368
369        $dbg = debug_backtrace();
370        $caller = $dbg[1]['function'];
371
372        $s = date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME'])."\t".$caller;
373        if ($status == 404) {
374            // $dest is referer of the $orig page
375            $s.= "\t".$status."\t".$orig."\t".$dest;
376        } else {
377            // redirect from $orig to $dest
378            $s.= "\t".$status."\t".$orig."\t".$dest;
379        }
380        io_saveFile($this->LogFile, $s."\n", true);
381    }
382
383}
384