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