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