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