xref: /dokuwiki/inc/Remote/ApiCore.php (revision be5c1ea2da9ce21461b05de9ad624446de1786be)
1<?php
2
3namespace dokuwiki\Remote;
4
5use Doku_Renderer_xhtml;
6use dokuwiki\ChangeLog\MediaChangeLog;
7use dokuwiki\ChangeLog\PageChangeLog;
8use dokuwiki\Extension\Event;
9use dokuwiki\Search\FulltextSearch;
10use dokuwiki\Search\MetadataSearch;
11use dokuwiki\Search\PageIndex;
12
13use const dokuwiki\Search\FT_SNIPPET_NUMBER;
14
15define('DOKU_API_VERSION', 10);
16
17/**
18 * Provides the core methods for the remote API.
19 * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
20 */
21class ApiCore
22{
23    /** @var int Increased whenever the API is changed */
24    const API_VERSION = 10;
25
26
27    /** @var Api */
28    private $api;
29
30    /**
31     * @param Api $api
32     */
33    public function __construct(Api $api)
34    {
35        $this->api = $api;
36    }
37
38    /**
39     * Returns details about the core methods
40     *
41     * @return array
42     */
43    public function __getRemoteInfo()
44    {
45        return array(
46            'dokuwiki.getVersion' => array(
47                'args' => array(),
48                'return' => 'string',
49                'doc' => 'Returns the running DokuWiki version.'
50            ), 'dokuwiki.login' => array(
51                'args' => array('string', 'string'),
52                'return' => 'int',
53                'doc' => 'Tries to login with the given credentials and sets auth cookies.',
54                'public' => '1'
55            ), 'dokuwiki.logoff' => array(
56                'args' => array(),
57                'return' => 'int',
58                'doc' => 'Tries to logoff by expiring auth cookies and the associated PHP session.'
59            ), 'dokuwiki.getPagelist' => array(
60                'args' => array('string', 'array'),
61                'return' => 'array',
62                'doc' => 'List all pages within the given namespace.',
63                'name' => 'readNamespace'
64            ), 'dokuwiki.search' => array(
65                'args' => array('string'),
66                'return' => 'array',
67                'doc' => 'Perform a fulltext search and return a list of matching pages'
68            ), 'dokuwiki.getTime' => array(
69                'args' => array(),
70                'return' => 'int',
71                'doc' => 'Returns the current time at the remote wiki server as Unix timestamp.',
72            ), 'dokuwiki.setLocks' => array(
73                'args' => array('array'),
74                'return' => 'array',
75                'doc' => 'Lock or unlock pages.'
76            ), 'dokuwiki.getTitle' => array(
77                'args' => array(),
78                'return' => 'string',
79                'doc' => 'Returns the wiki title.',
80                'public' => '1'
81            ), 'dokuwiki.appendPage' => array(
82                'args' => array('string', 'string', 'array'),
83                'return' => 'bool',
84                'doc' => 'Append text to a wiki page.'
85            ), 'dokuwiki.deleteUsers' => array(
86                'args' => array('array'),
87                'return' => 'bool',
88                'doc' => 'Remove one or more users from the list of registered users.'
89            ),  'wiki.getPage' => array(
90                'args' => array('string'),
91                'return' => 'string',
92                'doc' => 'Get the raw Wiki text of page, latest version.',
93                'name' => 'rawPage',
94            ), 'wiki.getPageVersion' => array(
95                'args' => array('string', 'int'),
96                'name' => 'rawPage',
97                'return' => 'string',
98                'doc' => 'Return a raw wiki page'
99            ), 'wiki.getPageHTML' => array(
100                'args' => array('string'),
101                'return' => 'string',
102                'doc' => 'Return page in rendered HTML, latest version.',
103                'name' => 'htmlPage'
104            ), 'wiki.getPageHTMLVersion' => array(
105                'args' => array('string', 'int'),
106                'return' => 'string',
107                'doc' => 'Return page in rendered HTML.',
108                'name' => 'htmlPage'
109            ), 'wiki.getAllPages' => array(
110                'args' => array(),
111                'return' => 'array',
112                'doc' => 'Returns a list of all pages. The result is an array of utf8 pagenames.',
113                'name' => 'listPages'
114            ), 'wiki.getAttachments' => array(
115                'args' => array('string', 'array'),
116                'return' => 'array',
117                'doc' => 'Returns a list of all media files.',
118                'name' => 'listAttachments'
119            ), 'wiki.getBackLinks' => array(
120                'args' => array('string'),
121                'return' => 'array',
122                'doc' => 'Returns the pages that link to this page.',
123                'name' => 'listBackLinks'
124            ), 'wiki.getPageInfo' => array(
125                'args' => array('string'),
126                'return' => 'array',
127                'doc' => 'Returns a struct with info about the page, latest version.',
128                'name' => 'pageInfo'
129            ), 'wiki.getPageInfoVersion' => array(
130                'args' => array('string', 'int'),
131                'return' => 'array',
132                'doc' => 'Returns a struct with info about the page.',
133                'name' => 'pageInfo'
134            ), 'wiki.getPageVersions' => array(
135                'args' => array('string', 'int'),
136                'return' => 'array',
137                'doc' => 'Returns the available revisions of the page.',
138                'name' => 'pageVersions'
139            ), 'wiki.putPage' => array(
140                'args' => array('string', 'string', 'array'),
141                'return' => 'bool',
142                'doc' => 'Saves a wiki page.'
143            ), 'wiki.listLinks' => array(
144                'args' => array('string'),
145                'return' => 'array',
146                'doc' => 'Lists all links contained in a wiki page.'
147            ), 'wiki.getRecentChanges' => array(
148                'args' => array('int'),
149                'return' => 'array',
150                'Returns a struct about all recent changes since given timestamp.'
151            ), 'wiki.getRecentMediaChanges' => array(
152                'args' => array('int'),
153                'return' => 'array',
154                'Returns a struct about all recent media changes since given timestamp.'
155            ), 'wiki.aclCheck' => array(
156                'args' => array('string', 'string', 'array'),
157                'return' => 'int',
158                'doc' => 'Returns the permissions of a given wiki page. By default, for current user/groups'
159            ), 'wiki.putAttachment' => array(
160                'args' => array('string', 'file', 'array'),
161                'return' => 'array',
162                'doc' => 'Upload a file to the wiki.'
163            ), 'wiki.deleteAttachment' => array(
164                'args' => array('string'),
165                'return' => 'int',
166                'doc' => 'Delete a file from the wiki.'
167            ), 'wiki.getAttachment' => array(
168                'args' => array('string'),
169                'doc' => 'Return a media file',
170                'return' => 'file',
171                'name' => 'getAttachment',
172            ), 'wiki.getAttachmentInfo' => array(
173                'args' => array('string'),
174                'return' => 'array',
175                'doc' => 'Returns a struct with info about the attachment.'
176            ), 'dokuwiki.getXMLRPCAPIVersion' => array(
177                'args' => array(),
178                'name' => 'getAPIVersion',
179                'return' => 'int',
180                'doc' => 'Returns the XMLRPC API version.',
181                'public' => '1',
182            ), 'wiki.getRPCVersionSupported' => array(
183                'args' => array(),
184                'name' => 'wikiRpcVersion',
185                'return' => 'int',
186                'doc' => 'Returns 2 with the supported RPC API version.',
187                'public' => '1'
188            ),
189
190        );
191    }
192
193    /**
194     * @return string
195     */
196    public function getVersion()
197    {
198        return getVersion();
199    }
200
201    /**
202     * @return int unix timestamp
203     */
204    public function getTime()
205    {
206        return time();
207    }
208
209    /**
210     * Return a raw wiki page
211     *
212     * @param string $id wiki page id
213     * @param int|string $rev revision timestamp of the page or empty string
214     * @return string page text.
215     * @throws AccessDeniedException if no permission for page
216     */
217    public function rawPage($id, $rev = '')
218    {
219        $id = $this->resolvePageId($id);
220        if (auth_quickaclcheck($id) < AUTH_READ) {
221            throw new AccessDeniedException('You are not allowed to read this file', 111);
222        }
223        $text = rawWiki($id, $rev);
224        if (!$text) {
225            return pageTemplate($id);
226        } else {
227            return $text;
228        }
229    }
230
231    /**
232     * Return a media file
233     *
234     * @author Gina Haeussge <osd@foosel.net>
235     *
236     * @param string $id file id
237     * @return mixed media file
238     * @throws AccessDeniedException no permission for media
239     * @throws RemoteException not exist
240     */
241    public function getAttachment($id)
242    {
243        $id = cleanID($id);
244        if (auth_quickaclcheck(getNS($id) . ':*') < AUTH_READ) {
245            throw new AccessDeniedException('You are not allowed to read this file', 211);
246        }
247
248        $file = mediaFN($id);
249        if (!@ file_exists($file)) {
250            throw new RemoteException('The requested file does not exist', 221);
251        }
252
253        $data = io_readFile($file, false);
254        return $this->api->toFile($data);
255    }
256
257    /**
258     * Return info about a media file
259     *
260     * @author Gina Haeussge <osd@foosel.net>
261     *
262     * @param string $id page id
263     * @return array
264     */
265    public function getAttachmentInfo($id)
266    {
267        $id = cleanID($id);
268        $info = array(
269            'lastModified' => $this->api->toDate(0),
270            'size' => 0,
271        );
272
273        $file = mediaFN($id);
274        if (auth_quickaclcheck(getNS($id) . ':*') >= AUTH_READ) {
275            if (file_exists($file)) {
276                $info['lastModified'] = $this->api->toDate(filemtime($file));
277                $info['size'] = filesize($file);
278            } else {
279                //Is it deleted media with changelog?
280                $medialog = new MediaChangeLog($id);
281                $revisions = $medialog->getRevisions(0, 1);
282                if (!empty($revisions)) {
283                    $info['lastModified'] = $this->api->toDate($revisions[0]);
284                }
285            }
286        }
287
288        return $info;
289    }
290
291    /**
292     * Return a wiki page rendered to html
293     *
294     * @param string $id page id
295     * @param string|int $rev revision timestamp or empty string
296     * @return null|string html
297     * @throws AccessDeniedException no access to page
298     */
299    public function htmlPage($id, $rev = '')
300    {
301        $id = $this->resolvePageId($id);
302        if (auth_quickaclcheck($id) < AUTH_READ) {
303            throw new AccessDeniedException('You are not allowed to read this page', 111);
304        }
305        return p_wiki_xhtml($id, $rev, false);
306    }
307
308    /**
309     * List all pages - we use the indexer list here
310     *
311     * @return array
312     */
313    public function listPages()
314    {
315        $list = array();
316
317        $PageIndex = PageIndex::getInstance();
318        $pages = $PageIndex->getPages();
319        $pages = array_filter(array_filter($pages, 'isVisiblePage'), 'page_exists');
320
321        foreach (array_keys($pages) as $idx) {
322            $perm = auth_quickaclcheck($pages[$idx]);
323            if ($perm < AUTH_READ) {
324                continue;
325            }
326            $page = array();
327            $page['id'] = trim($pages[$idx]);
328            $page['perms'] = $perm;
329            $page['size'] = @filesize(wikiFN($pages[$idx]));
330            $page['lastModified'] = $this->api->toDate(@filemtime(wikiFN($pages[$idx])));
331            $list[] = $page;
332        }
333
334        return $list;
335    }
336
337    /**
338     * List all pages in the given namespace (and below)
339     *
340     * @param string $ns
341     * @param array $opts
342     *    $opts['depth']   recursion level, 0 for all
343     *    $opts['hash']    do md5 sum of content?
344     * @return array
345     */
346    public function readNamespace($ns, $opts)
347    {
348        global $conf;
349
350        if (!is_array($opts)) $opts = array();
351
352        $ns = cleanID($ns);
353        $dir = utf8_encodeFN(str_replace(':', '/', $ns));
354        $data = array();
355        $opts['skipacl'] = 0; // no ACL skipping for XMLRPC
356        search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
357        return $data;
358    }
359
360    /**
361     * List all pages in the given namespace (and below)
362     *
363     * @param string $query
364     * @return array
365     */
366    public function search($query)
367    {
368        $regex = array();
369        $data = FulltextSearch::pageSearch($query, $regex);
370        $pages = array();
371
372        // prepare additional data
373        $idx = 0;
374        foreach ($data as $id => $score) {
375            $file = wikiFN($id);
376
377            if ($idx < FT_SNIPPET_NUMBER) {
378                $snippet = FulltextSearch::snippet($id, $regex);
379                $idx++;
380            } else {
381                $snippet = '';
382            }
383
384            $pages[] = array(
385                'id' => $id,
386                'score' => intval($score),
387                'rev' => filemtime($file),
388                'mtime' => filemtime($file),
389                'size' => filesize($file),
390                'snippet' => $snippet,
391                'title' => useHeading('navigation') ? p_get_first_heading($id) : $id
392            );
393        }
394        return $pages;
395    }
396
397    /**
398     * Returns the wiki title.
399     *
400     * @return string
401     */
402    public function getTitle()
403    {
404        global $conf;
405        return $conf['title'];
406    }
407
408    /**
409     * List all media files.
410     *
411     * Available options are 'recursive' for also including the subnamespaces
412     * in the listing, and 'pattern' for filtering the returned files against
413     * a regular expression matching their name.
414     *
415     * @author Gina Haeussge <osd@foosel.net>
416     *
417     * @param string $ns
418     * @param array $options
419     *   $options['depth']     recursion level, 0 for all
420     *   $options['showmsg']   shows message if invalid media id is used
421     *   $options['pattern']   check given pattern
422     *   $options['hash']      add hashes to result list
423     * @return array
424     * @throws AccessDeniedException no access to the media files
425     */
426    public function listAttachments($ns, $options = array())
427    {
428        global $conf;
429
430        $ns = cleanID($ns);
431
432        if (!is_array($options)) $options = array();
433        $options['skipacl'] = 0; // no ACL skipping for XMLRPC
434
435        if (auth_quickaclcheck($ns . ':*') >= AUTH_READ) {
436            $dir = utf8_encodeFN(str_replace(':', '/', $ns));
437
438            $data = array();
439            search($data, $conf['mediadir'], 'search_media', $options, $dir);
440            $len = count($data);
441            if (!$len) return array();
442
443            for ($i = 0; $i < $len; $i++) {
444                unset($data[$i]['meta']);
445                $data[$i]['perms'] = $data[$i]['perm'];
446                unset($data[$i]['perm']);
447                $data[$i]['lastModified'] = $this->api->toDate($data[$i]['mtime']);
448            }
449            return $data;
450        } else {
451            throw new AccessDeniedException('You are not allowed to list media files.', 215);
452        }
453    }
454
455    /**
456     * Return a list of backlinks
457     *
458     * @param string $id page id
459     * @return array
460     */
461    public function listBackLinks($id)
462    {
463        return MetadataSearch::backlinks($this->resolvePageId($id));
464    }
465
466    /**
467     * Return some basic data about a page
468     *
469     * @param string $id page id
470     * @param string|int $rev revision timestamp or empty string
471     * @return array
472     * @throws AccessDeniedException no access for page
473     * @throws RemoteException page not exist
474     */
475    public function pageInfo($id, $rev = '')
476    {
477        $id = $this->resolvePageId($id);
478        if (auth_quickaclcheck($id) < AUTH_READ) {
479            throw new AccessDeniedException('You are not allowed to read this page', 111);
480        }
481        $file = wikiFN($id, $rev);
482        $time = @filemtime($file);
483        if (!$time) {
484            throw new RemoteException('The requested page does not exist', 121);
485        }
486
487        // set revision to current version if empty, use revision otherwise
488        // as the timestamps of old files are not necessarily correct
489        if ($rev === '') {
490            $rev = $time;
491        }
492
493        $pagelog = new PageChangeLog($id, 1024);
494        $info = $pagelog->getRevisionInfo($rev);
495
496        $data = array(
497            'name' => $id,
498            'lastModified' => $this->api->toDate($rev),
499            'author' => is_array($info) ? (($info['user']) ? $info['user'] : $info['ip']) : null,
500            'version' => $rev
501        );
502
503        return ($data);
504    }
505
506    /**
507     * Save a wiki page
508     *
509     * @author Michael Klier <chi@chimeric.de>
510     *
511     * @param string $id page id
512     * @param string $text wiki text
513     * @param array $params parameters: summary, minor edit
514     * @return bool
515     * @throws AccessDeniedException no write access for page
516     * @throws RemoteException no id, empty new page or locked
517     */
518    public function putPage($id, $text, $params)
519    {
520        global $TEXT;
521        global $lang;
522
523        $id = $this->resolvePageId($id);
524        $TEXT = cleanText($text);
525        $sum = $params['sum'];
526        $minor = $params['minor'];
527
528        if (empty($id)) {
529            throw new RemoteException('Empty page ID', 131);
530        }
531
532        if (!page_exists($id) && trim($TEXT) == '') {
533            throw new RemoteException('Refusing to write an empty new wiki page', 132);
534        }
535
536        if (auth_quickaclcheck($id) < AUTH_EDIT) {
537            throw new AccessDeniedException('You are not allowed to edit this page', 112);
538        }
539
540        // Check, if page is locked
541        if (checklock($id)) {
542            throw new RemoteException('The page is currently locked', 133);
543        }
544
545        // SPAM check
546        if (checkwordblock()) {
547            throw new RemoteException('Positive wordblock check', 134);
548        }
549
550        // autoset summary on new pages
551        if (!page_exists($id) && empty($sum)) {
552            $sum = $lang['created'];
553        }
554
555        // autoset summary on deleted pages
556        if (page_exists($id) && empty($TEXT) && empty($sum)) {
557            $sum = $lang['deleted'];
558        }
559
560        lock($id);
561
562        saveWikiText($id, $TEXT, $sum, $minor);
563
564        unlock($id);
565
566        // run the indexer if page wasn't indexed yet
567        $PageIndex = PageIndex::getInstance();
568        $PageIndex->addPage($id);
569
570        return true;
571    }
572
573    /**
574     * Appends text to a wiki page.
575     *
576     * @param string $id page id
577     * @param string $text wiki text
578     * @param array $params such as summary,minor
579     * @return bool|string
580     * @throws RemoteException
581     */
582    public function appendPage($id, $text, $params)
583    {
584        $currentpage = $this->rawPage($id);
585        if (!is_string($currentpage)) {
586            return $currentpage;
587        }
588        return $this->putPage($id, $currentpage . $text, $params);
589    }
590
591    /**
592     * Remove one or more users from the list of registered users
593     *
594     * @param string[] $usernames List of usernames to remove
595     *
596     * @return bool
597     *
598     * @throws AccessDeniedException
599     */
600    public function deleteUsers($usernames)
601    {
602        if (!auth_isadmin()) {
603            throw new AccessDeniedException('Only admins are allowed to delete users', 114);
604        }
605        /** @var \dokuwiki\Extension\AuthPlugin $auth */
606        global $auth;
607        return (bool)$auth->triggerUserMod('delete', array($usernames));
608    }
609
610    /**
611     * Uploads a file to the wiki.
612     *
613     * Michael Klier <chi@chimeric.de>
614     *
615     * @param string $id page id
616     * @param string $file
617     * @param array $params such as overwrite
618     * @return false|string
619     * @throws RemoteException
620     */
621    public function putAttachment($id, $file, $params)
622    {
623        $id = cleanID($id);
624        $auth = auth_quickaclcheck(getNS($id) . ':*');
625
626        if (!isset($id)) {
627            throw new RemoteException('Filename not given.', 231);
628        }
629
630        global $conf;
631
632        $ftmp = $conf['tmpdir'] . '/' . md5($id . clientIP());
633
634        // save temporary file
635        @unlink($ftmp);
636        io_saveFile($ftmp, $file);
637
638        $res = media_save(array('name' => $ftmp), $id, $params['ow'], $auth, 'rename');
639        if (is_array($res)) {
640            throw new RemoteException($res[0], -$res[1]);
641        } else {
642            return $res;
643        }
644    }
645
646    /**
647     * Deletes a file from the wiki.
648     *
649     * @author Gina Haeussge <osd@foosel.net>
650     *
651     * @param string $id page id
652     * @return int
653     * @throws AccessDeniedException no permissions
654     * @throws RemoteException file in use or not deleted
655     */
656    public function deleteAttachment($id)
657    {
658        $id = cleanID($id);
659        $auth = auth_quickaclcheck(getNS($id) . ':*');
660        $res = media_delete($id, $auth);
661        if ($res & DOKU_MEDIA_DELETED) {
662            return 0;
663        } elseif ($res & DOKU_MEDIA_NOT_AUTH) {
664            throw new AccessDeniedException('You don\'t have permissions to delete files.', 212);
665        } elseif ($res & DOKU_MEDIA_INUSE) {
666            throw new RemoteException('File is still referenced', 232);
667        } else {
668            throw new RemoteException('Could not delete file', 233);
669        }
670    }
671
672    /**
673     * Returns the permissions of a given wiki page for the current user or another user
674     *
675     * @param string $id page id
676     * @param string|null $user username
677     * @param array|null $groups array of groups
678     * @return int permission level
679     */
680    public function aclCheck($id, $user = null, $groups = null)
681    {
682        /** @var \dokuwiki\Extension\AuthPlugin $auth */
683        global $auth;
684
685        $id = $this->resolvePageId($id);
686        if ($user === null) {
687            return auth_quickaclcheck($id);
688        } else {
689            if ($groups === null) {
690                $userinfo = $auth->getUserData($user);
691                if ($userinfo === false) {
692                    $groups = array();
693                } else {
694                    $groups = $userinfo['grps'];
695                }
696            }
697            return auth_aclcheck($id, $user, $groups);
698        }
699    }
700
701    /**
702     * Lists all links contained in a wiki page
703     *
704     * @author Michael Klier <chi@chimeric.de>
705     *
706     * @param string $id page id
707     * @return array
708     * @throws AccessDeniedException  no read access for page
709     */
710    public function listLinks($id)
711    {
712        $id = $this->resolvePageId($id);
713        if (auth_quickaclcheck($id) < AUTH_READ) {
714            throw new AccessDeniedException('You are not allowed to read this page', 111);
715        }
716        $links = array();
717
718        // resolve page instructions
719        $ins = p_cached_instructions(wikiFN($id));
720
721        // instantiate new Renderer - needed for interwiki links
722        $Renderer = new Doku_Renderer_xhtml();
723        $Renderer->interwiki = getInterwiki();
724
725        // parse parse instructions
726        foreach ($ins as $in) {
727            $link = array();
728            switch ($in[0]) {
729                case 'internallink':
730                    $link['type'] = 'local';
731                    $link['page'] = $in[1][0];
732                    $link['href'] = wl($in[1][0]);
733                    array_push($links, $link);
734                    break;
735                case 'externallink':
736                    $link['type'] = 'extern';
737                    $link['page'] = $in[1][0];
738                    $link['href'] = $in[1][0];
739                    array_push($links, $link);
740                    break;
741                case 'interwikilink':
742                    $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]);
743                    $link['type'] = 'extern';
744                    $link['page'] = $url;
745                    $link['href'] = $url;
746                    array_push($links, $link);
747                    break;
748            }
749        }
750
751        return ($links);
752    }
753
754    /**
755     * Returns a list of recent changes since give timestamp
756     *
757     * @author Michael Hamann <michael@content-space.de>
758     * @author Michael Klier <chi@chimeric.de>
759     *
760     * @param int $timestamp unix timestamp
761     * @return array
762     * @throws RemoteException no valid timestamp
763     */
764    public function getRecentChanges($timestamp)
765    {
766        if (strlen($timestamp) != 10) {
767            throw new RemoteException('The provided value is not a valid timestamp', 311);
768        }
769
770        $recents = getRecentsSince($timestamp);
771
772        $changes = array();
773
774        foreach ($recents as $recent) {
775            $change = array();
776            $change['name'] = $recent['id'];
777            $change['lastModified'] = $this->api->toDate($recent['date']);
778            $change['author'] = $recent['user'];
779            $change['version'] = $recent['date'];
780            $change['perms'] = $recent['perms'];
781            $change['size'] = @filesize(wikiFN($recent['id']));
782            array_push($changes, $change);
783        }
784
785        if (!empty($changes)) {
786            return $changes;
787        } else {
788            // in case we still have nothing at this point
789            throw new RemoteException('There are no changes in the specified timeframe', 321);
790        }
791    }
792
793    /**
794     * Returns a list of recent media changes since give timestamp
795     *
796     * @author Michael Hamann <michael@content-space.de>
797     * @author Michael Klier <chi@chimeric.de>
798     *
799     * @param int $timestamp unix timestamp
800     * @return array
801     * @throws RemoteException no valid timestamp
802     */
803    public function getRecentMediaChanges($timestamp)
804    {
805        if (strlen($timestamp) != 10)
806            throw new RemoteException('The provided value is not a valid timestamp', 311);
807
808        $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
809
810        $changes = array();
811
812        foreach ($recents as $recent) {
813            $change = array();
814            $change['name'] = $recent['id'];
815            $change['lastModified'] = $this->api->toDate($recent['date']);
816            $change['author'] = $recent['user'];
817            $change['version'] = $recent['date'];
818            $change['perms'] = $recent['perms'];
819            $change['size'] = @filesize(mediaFN($recent['id']));
820            array_push($changes, $change);
821        }
822
823        if (!empty($changes)) {
824            return $changes;
825        } else {
826            // in case we still have nothing at this point
827            throw new RemoteException('There are no changes in the specified timeframe', 321);
828        }
829    }
830
831    /**
832     * Returns a list of available revisions of a given wiki page
833     * Number of returned pages is set by $conf['recent']
834     * However not accessible pages are skipped, so less than $conf['recent'] could be returned
835     *
836     * @author Michael Klier <chi@chimeric.de>
837     *
838     * @param string $id page id
839     * @param int $first skip the first n changelog lines
840     *                      0 = from current(if exists)
841     *                      1 = from 1st old rev
842     *                      2 = from 2nd old rev, etc
843     * @return array
844     * @throws AccessDeniedException no read access for page
845     * @throws RemoteException empty id
846     */
847    public function pageVersions($id, $first)
848    {
849        $id = $this->resolvePageId($id);
850        if (auth_quickaclcheck($id) < AUTH_READ) {
851            throw new AccessDeniedException('You are not allowed to read this page', 111);
852        }
853        global $conf;
854
855        $versions = array();
856
857        if (empty($id)) {
858            throw new RemoteException('Empty page ID', 131);
859        }
860
861        $first = (int) $first;
862        $first_rev = $first - 1;
863        $first_rev = $first_rev < 0 ? 0 : $first_rev;
864        $pagelog = new PageChangeLog($id);
865        $revisions = $pagelog->getRevisions($first_rev, $conf['recent']);
866
867        if ($first == 0) {
868            array_unshift($revisions, '');  // include current revision
869            if (count($revisions) > $conf['recent']) {
870                array_pop($revisions);          // remove extra log entry
871            }
872        }
873
874        if (!empty($revisions)) {
875            foreach ($revisions as $rev) {
876                $file = wikiFN($id, $rev);
877                $time = @filemtime($file);
878                // we check if the page actually exists, if this is not the
879                // case this can lead to less pages being returned than
880                // specified via $conf['recent']
881                if ($time) {
882                    $pagelog->setChunkSize(1024);
883                    $info = $pagelog->getRevisionInfo($rev ? $rev : $time);
884                    if (!empty($info)) {
885                        $data = array();
886                        $data['user'] = $info['user'];
887                        $data['ip'] = $info['ip'];
888                        $data['type'] = $info['type'];
889                        $data['sum'] = $info['sum'];
890                        $data['modified'] = $this->api->toDate($info['date']);
891                        $data['version'] = $info['date'];
892                        array_push($versions, $data);
893                    }
894                }
895            }
896            return $versions;
897        } else {
898            return array();
899        }
900    }
901
902    /**
903     * The version of Wiki RPC API supported
904     */
905    public function wikiRpcVersion()
906    {
907        return 2;
908    }
909
910    /**
911     * Locks or unlocks a given batch of pages
912     *
913     * Give an associative array with two keys: lock and unlock. Both should contain a
914     * list of pages to lock or unlock
915     *
916     * Returns an associative array with the keys locked, lockfail, unlocked and
917     * unlockfail, each containing lists of pages.
918     *
919     * @param array[] $set list pages with array('lock' => array, 'unlock' => array)
920     * @return array
921     */
922    public function setLocks($set)
923    {
924        $locked = array();
925        $lockfail = array();
926        $unlocked = array();
927        $unlockfail = array();
928
929        foreach ((array) $set['lock'] as $id) {
930            $id = $this->resolvePageId($id);
931            if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) {
932                $lockfail[] = $id;
933            } else {
934                lock($id);
935                $locked[] = $id;
936            }
937        }
938
939        foreach ((array) $set['unlock'] as $id) {
940            $id = $this->resolvePageId($id);
941            if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) {
942                $unlockfail[] = $id;
943            } else {
944                $unlocked[] = $id;
945            }
946        }
947
948        return array(
949            'locked' => $locked,
950            'lockfail' => $lockfail,
951            'unlocked' => $unlocked,
952            'unlockfail' => $unlockfail,
953        );
954    }
955
956    /**
957     * Return API version
958     *
959     * @return int
960     */
961    public function getAPIVersion()
962    {
963        return self::API_VERSION;
964    }
965
966    /**
967     * Login
968     *
969     * @param string $user
970     * @param string $pass
971     * @return int
972     */
973    public function login($user, $pass)
974    {
975        global $conf;
976        /** @var \dokuwiki\Extension\AuthPlugin $auth */
977        global $auth;
978
979        if (!$conf['useacl']) return 0;
980        if (!$auth) return 0;
981
982        @session_start(); // reopen session for login
983        if ($auth->canDo('external')) {
984            $ok = $auth->trustExternal($user, $pass, false);
985        } else {
986            $evdata = array(
987                'user' => $user,
988                'password' => $pass,
989                'sticky' => false,
990                'silent' => true,
991            );
992            $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
993        }
994        session_write_close(); // we're done with the session
995
996        return $ok;
997    }
998
999    /**
1000     * Log off
1001     *
1002     * @return int
1003     */
1004    public function logoff()
1005    {
1006        global $conf;
1007        global $auth;
1008        if (!$conf['useacl']) return 0;
1009        if (!$auth) return 0;
1010
1011        auth_logoff();
1012
1013        return 1;
1014    }
1015
1016    /**
1017     * Resolve page id
1018     *
1019     * @param string $id page id
1020     * @return string
1021     */
1022    private function resolvePageId($id)
1023    {
1024        $id = cleanID($id);
1025        if (empty($id)) {
1026            global $conf;
1027            $id = cleanID($conf['start']);
1028        }
1029        return $id;
1030    }
1031}
1032