1<?php 2/** 3 * Move Plugin Rewriting Handler 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Michael Hamann <michael@content-space.de> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12/** 13 * Handler class for move. It does the actual rewriting of the content. 14 * 15 * Note: This is not actually a valid DokuWiki Helper plugin and can not be loaded via plugin_load() 16 */ 17class helper_plugin_move_handler extends DokuWiki_Plugin { 18 public $calls = ''; 19 20 protected $id; 21 protected $ns; 22 protected $origID; 23 protected $origNS; 24 protected $page_moves; 25 protected $media_moves; 26 protected $handlers; 27 28 /** 29 * Do not allow re-using instances. 30 * 31 * @return bool false - the handler must not be re-used. 32 */ 33 public function isSingleton() { 34 return false; 35 } 36 37 /** 38 * Initialize the move handler. 39 * 40 * @param string $id The id of the text that is passed to the handler 41 * @param string $original The name of the original ID of this page. Same as $id if this page wasn't moved 42 * @param array $page_moves Moves that shall be considered in the form [[$old,$new],...] ($old can be $original) 43 * @param array $media_moves Moves of media files that shall be considered in the form $old => $new 44 * @param array $handlers Handlers for plugin content in the form $plugin_name => $callback 45 */ 46 public function init($id, $original, $page_moves, $media_moves, $handlers) { 47 $this->id = $id; 48 $this->ns = getNS($id); 49 $this->origID = $original; 50 $this->origNS = getNS($original); 51 $this->page_moves = $page_moves; 52 $this->media_moves = $media_moves; 53 $this->handlers = $handlers; 54 } 55 56 /** 57 * Go through the list of moves and find the new value for the given old ID 58 * 59 * @param string $old the old, full qualified ID 60 * @param string $type 'media' or 'page' 61 * @throws Exception on bad argument 62 * @return string the new full qualified ID 63 */ 64 public function resolveMoves($old, $type) { 65 global $conf; 66 67 if($type != 'media' && $type != 'page') throw new Exception('Not a valid type'); 68 69 $old = resolve_id($this->origNS, $old, false); 70 71 if($type == 'page') { 72 // FIXME this simply assumes that the link pointed to :$conf['start'], but it could also point to another page 73 // resolve_pageid does a lot more here, but we can't really assume this as the original pages might have been 74 // deleted already 75 if(substr($old, -1) === ':' || $old === '') $old .= $conf['start']; 76 77 $moves = $this->page_moves; 78 } else { 79 $moves = $this->media_moves; 80 } 81 82 $old = cleanID($old); 83 84 foreach($moves as $move) { 85 if($move[0] == $old) { 86 $old = $move[1]; 87 } 88 } 89 90 return $old; // this is now new 91 } 92 93 /** 94 * if the old link ended with a colon and the new one is a start page, adjust 95 * 96 * @param $relold string the old, possibly relative ID 97 * @param $new string the new, full qualified ID 98 * @param $type 'media' or 'page' 99 * @return string 100 */ 101 protected function _nsStartCheck($relold, $new, $type) { 102 global $conf; 103 if($type == 'page' && substr($relold, -1) == ':') { 104 $len = strlen($conf['start']); 105 if($new == $conf['start']) { 106 $new = '.:'; 107 } else if(substr($new, -1 * ($len + 1)) == ':' . $conf['start']) { 108 $new = substr($new, 0, -1 * $len); 109 } 110 } 111 return $new; 112 } 113 114 /** 115 * Construct a new ID relative to the current page's location 116 * 117 * Uses a relative link only if the original was relative, too. This function is for 118 * pages and media files. 119 * 120 * @param string $relold the old, possibly relative ID 121 * @param string $new the new, full qualified ID 122 * @param string $type 'media' or 'page' 123 * @throws Exception on bad argument 124 * @return string 125 */ 126 public function relativeLink($relold, $new, $type) { 127 global $conf; 128 if($type != 'media' && $type != 'page') throw new Exception('Not a valid type'); 129 130 // first check if the old link still resolves 131 $exists = false; 132 $old = $relold; 133 if($type == 'page') { 134 resolve_pageid($this->ns, $old, $exists); 135 // Work around bug in DokuWiki 2020-07-29 where resolve_pageid doesn't append the start page to a link to 136 // the root. 137 if ($old === '') { 138 $old = $conf['start']; 139 } 140 } else { 141 resolve_mediaid($this->ns, $old, $exists); 142 } 143 if($old == $new) { 144 return $relold; // old link still resolves, keep as is 145 } 146 147 if($conf['useslash']) $relold = str_replace('/', ':', $relold); 148 149 // check if the link was relative 150 if(strpos($relold, ':') === false ||$relold[0] == '.') { 151 $wasrel = true; 152 } else { 153 $wasrel = false; 154 } 155 156 // if it wasn't relative then, leave it absolute now, too 157 if(!$wasrel) { 158 if($this->ns && !getNS($new)) $new = ':' . $new; 159 $new = $this->_nsStartCheck($relold, $new, $type); 160 return $new; 161 } 162 163 // split the paths and see how much common parts there are 164 $selfpath = explode(':', $this->ns); 165 $goalpath = explode(':', getNS($new)); 166 $min = min(count($selfpath), count($goalpath)); 167 for($common = 0; $common < $min; $common++) { 168 if($selfpath[$common] != $goalpath[$common]) break; 169 } 170 171 // we now have the non-common part and a number of uppers 172 $ups = max(count($selfpath) - $common, 0); 173 $remainder = array_slice($goalpath, $common); 174 $upper = $ups ? array_fill(0, $ups, '..:') : array(); 175 176 // build the new relative path 177 $newrel = join(':', $upper); 178 if($remainder) $newrel .= join(':', $remainder) . ':'; 179 $newrel .= noNS($new); 180 $newrel = str_replace('::', ':', trim($newrel, ':')); 181 if($newrel[0] != '.' && $this->ns && getNS($newrel)) $newrel = '.' . $newrel; 182 183 // if the old link ended with a colon and the new one is a start page, adjust 184 $newrel = $this->_nsStartCheck($relold,$newrel,$type); 185 186 // don't use relative paths if it is ridicoulus: 187 if(strlen($newrel) > strlen($new)) { 188 $newrel = $new; 189 if($this->ns && !getNS($new)) $newrel = ':' . $newrel; 190 $newrel = $this->_nsStartCheck($relold,$newrel,$type); 191 } 192 193 return $newrel; 194 } 195 196 /** 197 * Handle camelcase links 198 * 199 * @param string $match The text match 200 * @param string $state The starte of the parser 201 * @param int $pos The position in the input 202 * @return bool If parsing should be continued 203 */ 204 public function camelcaselink($match, $state, $pos) { 205 $oldID = cleanID($this->origNS . ':' . $match); 206 $newID = $this->resolveMoves($oldID, 'page'); 207 $newNS = getNS($newID); 208 209 if($oldID == $newID || $this->origNS == $newNS) { 210 // link is still valid as is 211 $this->calls .= $match; 212 } else { 213 if(noNS($oldID) == noNS($newID)) { 214 // only namespace changed, keep CamelCase in link 215 $this->calls .= "[[$newNS:$match]]"; 216 } else { 217 // all new, keep CamelCase in title 218 $this->calls .= "[[$newID|$match]]"; 219 } 220 } 221 return true; 222 } 223 224 /** 225 * Handle rewriting of internal links 226 * 227 * @param string $match The text match 228 * @param string $state The starte of the parser 229 * @param int $pos The position in the input 230 * @return bool If parsing should be continued 231 */ 232 public function internallink($match, $state, $pos) { 233 // Strip the opening and closing markup 234 $link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match); 235 236 // Split title from URL 237 $link = explode('|', $link, 2); 238 if(!isset($link[1])) { 239 $link[1] = null; 240 } else if(preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) { 241 // If the title is an image, rewrite it 242 $old_title = $link[1]; 243 $link[1] = $this->rewrite_media($link[1]); 244 // do a simple replace of the first match so really only the id is changed and not e.g. the alignment 245 $oldpos = strpos($match, $old_title); 246 $oldlen = strlen($old_title); 247 $match = substr_replace($match, $link[1], $oldpos, $oldlen); 248 } 249 $link[0] = trim($link[0]); 250 251 //decide which kind of link it is 252 253 if(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0])) { 254 // Interwiki 255 $this->calls .= $match; 256 } elseif(preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) { 257 // Windows Share 258 $this->calls .= $match; 259 } elseif(preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) { 260 // external link (accepts all protocols) 261 $this->calls .= $match; 262 } elseif(preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) { 263 // E-Mail (pattern above is defined in inc/mail.php) 264 $this->calls .= $match; 265 } elseif(preg_match('!^#.+!', $link[0])) { 266 // local hash link 267 $this->calls .= $match; 268 } else { 269 $id = $link[0]; 270 271 $hash = ''; 272 $parts = explode('#', $id, 2); 273 if(count($parts) === 2) { 274 $id = $parts[0]; 275 $hash = $parts[1]; 276 } 277 278 $params = ''; 279 $parts = explode('?', $id, 2); 280 if(count($parts) === 2) { 281 $id = $parts[0]; 282 $params = $parts[1]; 283 } 284 285 $new_id = $this->resolveMoves($id, 'page'); 286 $new_id = $this->relativeLink($id, $new_id, 'page'); 287 288 if($id == $new_id) { 289 $this->calls .= $match; 290 } else { 291 if($params !== '') { 292 $new_id .= '?' . $params; 293 } 294 295 if($hash !== '') { 296 $new_id .= '#' . $hash; 297 } 298 299 if($link[1] != null) { 300 $new_id .= '|' . $link[1]; 301 } 302 303 $this->calls .= '[[' . $new_id . ']]'; 304 } 305 306 } 307 308 return true; 309 } 310 311 /** 312 * Handle rewriting of media links 313 * 314 * @param string $match The text match 315 * @param string $state The starte of the parser 316 * @param int $pos The position in the input 317 * @return bool If parsing should be continued 318 */ 319 public function media($match, $state, $pos) { 320 $this->calls .= $this->rewrite_media($match); 321 return true; 322 } 323 324 /** 325 * Rewrite a media syntax 326 * 327 * @param string $match The text match of the media syntax 328 * @return string The rewritten syntax 329 */ 330 protected function rewrite_media($match) { 331 $p = Doku_Handler_Parse_Media($match); 332 if($p['type'] == 'internalmedia') { // else: external media 333 334 $new_src = $this->resolveMoves($p['src'], 'media'); 335 $new_src = $this->relativeLink($p['src'], $new_src, 'media'); 336 337 if($new_src !== $p['src']) { 338 // do a simple replace of the first match so really only the id is changed and not e.g. the alignment 339 $srcpos = strpos($match, $p['src']); 340 $srclen = strlen($p['src']); 341 return substr_replace($match, $new_src, $srcpos, $srclen); 342 } 343 } 344 return $match; 345 } 346 347 /** 348 * Handle rewriting of plugin syntax, calls the registered handlers 349 * 350 * @param string $match The text match 351 * @param string $state The starte of the parser 352 * @param int $pos The position in the input 353 * @param string $pluginname The name of the plugin 354 * @return bool If parsing should be continued 355 */ 356 public function plugin($match, $state, $pos, $pluginname) { 357 if(isset($this->handlers[$pluginname])) { 358 $this->calls .= call_user_func($this->handlers[$pluginname], $match, $state, $pos, $pluginname, $this); 359 } else { 360 $this->calls .= $match; 361 } 362 return true; 363 } 364 365 /** 366 * Catchall handler for the remaining syntax 367 * 368 * @param string $name Function name that was called 369 * @param array $params Original parameters 370 * @return bool If parsing should be continue 371 */ 372 public function __call($name, $params) { 373 if(count($params) == 3) { 374 $this->calls .= $params[0]; 375 return true; 376 } else { 377 trigger_error('Error, handler function ' . hsc($name) . ' with ' . count($params) . ' parameters called which isn\'t implemented', E_USER_ERROR); 378 return false; 379 } 380 } 381 382 public function _finalize() { 383 // remove padding that is added by the parser in parse() 384 $this->calls = substr($this->calls, 1, -1); 385 } 386 387} 388