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}