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