xref: /dokuwiki/lib/exe/xmlrpc.php (revision 1b11c097057df1982d9ba345f40ecf4cac06804f)
1<?php
2if(!defined('DOKU_INC')) define('DOKU_INC',dirname(__FILE__).'/../../');
3
4// fix when '<?xml' isn't on the very first line
5if(isset($HTTP_RAW_POST_DATA)) $HTTP_RAW_POST_DATA = trim($HTTP_RAW_POST_DATA);
6
7
8require_once(DOKU_INC.'inc/init.php');
9require_once(DOKU_INC.'inc/common.php');
10require_once(DOKU_INC.'inc/auth.php');
11session_write_close();  //close session
12
13if(!$conf['xmlrpc']) {
14    die('XML-RPC server not enabled.');
15    // FIXME check for groups allowed
16}
17
18require_once(DOKU_INC.'inc/IXR_Library.php');
19
20
21/**
22 * Contains needed wrapper functions and registers all available
23 * XMLRPC functions.
24 */
25class dokuwiki_xmlrpc_server extends IXR_IntrospectionServer {
26    var $methods = array();
27
28    /**
29     * Constructor. Register methods and run Server
30     */
31    function dokuwiki_xmlrpc_server(){
32        $this->IXR_IntrospectionServer();
33
34        /* DokuWiki's own methods */
35        $this->addCallback(
36            'dokuwiki.getVersion',
37            'getVersion',
38            array('string'),
39            'Returns the running DokuWiki version.'
40        );
41
42        $this->addCallback(
43            'dokuwiki.getPagelist',
44            'this:readNamespace',
45            array('struct','string','struct'),
46            'List all pages within the given namespace.'
47        );
48
49        $this->addCallback(
50            'dokuwiki.getTime',
51            'time',
52            array('int'),
53            'Return the current time at the wiki server.'
54        );
55
56        /* Wiki API v2 http://www.jspwiki.org/wiki/WikiRPCInterface2 */
57        $this->addCallback(
58            'wiki.getRPCVersionSupported',
59            'this:wiki_RPCVersion',
60            array('int'),
61            'Returns 2 with the supported RPC API version.'
62        );
63        $this->addCallback(
64            'wiki.getPage',
65            'this:rawPage',
66            array('string','string'),
67            'Get the raw Wiki text of page, latest version.'
68        );
69        $this->addCallback(
70            'wiki.getPageVersion',
71            'this:rawPage',
72            array('string','string','int'),
73            'Get the raw Wiki text of page.'
74        );
75        $this->addCallback(
76            'wiki.getPageHTML',
77            'this:htmlPage',
78            array('string','string'),
79            'Return page in rendered HTML, latest version.'
80        );
81        $this->addCallback(
82            'wiki.getPageHTMLVersion',
83            'this:htmlPage',
84            array('string','string','int'),
85            'Return page in rendered HTML.'
86        );
87        $this->addCallback(
88            'wiki.getAllPages',
89            'this:listPages',
90            array('struct'),
91            'Returns a list of all pages. The result is an array of utf8 pagenames.'
92        );
93        $this->addCallback(
94            'wiki.getAttachments',
95            'this:listAttachments',
96            array('struct', 'string', 'struct'),
97            'Returns a list of all media files.'
98        );
99        $this->addCallback(
100            'wiki.getBackLinks',
101            'this:listBackLinks',
102            array('struct','string'),
103            'Returns the pages that link to this page.'
104        );
105        $this->addCallback(
106            'wiki.getPageInfo',
107            'this:pageInfo',
108            array('struct','string'),
109            'Returns a struct with infos about the page.'
110        );
111        $this->addCallback(
112            'wiki.getPageInfoVersion',
113            'this:pageInfo',
114            array('struct','string','int'),
115            'Returns a struct with infos about the page.'
116        );
117        $this->addCallback(
118            'wiki.getPageVersions',
119            'this:pageVersions',
120            array('struct','string','int'),
121            'Returns the available revisions of the page.'
122        );
123        $this->addCallback(
124            'wiki.putPage',
125            'this:putPage',
126            array('int', 'string', 'string', 'struct'),
127            'Saves a wiki page.'
128        );
129        $this->addCallback(
130            'wiki.listLinks',
131            'this:listLinks',
132            array('struct','string'),
133            'Lists all links contained in a wiki page.'
134        );
135        $this->addCallback(
136            'wiki.getRecentChanges',
137            'this:getRecentChanges',
138            array('struct','int'),
139            'Returns a struct about all recent changes since given timestamp.'
140        );
141        $this->addCallback(
142            'wiki.getRecentMediaChanges',
143            'this:getRecentMediaChanges',
144            array('struct','int'),
145            'Returns a struct about all recent media changes since given timestamp.'
146        );
147        $this->addCallback(
148            'wiki.aclCheck',
149            'this:aclCheck',
150            array('int', 'string'),
151            'Returns the permissions of a given wiki page.'
152        );
153        $this->addCallback(
154            'wiki.putAttachment',
155            'this:putAttachment',
156            array('struct', 'string', 'base64', 'struct'),
157            'Upload a file to the wiki.'
158        );
159        $this->addCallback(
160            'wiki.deleteAttachment',
161            'this:deleteAttachment',
162            array('int', 'string'),
163            'Delete a file from the wiki.'
164        );
165        $this->addCallback(
166            'wiki.getAttachment',
167            'this:getAttachment',
168            array('base64', 'string'),
169            'Download a file from the wiki.'
170        );
171        $this->addCallback(
172            'wiki.getAttachmentInfo',
173            'this:getAttachmentInfo',
174            array('struct', 'string'),
175            'Returns a struct with infos about the attachment.'
176        );
177
178        /**
179         * Trigger XMLRPC_CALLBACK_REGISTER, action plugins can use this event
180         * to extend the XMLRPC interface and register their own callbacks.
181         *
182         * Event data:
183         *  The XMLRPC server object:
184         *
185         *  $event->data->addCallback() - register a callback, the second
186         *  paramter has to be of the form "plugin:<pluginname>:<plugin
187         *  method>"
188         *
189         *  $event->data->callbacks - an array which holds all awaylable
190         *  callbacks
191         */
192        trigger_event('XMLRPC_CALLBACK_REGISTER', $this);
193
194        $this->serve();
195    }
196
197    /**
198     * Return a raw wiki page
199     */
200    function rawPage($id,$rev=''){
201        if(auth_quickaclcheck($id) < AUTH_READ){
202            return new IXR_Error(1, 'You are not allowed to read this page');
203        }
204        $text = rawWiki($id,$rev);
205        if(!$text) {
206            $data = array($id);
207            return trigger_event('HTML_PAGE_FROMTEMPLATE',$data,'pageTemplate',true);
208        } else {
209            return $text;
210        }
211    }
212
213    /**
214     * Return a media file encoded in base64
215     *
216     * @author Gina Haeussge <osd@foosel.net>
217     */
218    function getAttachment($id){
219        $id = cleanID($id);
220        if (auth_quickaclcheck(getNS($id).':*') < AUTH_READ)
221            return new IXR_Error(1, 'You are not allowed to read this file');
222
223        $file = mediaFN($id);
224        if (!@ file_exists($file))
225            return new IXR_Error(1, 'The requested file does not exist');
226
227        $data = io_readFile($file, false);
228        $base64 = base64_encode($data);
229        return $base64;
230    }
231
232    /**
233     * Return info about a media file
234     *
235     * @author Gina Haeussge <osd@foosel.net>
236     */
237    function getAttachmentInfo($id){
238        $id = cleanID($id);
239        $info = array(
240            'lastModified' => 0,
241            'size' => 0,
242        );
243
244        $file = mediaFN($id);
245        if ((auth_quickaclcheck(getNS($id).':*') >= AUTH_READ) && file_exists($file)){
246            $info['lastModified'] = new IXR_Date(filemtime($file));
247            $info['size'] = filesize($file);
248        }
249
250        return $info;
251    }
252
253    /**
254     * Return a wiki page rendered to html
255     */
256    function htmlPage($id,$rev=''){
257        if(auth_quickaclcheck($id) < AUTH_READ){
258            return new IXR_Error(1, 'You are not allowed to read this page');
259        }
260        return p_wiki_xhtml($id,$rev,false);
261    }
262
263    /**
264     * List all pages - we use the indexer list here
265     */
266    function listPages(){
267        global $conf;
268
269        $list  = array();
270        $pages = file($conf['indexdir'] . '/page.idx');
271        $pages = array_filter($pages, 'isVisiblePage');
272
273        foreach(array_keys($pages) as $idx) {
274            if(page_exists($pages[$idx])) {
275                $perm = auth_quickaclcheck($pages[$idx]);
276                if($perm >= AUTH_READ) {
277                    $page = array();
278                    $page['id'] = trim($pages[$idx]);
279                    $page['perms'] = $perm;
280                    $page['size'] = @filesize(wikiFN($pages[$idx]));
281                    $page['lastModified'] = new IXR_Date(@filemtime(wikiFN($pages[$idx])));
282                    $list[] = $page;
283                }
284            }
285        }
286
287        return $list;
288    }
289
290    /**
291     * List all pages in the given namespace (and below)
292     */
293    function readNamespace($ns,$opts){
294        global $conf;
295
296        if(!is_array($opts)) $opts=array();
297
298        $ns = cleanID($ns);
299        $dir = utf8_encodeFN(str_replace(':', '/', $ns));
300dbglog('ggg');
301        $data = array();
302        require_once(DOKU_INC.'inc/search.php');
303        search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
304dbglog($data);
305        return $data;
306    }
307
308    /**
309     * List all media files.
310     *
311     * Available options are 'recursive' for also including the subnamespaces
312     * in the listing, and 'pattern' for filtering the returned files against
313     * a regular expression matching their name.
314     *
315     * @author Gina Haeussge <osd@foosel.net>
316     */
317    function listAttachments($ns, $options = array()) {
318        global $conf;
319        global $lang;
320
321        $ns = cleanID($ns);
322
323        if (!is_array($options))
324            $options = array();
325
326        if (!isset($options['recursive'])) $options['recursive'] = false;
327
328        if(auth_quickaclcheck($ns.':*') >= AUTH_READ) {
329            $dir = utf8_encodeFN(str_replace(':', '/', $ns));
330
331            $data = array();
332            require_once(DOKU_INC.'inc/search.php');
333            search($data, $conf['mediadir'], 'search_media', array('recursive' => $options['recursive']), $dir);
334
335            if(!count($data)) {
336                return array();
337            }
338
339            $files = array();
340            foreach($data as $item) {
341                if (isset($options['pattern']) && !@preg_match($options['pattern'], $item['id']))
342                    continue;
343                $file = array();
344                $file['id']       = $item['id'];
345                $file['size']     = $item['size'];
346                $file['lastModified'] = new IXR_Date($item['mtime']);
347                $file['isimg']    = $item['isimg'];
348                $file['writable'] = $item['writeable'];
349                $file['perms'] = auth_quickaclcheck(getNS($item['id']).':*');
350                array_push($files, $file);
351            }
352
353            return $files;
354
355        } else {
356            return new IXR_Error(1, 'You are not allowed to list media files.');
357        }
358    }
359
360    /**
361     * Return a list of backlinks
362     */
363    function listBackLinks($id){
364        require_once(DOKU_INC.'inc/fulltext.php');
365        return ft_backlinks($id);
366    }
367
368    /**
369     * Return some basic data about a page
370     */
371    function pageInfo($id,$rev=''){
372        if(auth_quickaclcheck($id) < AUTH_READ){
373            return new IXR_Error(1, 'You are not allowed to read this page');
374        }
375        $file = wikiFN($id,$rev);
376        $time = @filemtime($file);
377        if(!$time){
378            return new IXR_Error(10, 'The requested page does not exist');
379        }
380
381        $info = getRevisionInfo($id, $time, 1024);
382
383        $data = array(
384            'name'         => $id,
385            'lastModified' => new IXR_Date($time),
386            'author'       => (($info['user']) ? $info['user'] : $info['ip']),
387            'version'      => $time
388        );
389
390        return ($data);
391    }
392
393    /**
394     * Save a wiki page
395     *
396     * @author Michael Klier <chi@chimeric.de>
397     */
398    function putPage($id, $text, $params) {
399        global $TEXT;
400        global $lang;
401        global $conf;
402
403        $id    = cleanID($id);
404        $TEXT  = trim($text);
405        $sum   = $params['sum'];
406        $minor = $params['minor'];
407
408        if(empty($id))
409            return new IXR_Error(1, 'Empty page ID');
410
411        if(!page_exists($id) && empty($TEXT)) {
412            return new IXR_ERROR(1, 'Refusing to write an empty new wiki page');
413        }
414
415        if(auth_quickaclcheck($id) < AUTH_EDIT)
416            return new IXR_Error(1, 'You are not allowed to edit this page');
417
418        // Check, if page is locked
419        if(checklock($id))
420            return new IXR_Error(1, 'The page is currently locked');
421
422        // SPAM check
423        if(checkwordblock())
424            return new IXR_Error(1, 'Positive wordblock check');
425
426        // autoset summary on new pages
427        if(!page_exists($id) && empty($sum)) {
428            $sum = $lang['created'];
429        }
430
431        // autoset summary on deleted pages
432        if(page_exists($id) && empty($TEXT) && empty($sum)) {
433            $sum = $lang['deleted'];
434        }
435
436        lock($id);
437
438        saveWikiText($id,$TEXT,$sum,$minor);
439
440        unlock($id);
441
442        // run the indexer if page wasn't indexed yet
443        if(!@file_exists(metaFN($id, '.indexed'))) {
444            // try to aquire a lock
445            $lock = $conf['lockdir'].'/_indexer.lock';
446            while(!@mkdir($lock,$conf['dmode'])){
447                usleep(50);
448                if(time()-@filemtime($lock) > 60*5){
449                    // looks like a stale lock - remove it
450                    @rmdir($lock);
451                }else{
452                    return false;
453                }
454            }
455            if($conf['dperm']) chmod($lock, $conf['dperm']);
456
457            require_once(DOKU_INC.'inc/indexer.php');
458
459            // do the work
460            idx_addPage($id);
461
462            // we're finished - save and free lock
463            io_saveFile(metaFN($id,'.indexed'),INDEXER_VERSION);
464            @rmdir($lock);
465        }
466
467        return 0;
468    }
469
470    /**
471     * Uploads a file to the wiki.
472     *
473     * Michael Klier <chi@chimeric.de>
474     */
475    function putAttachment($id, $file, $params) {
476        global $conf;
477        global $lang;
478
479        $auth = auth_quickaclcheck(getNS($id).':*');
480        if($auth >= AUTH_UPLOAD) {
481            if(!isset($id)) {
482                return new IXR_ERROR(1, 'Filename not given.');
483            }
484
485            $ftmp = $conf['tmpdir'] . '/' . $id;
486
487            // save temporary file
488            @unlink($ftmp);
489            $buff = base64_decode($file);
490            io_saveFile($ftmp, $buff);
491
492            // get filename
493            list($iext, $imime,$dl) = mimetype($id);
494            $id = cleanID($id);
495            $fn = mediaFN($id);
496
497            // get filetype regexp
498            $types = array_keys(getMimeTypes());
499            $types = array_map(create_function('$q','return preg_quote($q,"/");'),$types);
500            $regex = join('|',$types);
501
502            // because a temp file was created already
503            if(preg_match('/\.('.$regex.')$/i',$fn)) {
504                //check for overwrite
505                $overwrite = @file_exists($fn);
506                if($overwrite && (!$params['ow'] || $auth < AUTH_DELETE)) {
507                    return new IXR_ERROR(1, $lang['uploadexist']);
508                }
509                // check for valid content
510                @require_once(DOKU_INC.'inc/media.php');
511                $ok = media_contentcheck($ftmp, $imime);
512                if($ok == -1) {
513                    return new IXR_ERROR(1, sprintf($lang['uploadexist'], ".$iext"));
514                } elseif($ok == -2) {
515                    return new IXR_ERROR(1, $lang['uploadspam']);
516                } elseif($ok == -3) {
517                    return new IXR_ERROR(1, $lang['uploadxss']);
518                }
519
520                // prepare event data
521                $data[0] = $ftmp;
522                $data[1] = $fn;
523                $data[2] = $id;
524                $data[3] = $imime;
525                $data[4] = $overwrite;
526
527                // trigger event
528                require_once(DOKU_INC.'inc/events.php');
529                return trigger_event('MEDIA_UPLOAD_FINISH', $data, array($this, '_media_upload_action'), true);
530
531            } else {
532                return new IXR_ERROR(1, $lang['uploadwrong']);
533            }
534        } else {
535            return new IXR_ERROR(1, "You don't have permissions to upload files.");
536        }
537    }
538
539    /**
540     * Deletes a file from the wiki.
541     *
542     * @author Gina Haeussge <osd@foosel.net>
543     */
544    function deleteAttachment($id){
545        $auth = auth_quickaclcheck(getNS($id).':*');
546        if($auth < AUTH_DELETE) return new IXR_ERROR(1, "You don't have permissions to delete files.");
547        global $conf;
548        global $lang;
549
550        // check for references if needed
551        $mediareferences = array();
552        if($conf['refcheck']){
553            require_once(DOKU_INC.'inc/fulltext.php');
554            $mediareferences = ft_mediause($id,$conf['refshow']);
555        }
556
557        if(!count($mediareferences)){
558            $file = mediaFN($id);
559            if(@unlink($file)){
560                require_once(DOKU_INC.'inc/changelog.php');
561                addMediaLogEntry(time(), $id, DOKU_CHANGE_TYPE_DELETE);
562                io_sweepNS($id,'mediadir');
563                return 0;
564            }
565            //something went wrong
566               return new IXR_ERROR(1, 'Could not delete file');
567        } else {
568            return new IXR_ERROR(1, 'File is still referenced');
569        }
570    }
571
572    /**
573     * Moves the temporary file to its final destination.
574     *
575     * Michael Klier <chi@chimeric.de>
576     */
577    function _media_upload_action($data) {
578        global $conf;
579
580        if(is_array($data) && count($data)===5) {
581            io_createNamespace($data[2], 'media');
582            if(rename($data[0], $data[1])) {
583                chmod($data[1], $conf['fmode']);
584                media_notify($data[2], $data[1], $data[3]);
585                // add a log entry to the media changelog
586                require_once(DOKU_INC.'inc/changelog.php');
587                if ($data[4]) {
588                    addMediaLogEntry(time(), $data[2], DOKU_CHANGE_TYPE_EDIT);
589                } else {
590                    addMediaLogEntry(time(), $data[2], DOKU_CHANGE_TYPE_CREATE);
591                }
592                return $data[2];
593            } else {
594                return new IXR_ERROR(1, 'Upload failed.');
595            }
596        } else {
597            return new IXR_ERROR(1, 'Upload failed.');
598        }
599    }
600
601    /**
602    * Returns the permissions of a given wiki page
603    */
604    function aclCheck($id) {
605        return auth_quickaclcheck($id);
606    }
607
608    /**
609     * Lists all links contained in a wiki page
610     *
611     * @author Michael Klier <chi@chimeric.de>
612     */
613    function listLinks($id) {
614        if(auth_quickaclcheck($id) < AUTH_READ){
615            return new IXR_Error(1, 'You are not allowed to read this page');
616        }
617        $links = array();
618
619        // resolve page instructions
620        $ins   = p_cached_instructions(wikiFN(cleanID($id)));
621
622        // instantiate new Renderer - needed for interwiki links
623        include(DOKU_INC.'inc/parser/xhtml.php');
624        $Renderer = new Doku_Renderer_xhtml();
625        $Renderer->interwiki = getInterwiki();
626
627        // parse parse instructions
628        foreach($ins as $in) {
629            $link = array();
630            switch($in[0]) {
631                case 'internallink':
632                    $link['type'] = 'local';
633                    $link['page'] = $in[1][0];
634                    $link['href'] = wl($in[1][0]);
635                    array_push($links,$link);
636                    break;
637                case 'externallink':
638                    $link['type'] = 'extern';
639                    $link['page'] = $in[1][0];
640                    $link['href'] = $in[1][0];
641                    array_push($links,$link);
642                    break;
643                case 'interwikilink':
644                    $url = $Renderer->_resolveInterWiki($in[1][2],$in[1][3]);
645                    $link['type'] = 'extern';
646                    $link['page'] = $url;
647                    $link['href'] = $url;
648                    array_push($links,$link);
649                    break;
650            }
651        }
652
653        return ($links);
654    }
655
656    /**
657     * Returns a list of recent changes since give timestamp
658     *
659     * @author Michael Hamann <michael@content-space.de>
660     * @author Michael Klier <chi@chimeric.de>
661     */
662    function getRecentChanges($timestamp) {
663        if(strlen($timestamp) != 10)
664            return new IXR_Error(20, 'The provided value is not a valid timestamp');
665
666        require_once(DOKU_INC.'inc/changelog.php');
667        require_once(DOKU_INC.'inc/pageutils.php');
668
669        $recents = getRecentsSince($timestamp);
670
671        $changes = array();
672
673        foreach ($recents as $recent) {
674            $change = array();
675            $change['name']         = $recent['id'];
676            $change['lastModified'] = new IXR_Date($recent['date']);
677            $change['author']       = $recent['user'];
678            $change['version']      = $recent['date'];
679            $change['perms']        = $recent['perms'];
680            $change['size']         = @filesize(wikiFN($recent['id']));
681            array_push($changes, $change);
682        }
683
684        if (!empty($changes)) {
685            return $changes;
686        } else {
687            // in case we still have nothing at this point
688            return new IXR_Error(30, 'There are no changes in the specified timeframe');
689        }
690    }
691
692    /**
693     * Returns a list of recent media changes since give timestamp
694     *
695     * @author Michael Hamann <michael@content-space.de>
696     * @author Michael Klier <chi@chimeric.de>
697     */
698    function getRecentMediaChanges($timestamp) {
699        if(strlen($timestamp) != 10)
700            return new IXR_Error(20, 'The provided value is not a valid timestamp');
701
702        require_once(DOKU_INC.'inc/changelog.php');
703        require_once(DOKU_INC.'inc/pageutils.php');
704
705        $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
706
707        $changes = array();
708
709        foreach ($recents as $recent) {
710            $change = array();
711            $change['name']         = $recent['id'];
712            $change['lastModified'] = new IXR_Date($recent['date']);
713            $change['author']       = $recent['user'];
714            $change['version']      = $recent['date'];
715            $change['perms']        = $recent['perms'];
716            $change['size']         = @filesize(wikiFN($recent['id']));
717            array_push($changes, $change);
718        }
719
720        if (!empty($changes)) {
721            return $changes;
722        } else {
723            // in case we still have nothing at this point
724            return new IXR_Error(30, 'There are no changes in the specified timeframe');
725        }
726    }
727
728    /**
729     * Returns a list of available revisions of a given wiki page
730     *
731     * @author Michael Klier <chi@chimeric.de>
732     */
733    function pageVersions($id, $first) {
734        global $conf;
735
736        $versions = array();
737
738        if(empty($id))
739            return new IXR_Error(1, 'Empty page ID');
740
741        require_once(DOKU_INC.'inc/changelog.php');
742
743        $revisions = getRevisions($id, $first, $conf['recent']+1);
744
745        if(count($revisions)==0 && $first!=0) {
746            $first=0;
747            $revisions = getRevisions($id, $first, $conf['recent']+1);
748        }
749
750        if(count($revisions)>0 && $first==0) {
751            array_unshift($revisions, '');  // include current revision
752            array_pop($revisions);          // remove extra log entry
753        }
754
755        $hasNext = false;
756        if(count($revisions)>$conf['recent']) {
757            $hasNext = true;
758            array_pop($revisions); // remove extra log entry
759        }
760
761        if(!empty($revisions)) {
762            foreach($revisions as $rev) {
763                $file = wikiFN($id,$rev);
764                $time = @filemtime($file);
765                // we check if the page actually exists, if this is not the
766                // case this can lead to less pages being returned than
767                // specified via $conf['recent']
768                if($time){
769                    $info = getRevisionInfo($id, $time, 1024);
770                    if(!empty($info)) {
771                        $data['user'] = $info['user'];
772                        $data['ip']   = $info['ip'];
773                        $data['type'] = $info['type'];
774                        $data['sum']  = $info['sum'];
775                        $data['modified'] = new IXR_Date($info['date']);
776                        $data['version'] = $info['date'];
777                        array_push($versions, $data);
778                    }
779                }
780            }
781            return $versions;
782        } else {
783            return array();
784        }
785    }
786
787    /**
788     * The version of Wiki RPC API supported
789     */
790    function wiki_RPCVersion(){
791        return 2;
792    }
793
794}
795
796$server = new dokuwiki_xmlrpc_server();
797
798// vim:ts=4:sw=4:et:enc=utf-8:
799