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