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