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