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