xref: /dokuwiki/inc/Remote/ApiCore.php (revision d3856637cf1c8c054f0b89e003bfed5bff1b60f9)
1<?php
2
3namespace dokuwiki\Remote;
4
5use Doku_Renderer_xhtml;
6use dokuwiki\ChangeLog\PageChangeLog;
7use dokuwiki\Extension\AuthPlugin;
8use dokuwiki\Extension\Event;
9use dokuwiki\Remote\Response\Link;
10use dokuwiki\Remote\Response\Media;
11use dokuwiki\Remote\Response\MediaRevision;
12use dokuwiki\Remote\Response\Page;
13use dokuwiki\Remote\Response\PageHit;
14use dokuwiki\Remote\Response\PageRevision;
15use dokuwiki\Remote\Response\User;
16use dokuwiki\Utf8\Sort;
17
18/**
19 * Provides the core methods for the remote API.
20 * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
21 */
22class ApiCore
23{
24    /** @var int Increased whenever the API is changed */
25    public const API_VERSION = 12;
26
27    /**
28     * Returns details about the core methods
29     *
30     * @return array
31     */
32    public function getMethods()
33    {
34        return [
35            'core.getAPIVersion' => (new ApiCall([$this, 'getAPIVersion'], 'info'))->setPublic(),
36
37            'core.getWikiVersion' => new ApiCall('getVersion', 'info'),
38            'core.getWikiTitle' => (new ApiCall([$this, 'getWikiTitle'], 'info'))->setPublic(),
39            'core.getWikiTime' => (new ApiCall([$this, 'getWikiTime'], 'info')),
40
41            'core.login' => (new ApiCall([$this, 'login'], 'user'))->setPublic(),
42            'core.logoff' => new ApiCall([$this, 'logoff'], 'user'),
43            'core.whoAmI' => (new ApiCall([$this, 'whoAmI'], 'user')),
44            'core.aclCheck' => new ApiCall([$this, 'aclCheck'], 'user'),
45
46            'core.listPages' => new ApiCall([$this, 'listPages'], 'pages'),
47            'core.searchPages' => new ApiCall([$this, 'searchPages'], 'pages'),
48            'core.getRecentPageChanges' => new ApiCall([$this, 'getRecentPageChanges'], 'pages'),
49
50            'core.getPage' => (new ApiCall([$this, 'getPage'], 'pages')),
51            'core.getPageHTML' => (new ApiCall([$this, 'getPageHTML'], 'pages')),
52            'core.getPageInfo' => (new ApiCall([$this, 'getPageInfo'], 'pages')),
53            'core.getPageVersions' => new ApiCall([$this, 'getPageVersions'], 'pages'),
54            'core.getPageLinks' => new ApiCall([$this, 'getPageLinks'], 'pages'),
55            'core.getPageBackLinks' => new ApiCall([$this, 'getPageBackLinks'], 'pages'),
56
57            'core.lockPages' => new ApiCall([$this, 'lockPages'], 'pages'),
58            'core.unlockPages' => new ApiCall([$this, 'unlockPages'], 'pages'),
59            'core.savePage' => new ApiCall([$this, 'savePage'], 'pages'),
60            'core.appendPage' => new ApiCall([$this, 'appendPage'], 'pages'),
61
62            'core.listMedia' => new ApiCall([$this, 'listMedia'], 'media'),
63            'core.getRecentMediaChanges' => new ApiCall([$this, 'getRecentMediaChanges'], 'media'),
64
65            'core.getMedia' => new ApiCall([$this, 'getMedia'], 'media'),
66            'core.getMediaInfo' => new ApiCall([$this, 'getMediaInfo'], 'media'),
67            // todo: implement getMediaVersions
68            // todo: implement getMediaUsage
69
70            'core.saveMedia' => new ApiCall([$this, 'saveMedia'], 'media'),
71            'core.deleteMedia' => new ApiCall([$this, 'deleteMedia'], 'media'),
72        ];
73    }
74
75    // region info
76
77    /**
78     * Return the API version
79     *
80     * This is the version of the DokuWiki API. It increases whenever the API definition changes.
81     *
82     * When developing a client, you should check this version and make sure you can handle it.
83     *
84     * @return int
85     */
86    public function getAPIVersion()
87    {
88        return self::API_VERSION;
89    }
90
91    /**
92     * Returns the wiki title
93     *
94     * @link https://www.dokuwiki.org/config:title
95     * @return string
96     */
97    public function getWikiTitle()
98    {
99        global $conf;
100        return $conf['title'];
101    }
102
103    /**
104     * Return the current server time
105     *
106     * Returns a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC).
107     *
108     * You can use this to compensate for differences between your client's time and the
109     * server's time when working with last modified timestamps (revisions).
110     *
111     * @return int A unix timestamp
112     */
113    public function getWikiTime()
114    {
115        return time();
116    }
117
118    // endregion
119
120    // region user
121
122    /**
123     * Login
124     *
125     * This will use the given credentials and attempt to login the user. This will set the
126     * appropriate cookies, which can be used for subsequent requests.
127     *
128     * Use of this mechanism is discouraged. Using token authentication is preferred.
129     *
130     * @param string $user The user name
131     * @param string $pass The password
132     * @return int If the login was successful
133     */
134    public function login($user, $pass)
135    {
136        global $conf;
137        /** @var AuthPlugin $auth */
138        global $auth;
139
140        if (!$conf['useacl']) return 0;
141        if (!$auth instanceof AuthPlugin) return 0;
142
143        @session_start(); // reopen session for login
144        $ok = null;
145        if ($auth->canDo('external')) {
146            $ok = $auth->trustExternal($user, $pass, false);
147        }
148        if ($ok === null) {
149            $evdata = [
150                'user' => $user,
151                'password' => $pass,
152                'sticky' => false,
153                'silent' => true
154            ];
155            $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
156        }
157        session_write_close(); // we're done with the session
158
159        return $ok;
160    }
161
162    /**
163     * Log off
164     *
165     * Attempt to log out the current user, deleting the appropriate cookies
166     *
167     * Use of this mechanism is discouraged. Using token authentication is preferred.
168     *
169     * @return int 0 on failure, 1 on success
170     */
171    public function logoff()
172    {
173        global $conf;
174        global $auth;
175        if (!$conf['useacl']) return 0;
176        if (!$auth instanceof AuthPlugin) return 0;
177
178        auth_logoff();
179
180        return 1;
181    }
182
183    /**
184     * Info about the currently authenticated user
185     *
186     * @return User
187     */
188    public function whoAmI()
189    {
190        return new User([]);
191    }
192
193    /**
194     * Check ACL Permissions
195     *
196     * This call allows to check the permissions for a given page/media and user/group combination.
197     * If no user/group is given, the current user is used.
198     *
199     * Read the link below to learn more about the permission levels.
200     *
201     * @link https://www.dokuwiki.org/acl#background_info
202     * @param string $page A page or media ID
203     * @param string $user username
204     * @param string[] $groups array of groups
205     * @return int permission level
206     * @throws RemoteException
207     */
208    public function aclCheck($page, $user = '', $groups = [])
209    {
210        /** @var AuthPlugin $auth */
211        global $auth;
212
213        $page = $this->checkPage($page, false, AUTH_NONE);
214
215        if ($user === '') {
216            return auth_quickaclcheck($page);
217        } else {
218            if ($groups === []) {
219                $userinfo = $auth->getUserData($user);
220                if ($userinfo === false) {
221                    $groups = [];
222                } else {
223                    $groups = $userinfo['grps'];
224                }
225            }
226            return auth_aclcheck($page, $user, $groups);
227        }
228    }
229
230    // endregion
231
232    // region pages
233
234    /**
235     * List all pages in the given namespace (and below)
236     *
237     * Setting the `depth` to `0` and the `namespace` to `""` will return all pages in the wiki.
238     *
239     * @param string $namespace The namespace to search. Empty string for root namespace
240     * @param int $depth How deep to search. 0 for all subnamespaces
241     * @param bool $hash Whether to include a MD5 hash of the page content
242     * @return Page[] A list of matching pages
243     */
244    public function listPages($namespace = '', $depth = 1, $hash = false)
245    {
246        global $conf;
247
248        $namespace = cleanID($namespace);
249
250        // shortcut for all pages
251        if ($namespace === '' && $depth === 0) {
252            return $this->getAllPages($hash);
253        }
254
255        // run our search iterator to get the pages
256        $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
257        $data = [];
258        $opts['skipacl'] = 0;
259        $opts['depth'] = $depth; // FIXME depth needs to be calculated relative to $dir
260        $opts['hash'] = $hash;
261        search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
262
263        return array_map(fn($item) => new Page($item), $data);
264    }
265
266    /**
267     * Get all pages at once
268     *
269     * This is uses the page index and is quicker than iterating which is done in listPages()
270     *
271     * @return Page[] A list of all pages
272     * @see listPages()
273     */
274    protected function getAllPages($hash = false)
275    {
276        $list = [];
277        $pages = idx_get_indexer()->getPages();
278        Sort::ksort($pages);
279
280        foreach (array_keys($pages) as $idx) {
281            $perm = auth_quickaclcheck($pages[$idx]);
282            if ($perm < AUTH_READ || isHiddenPage($pages[$idx]) || !page_exists($pages[$idx])) {
283                continue;
284            }
285
286            $page = new Page([
287                'id' => $pages[$idx],
288                'perm' => $perm,
289            ]);
290            if ($hash) $page->calculateHash();
291
292            $list[] = $page;
293        }
294
295        return $list;
296    }
297
298    /**
299     * Do a fulltext search
300     *
301     * This executes a full text search and returns the results. The query uses the standard
302     * DokuWiki search syntax.
303     *
304     * Snippets are provided for the first 15 results only. The title is either the first heading
305     * or the page id depending on the wiki's configuration.
306     *
307     * @link https://www.dokuwiki.org/search#syntax
308     * @param string $query The search query as supported by the DokuWiki search
309     * @return PageHit[] A list of matching pages
310     */
311    public function searchPages($query)
312    {
313        $regex = [];
314        $data = ft_pageSearch($query, $regex);
315        $pages = [];
316
317        // prepare additional data
318        $idx = 0;
319        foreach ($data as $id => $score) {
320            $file = wikiFN($id);
321
322            if ($idx < FT_SNIPPET_NUMBER) {
323                $snippet = ft_snippet($id, $regex);
324                $idx++;
325            } else {
326                $snippet = '';
327            }
328
329            $pages[] = new PageHit([
330                'id' => $id,
331                'score' => (int)$score,
332                'rev' => filemtime($file),
333                'mtime' => filemtime($file),
334                'size' => filesize($file),
335                'snippet' => $snippet,
336                'title' => useHeading('navigation') ? p_get_first_heading($id) : $id
337            ]);
338        }
339        return $pages;
340    }
341
342    /**
343     * Get recent page changes
344     *
345     * Returns a list of recent changes to wiki pages. The results can be limited to changes newer than
346     * a given timestamp.
347     *
348     * Only changes within the configured `$conf['recent']` range are returned. This is the default
349     * when no timestamp is given.
350     *
351     * @link https://www.dokuwiki.org/config:recent
352     * @param int $timestamp Only show changes newer than this unix timestamp
353     * @return PageRevision[]
354     * @author Michael Klier <chi@chimeric.de>
355     * @author Michael Hamann <michael@content-space.de>
356     */
357    public function getRecentPageChanges($timestamp = 0)
358    {
359        $recents = getRecentsSince($timestamp);
360
361        $changes = [];
362        foreach ($recents as $recent) {
363            $changes[] = new PageRevision([
364                'id' => $recent['id'],
365                'revision' => $recent['date'],
366                'author' => $recent['user'],
367                'ip' => $recent['ip'],
368                'summary' => $recent['sum'],
369                'type' => $recent['type'],
370                'sizechange' => $recent['sizechange'],
371            ]);
372        }
373
374        return $changes;
375    }
376
377    /**
378     * Get a wiki page's syntax
379     *
380     * Returns the syntax of the given page. When no revision is given, the current revision is returned.
381     *
382     * A non-existing page (or revision) will return an empty string usually. For the current revision
383     * a page template will be returned if configured.
384     *
385     * Read access is required for the page.
386     *
387     * @param string $page wiki page id
388     * @param string $rev Revision timestamp to access an older revision
389     * @return string the syntax of the page
390     * @throws AccessDeniedException
391     * @throws RemoteException
392     */
393    public function getPage($page, $rev = '')
394    {
395        $page = $this->checkPage($page, false);
396
397        $text = rawWiki($page, $rev);
398        if (!$text && !$rev) {
399            return pageTemplate($page);
400        } else {
401            return $text;
402        }
403    }
404
405    /**
406     * Return a wiki page rendered to HTML
407     *
408     * The page is rendered to HTML as it would be in the wiki. The HTML consist only of the data for the page
409     * content itself, no surrounding structural tags, header, footers, sidebars etc are returned.
410     *
411     * References in the HTML are relative to the wiki base URL unless the `canonical` configuration is set.
412     *
413     * If the page does not exist, an error is returned.
414     *
415     * @link https://www.dokuwiki.org/config:canonical
416     * @param string $page page id
417     * @param string $rev revision timestamp
418     * @return string Rendered HTML for the page
419     * @throws AccessDeniedException
420     * @throws RemoteException
421     */
422    public function getPageHTML($page, $rev = '')
423    {
424        $page = $this->checkPage($page);
425
426        return (string)p_wiki_xhtml($page, $rev, false);
427    }
428
429    /**
430     * Return some basic data about a page
431     *
432     * The call will return an error if the requested page does not exist.
433     *
434     * Read access is required for the page.
435     *
436     * @param string $page page id
437     * @param string $rev revision timestamp
438     * @param bool $author whether to include the author information
439     * @param bool $hash whether to include the MD5 hash of the page content
440     * @return Page
441     * @throws AccessDeniedException
442     * @throws RemoteException
443     */
444    public function getPageInfo($page, $rev = '', $author = false, $hash = false)
445    {
446        $page = $this->checkPage($page);
447
448        $result = new Page(['id' => $page, 'rev' => $rev]);
449        if ($author) $result->retrieveAuthor();
450        if ($hash) $result->calculateHash();
451
452        return $result;
453    }
454
455    /**
456     * Returns a list of available revisions of a given wiki page
457     *
458     * The number of returned pages is set by `$conf['recent']`, but non accessible revisions pages
459     * are skipped, so less than that may be returned.
460     *
461     * @link https://www.dokuwiki.org/config:recent
462     * @param string $page page id
463     * @param int $first skip the first n changelog lines, 0 starts at the current revision
464     * @return PageRevision[]
465     * @throws AccessDeniedException
466     * @throws RemoteException
467     * @author Michael Klier <chi@chimeric.de>
468     */
469    public function getPageVersions($page, $first = 0)
470    {
471        global $conf;
472
473        $page = $this->checkPage($page, false);
474
475        $pagelog = new PageChangeLog($page);
476        $pagelog->setChunkSize(1024);
477        // old revisions are counted from 0, so we need to subtract 1 for the current one
478        $revisions = $pagelog->getRevisions($first - 1, $conf['recent']);
479
480        $result = [];
481        foreach ($revisions as $rev) {
482            if (!page_exists($page, $rev)) continue; // skip non-existing revisions
483            $info = $pagelog->getRevisionInfo($rev);
484
485            $result[] = new PageRevision([
486                'id' => $page,
487                'revision' => $rev,
488                'author' => $info['user'],
489                'ip' => $info['ip'],
490                'summary' => $info['sum'],
491                'type' => $info['type'],
492                'sizechange' => $info['sizechange'],
493            ]);
494        }
495
496        return $result;
497    }
498
499    /**
500     * Get a page's links
501     *
502     * This returns a list of links found in the given page. This includes internal, external and interwiki links
503     *
504     * Read access for the given page is needed and page has to exist.
505     *
506     * @param string $page page id
507     * @return Link[] A list of links found on the given page
508     * @throws AccessDeniedException
509     * @throws RemoteException
510     * @todo returning link titles would be a nice addition
511     * @todo hash handling seems not to be correct
512     * @author Michael Klier <chi@chimeric.de>
513     */
514    public function getPageLinks($page)
515    {
516        $page = $this->checkPage($page);
517
518        // resolve page instructions
519        $ins = p_cached_instructions(wikiFN($page));
520
521        // instantiate new Renderer - needed for interwiki links
522        $Renderer = new Doku_Renderer_xhtml();
523        $Renderer->interwiki = getInterwiki();
524
525        // parse instructions
526        $links = [];
527        foreach ($ins as $in) {
528            switch ($in[0]) {
529                case 'internallink':
530                    $links[] = new Link([
531                        'type' => 'local',
532                        'page' => $in[1][0],
533                        'href' => wl($in[1][0]),
534                    ]);
535                    break;
536                case 'externallink':
537                    $links[] = new Link([
538                        'type' => 'extern',
539                        'page' => $in[1][0],
540                        'href' => $in[1][0],
541                    ]);
542                    break;
543                case 'interwikilink':
544                    $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]);
545                    $links[] = new Link([
546                        'type' => 'interwiki',
547                        'page' => $in[1][0],
548                        'href' => $url,
549                    ]);
550                    break;
551            }
552        }
553
554        return ($links);
555    }
556
557    /**
558     * Get a page's backlinks
559     *
560     * A backlink is a wiki link on another page that links to the given page.
561     *
562     * Only links from pages readable by the current user are returned. The page itself
563     * needs to be readable. Otherwise an error is returned.
564     *
565     * @param string $page page id
566     * @return string[] A list of pages linking to the given page
567     * @throws AccessDeniedException
568     * @throws RemoteException
569     */
570    public function getPageBackLinks($page)
571    {
572        $page = $this->checkPage($page, false);
573
574        return ft_backlinks($page);
575    }
576
577    /**
578     * Lock the given set of pages
579     *
580     * This call will try to lock all given pages. It will return a list of pages that were
581     * successfully locked. If a page could not be locked, eg. because a different user is
582     * currently holding a lock, that page will be missing from the returned list.
583     *
584     * You should always ensure that the list of returned pages matches the given list of
585     * pages. It's up to you to decide how to handle failed locking.
586     *
587     * Note: you can only lock pages that you have write access for. It is possible to create
588     * a lock for a page that does not exist, yet.
589     *
590     * Note: it is not necessary to lock a page before saving it. The `savePage()` call will
591     * automatically lock and unlock the page for you. However if you plan to do related
592     * operations on multiple pages, locking them all at once beforehand can be useful.
593     *
594     * @param string[] $pages A list of pages to lock
595     * @return string[] A list of pages that were successfully locked
596     */
597    public function lockPages($pages)
598    {
599        $locked = [];
600
601        foreach ($pages as $id) {
602            $id = cleanID($id);
603            if ($id === '') continue;
604            if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) {
605                continue;
606            }
607            lock($id);
608            $locked[] = $id;
609        }
610        return $locked;
611    }
612
613    /**
614     * Unlock the given set of pages
615     *
616     * This call will try to unlock all given pages. It will return a list of pages that were
617     * successfully unlocked. If a page could not be unlocked, eg. because a different user is
618     * currently holding a lock, that page will be missing from the returned list.
619     *
620     * You should always ensure that the list of returned pages matches the given list of
621     * pages. It's up to you to decide how to handle failed unlocking.
622     *
623     * Note: you can only unlock pages that you have write access for.
624     *
625     * @param string[] $pages A list of pages to unlock
626     * @return string[] A list of pages that were successfully unlocked
627     */
628    public function unlockPages($pages)
629    {
630        $unlocked = [];
631
632        foreach ($pages as $id) {
633            $id = cleanID($id);
634            if ($id === '') continue;
635            if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) {
636                continue;
637            }
638            $unlocked[] = $id;
639        }
640
641        return $unlocked;
642    }
643
644    /**
645     * Save a wiki page
646     *
647     * Saves the given wiki text to the given page. If the page does not exist, it will be created.
648     * Just like in the wiki, saving an empty text will delete the page.
649     *
650     * You need write permissions for the given page and the page may not be locked by another user.
651     *
652     * @param string $page page id
653     * @param string $text wiki text
654     * @param string $summary edit summary
655     * @param bool $isminor whether this is a minor edit
656     * @return bool Returns true on success
657     * @throws AccessDeniedException no write access for page
658     * @throws RemoteException no id, empty new page or locked
659     * @author Michael Klier <chi@chimeric.de>
660     */
661    public function savePage($page, $text, $summary = '', $isminor = false)
662    {
663        global $TEXT;
664        global $lang;
665
666        $page = $this->checkPage($page, false, AUTH_EDIT);
667        $TEXT = cleanText($text);
668
669
670        if (!page_exists($page) && trim($TEXT) == '') {
671            throw new RemoteException('Refusing to write an empty new wiki page', 132);
672        }
673
674        // Check, if page is locked
675        if (checklock($page)) {
676            throw new RemoteException('The page is currently locked', 133);
677        }
678
679        // SPAM check
680        if (checkwordblock()) {
681            throw new RemoteException('The page content was blocked', 134);
682        }
683
684        // autoset summary on new pages
685        if (!page_exists($page) && empty($summary)) {
686            $summary = $lang['created'];
687        }
688
689        // autoset summary on deleted pages
690        if (page_exists($page) && empty($TEXT) && empty($summary)) {
691            $summary = $lang['deleted'];
692        }
693
694        // FIXME auto set a summary in other cases "API Edit" might be a good idea?
695
696        lock($page);
697        saveWikiText($page, $TEXT, $summary, $isminor);
698        unlock($page);
699
700        // run the indexer if page wasn't indexed yet
701        idx_addPage($page);
702
703        return true;
704    }
705
706    /**
707     * Appends text to the end of a wiki page
708     *
709     * If the page does not exist, it will be created. If a page template for the non-existant
710     * page is configured, the given text will appended to that template.
711     *
712     * The call will create a new page revision.
713     *
714     * You need write permissions for the given page.
715     *
716     * @param string $page page id
717     * @param string $text wiki text
718     * @param string $summary edit summary
719     * @param bool $isminor whether this is a minor edit
720     * @return bool Returns true on success
721     * @throws AccessDeniedException
722     * @throws RemoteException
723     */
724    public function appendPage($page, $text, $summary, $isminor)
725    {
726        $currentpage = $this->getPage($page);
727        if (!is_string($currentpage)) {
728            $currentpage = '';
729        }
730        return $this->savePage($page, $currentpage . $text, $summary, $isminor);
731    }
732
733    // endregion
734
735    // region media
736
737    /**
738     * List all media files in the given namespace (and below)
739     *
740     * Setting the `depth` to `0` and the `namespace` to `""` will return all media files in the wiki.
741     *
742     * When `pattern` is given, it needs to be a valid regular expression as understood by PHP's
743     * `preg_match()` including delimiters.
744     * The pattern is matched against the full media ID, including the namespace.
745     *
746     * @link https://www.php.net/manual/en/reference.pcre.pattern.syntax.php
747     * @param string $namespace The namespace to search. Empty string for root namespace
748     * @param string $pattern A regular expression to filter the returned files
749     * @param int $depth How deep to search. 0 for all subnamespaces
750     * @param bool $hash Whether to include a MD5 hash of the media content
751     * @return Media[]
752     * @throws AccessDeniedException no access to the media files
753     * @author Gina Haeussge <osd@foosel.net>
754     */
755    public function listMedia($namespace = '', $pattern = '', $depth = 1, $hash = false)
756    {
757        global $conf;
758
759        $namespace = cleanID($namespace);
760
761        $options = [
762            'skipacl' => 0,
763            'depth' => $depth,
764            'hash' => $hash,
765            'pattern' => $pattern,
766        ];
767
768        $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
769        $data = [];
770        search($data, $conf['mediadir'], 'search_media', $options, $dir);
771        return array_map(fn($item) => new Media($item), $data);
772    }
773
774    /**
775     * Get recent media changes
776     *
777     * Returns a list of recent changes to media files. The results can be limited to changes newer than
778     * a given timestamp.
779     *
780     * Only changes within the configured `$conf['recent']` range are returned. This is the default
781     * when no timestamp is given.
782     *
783     * @link https://www.dokuwiki.org/config:recent
784     * @param int $timestamp Only show changes newer than this unix timestamp
785     * @return MediaRevision[]
786     * @author Michael Klier <chi@chimeric.de>
787     * @author Michael Hamann <michael@content-space.de>
788     */
789    public function getRecentMediaChanges($timestamp = 0)
790    {
791
792        $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
793
794        $changes = [];
795        foreach ($recents as $recent) {
796            $changes[] = new MediaRevision([
797                'id' => $recent['id'],
798                'revision' => $recent['date'],
799                'author' => $recent['user'],
800                'ip' => $recent['ip'],
801                'summary' => $recent['sum'],
802                'type' => $recent['type'],
803                'sizechange' => $recent['sizechange'],
804            ]);
805        }
806
807        return $changes;
808    }
809
810    /**
811     * Get a media file's content
812     *
813     * Returns the content of the given media file. When no revision is given, the current revision is returned.
814     *
815     * @link https://en.wikipedia.org/wiki/Base64
816     * @param string $media file id
817     * @param int $rev revision timestamp
818     * @return string Base64 encoded media file contents
819     * @throws AccessDeniedException no permission for media
820     * @throws RemoteException not exist
821     * @author Gina Haeussge <osd@foosel.net>
822     *
823     */
824    public function getMedia($media, $rev = '')
825    {
826        $media = cleanID($media);
827        if (auth_quickaclcheck($media) < AUTH_READ) {
828            throw new AccessDeniedException('You are not allowed to read this media file', 211);
829        }
830
831        $file = mediaFN($media, $rev);
832        if (!@ file_exists($file)) {
833            throw new RemoteException('The requested media file does not exist', 221);
834        }
835
836        $data = io_readFile($file, false);
837        return base64_encode($data);
838    }
839
840    /**
841     * Return info about a media file
842     *
843     * The call will return an error if the requested media file does not exist.
844     *
845     * Read access is required for the media file.
846     *
847     * @param string $media file id
848     * @param int $rev revision timestamp
849     * @param bool $hash whether to include the MD5 hash of the media content
850     * @return Media
851     * @throws AccessDeniedException no permission for media
852     * @throws RemoteException if not exist
853     * @author Gina Haeussge <osd@foosel.net>
854     */
855    public function getMediaInfo($media, $rev = '', $hash = false)
856    {
857        $media = cleanID($media);
858        if (auth_quickaclcheck($media) < AUTH_READ) {
859            throw new AccessDeniedException('You are not allowed to read this media file', 211);
860        }
861        if (!media_exists($media, $rev)) {
862            throw new RemoteException('The requested media file does not exist', 221);
863        }
864
865        $file = mediaFN($media, $rev);
866
867        $info = new Media([
868            'id' => $media,
869            'mtime' => filemtime($file),
870            'size' => filesize($file),
871        ]);
872        if ($hash) $info->calculateHash();
873
874        return $info;
875    }
876
877    /**
878     * Uploads a file to the wiki
879     *
880     * The file data has to be passed as a base64 encoded string.
881     *
882     * @link https://en.wikipedia.org/wiki/Base64
883     * @param string $media media id
884     * @param string $base64 Base64 encoded file contents
885     * @param bool $overwrite Should an existing file be overwritten?
886     * @return bool Should always be true
887     * @throws RemoteException
888     * @author Michael Klier <chi@chimeric.de>
889     */
890    public function saveMedia($media, $base64, $overwrite = false)
891    {
892        $media = cleanID($media);
893        $auth = auth_quickaclcheck(getNS($media) . ':*');
894
895        if ($media === '') {
896            throw new RemoteException('Empty or invalid media ID given', 231);
897        }
898
899        // clean up base64 encoded data
900        $base64 = strtr($base64, [
901            "\n" => '', // strip newlines
902            "\r" => '', // strip carriage returns
903            '-' => '+', // RFC4648 base64url
904            '_' => '/', // RFC4648 base64url
905            ' ' => '+', // JavaScript data uri
906        ]);
907
908        $data = base64_decode($base64, true);
909        if ($data === false) {
910            throw new RemoteException('Invalid base64 encoded data', 234);
911        }
912
913        // save temporary file
914        global $conf;
915        $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP());
916        @unlink($ftmp);
917        io_saveFile($ftmp, $data);
918
919        $res = media_save(['name' => $ftmp], $media, $overwrite, $auth, 'rename');
920        if (is_array($res)) {
921            throw new RemoteException('Failed to save media: ' . $res[0], 235);
922        }
923        return (bool)$res; // should always be true at this point
924    }
925
926    /**
927     * Deletes a file from the wiki
928     *
929     * You need to have delete permissions for the file.
930     *
931     * @param string $media media id
932     * @return bool Should always be true
933     * @throws AccessDeniedException no permissions
934     * @throws RemoteException file in use or not deleted
935     * @author Gina Haeussge <osd@foosel.net>
936     *
937     */
938    public function deleteMedia($media)
939    {
940        $media = cleanID($media);
941        $auth = auth_quickaclcheck($media);
942        $res = media_delete($media, $auth);
943        if ($res & DOKU_MEDIA_DELETED) {
944            return true;
945        } elseif ($res & DOKU_MEDIA_NOT_AUTH) {
946            throw new AccessDeniedException('You are not allowed to delete this media file', 212);
947        } elseif ($res & DOKU_MEDIA_INUSE) {
948            throw new RemoteException('Media file is still referenced', 232);
949        } else {
950            throw new RemoteException('Failed to delete media file', 233);
951        }
952    }
953
954    // endregion
955
956
957    /**
958     * Convenience method for page checks
959     *
960     * This method will perform multiple tasks:
961     *
962     * - clean the given page id
963     * - disallow an empty page id
964     * - check if the page exists (unless disabled)
965     * - check if the user has the required access level (pass AUTH_NONE to skip)
966     *
967     * @param string $id page id
968     * @return string the cleaned page id
969     * @throws RemoteException
970     * @throws AccessDeniedException
971     */
972    private function checkPage($id, $existCheck = true, $minAccess = AUTH_READ)
973    {
974        $id = cleanID($id);
975        if ($id === '') {
976            throw new RemoteException('Empty or invalid page ID given', 131);
977        }
978
979        if ($existCheck && !page_exists($id)) {
980            throw new RemoteException('The requested page does not exist', 121);
981        }
982
983        if ($minAccess && auth_quickaclcheck($id) < $minAccess) {
984            throw new AccessDeniedException('You are not allowed to read this page', 111);
985        }
986
987        return $id;
988    }
989}
990