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 .= $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 } else { 136 resolve_mediaid($this->ns, $old, $exists); 137 } 138 if($old == $new) { 139 return $relold; // old link still resolves, keep as is 140 } 141 142 if($conf['useslash']) $relold = str_replace('/', ':', $relold); 143 144 // check if the link was relative 145 if(strpos($relold, ':') === false ||$relold[0] == '.') { 146 $wasrel = true; 147 } else { 148 $wasrel = false; 149 } 150 151 // if it wasn't relative then, leave it absolute now, too 152 if(!$wasrel) { 153 if($this->ns && !getNS($new)) $new = ':' . $new; 154 $new = $this->_nsStartCheck($relold, $new, $type); 155 return $new; 156 } 157 158 // split the paths and see how much common parts there are 159 $selfpath = explode(':', $this->ns); 160 $goalpath = explode(':', getNS($new)); 161 $min = min(count($selfpath), count($goalpath)); 162 for($common = 0; $common < $min; $common++) { 163 if($selfpath[$common] != $goalpath[$common]) break; 164 } 165 166 // we now have the non-common part and a number of uppers 167 $ups = max(count($selfpath) - $common, 0); 168 $remainder = array_slice($goalpath, $common); 169 $upper = $ups ? array_fill(0, $ups, '..:') : array(); 170 171 // build the new relative path 172 $newrel = join(':', $upper); 173 if($remainder) $newrel .= join(':', $remainder) . ':'; 174 $newrel .= noNS($new); 175 $newrel = str_replace('::', ':', trim($newrel, ':')); 176 if($newrel[0] != '.' && $this->ns && getNS($newrel)) $newrel = '.' . $newrel; 177 178 // if the old link ended with a colon and the new one is a start page, adjust 179 $newrel = $this->_nsStartCheck($relold,$newrel,$type); 180 181 // don't use relative paths if it is ridicoulus: 182 if(strlen($newrel) > strlen($new)) { 183 $newrel = $new; 184 if($this->ns && !getNS($new)) $newrel = ':' . $newrel; 185 $newrel = $this->_nsStartCheck($relold,$newrel,$type); 186 } 187 188 return $newrel; 189 } 190 191 /** 192 * Handle camelcase links 193 * 194 * @param string $match The text match 195 * @param string $state The starte of the parser 196 * @param int $pos The position in the input 197 * @return bool If parsing should be continued 198 */ 199 public function camelcaselink($match, $state, $pos) { 200 $oldID = cleanID($this->origNS . ':' . $match); 201 $newID = $this->resolveMoves($oldID, 'page'); 202 $newNS = getNS($newID); 203 204 if($oldID == $newID || $this->origNS == $newNS) { 205 // link is still valid as is 206 $this->calls .= $match; 207 } else { 208 if(noNS($oldID) == noNS($newID)) { 209 // only namespace changed, keep CamelCase in link 210 $this->calls .= "[[$newNS:$match]]"; 211 } else { 212 // all new, keep CamelCase in title 213 $this->calls .= "[[$newID|$match]]"; 214 } 215 } 216 return true; 217 } 218 219 /** 220 * Handle rewriting of internal links 221 * 222 * @param string $match The text match 223 * @param string $state The starte of the parser 224 * @param int $pos The position in the input 225 * @return bool If parsing should be continued 226 */ 227 public function internallink($match, $state, $pos) { 228 // Strip the opening and closing markup 229 $link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match); 230 231 // Split title from URL 232 $link = explode('|', $link, 2); 233 if(!isset($link[1])) { 234 $link[1] = null; 235 } else if(preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) { 236 // If the title is an image, rewrite it 237 $old_title = $link[1]; 238 $link[1] = $this->rewrite_media($link[1]); 239 // do a simple replace of the first match so really only the id is changed and not e.g. the alignment 240 $oldpos = strpos($match, $old_title); 241 $oldlen = strlen($old_title); 242 $match = substr_replace($match, $link[1], $oldpos, $oldlen); 243 } 244 $link[0] = trim($link[0]); 245 246 //decide which kind of link it is 247 248 if(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0])) { 249 // Interwiki 250 $this->calls .= $match; 251 } elseif(preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) { 252 // Windows Share 253 $this->calls .= $match; 254 } elseif(preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) { 255 // external link (accepts all protocols) 256 $this->calls .= $match; 257 } elseif(preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) { 258 // E-Mail (pattern above is defined in inc/mail.php) 259 $this->calls .= $match; 260 } elseif(preg_match('!^#.+!', $link[0])) { 261 // local hash link 262 $this->calls .= $match; 263 } else { 264 $id = $link[0]; 265 266 $hash = ''; 267 $parts = explode('#', $id, 2); 268 if(count($parts) === 2) { 269 $id = $parts[0]; 270 $hash = $parts[1]; 271 } 272 273 $params = ''; 274 $parts = explode('?', $id, 2); 275 if(count($parts) === 2) { 276 $id = $parts[0]; 277 $params = $parts[1]; 278 } 279 280 $new_id = $this->resolveMoves($id, 'page'); 281 $new_id = $this->relativeLink($id, $new_id, 'page'); 282 283 if($id == $new_id) { 284 $this->calls .= $match; 285 } else { 286 if($params !== '') { 287 $new_id .= '?' . $params; 288 } 289 290 if($hash !== '') { 291 $new_id .= '#' . $hash; 292 } 293 294 if($link[1] != null) { 295 $new_id .= '|' . $link[1]; 296 } 297 298 $this->calls .= '[[' . $new_id . ']]'; 299 } 300 301 } 302 303 return true; 304 } 305 306 /** 307 * Handle rewriting of media links 308 * 309 * @param string $match The text match 310 * @param string $state The starte of the parser 311 * @param int $pos The position in the input 312 * @return bool If parsing should be continued 313 */ 314 public function media($match, $state, $pos) { 315 $this->calls .= $this->rewrite_media($match); 316 return true; 317 } 318 319 /** 320 * Rewrite a media syntax 321 * 322 * @param string $match The text match of the media syntax 323 * @return string The rewritten syntax 324 */ 325 protected function rewrite_media($match) { 326 $p = Doku_Handler_Parse_Media($match); 327 if($p['type'] == 'internalmedia') { // else: external media 328 329 $new_src = $this->resolveMoves($p['src'], 'media'); 330 $new_src = $this->relativeLink($p['src'], $new_src, 'media'); 331 332 if($new_src !== $p['src']) { 333 // do a simple replace of the first match so really only the id is changed and not e.g. the alignment 334 $srcpos = strpos($match, $p['src']); 335 $srclen = strlen($p['src']); 336 return substr_replace($match, $new_src, $srcpos, $srclen); 337 } 338 } 339 return $match; 340 } 341 342 /** 343 * Handle rewriting of plugin syntax, calls the registered handlers 344 * 345 * @param string $match The text match 346 * @param string $state The starte of the parser 347 * @param int $pos The position in the input 348 * @param string $pluginname The name of the plugin 349 * @return bool If parsing should be continued 350 */ 351 public function plugin($match, $state, $pos, $pluginname) { 352 if(isset($this->handlers[$pluginname])) { 353 $this->calls .= call_user_func($this->handlers[$pluginname], $match, $state, $pos, $pluginname, $this); 354 } else { 355 $this->calls .= $match; 356 } 357 return true; 358 } 359 360 /** 361 * Catchall handler for the remaining syntax 362 * 363 * @param string $name Function name that was called 364 * @param array $params Original parameters 365 * @return bool If parsing should be continue 366 */ 367 public function __call($name, $params) { 368 if(count($params) == 3) { 369 $this->calls .= $params[0]; 370 return true; 371 } else { 372 trigger_error('Error, handler function ' . hsc($name) . ' with ' . count($params) . ' parameters called which isn\'t implemented', E_USER_ERROR); 373 return false; 374 } 375 } 376 377 public function _finalize() { 378 // remove padding that is added by the parser in parse() 379 $this->calls = substr($this->calls, 1, -1); 380 } 381 382} 383