1<?php 2/** 3 * Plugin : Pagemove 4 * Version : 0.10 (2010-06-17) 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Gary Owen, 8 */ 9 10// must be run within Dokuwiki 11if (!defined('DOKU_INC')) die(); 12 13if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 14if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 15if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 16 17require_once DOKU_PLUGIN.'admin.php'; 18 19require_once(DOKU_INC.'inc/search.php'); 20 21 22class admin_plugin_pagemove extends DokuWiki_Admin_Plugin { 23 24 var $show_form = true; 25 var $have_rights = true; 26 var $locked_files = array(); 27 var $errors = array(); 28 var $opts = array(); 29 var $text = ''; 30 var $idsToDelete = array(); 31 32 33 function getMenuSort() { return FIXME; } 34 function forAdminOnly() { return false; } 35 36 /** 37 * function constructor 38 */ 39 function admin_plugin_pagemove(){ 40 // enable direct access to language strings 41 $this->setupLocale(); 42 } 43 44 /** 45 * return some info 46 */ 47 function getInfo(){ 48 return array( 49 'author' => 'Gary Owen, Arno Puschmann, Christoph Jähnigen', 50 'email' => 'pagemove@gmail.com', 51 'date' => '2011-08-11', 52 'name' => 'Pagemove', 53 'desc' => $this->lang['desc'], 54 'url' => 'http://www.dokuwiki.org/plugin:pagemove', 55 ); 56 } 57 58 /** 59 * Only show the menu text for pages we can move or rename. 60 */ 61 function getMenuText() { 62 global $INFO; 63 global $ID; 64 global $conf; 65 66 if( !$INFO['exists'] ) 67 return $this->lang['menu'].' ('.$this->lang['pm_notexist'].')'; 68 elseif( $ID == $conf['start'] ) 69 return $this->lang['menu'].' ('.$this->lang['pm_notstart'].')'; 70 elseif( !$INFO['writable'] ) 71 return $this->lang['menu'].' ('.$this->lang['pm_notwrite'].')'; 72 else 73 return $this->lang['menu']; 74 } 75 76 77 78 /** 79 * output appropriate html 80 * 81 * @author Gary Owen <gary@isection.co.uk> 82 */ 83 function html() { 84 global $lang; 85 86 ptln('<!-- Pagemove Plugin start -->'); 87 if( $this->show_form ) { 88 ptln( $this->locale_xhtml('pagemove') ); 89 //We didn't get here from submit. 90 if( $this->have_rights && count($this->locked_files) == 0 ) { 91 $this->_pm_form(); 92 } 93 else { 94 ptln( '<p><strong>' ); 95 if ( !$this->have_rights ) { 96 ptln( $this->errors[0].'<br>' ); 97 } 98 $c = count($this->locked_files); 99 if ( $c == 1 ) { 100 ptln( $this->lang['pm_filelocked'].$this->locked_files[0].'<br>'.$this->lang['pm_tryagain'] ); 101 } 102 elseif ( $c > 1 ) { 103 ptln( $this->lang['pm_fileslocked'] ); 104 for ( $i = 0 ; $i < $c ; $i++ ) { 105 ptln ( ($i > 0 ? ', ' : '').$this->locked_files[$i] ); 106 } 107 ptln( '<br>'.$this->lang['pm_tryagain'] ); 108 } 109 ptln ( '</strong></p>' ); 110 } 111 } 112 else { 113 // display the moved/renamed page 114 ptln( $this->render($this->text) ); 115 } 116 ptln('<!-- Pagemove Plugin end -->'); 117 } 118 119 /** 120 * show the move and/or rename a page form 121 * 122 * @author Gary Owen <gary@isection.co.uk> 123 */ 124 function _pm_form() { 125 global $ID; 126 global $lang; 127 global $conf; 128 129 $ns = getNS($ID); 130 $name = noNS($ID); 131 132 ptln(' <div align="center">'); 133 ptln(' <script language="Javascript">'); 134 ptln(' function setradio( group, choice ) {'); 135 ptln(' for ( i = 0 ; i < group.length ; i++ ) {'); 136 ptln(' if ( group[i].value == choice )'); 137 ptln(' group[i].checked = true;'); 138 ptln(' }'); 139 ptln(' }'); 140 ptln(' </script>'); 141 ptln(' <form name="frm" action="'.wl($ID).'" method="post">'); 142 // output hidden values to ensure dokuwiki will return back to this plugin 143 ptln(' <input type="hidden" name="do" value="admin" />'); 144 ptln(' <input type="hidden" name="page" value="'.$this->getPluginName().'" />'); 145 ptln(' <input type="hidden" name="id" value="'.$ID.'" />'); 146 ptln(' <fieldset id="fieldset_page">'); 147 ptln(' <legend><input type="radio" name="page_ns" id="page_ns_0" value="page" CHECKED> '. $this->lang['pm_movepage'] .'</legend>'); 148 ptln(' <table border="0" id="table_page">'); 149 150 //Show any errors 151 if (count($this->errors) > 0) { 152 ptln ('<tr><td bgcolor="red" colspan="3">'); 153 foreach($this->errors as $error) { 154 ptln ($error.'<br>'); 155 } 156 ptln ('</td></tr>'); 157 } 158 //create a list of namespaces 159 ptln( ' <tr><td align="right" nowrap><label><span>'.$this->lang['pm_targetns'].'</span></label></td>'); 160 ptln( ' <td width="25"><input type="radio" name="nsr" id="nsr_0" value="<old>" '.($_REQUEST['nsr'] != '<new>' ? 'CHECKED' : '').'></td>'); 161 ptln( ' <td><select name="ns_for_page" id="nsr_select" onChange="setradio(document.frm.nsr, \'<old>\');setradio(document.frm.page_ns, \'page\');">'); 162 $this->_pm_form_create_list_ns($ns); 163 164 ptln( " </select></td>\n </tr><tr>"); 165 166 ptln( ' <td align="right" nowrap><label><span>'.$this->lang['pm_newtargetns'].'</span></label></td>'); 167 ptln( ' <td width="25"><input type="radio" name="nsr" id="nsr_1" value="<new>" '.($_REQUEST['nsr'] == '<new>' ? 'CHECKED' : '').'></td>'); 168 ptln( ' <td align="left" nowrap><input type="text" name="newns" id="newns" value="'.formtext($this->opts['newns']).'" class="edit" onClick="setradio(document.frm.nsr, \'<new>\');setradio(document.frm.page_ns, \'page\');" /></td>'); 169 ptln( ' </tr>'); 170 ptln( ' <tr>'); 171 ptln( ' <td align="right" nowrap><label><span>'.$this->lang['pm_newname'].'</span></label></td>'); 172 ptln(' <td width="25"></td>'); //<input type="radio" name="pageradio" value="<page>" '.($_REQUEST['pageradio']!= '<namespace>' ? 'CHECKED' : '').'> 173 ptln( ' <td align="left" nowrap><input type="text" name="pagename" id="pagename" value="'.formtext(isset($this->opts['newname']) ? $this->opts['newname'] : $name).'" class="edit" onClick="setradio(document.frm.page_ns, \'page\');" /></td>'); 174 ptln( ' </tr>'); 175 ptln( ' </tr>'); 176 ptln( ' </tr>'); 177 ptln( ' </table>'); 178 ptln( ' </fieldset>'); 179 180 ptln(' <br>'); 181 ptln(' <fieldset id="fieldset_ns" >'); 182 ptln(' <legend><input type="radio" name="page_ns" id="page_ns_1" value="ns"> '. $this->lang['pm_movens'] .'</legend>'); 183 ptln(' <table border="0" id="table_ns">'); 184 ptln( ' <tr><td align="right" nowrap><label><span>'.$this->lang['pm_targetns'].'</span></label></td>'); 185 ptln( ' <td><select name="ns" id="ns_select" onChange="setradio(document.frm.page_ns, \'ns\');">'); 186 $this->_pm_form_create_list_ns($ns); 187 ptln( " </select></td>\n </tr>"); 188 ptln( ' <tr>'); 189 ptln( ' <td align="right" nowrap><label><span>'.$this->lang['pm_newnsname'].'</span></label></td>'); 190 ptln( ' <td align="left" nowrap><input type="text" name="namespacename" id="namespacename" value="'.formtext(isset($this->opts['newnsname']) ? $this->opts['newnsname'] : $this->opts['nsname']).'" class="edit" onClick="setradio(document.frm.page_ns, \'ns\');" /></td>'); 191 ptln( ' </tr>'); 192 ptln( ' </table>'); 193 ptln(' </fieldset>'); 194 ptln( '<br><center><input type="submit" value="'.formtext($this->lang['pm_submit']).'" class="button" /><input type="button" value="'.$this->lang['pm_preview'].'" class="button" onClick="Javascript:preview();"/></center>'); 195 ptln( '</form>'); 196 197 ptln('<font id="preview_output"></font>'); 198 199 ptln(' <script language="Javascript">'); 200 ptln(" table_page_width = document.getElementById('table_page').offsetWidth;"); 201 ptln(" table_ns_width = document.getElementById('table_ns').offsetWidth;"); 202 ptln(" max_width = Math.max(table_page_width,table_ns_width)+'px';"); 203 ptln(" document.getElementById('fieldset_page').style.width = max_width;"); 204 ptln(" document.getElementById('fieldset_ns').style.width = max_width;"); 205 206 ptln("function preview(){"); 207 ptln("if(document.getElementById('page_ns_0').checked == true)"); 208 ptln("{"); 209 ptln(" if(document.getElementById('nsr_0').checked == true)"); 210 ptln(" {"); 211 ptln(" preview_text = \"".$ID . $this->lang['pm_previewpage']. " \" + document.getElementById('nsr_select').value + (document.getElementById('nsr_select').value==':'? '' : ':') + document.getElementById('pagename').value;"); 212 ptln(" }"); 213 ptln(" else"); 214 ptln(" {"); 215 ptln(" preview_text = \"".$ID . $this->lang['pm_previewpage']. " \" + document.getElementById('newns').value + ':' + document.getElementById('pagename').value;"); 216 ptln(" }"); 217 ptln("}"); 218 ptln("else{"); 219 ptln(" preview_text = \"". sprintf($this->lang['pm_previewns'], $ns). " \" + document.getElementById('ns_select').value + (document.getElementById('ns_select').value==':'? '' : ':') + document.getElementById('namespacename').value;"); 220 ptln("}"); 221 ptln("document.getElementById('preview_output').innerHTML = preview_text;"); 222 ptln(""); 223 ptln("}"); 224 ptln(" </script>"); 225 226 ptln( '</div>'); 227 } 228 229 230 /** 231 * create a list of namespaces for the html form 232 * 233 * @author Gary Owen <gary@isection.co.uk> 234 * @author Arno Puschmann (bin out of _pm_form) 235 */ 236 function _pm_form_create_list_ns($ns) { 237 global $conf; 238 239 $namesp = array( 0 => '' ); //Include root 240 search($namesp, $conf['datadir'], 'search_namespaces', array()); 241 sort($namesp); 242 foreach($namesp as $row) { 243 if ( auth_quickaclcheck($row['id'].':*') >= AUTH_CREATE || $row['id'] == $ns ) { 244 ptln ( ' <option value="'. 245 ($row['id'] ? $row['id'] : ':'). 246 ($_REQUEST['ns'] ? 247 (($row['id'] ? $row['id'] : ":") == $_REQUEST['ns'] ? '" SELECTED>' : '">') : 248 ($row['id'] == $ns ? '" SELECTED>' : '">') ). 249 ($row['id'] ? $row['id'].':' : ": ".$this->lang['pm_root']). 250 ($row['id'] == $ns ? ' '.$this->lang['pm_current'] : ''). 251 "</option>" ); 252 } 253 } 254 } 255 256 257 /** 258 * handle user request 259 * 260 * @author Gary Owen <gary@isection.co.uk> 261 */ 262 function handle() { 263 264 global $conf; 265 global $lang; 266 global $ID; 267 global $INFO; 268 global $ACT; 269 270 // check we have rights to move this document 271 if( !$INFO['exists'] ) { 272 $this->have_rights = false; 273 $this->errors[] = $this->lang['pm_notexist']; 274 return; 275 } 276 // do not move start page 277 if( $ID == $conf['start'] ) { 278 $this->have_rights = false; 279 $this->errors[] = $this->lang['pm_notstart']; 280 return; 281 } 282 283 // was a form send? 284 if (! array_key_exists('page_ns', $_REQUEST)) { 285 // @fixme do something more intelligent like showing in message 286 return; 287 } 288 289 // extract namespace and document name from ID 290 $this->opts['ns'] = getNS($ID); 291 $this->opts['name'] = noNS($ID); 292 $this->opts['page_ns'] = $_REQUEST['page_ns']; 293 294 // check the input for completeness 295 if( $this->opts['page_ns'] == 'ns' ) { 296 // @todo Target namespace needn't be new (check pages for overwrite!) 297 if( $_REQUEST['namespacename'] == '' ) { 298 $this->errors[] = $this->lang['pm_emptynamespace']; 299 return; 300 } 301 $this->opts['newnsname'] = $_REQUEST['namespacename']; 302 if ( cleanID($this->opts['newnsname']) == '' ) { 303 $this->errors[] = $this->lang['pm_badns']; 304 return; 305 } 306 if ($_REQUEST['ns'] == ':') { 307 $this->opts['newns'] = $this->opts['newnsname']; 308 } 309 else { 310 $this->opts['newns'] = $_REQUEST['ns'].':'.$this->opts['newnsname']; 311 } 312 313 // check the NS if a recursion is needed 314 // @fixme Is this still needed? 315 $pagelist = array(); 316 $needrecursion = false; 317 $nsRelPath = utf8_encodeFN(str_replace(':', '/', $this->opts['ns'])); 318 search($items, $conf['datadir'], 'search_index', '', $nsRelPath); 319 foreach ($items as $item) { 320 if ($item['type'] == 'd') { 321 $needrecursion = true; 322 break; 323 } 324 } 325 326 $nsRelPath = utf8_encodeFN(str_replace(':', '/', $this->opts['ns'])); 327 $this->_pm_move_recursive($nsRelPath, $this->opts); 328 329 $newNsAbsPath = $conf['datadir'].'/'.str_replace(':', '/', $this->opts['newns']); 330 $this->_pm_disable_cache($newNsAbsPath); 331 } 332 elseif( $this->opts['page_ns'] == 'page' ) { 333 if( $_REQUEST['pagename'] == '' ) { 334 $this->errors[] = $this->lang['pm_emptypagename']; 335 return; 336 } 337 $this->opts['newname'] = $_REQUEST['pagename']; 338 // check that the pagename is valid 339 if ( cleanID($this->opts['newname']) == '' ) { 340 $this->errors[] = $this->lang['pm_badname']; 341 return; 342 } 343 344 if ($_REQUEST['nsr'] == '<old>') { 345 $this->opts['newns'] = ($_REQUEST['ns_for_page'] == ':' ? '' : $_REQUEST['ns_for_page']); 346 } 347 elseif ($_REQUEST['nsr'] =='<new>') { 348 // if a new namespace was requested, check and use it 349 if ($_REQUEST['newns'] != '') { 350 $this->opts['newns'] = $_REQUEST['newns']; 351 // check that the new namespace is valid 352 if ( cleanID($this->opts['newns']) == '' ) { 353 $this->errors[] = $this->lang['pm_badns']; 354 return; 355 } 356 } 357 else { 358 $this->errors[] = $this->lang['pm_badns']; 359 return; 360 } 361 } 362 else { 363 $this->errors[] = $this->lang['pm_fatal']; 364 return; 365 } 366 367 $this->_pm_move_page($this->opts); 368 369 // @todo if the namespace is now empty, delete it 370 371 // Set things up to display the new page. 372 io_saveFile($conf['cachedir'].'/purgefile', time()); 373 $ID = $opts['new_id']; 374 $ACT = 'show'; 375 $INFO = pageinfo(); 376 $this->show_form = false; 377 } 378 else { 379 $this->errors[] = $this->lang['pm_fatal']; 380 return; 381 } 382 383 384 // only go on if no errors occured and inputs are not empty 385 if (count($this->errors) != 0 ) { 386 return; 387 } 388 // delete empty namespaces if possible 389 // @fixme does not work like that 390 foreach ($this->idsToDelete as $idToDelete) { 391 io_sweepNS($idToDelete); 392 } 393 394 } 395 396 397 /** 398 * touch every file which was moved, because of cached backlinks inside of moved namespace 399 * 400 * @author Arno Puschmann 2010-01-29 401 * @param $pathToSearch 402 * @return unknown_type 403 */ 404 function _pm_disable_cache($pathToSearch) { 405 $files = scandir($pathToSearch); 406 if( !empty($files) ) { 407 foreach($files as $file) { 408 if( $file == '.' || $file == '..' ) continue; 409 if( is_dir($pathToSearch.'/'.$file) ) { 410 $this->_pm_disable_cache($pathToSearch.'/'.$file); 411 } 412 else { 413 if( preg_match('#\.txt$#', $file) ) { 414 touch($pathToSearch.'/'.$file, time()+1); 415 } 416 } 417 } 418 } 419 } 420 421 422 /** 423 * 424 * @author Bastian Wolf 425 * @param $pathToSearch 426 * @param $opts 427 * @return unknown_type 428 */ 429 function _pm_move_recursive($pathToSearch, $opts) { 430 global $ID; 431 global $conf; 432 433 $pagelist = array(); 434 search($pagelist, $conf['datadir'], 'search_index', '', $pathToSearch); 435 436 foreach ($pagelist as $page) { 437 if ($page['type'] == 'd') { 438 $pathToSearch = utf8_encodeFN(str_replace(':', '/', $page['id'])); 439 // @fixme shouldn't be necessary as ID already exists 440 io_createNamespace($page['id']); 441 // NS to move is this one 442 $nsOpts = $opts; 443 $nsOpts['ns'] = $page['id']; 444 // target NS is this folder under the current target NS 445 $thisFolder = end(explode(':', $page['id'])); 446 $nsOpts['newns'] .= ':'.$thisFolder; 447 array_push($this->idsToDelete, $page['id']); 448 // Recursion 449 $this->_pm_move_recursive($pathToSearch, $nsOpts); 450 } 451 elseif ($page['type'] == 'f') { 452 $ID = $page['id']; 453 $pageOpts = $opts; 454 $pageOpts['ns'] = getNS($ID); 455 $pageOpts['name'] = noNS($ID); 456 $pageOpts['newname'] = noNS($ID); 457 $this->_pm_move_page($pageOpts); 458 } 459 else { 460 $this->errors[] = $this->lang['pm_unknown_file_type']; 461 return; 462 } 463 } 464 } 465 466 467 /** 468 * move page 469 * 470 * @author Gary Owen <gary@isection.co.uk>, modified by Kay Roesler 471 * 472 * @param array $opts 473 */ 474 function _pm_move_page($opts) { 475 476 global $conf; 477 global $lang; 478 global $ID; 479 global $INFO; 480 global $ACT; 481 482 // Check we have rights to move this document 483 if ( !$INFO['exists']) { 484 $this->have_rights = false; 485 $this->errors[] = $this->lang['pm_notexist']; 486 return; 487 } 488 if ( $ID == $conf['start']) { 489 $this->have_rights = false; 490 $this->errors[] = $this->lang['pm_notstart']; 491 return; 492 } 493 if ( auth_quickaclcheck($ID) < AUTH_EDIT ) { 494 $this->have_rights = false; 495 $this->errors[] = $this->lang['pm_norights']; 496 return; 497 } 498 499 // Check file is not locked 500 if (checklock($ID)) { 501 $this->locked_files[] = $ID; 502 } 503 504 // get all backlink information 505 $backlinksById = array(); 506 $this->_pm_search($backlinksById, $conf['datadir'], '_pm_search_backlinks', $opts); 507 508 // Check we have edit rights on the backlinks and they are not locked 509 foreach($backlinksById as $backlinkingId=>$backlinks) { 510 if (auth_quickaclcheck($backlinkingId) < AUTH_EDIT) { 511 $this->have_rights = false; 512 } 513 if (checklock($backlinkingId)) { 514 $this->locked_files[] = $backlinkingId; 515 } 516 } 517 518 // Assemble fill document name and path 519 $opts['new_id'] = cleanID($opts['newns'].':'.$opts['newname']); 520 $opts['new_path'] = wikiFN($opts['new_id']); 521 522 // Has the document name and/or namespace changed? 523 if ( $opts['newns'] == $opts['ns'] && $opts['newname'] == $opts['name'] ) { 524 $this->errors[] = $this->lang['pm_nochange']; 525 return; 526 } 527 // Check the page does not already exist 528 if ( @file_exists($opts['new_path']) ) { 529 $this->errors[] = sprintf($this->lang['pm_existing'], $opts['newname'], 530 ($opts['newns'] == '' ? $this->lang['pm_root'] : $opts['newns'])); 531 return; 532 } 533 534 if ( count($this->errors) != 0 ) { 535 return; 536 } 537 538 /** 539 * End of init (checks) 540 */ 541 542 // Open the old document and change forward links 543 lock($ID); 544 $this->text = io_readFile(wikiFN($ID), True); 545 546 // Get an array of forward links from the document 547 $forward = $this->_pm_getforwardlinks($ID); 548 549 // Change the forward links 550 foreach($forward as $lnk => $lid) { 551 // Get namespace of target document 552 $tns = getNS($lid); 553 $tname = noNS($lid); 554 // Form new document ID for the target 555 556 $matches = array(); 557 if ( $tns == $opts['newns'] ) { 558 // Document is in same namespace as target 559 $this->_pm_updatelinks($this->text, array($lnk => $tname)); 560 } 561 elseif ( preg_match('#^'.$opts['newns'].':(.*:)$#', $tns, $matches) ) { 562 // Target is in a sub-namespace 563 $this->_pm_updatelinks($this->text, array($lnk => '.:'.$matches[1].':'.$tname)); 564 } 565 elseif ( $tns == "" ) { 566 // Target is in root namespace 567 $this->_pm_updatelinks($this->text, array($lnk => $lid )); 568 } 569 else { 570 $this->_pm_updatelinks($this->text, array($lnk => $lid )); 571 } 572 } 573 574 if ( $opts['ns'] != $opts['newns'] ) { 575 // Change media links when moving between namespaces 576 $media = $this->_pm_getmedialinks($ID); 577 foreach($media as $lnk => $lid) { 578 $tns = getNS($lid); 579 $tname = noNS($lid); 580 // Form new document id for the target 581 $matches = array(); 582 if ( $tns == $opts['newns'] ) { 583 // Document is in same namespace as target 584 $this->_pm_updatemedialinks($this->text, $lnk, $tname ); 585 } 586 elseif ( preg_match('#^'.$opts['newns'].':(.*:)$#', $tns, $matches) ) { 587 // Target is in a sub-namespace 588 $this->_pm_updatemedialinks($this->text, $lnk, '.:'.$matches[1].':'.$tname ); 589 } 590 elseif ( $tns == "" ) { 591 // Target is in root namespace 592 $this->_pm_updatemedialinks($this->text, $lnk, ':'.$lid ); 593 } 594 else { 595 $this->_pm_updatemedialinks($this->text, $lnk, $lid ); 596 } 597 } 598 } 599 600 // Move the Subscriptions & Indexes 601 $this->_pm_movemeta('metadir', '/^'.$opts['name'].'\.\w*?$/', $opts); 602 603 // Save the updated document in its new location 604 if ($opts['ns'] == $opts['newns']) { 605 $lang_key = 'pm_renamed'; 606 } 607 elseif ( $opts['name'] == $opts['newname'] ) { 608 $lang_key = 'pm_moved'; 609 } 610 else { 611 $lang_key = 'pm_move_rename'; 612 } 613 $summary = sprintf($this->lang[$lang_key], $ID, $opts['new_id']); 614 saveWikiText($opts['new_id'], $this->text, $summary); 615 616 // Delete the orginal file 617 if (@file_exists(wikiFN($opts['new_id']))) { 618 saveWikiText($ID, '', $this->lang['pm_delete'] ); 619 } 620 621 // Loop through backlinks 622 foreach($backlinksById as $backlinkingId => $backlinks) { 623 $this->_pm_updatebacklinks($backlinkingId, $backlinks, $opts, $brackets); 624 } 625 626 // Move the old revisions 627 $this->_pm_movemeta('olddir', '/^'.$opts['name'].'\.[0-9]{10}\.txt(\.gz)?$/', $opts); 628 629 } 630 631 632 /** 633 * Modify the links in a backlink. 634 * 635 * @param id Page ID of the backlinking page 636 * @param links Array of page names on this page. 637 * 638 * @author Gary Owen <gary@isection.co.uk> 639 */ 640 function _pm_updatebacklinks($backlinkingId, $links, $opts, &$brackets) { 641 global $ID; 642 643 // Get namespace of document we are editing 644 $bns = getNS($backlinkingId); 645 646 // Create a clean version of the new name 647 $cleanname = cleanID($opts['newname']); 648 649 // Open backlink 650 lock($backlinkingId); 651 $text = io_readFile(wikiFN($backlinkingId),True); 652 653 // Form new document ID for this backlink 654 $matches = array(); 655 // new page is in same namespace as backlink 656 if ( $bns == $opts['newns'] ) { 657 $replacementNamespace = ''; 658 } 659 // new page is in sub-namespace of backlink 660 elseif ( preg_match('#^'.$bns.':(.*)$#', $opts['newns'], $matches) ) { 661 $replacementNamespace = '.:'.$matches[1].':'; 662 } 663 // not same or sub namespace: use absolute reference 664 else { 665 $replacementNamespace = $opts['newns'].':'; 666 } 667 668 // @fixme stupid: for each page get original backlink and its replacement 669 $matches = array(); 670 // get an array of: backlinks => replacement 671 $oid = array(); 672 if ( $bns == $opts['ns'] ) { 673 // old page was in same namespace as backlink 674 foreach ( $links as $link ) { 675 $oid[$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 676 $oid['.:'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 677 $oid['.'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 678 } 679 } 680 if ( preg_match('#^'.$bns.':(.*)$#', $opts['ns'], $matches) ) { 681 // old page was in sub namespace of backlink namespace 682 foreach ( $links as $link ) { 683 $oid['.:'.$matches[1].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 684 $oid['.'.$matches[1].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 685 } 686 } 687 if ( preg_match('#^'.$opts['ns'].':(.*)$#', $bns , $matches) && $opts['page_ns'] == 'page' ) { 688 // old page was in upper namespace of backlink 689 foreach ( $links as $link ) { 690 $oid['..:'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 691 $oid['..'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 692 $oid['.:..:'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 693 } 694 } 695 // replace all other links 696 foreach ( $links as $link ) { 697 // absolute links 698 $oid[$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 699 //$oid['.:'.$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 700 701 // check backwards relative links 702 $relLink = $link; 703 $relDots = '..'; 704 $backlinkingNamespaceCount = count(explode(':', $bns)); 705 $oldNamespaces = explode(':', $opts['ns'], $backlinkingNamespaceCount); 706 $oldNamespaceCount = count($oldNamespaces); 707 if ($backlinkingNamespaceCount > $oldNamespaceCount) { 708 $levelDiff = $backlinkingNamespaceCount - $oldNamespaceCount; 709 for ($i = 0; $i < $levelDiff; $i++) { 710 $relDots .= ':..'; 711 } 712 } 713 714 foreach (array_reverse($oldNamespaces) as $nextUpperNs) { 715 $relLink = $nextUpperNs.':'.$relLink; 716 foreach (array($relDots.$relLink, $relDots.':'.$relLink) as $dottedRelLink) { 717 $absLink=$dottedRelLink; 718 resolve_pageid($bns, $absLink, $exists); 719 if ($absLink == $ID) { 720 $oid[$dottedRelLink] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 721 } 722 } 723 $relDots = '..:'.$relDots; 724 } 725 726 //$oid['..:'.$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 727 //$oid['..'.$opts['ns'].':'.$link] = $replacementNamespace.(($cleanname == cleanID($link)) ? $link : $opts['newname']); 728 } 729 730 // Make the changes 731 $this->_pm_updatelinks($text, $oid); 732 733 // Save backlink and release lock 734 saveWikiText($backlinkingId, $text, sprintf($this->lang['pm_linkchange'], $ID, $opts['new_id'])); 735 unlock($backlinkingId); 736 } 737 738 /** 739 * modify the links using the pairs in $links 740 * 741 * @author Gary Owen <gary@isection.co.uk> 742 */ 743 function _pm_updatelinks(&$text, $links) { 744 foreach( $links as $old => $new ) { 745 $text = preg_replace( '#\[\[:?' . $old . '((\]\])|[\|\#])#i', '[[' . $new . '\1', $text); 746 } 747 } 748 749 /** 750 * modify the medialinks from namepspace $old to namespace $new 751 * 752 * @author Gary Owen <gary@isection.co.uk> 753 */ 754 function _pm_updatemedialinks(&$text, $old, $new) { 755 // Question marks in media links need some extra handling 756 $text = preg_replace('#\{\{' . $old . '([\?\|]|(\}\}))#i', '{{' . $new . '\1', $text); 757 } 758 759 /** 760 * Get forward links in a given page which need to be changed. 761 * 762 * Not changed: local sections, absolute links 763 * Changed need to be 764 * 765 * @author Gary Owen <gary@isection.co.uk> 766 */ 767 function _pm_getforwardlinks($id) { 768 $data = array(); 769 $text = io_readfile(wikiFN($id)); 770 771 // match all links 772 // FIXME may be incorrect because of code blocks 773 // TODO CamelCase isn't supported, too 774 preg_match_all('#\[\[(.+?)\]\]#si', $text, $matches, PREG_SET_ORDER); 775 foreach($matches as $match) { 776 // ignore local headings [[#some_heading]] 777 if ( preg_match('/^#/', $match[1])) continue; 778 779 // get ID from link and discard most non wikilinks 780 list($mid) = split('[\|#]', $match[1], 2); 781 // ignore links with URL schema prefix ([[prefix://]]) 782 if(preg_match('#^\w+://#', $mid)) continue; 783 // if(preg_match('#^(https?|telnet|gopher|file|wais|ftp|ed2k|irc)://#',$mid)) continue; 784 // inter-wiki link 785 if(preg_match('#\w+>#', $mid)) continue; 786 // baselink ([[/some_link]]) 787 if(preg_match('#^/#', $mid)) continue; 788 // email addresses 789 if(strpos($mid, '@') !== FALSE) continue; 790 // ignore absolute links 791 if( strpos($mid, ':') === 0 ) continue; 792 793 $absoluteMatchId = $mid; 794 $exists = FALSE; 795 resolve_pageid(getNS($id), $absoluteMatchId, $exists); 796 if($absoluteMatchId != FALSE) { 797 $data[$mid] = $absoluteMatchId; 798 } 799 } 800 return $data; 801 } 802 803 /** 804 * Get media links in a given page 805 * 806 * @author Gary Owen <gary@isection.co.uk> 807 */ 808 function _pm_getmedialinks($id) { 809 $data = array(); 810 $text = io_readfile(wikiFN($id)); 811 // match all links 812 // FIXME may be incorrect because of code blocks 813 // TODO CamelCase isn't supported, too 814 preg_match_all('#{{(.[^>]+?)}}#si', $text, $matches, PREG_SET_ORDER); 815 foreach($matches as $match) { 816 // get ID from link and discard most non wikilinks 817 list($mid) = split('(\?|\|)', $match[1], 2); 818 $mns = getNS($mid); 819 $lnk = $mid; 820 821 // namespace starting with "." - prepend current namespace 822 if(strpos($mns, '.')===0) { 823 $mid = getNS($id).':'.substr($mid, 1); 824 } 825 elseif($mns === FALSE){ 826 // no namespace in link? add current 827 $mid = getNS($id) . ':' . $mid; 828 } 829 $data[$lnk] = preg_replace('#:+#', ':', $mid); 830 } 831 return $data; 832 } 833 834 /** 835 * move meta files (Old Revs, Subscriptions, Meta, etc) 836 * 837 * This function meta files between directories 838 * 839 * @author Gary Owen <gary@isection.co.uk> 840 */ 841 function _pm_movemeta($dir, $regex, $opts) { 842 global $conf; 843 844 $old_path = $conf[$dir].'/'.str_replace(':','/',$opts['ns']).'/'; 845 $new_path = $conf[$dir].'/'.str_replace(':','/',$opts['newns']).'/'; 846 $dh = @opendir($old_path); 847 if($dh) { 848 while(($file = readdir($dh)) !== false) { 849 // skip hidden files and upper dirs 850 if(preg_match('/^\./',$file)) continue; 851 if(is_file($old_path.$file) and preg_match($regex,$file)) { 852 io_mkdir_p($new_path); 853 io_rename($old_path.$file,$new_path.str_replace($opts['name'], $opts['newname'], $file)); 854 continue; 855 } 856 } 857 closedir($dh); 858 } 859 } 860 861 862 /** 863 * recurse directory 864 * 865 * This function recurses into a given base directory 866 * and calls the supplied function for each file and directory 867 * 868 * @author Andreas Gohr <andi@splitbrain.org> 869 * @param array $data Found data is collected 870 * @param string $base Directory to be searched in 871 * @param string $func Name of real search function 872 * @param array $opts Options to the search functions 873 * @param string $dir Current relative directory 874 * @param integer $lvl Level of recursion 875 */ 876 function _pm_search(&$data, $base, $func, $opts, $dir='' ,$lvl=1) { 877 $dirs = array(); 878 $files = array(); 879 880 // read in directories and files 881 $dh = @opendir($base.'/'.$dir); 882 if(!$dh) return; 883 while(($file = readdir($dh)) !== false) { 884 // skip hidden files and upper dirs 885 if(preg_match('/^\./',$file)) continue; 886 if(is_dir($base.'/'.$dir.'/'.$file)) { 887 $dirs[] = $dir.'/'.$file; 888 continue; 889 } 890 $files[] = $dir.'/'.$file; 891 } 892 closedir($dh); 893 sort($files); 894 sort($dirs); 895 896 // give directories to userfunction then recurse 897 foreach($dirs as $dir) { 898 if ($this->$func($data, $base, $dir, 'd', $lvl, $opts)) { 899 $this->_pm_search($data, $base, $func, $opts, $dir, $lvl+1); 900 } 901 } 902 // now handle the files 903 foreach($files as $file) { 904 $this->$func($data, $base, $file, 'f', $lvl, $opts); 905 } 906 } 907 908 /** 909 * Search for backlinks to a given page 910 * 911 * $opts['ns'] namespace of the page 912 * $opts['name'] name of the page without namespace 913 * 914 * @author Andreas Gohr <andi@splitbrain.org> 915 * @author Gary Owen <gary@isection.co.uk> 916 */ 917 function _pm_search_backlinks(&$data, $base, $file, $type, $lvl, $opts) { 918 // we do nothing with directories 919 if($type == 'd') return true; 920 // only search txt files 921 if(!preg_match('#\.txt$#', $file)) return true; 922 923 $text = io_readfile($base.'/'.$file); 924 // absolute search ID 925// $absSearchedId = cleanID($opts['ns'].':'.$opts['name']); 926 $absSearchedId = $opts['name']; 927 resolve_pageid($opts['ns'], $absSearchedId, $exists); 928 929 // construct current namespace 930 $cid = pathID($file); 931 $cns = getNS($cid); 932 933 // match all links 934 // FIXME may be incorrect because of code blocks 935 // FIXME CamelCase isn't supported, too 936 preg_match_all('#\[\[(.+?)\]\]#si', $text, $matches, PREG_SET_ORDER); 937 foreach($matches as $match) { 938 // get ID from link and discard most non wikilinks 939 list($matchLink) = split('[\|#]', $match[1], 2); 940 // all URLs with a scheme 941 if(preg_match('#^\w+://#', $matchLink)) continue; 942// if(preg_match('#^(https?|telnet|gopher|file|wais|ftp|ed2k|irc)://#',$matchLink)) continue; 943 // baselinks 944 if(preg_match('#^/#', $matchLink)) continue; 945 // inter-wiki links 946 if(preg_match('#\w+>#', $matchLink)) continue; 947 // email addresses 948 if(strpos($matchLink, '@') !== FALSE) continue; 949 950 // get the ID the link refers to by cleaning and resolving it 951 $matchId = cleanID($matchLink); 952 resolve_pageid($cns, $matchId, $exists); 953 $matchPagename = ltrim(noNS($matchId), '.:'); 954 955 // only collect IDs not in collected $data already 956 if ($matchId == $absSearchedId // matching link refers to the searched ID 957 && (! array_key_exists($cid, $data) // not in $data already 958 || empty($data[$cid]) 959 || ! in_array($matchPagename, $data[$cid]))) { 960 // @fixme return original link and its replacement 961 $data[$cid][] = $matchPagename; 962 } 963 } 964 } 965}