1<?php
2/**
3 * DokuWiki Plugin pageredirect (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Elan Ruusamäe <glen@delfi.ee>
7 * @author  David Lorentsen <zyberdog@quakenet.org>
8 */
9
10// must be run within Dokuwiki
11if(!defined('DOKU_INC')) die();
12
13class action_plugin_pageredirect extends DokuWiki_Action_Plugin {
14    /**
15     * Registers a callback function for a given event
16     *
17     * @param Doku_Event_Handler $controller DokuWiki's event controller object
18     */
19    public function register(Doku_Event_Handler $controller) {
20        /* @see action_plugin_pageredirect::handle_dokuwiki_started() */
21        $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'handle_dokuwiki_started');
22        /* @see action_plugin_pageredirect::handle_parser_metadata_render() */
23        $controller->register_hook('PARSER_METADATA_RENDER', 'BEFORE', $this, 'handle_parser_metadata_render');
24
25        // This plugin goes first, PR#555, requires dokuwiki 2014-05-05 (Ponder Stibbons)
26        /* @see action_plugin_pageredirect::handle_tpl_act_render() */
27        $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'handle_tpl_act_render', null, PHP_INT_MIN);
28
29        $controller->register_hook('INDEXER_PAGE_ADD', 'BEFORE', $this, 'handle_indexer');
30
31        // Handle move plugin
32        $controller->register_hook('PLUGIN_MOVE_HANDLERS_REGISTER', 'BEFORE', $this, 'handle_move_register');
33    }
34
35    public function handle_dokuwiki_started(&$event, $param) {
36        global $ID, $ACT, $REV;
37
38        // skip when looking page history or action is not 'show'
39        if(($ACT != 'show' && $ACT != '') || $REV) {
40            return;
41        }
42
43        $metadata = $this->get_metadata($ID);
44
45        // return if no redirection data
46        if(!$metadata) {
47            return;
48        }
49        list($page, $is_external) = $metadata;
50
51        // return if external redirect is not allowed
52        if($is_external && !$this->getConf('allow_external')) {
53            return;
54        }
55
56        global $INPUT;
57        $redirect = $INPUT->get->str('redirect', '0');
58
59        // return if redirection is temporarily disabled,
60        // or we have been redirected 5 times in a row
61        if($redirect == 'no' || $redirect > 4) {
62            return;
63        }
64        $redirect = (int)$redirect+1;
65
66        // verify metadata currency
67        // FIXME: why
68        if(@filemtime(metaFN($ID, '.meta')) < @filemtime(wikiFN($ID))) {
69            throw new Exception('should not get here');
70            return;
71        }
72
73        // preserve #section from $page
74        list($page, $section) = array_pad(explode('#', $page, 2), 2, null);
75        if(isset($section)) {
76            $section = '#' . $section;
77        } else {
78            $section = '';
79        }
80
81        // prepare link for internal redirects, keep external targets
82        if(!$is_external) {
83            $page = wl($page, array('redirect' => $redirect), true, '&');
84
85            if($this->getConf('show_note')) {
86                $this->flash_message($ID);
87            }
88
89            // add anchor if not external redirect
90            $page .= $section;
91        }
92
93        $this->redirect($page);
94    }
95
96    public function handle_tpl_act_render(&$event, $param) {
97        global $ACT;
98
99        // handle on do=show
100        if($ACT != 'show' && $ACT != '') {
101            return true;
102        }
103
104        if($this->getConf('show_note')) {
105            $this->render_flash();
106        }
107
108        return true;
109    }
110
111    public function handle_parser_metadata_render(&$event, $param) {
112        if(isset($event->data->meta['relation'])) {
113            // FIXME: why is this needed here?!
114            unset($event->data->meta['relation']['isreplacedby']);
115        }
116    }
117
118    public function handle_indexer(Doku_Event $event, $param) {
119        $new_references = array();
120        foreach ($event->data['metadata']['relation_references'] as $target) {
121            $redirect_target = $this->get_metadata($target);
122
123            if ($redirect_target) {
124                list($page, $is_external) = $redirect_target;
125
126                if (!$is_external) {
127                    $new_references[] = $page;
128                }
129            }
130        }
131
132        if (count($new_references) > 0) {
133            $event->data['metadata']['relation_references'] = array_unique(array_merge($new_references, $event->data['metadata']['relation_references']));
134        }
135
136        // FIXME: if the currently indexed page contains a redirect, all pages pointing to it need a new backlink entry!
137        // Note that these entries need to be added for every source page separately.
138        // An alternative could be to force re-indexing of all source pages by removing their ".indexed" file but this will only happen when they are visited.
139    }
140
141    /**
142     * remember to show note about being redirected from another page
143     * @param string $ID page id from where the redirect originated
144     */
145    private function flash_message($ID) {
146        if(headers_sent()) {
147            // too late to do start session
148            // and following code requires session
149            return;
150        }
151
152        session_start();
153        $_SESSION[DOKU_COOKIE]['redirect'] = $ID;
154    }
155
156    /**
157     * show note about being redirected from another page
158     */
159    private function render_flash() {
160        global $INPUT;
161
162        $redirect = $INPUT->get->str('redirect');
163
164        // loop counter
165        if($redirect <= 0 || $redirect > 5) {
166            return;
167        }
168
169        $ID = isset($_SESSION[DOKU_COOKIE]['redirect']) ? $_SESSION[DOKU_COOKIE]['redirect'] : null;
170        if(!$ID) {
171            return;
172        }
173        unset($_SESSION[DOKU_COOKIE]['redirect']);
174
175        $page        = cleanID($ID);
176        $use_heading = useHeading('navigation') && p_get_first_heading($page);
177        $title       = hsc($use_heading ? p_get_first_heading($page) : $page);
178
179        $url  = wl($page, array('redirect' => 'no'), true, '&');
180        $link = '<a href="' . $url . '" class="wikilink1" title="' . $page . '">' . $title . '</a>';
181        echo '<div class="noteredirect">' . sprintf($this->getLang('redirected_from'), $link) . '</div><br/>';
182    }
183
184    private function get_metadata($ID) {
185        // make sure we always get current metadata, but simple cache logic (i.e. render when page is newer than metadata) is enough
186        $metadata = p_get_metadata($ID, 'relation isreplacedby', METADATA_RENDER_USING_SIMPLE_CACHE|METADATA_RENDER_UNLIMITED);
187
188        // legacy compat
189        if(is_string($metadata)) {
190            $metadata = array($metadata);
191        }
192
193        return $metadata;
194    }
195
196    /**
197     * Redirect to url.
198     * @param string $url
199     */
200    private function redirect($url) {
201        header("HTTP/1.1 301 Moved Permanently");
202        send_redirect($url);
203    }
204
205    public function handle_move_register(Doku_Event $event, $params) {
206        $event->data['handlers']['pageredirect'] = array($this, 'rewrite_redirect');
207    }
208
209    public function rewrite_redirect($match, $state, $pos, $plugin, helper_plugin_move_handler $handler) {
210        $metadata = $this->get_metadata($ID);
211        if ($metadata[1]) return $match;  // Fail-safe for external redirection (Do not rewrite)
212
213        $match = trim($match);
214
215        if (substr($match, 0, 1) == "~") {
216            // "~~REDIRECT>pagename#anchor~~" pattern
217
218            // Strip syntax
219            $match = substr($match, 2, strlen($match) - 4);
220
221            list($syntax, $src, $anchor) = array_pad(preg_split("/>|#/", $match), 3, "");
222
223            // Resolve new source.
224            if (method_exists($handler, 'adaptRelativeId')) {
225                $new_src = $handler->adaptRelativeId($src);
226            } else {
227                $new_src = $handler->resolveMoves($src, 'page');
228                $new_src = $handler->relativeLink($src, $new_src, 'page');
229            }
230
231            $result = "~~".$syntax.">".$new_src;
232            if (!empty($anchor)) $result .= "#".$anchor;
233            $result .= "~~";
234
235            return $result;
236
237        } else if (substr($match, 0, 1) == "#") {
238            // "#REDIRECT pagename#anchor" pattern
239
240            // Strip syntax
241            $match = substr($match, 1);
242
243            list($syntax, $src, $anchor) = array_pad(preg_split("/ |#/", $match), 3, "");
244
245            // Resolve new source.
246            if (method_exists($handler, 'adaptRelativeId')) {
247                $new_src = $handler->adaptRelativeId($src);
248            } else {
249                $new_src = $handler->resolveMoves($src, 'page');
250                $new_src = $handler->relativeLink($src, $new_src, 'page');
251            }
252
253            $result = "\n#".$syntax." ".$new_src;
254            if (!empty($anchor)) $result .= "#".$anchor;
255
256            return $result;
257        }
258
259        // Fail-safe
260        return $match;
261
262    }
263}
264