1<?php
2
3/**
4 * All DokuWiki plugins to extend the admin function
5 * need to inherit from this class
6 */
7class admin_plugin_sync extends DokuWiki_Admin_Plugin {
8
9    protected $profiles = array();
10    protected $profno = '';
11    protected $client = null;
12    protected $apiversion = 0;
13    protected $defaultTimeout = 15;
14
15    /**
16     * Constructor.
17     */
18    function __construct(){
19        $this->_profileLoad();
20        $this->profno = preg_replace('/[^0-9]+/','',$_REQUEST['no']);
21    }
22
23    function _connect(){
24        if(!is_null($this->client)) return true;
25
26        if ( isset($this->profiles[$this->profno]['timeout']) ){
27          $timeout = (int) $this->profiles[$this->profno]['timeout'];
28        } else {
29          $timeout = $this->defaultTimeout;
30        }
31
32        if(class_exists('IXR_Client')) {
33            $this->client = new IXR_Client($this->profiles[$this->profno]['server'], false, 80, $timeout);
34        }
35        else {
36            $this->client = new dokuwiki\Remote\IXR\Client($this->profiles[$this->profno]['server']);
37            $this->client->timeout = $timeout;
38        }
39
40        // do the login
41        if($this->profiles[$this->profno]['user']){
42            $ok = $this->client->query('dokuwiki.login',
43                    $this->profiles[$this->profno]['user'],
44                    $this->profiles[$this->profno]['pass']
45                  );
46            if(!$ok){
47                msg($this->getLang('xmlerr').' '.hsc($this->client->getErrorMessage()),-1);
48                $this->client = null;
49                return false;
50            }
51            if(!$this->client->getResponse()){
52                msg($this->getLang('loginerr'),-1);
53                $this->client = null;
54                return false;
55            }
56        }
57
58        $ok = $this->client->query('dokuwiki.getXMLRPCAPIVersion');
59        if(!$ok){
60            msg($this->getLang('xmlerr').' '.hsc($this->client->getErrorMessage()),-1);
61            $this->client = null;
62            return false;
63        }
64        $this->apiversion = (int) $this->client->getResponse();
65        if($this->apiversion < 1){
66            msg($this->getLang('versionerr'),-1);
67            $this->client = null;
68            return false;
69        }
70
71        return true;
72    }
73
74    /**
75     * return sort order for position in admin menu
76     */
77    function getMenuSort() {
78        return 1020;
79    }
80
81    /**
82     * handle profile saving/deleting
83     */
84    function handle() {
85        if(isset($_REQUEST['prf']) && is_array($_REQUEST['prf'])){
86            if(isset($_REQUEST['sync__delete']) && $this->profno !== ''){
87                // delete profile
88                unset($this->profiles[$this->profno]);
89                $this->profiles = array_values($this->profiles); //reindex
90                $this->profno = '';
91            }else{
92                // add/edit profile
93                if($this->profno === '') $this->profno = count($this->profiles);
94                if ( !isset($_REQUEST['prf']['timeout']) || !is_numeric($_REQUEST['prf']['timeout']) ){
95                  $_REQUEST['prf']['timeout'] = $this->defaultTimeout;
96                }
97                $this->profiles[$this->profno] = $_REQUEST['prf'];
98            }
99            $this->_profileSave();
100
101            // reset the client
102            $this->client = null;
103        }
104    }
105
106    /**
107     * output appropriate html
108     */
109    function html() {
110        if(($_POST['sync_pages'] || $_POST['sync_media']) && $this->profno!==''){
111            // do the sync
112            echo $this->locale_xhtml('sync');
113
114            //show progressbar
115            echo '<div class="centeralign" id="dw__loading">'.NL;
116            echo '<script type="text/javascript" charset="utf-8"><!--//--><![CDATA[//><!--'.NL;
117            echo 'showLoadBar();'.NL;
118            echo '//--><!]]></script>'.NL;
119            echo '<br /></div>'.NL;
120            flush();
121            ob_flush();
122
123            echo '<ul class="sync">';
124
125            if($_POST['sync_pages']){
126                $this->_sync($_POST['sync_pages'],'pages');
127            }
128            if($_POST['sync_media']){
129                $this->_sync($_POST['sync_media'],'media');
130            }
131            $this->_saveSyncTimes((int) $_POST['lnow'],
132                                 (int) $_POST['rnow']);
133
134            echo '</ul>';
135
136            //hide progressbar
137            echo '<script type="text/javascript" charset="utf-8"><!--//--><![CDATA[//><!--'.NL;
138            echo 'hideLoadBar("dw__loading");'.NL;
139            echo '//--><!]]></script>'.NL;
140            flush();
141            ob_flush();
142
143
144            echo '<p>'.$this->getLang('syncdone').'</p>';
145        }elseif($_REQUEST['startsync'] && $this->profno!==''){
146            // get sync list
147            list($lnow,$rnow) = $this->_getTimes();
148            $pages = array();
149            $media = array();
150            if($rnow){
151                if($this->profiles[$this->profno]['type'] == 0 ||
152                   $this->profiles[$this->profno]['type'] == 1){
153                    $pages = $this->_getSyncList('pages');
154                }
155                if(($this->profiles[$this->profno]['type'] == 0 ||
156                   $this->profiles[$this->profno]['type'] == 2)
157                    && $pages !== false ){
158                    $media = $this->_getSyncList('media');
159                }
160            }
161
162            if ( $pages === false || $media === false ){
163              return;
164            }
165
166            if(count($pages) || count($media)){
167                $this->_directionFormStart($lnow,$rnow);
168                if(count($pages))
169                    $this->_directionForm('pages',$pages);
170                if(count($media))
171                    $this->_directionForm('media',$media);
172
173                $this->_directionFormEnd();
174            }else{
175                echo $this->locale_xhtml('nochange');
176            }
177        }else{
178            echo $this->locale_xhtml('intro');
179
180            echo '<div class="sync_left">';
181            $this->_profilelist($this->profno);
182            if($this->profno !=='' ){
183                echo '<br />';
184                $this->_profileView($this->profno);
185            }
186            echo '</div>';
187            echo '<div class="sync_right">';
188            $this->_profileform($this->profno);
189            echo '</div>';
190        }
191    }
192
193    /**
194     * Load profiles from serialized storage
195     */
196    function _profileLoad(){
197        global $conf;
198        $profiles = $conf['metadir'].'/sync.profiles';
199        if(file_exists($profiles)){
200            $this->profiles = unserialize(io_readFile($profiles,false));
201        }
202    }
203
204    /**
205     * Save profiles to serialized storage
206     */
207    function _profileSave(){
208        global $conf;
209        $profiles = $conf['metadir'].'/sync.profiles';
210        io_saveFile($profiles,serialize($this->profiles));
211    }
212
213    /**
214     * Check connection for choosen profile and display last sync date.
215     */
216    function _profileView(){
217        if(!$this->_connect()) return false;
218
219        global $conf;
220        $no = $this->profno;
221
222        $ok = $this->client->query('dokuwiki.getVersion');
223        $version = '';
224        if($ok) $version = $this->client->getResponse();
225
226        echo '<form action="" method="post">';
227        echo '<input type="hidden" name="no" value="'.hsc($no).'" />';
228        echo '<fieldset><legend>'.$this->getLang('syncstart').'</legend>';
229        if($version){
230            echo '<p>'.$this->getLang('remotever').' '.hsc($version).'</p>';
231            if($this->profiles[$no]['ltime']){
232                echo '<p>'.$this->getLang('lastsync').' '.strftime($conf['dformat'],$this->profiles[$no]['ltime']).'</p>';
233            }else{
234                echo '<p>'.$this->getLang('neversync').'</p>';
235            }
236            echo '<input name="startsync" type="submit" value="'.$this->getLang('syncstart').'" class="button" />';
237        }else{
238            echo '<p class="error">'.$this->getLang('noconnect').'<br />'.hsc($this->client->getErrorMessage()).'</p>';
239        }
240        echo '</fieldset>';
241        echo '</form>';
242    }
243
244    /**
245     * Dropdown list of available sync profiles
246     */
247    function _profilelist($no=''){
248        echo '<form action="" method="post">';
249        echo '<fieldset><legend>'.$this->getLang('profile').'</legend>';
250        echo '<select name="no" class="edit">';
251        echo '  <option value="">'.$this->getLang('newprofile').'</option>';
252        foreach($this->profiles as $pno => $opts){
253            $srv = parse_url($opts['server']);
254
255            echo '<option value="'.hsc($pno).'" '.(($no!=='' && $pno == $no)?'selected="selected"':'').'>';
256            echo ($pno+1).'. ';
257            if($opts['user']) echo hsc($opts['user']).'@';
258            echo hsc($srv['host']);
259            if($opts['ns']) echo ':'.hsc($opts['ns']);
260            echo '</option>';
261        }
262        echo '</select>';
263        echo '<input type="submit" value="'.$this->getLang('select').'" class="button" />';
264        echo '</fieldset>';
265        echo '</form>';
266    }
267
268    /**
269     * Form to edit or create a sync profile
270     */
271    function _profileform($no=''){
272        echo '<form action="" method="post" class="sync_profile">';
273        echo '<fieldset><legend>';
274        if($no !== ''){
275            echo $this->getLang('edit');
276        }else{
277            echo $this->getLang('create');
278        }
279        echo '</legend>';
280
281        echo '<input type="hidden" name="no" value="'.hsc($no).'" />';
282
283        echo '<label for="sync__server">'.$this->getLang('server').'</label> ';
284        echo '<input type="text" name="prf[server]" id="sync__server" class="edit" value="'.hsc($this->profiles[$no]['server']).'" />';
285        echo '<samp>http://example.com/dokuwiki/lib/exe/xmlrpc.php</samp>';
286
287        echo '<label for="sync__ns">'.$this->getLang('ns').'</label> ';
288        echo '<input type="text" name="prf[ns]" id="sync__ns" class="edit" value="'.hsc($this->profiles[$no]['ns']).'" />';
289
290        echo '<label for="sync__depth">'.$this->getLang('depth').'</label> ';
291        echo '<select name="prf[depth]" id="sync__depth" class="edit">';
292        echo '<option value="0" '.(($this->profiles[$no]['depth']==0)?'selected="selected"':'').'>'.$this->getLang('level0').'</option>';
293        echo '<option value="1" '.(($this->profiles[$no]['depth']==1)?'selected="selected"':'').'>'.$this->getLang('level1').'</option>';
294        echo '<option value="2" '.(($this->profiles[$no]['depth']==2)?'selected="selected"':'').'>'.$this->getLang('level2').'</option>';
295        echo '<option value="3" '.(($this->profiles[$no]['depth']==3)?'selected="selected"':'').'>'.$this->getLang('level3').'</option>';
296        echo '</select>';
297
298
299        echo '<label for="sync__user">'.$this->getLang('user').'</label> ';
300        echo '<input type="text" name="prf[user]" id="sync__user" class="edit" value="'.hsc($this->profiles[$no]['user']).'" />';
301
302        echo '<label for="sync__pass">'.$this->getLang('pass').'</label> ';
303        echo '<input type="password" name="prf[pass]" id="sync__pass" class="edit" value="'.hsc($this->profiles[$no]['pass']).'" />';
304
305        echo '<label for="sync__timeout">'.$this->getLang('timeout').'</label>';
306        echo '<input type="number" name="prf[timeout]" id="sync__timeout" class="edit" value="'.hsc($this->profiles[$no]['timeout']).'" />';
307
308        echo '<span>'.$this->getLang('type').'</span>';
309
310        echo '<div class="type">';
311        echo '<input type="radio" name="prf[type]" id="sync__type0" value="0" '.(($this->profiles[$no]['type'] == 0)?'checked="checked"':'').'/>';
312        echo '<label for="sync__type0">'.$this->getLang('type0').'</label> ';
313
314        echo '<input type="radio" name="prf[type]" id="sync__type1" value="1" '.(($this->profiles[$no]['type'] == 1)?'checked="checked"':'').'/>';
315        echo '<label for="sync__type1">'.$this->getLang('type1').'</label> ';
316
317        echo '<input type="radio" name="prf[type]" id="sync__type2" value="2" '.(($this->profiles[$no]['type'] == 2)?'checked="checked"':'').'/>';
318        echo '<label for="sync__type2">'.$this->getLang('type2').'</label> ';
319        echo '</div>';
320
321
322        echo '<div class="submit">';
323        echo '<input type="submit" value="'.$this->getLang('save').'" class="button" />';
324        if($no !== '' && $this->profiles[$no]['ltime']){
325            echo '<small>'.$this->getLang('changewarn').'</small>';
326        }
327        echo '</div>';
328
329        echo '<div class="submit">';
330        echo '<input name="sync__delete" type="submit" value="'.$this->getLang('delete').'" class="button" />';
331        echo '</div>';
332
333        echo '</fieldset>';
334        echo '</form>';
335    }
336
337    /**
338     * Lock files that will be modified on either side.
339     *
340     * Lock fails are printed and removed from synclist
341     *
342     * @returns list of locked files
343     */
344    function _lockFiles(&$synclist){
345        if(!$this->_connect()) return array();
346        // lock the files
347        $lock = array();
348        foreach((array) $synclist as $id => $dir){
349            if($dir == 0) continue;
350            if(checklock($id)){
351                $this->_listOut($this->getLang('lockfail').' '.hsc($id),'error');
352                unset($synclist[$id]);
353            }else{
354                lock($id); // lock local
355                $lock[] = $id;
356            }
357        }
358        // lock remote files
359        $ok = $this->client->query('dokuwiki.setLocks',array('lock'=>$lock,'unlock'=>array()));
360        if(!$ok){
361            $this->_listOut('failed RPC communication');
362            $synclist = array();
363            return array();
364        }
365        $data = $this->client->getResponse();
366        foreach((array) $data['lockfail'] as $id){
367            $this->_listOut($this->getLang('lockfail').' '.hsc($id),'error');
368            unset($synclist[$id]);
369        }
370
371        return $lock;
372    }
373
374    /**
375     * Print a message as list item using the given class
376     */
377    function _listOut($msg,$class='ok'){
378        echo '<li class="'.hsc($class).'"><div class="li">';
379        echo hsc($msg);
380        echo "</div></li>\n";
381        flush();
382        ob_flush();
383    }
384
385    /**
386     * Execute the sync action and print the results
387     */
388    function _sync(&$synclist,$type){
389        if(!$this->_connect()) return false;
390        $no = $this->profno;
391        $sum = $_REQUEST['sum'];
392
393        if($type == 'pages')
394            $lock = $this->_lockfiles($synclist);
395
396        // do the sync
397        foreach((array) $synclist as $id => $dir){
398            @set_time_limit(30);
399            if($dir == 0){
400                $this->_listOut($this->getLang('skipped').' '.$id,'skipped');
401                continue;
402            }
403            if($dir == -2){
404                //delete local
405                if($type == 'pages'){
406                    saveWikiText($id,'',$sum,false);
407                    $this->_listOut($this->getLang('localdelok').' '.$id,'del_okay');
408                }else{
409                    if(unlink(mediaFN($id))){
410                        $this->_listOut($this->getLang('localdelok').' '.$id,'del_okay');
411                    }else{
412                        $this->_listOut($this->getLang('localdelfail').' '.$id,'del_fail');
413                    }
414                }
415                continue;
416            }
417            if($dir == -1){
418                //pull
419                if($type == 'pages'){
420                    $ok = $this->client->query('wiki.getPage',$id);
421                }else{
422                    $ok = $this->client->query('wiki.getAttachment',$id);
423                }
424                if(!$ok){
425                    $this->_listOut($this->getLang('pullfail').' '.$id.' '.
426                                    $this->client->getErrorMessage(),'pull_fail');
427                    continue;
428                }
429                $data = $this->client->getResponse();
430                if($type == 'pages'){
431                    saveWikiText($id,$data,$sum,false);
432                    idx_addPage($id);
433                }else{
434                    if($this->apiversion < 7){
435                        $data = base64_decode($data);
436                    }
437                    io_saveFile(mediaFN($id),$data);
438                }
439                $this->_listOut($this->getLang('pullok').' '.$id,'pull_okay');
440                continue;
441            }
442            if($dir == 1){
443                // push
444                if($type == 'pages'){
445                    $data = rawWiki($id);
446                    $ok = $this->client->query('wiki.putPage',$id,$data,array('sum'=>$sum));
447                }else{
448                    $data = io_readFile(mediaFN($id),false);
449                    if($this->apiversion < 6){
450                        $data = base64_encode($data);
451                    }else{
452                        $data = new IXR_Base64($data);
453                    }
454                    $ok = $this->client->query('wiki.putAttachment',$id,$data,array('ow'=>true));
455                }
456                if(!$ok){
457                    $this->_listOut($this->getLang('pushfail').' '.$id.' '.
458                                    $this->client->getErrorMessage(),'push_fail');
459                    continue;
460                }
461                $this->_listOut($this->getLang('pushok').' '.$id,'push_okay');
462                continue;
463            }
464            if($dir == 2){
465                // remote delete
466                if($type == 'pages'){
467                    $ok = $this->client->query('wiki.putPage',$id,'',array('sum'=>$sum));
468                }else{
469                    $ok = $this->client->query('wiki.deleteAttachment',$id);
470                }
471                if(!$ok){
472                    $this->_listOut($this->getLang('remotedelfail').' '.$id.' '.
473                                    $this->client->getErrorMessage(),'del_fail');
474                    continue;
475                }
476                $this->_listOut($this->getLang('remotedelok').' '.$id,'del_okay');
477                continue;
478            }
479        }
480
481        // unlock
482        if($type == 'pages'){
483            foreach((array) $synclist as $id => $dir){
484                unlock($id);
485            }
486            $this->client->query('dokuwiki.setLocks',array('lock'=>array(),'unlock'=>$lock));
487        }
488
489
490    }
491
492    /**
493     * Save synctimes
494     */
495    function _saveSyncTimes($ltime,$rtime){
496        $no = $this->profno;
497        list($letime,$retime) = $this->_getTimes();
498        $this->profiles[$no]['ltime'] = $ltime;
499        $this->profiles[$no]['rtime'] = $rtime;
500        $this->profiles[$no]['letime'] = $letime;
501        $this->profiles[$no]['retime'] = $retime;
502        $this->_profileSave();
503    }
504
505    /**
506     * Open the sync direction form and initialize the table
507     */
508    function _directionFormStart($lnow,$rnow){
509        $no = $this->profno;
510        echo $this->locale_xhtml('list');
511        echo '<form action="" method="post">';
512        echo '<table class="inline" id="sync__direction__table">';
513        echo '<input type="hidden" name="lnow" value="'.$lnow.'" />';
514        echo '<input type="hidden" name="rnow" value="'.$rnow.'" />';
515        echo '<input type="hidden" name="no" value="'.$no.'" />';
516        echo '<tr>
517                <th class="sync__file">'.$this->getLang('file').'</th>
518                <th class="sync__local">'.$this->getLang('local').'</th>
519                <th class="sync__push" id="sync__push">&gt;</th>
520                <th class="sync__skip" id="sync__skip">=</th>
521                <th class="sync__pull" id="sync__pull">&lt;</th>
522                <th class="sync__remote">'.$this->getLang('remote').'</th>
523                <th class="sync__diff">'.$this->getLang('diff').'</th>
524              </tr>';
525    }
526
527    /**
528     * Close the direction form and table
529     */
530    function _directionFormEnd(){
531        global $lang;
532        echo '</table>';
533        echo '<label for="the__summary">'.$lang['summary'].'</label> ';
534        echo '<input type="text" name="sum" id="the__summary" value="" class="edit" />';
535        echo '<input type="submit" value="'.$this->getLang('syncstart').'" class="button" />';
536        echo '</form>';
537    }
538
539    /**
540     * Print a list of changed files and ask for the sync direction
541     *
542     * Tries to be clever about suggesting the direction
543     */
544    function _directionForm($type,&$synclist){
545        global $conf;
546        global $lang;
547        $no = $this->profno;
548
549        $ltime = (int) $this->profiles[$no]['ltime'];
550        $rtime = (int) $this->profiles[$no]['rtime'];
551        $letime = (int) $this->profiles[$no]['letime'];
552        $retime = (int) $this->profiles[$no]['retime'];
553
554        foreach($synclist as $id => $item){
555            // check direction
556            $dir = 0;
557            if($ltime && $rtime){ // synced before
558                if($item['remote']['mtime'] > $rtime &&
559                   $item['local']['mtime'] <= $letime){
560                    $dir = -1;
561                }
562                if($item['remote']['mtime'] <= $retime &&
563                   $item['local']['mtime'] > $ltime){
564                    $dir = 1;
565                }
566            }else{ // never synced
567                if(!$item['local']['mtime'] && $item['remote']['mtime']){
568                    $dir = -1;
569                }
570                if($item['local']['mtime'] && !$item['remote']['mtime']){
571                    $dir = 1;
572                }
573            }
574
575            echo '<tr>';
576
577            echo '<td class="sync__file">'.hsc($id).'</td>';
578            echo '<td class="sync__local">';
579            if(!isset($item['local'])){
580                echo '&mdash;';
581            }else{
582                echo '<div>'.strftime($conf['dformat'],$item['local']['mtime']).'</div>';
583                echo ' <div>('.$item['local']['size'].' bytes)</div>';
584            }
585            echo '</td>';
586
587            echo '<td class="sync__push">';
588            if(!isset($item['local'])){
589                echo '<input type="radio" name="sync_'.$type.'['.hsc($id).']" value="2" class="syncpush" title="'.$this->getLang('pushdel').'" '.(($dir == 2)?'checked="checked"':'').' />';
590            }else{
591                echo '<input type="radio" name="sync_'.$type.'['.hsc($id).']" value="1" class="syncpush" title="'.$this->getLang('push').'" '.(($dir == 1)?'checked="checked"':'').' />';
592            }
593            echo '</td>';
594            echo '<td class="sync__skip">';
595            echo '<input type="radio" name="sync_'.$type.'['.hsc($id).']" value="0" class="syncskip" title="'.$this->getLang('keep').'" '.(($dir == 0)?'checked="checked"':'').' />';
596            echo '</td>';
597            echo '<td class="sync__pull">';
598            if(!isset($item['remote'])){
599                echo '<input type="radio" name="sync_'.$type.'['.hsc($id).']" value="-2" class="syncpull" title="'.$this->getLang('pulldel').'" '.(($dir == -2)?'checked="checked"':'').' />';
600            }else{
601                echo '<input type="radio" name="sync_'.$type.'['.hsc($id).']" value="-1" class="syncpull" title="'.$this->getLang('pull').'" '.(($dir == -1)?'checked="checked"':'').' />';
602            }
603            echo '</td>';
604
605            echo '<td class="sync__remote">';
606            if(!isset($item['remote'])){
607                echo '&mdash;';
608            }else{
609                echo '<div>'.strftime($conf['dformat'],$item['remote']['mtime']).'</div>';
610                echo ' <div>('.$item['remote']['size'].' bytes)</div>';
611            }
612            echo '</td>';
613
614            echo '<td class="sync__diff">';
615            if($type == 'pages'){
616                echo '<a href="'.DOKU_BASE.'lib/plugins/sync/diff.php?id='.$id.'&amp;no='.$no.'" target="_blank" class="sync_popup">'.$this->getLang('diff').'</a>';
617            }
618            echo '</td>';
619
620            echo '</tr>';
621        }
622    }
623
624    /**
625     * Get the local and remote time
626     */
627    function _getTimes(){
628        if(!$this->_connect()) return false;
629        // get remote time
630        $ok = $this->client->query('dokuwiki.getTime');
631        if(!$ok){
632            msg('Failed to fetch remote time. '.
633                $this->client->getErrorMessage(),-1);
634            return false;
635        }
636        $rtime = $this->client->getResponse();
637        $ltime = time();
638        return array($ltime,$rtime);
639    }
640
641    /**
642     * Get a list of changed files
643     */
644    function _getSyncList($type='pages'){
645        if(!$this->_connect()) return array();
646        global $conf;
647        $no = $this->profno;
648        $list = array();
649        $ns = $this->profiles[$no]['ns'];
650
651        // get remote file list
652        if($type == 'pages'){
653            $ok = $this->client->query('dokuwiki.getPagelist',$ns,
654                    array('depth' => (int) $this->profiles[$no]['depth'],
655                          'hash' => true));
656        }else{
657            $ok = $this->client->query('wiki.getAttachments',$ns,
658                    array('depth' => (int) $this->profiles[$no]['depth'],
659                          'hash' => true));
660        }
661        if(!$ok){
662            msg('Failed to fetch remote file list. '.
663                $this->client->getErrorMessage(),-1);
664            return false;
665        }
666        $remote = $this->client->getResponse();
667        // put into synclist
668        foreach($remote as $item){
669            $list[$item['id']]['remote'] = $item;
670            unset($list[$item['id']]['remote']['id']);
671        }
672        unset($remote);
673
674        // get local file list
675        $local = array();
676        $dir = utf8_encodeFN(str_replace(':', '/', $ns));
677        require_once(DOKU_INC.'inc/search.php');
678        if($type == 'pages'){
679            search($local, $conf['datadir'], 'search_allpages',
680                    array('depth' => (int) $this->profiles[$no]['depth'],
681                          'hash' => true), $dir);
682        }else{
683            search($local, $conf['mediadir'], 'search_media',
684                    array('depth' => (int) $this->profiles[$no]['depth'],
685                          'hash' => true), $dir);
686        }
687
688        // put into synclist
689        foreach($local as $item){
690            // skip identical files
691            if($list[$item['id']]['remote']['hash'] == $item['hash']){
692                unset($list[$item['id']]);
693                continue;
694            }
695
696            $list[$item['id']]['local'] = $item;
697            unset($list[$item['id']]['local']['id']);
698        }
699        unset($local);
700
701        ksort($list);
702        return $list;
703    }
704
705    /**
706     * show diff between the local and remote versions of the page
707     */
708    function _diff($id){
709        if(!$this->_connect()) return false;
710        $no = $this->profno;
711
712        $ok = $this->client->query('wiki.getPage',$id);
713        if(!$ok){
714            echo $this->getLang('pullfail').' '.hsc($id).' ';
715            echo hsc($this->client->getErrorMessage());
716            die();
717        }
718        $remote = $this->client->getResponse();
719        $local  = rawWiki($id);
720
721        $df = new Diff(explode("\n",htmlspecialchars($local)),
722                       explode("\n",htmlspecialchars($remote)));
723
724        $tdf = new TableDiffFormatter();
725        echo '<table class="diff">';
726        echo '<tr>';
727        echo '<th colspan="2">'.$this->getLang('local').'</th>';
728        echo '<th colspan="2">'.$this->getLang('remote').'</th>';
729        echo '</tr>';
730        echo $tdf->format($df);
731        echo '</table>';
732    }
733}
734//Setup VIM: ex: et ts=4 enc=utf-8 :
735