xref: /dokuwiki/inc/Remote/ApiCore.php (revision 902647e630bb637c62651241b186d7b5d5d810e2)
1dd87735dSAndreas Gohr<?php
2dd87735dSAndreas Gohr
3dd87735dSAndreas Gohrnamespace dokuwiki\Remote;
4dd87735dSAndreas Gohr
5dd87735dSAndreas Gohruse Doku_Renderer_xhtml;
60c3a5702SAndreas Gohruse dokuwiki\ChangeLog\PageChangeLog;
7104a3b7cSAndreas Gohruse dokuwiki\Extension\AuthPlugin;
8cbb44eabSAndreas Gohruse dokuwiki\Extension\Event;
96cce3332SAndreas Gohruse dokuwiki\Remote\Response\Link;
106cce3332SAndreas Gohruse dokuwiki\Remote\Response\Media;
116cce3332SAndreas Gohruse dokuwiki\Remote\Response\MediaRevision;
128ddd9b69SAndreas Gohruse dokuwiki\Remote\Response\Page;
136cce3332SAndreas Gohruse dokuwiki\Remote\Response\PageHit;
146cce3332SAndreas Gohruse dokuwiki\Remote\Response\PageRevision;
156cce3332SAndreas Gohruse dokuwiki\Remote\Response\User;
162d85e841SAndreas Gohruse dokuwiki\Utf8\Sort;
17dd87735dSAndreas Gohr
18dd87735dSAndreas Gohr/**
19dd87735dSAndreas Gohr * Provides the core methods for the remote API.
20dd87735dSAndreas Gohr * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
21dd87735dSAndreas Gohr */
22dd87735dSAndreas Gohrclass ApiCore
23dd87735dSAndreas Gohr{
24dd87735dSAndreas Gohr    /** @var int Increased whenever the API is changed */
256cce3332SAndreas Gohr    public const API_VERSION = 12;
26dd87735dSAndreas Gohr
27dd87735dSAndreas Gohr    /**
28dd87735dSAndreas Gohr     * Returns details about the core methods
29dd87735dSAndreas Gohr     *
30dd87735dSAndreas Gohr     * @return array
31dd87735dSAndreas Gohr     */
326cce3332SAndreas Gohr    public function getMethods()
33dd87735dSAndreas Gohr    {
34104a3b7cSAndreas Gohr        return [
356cce3332SAndreas Gohr            'core.getAPIVersion' => (new ApiCall([$this, 'getAPIVersion'], 'info'))->setPublic(),
366cce3332SAndreas Gohr
376cce3332SAndreas Gohr            'core.getWikiVersion' => new ApiCall('getVersion', 'info'),
386cce3332SAndreas Gohr            'core.getWikiTitle' => (new ApiCall([$this, 'getWikiTitle'], 'info'))->setPublic(),
396cce3332SAndreas Gohr            'core.getWikiTime' => (new ApiCall([$this, 'getWikiTime'], 'info')),
406cce3332SAndreas Gohr
416cce3332SAndreas Gohr            'core.login' => (new ApiCall([$this, 'login'], 'user'))->setPublic(),
426cce3332SAndreas Gohr            'core.logoff' => new ApiCall([$this, 'logoff'], 'user'),
436cce3332SAndreas Gohr            'core.whoAmI' => (new ApiCall([$this, 'whoAmI'], 'user')),
446cce3332SAndreas Gohr            'core.aclCheck' => new ApiCall([$this, 'aclCheck'], 'user'),
456cce3332SAndreas Gohr
466cce3332SAndreas Gohr            'core.listPages' => new ApiCall([$this, 'listPages'], 'pages'),
476cce3332SAndreas Gohr            'core.searchPages' => new ApiCall([$this, 'searchPages'], 'pages'),
486cce3332SAndreas Gohr            'core.getRecentPageChanges' => new ApiCall([$this, 'getRecentPageChanges'], 'pages'),
496cce3332SAndreas Gohr
506cce3332SAndreas Gohr            'core.getPage' => (new ApiCall([$this, 'getPage'], 'pages')),
516cce3332SAndreas Gohr            'core.getPageHTML' => (new ApiCall([$this, 'getPageHTML'], 'pages')),
526cce3332SAndreas Gohr            'core.getPageInfo' => (new ApiCall([$this, 'getPageInfo'], 'pages')),
536cce3332SAndreas Gohr            'core.getPageVersions' => new ApiCall([$this, 'getPageVersions'], 'pages'),
546cce3332SAndreas Gohr            'core.getPageLinks' => new ApiCall([$this, 'getPageLinks'], 'pages'),
556cce3332SAndreas Gohr            'core.getPageBackLinks' => new ApiCall([$this, 'getPageBackLinks'], 'pages'),
566cce3332SAndreas Gohr
576cce3332SAndreas Gohr            'core.lockPages' => new ApiCall([$this, 'lockPages'], 'pages'),
586cce3332SAndreas Gohr            'core.unlockPages' => new ApiCall([$this, 'unlockPages'], 'pages'),
596cce3332SAndreas Gohr            'core.savePage' => new ApiCall([$this, 'savePage'], 'pages'),
606cce3332SAndreas Gohr            'core.appendPage' => new ApiCall([$this, 'appendPage'], 'pages'),
616cce3332SAndreas Gohr
626cce3332SAndreas Gohr            'core.listMedia' => new ApiCall([$this, 'listMedia'], 'media'),
636cce3332SAndreas Gohr            // todo: implement searchMedia
646cce3332SAndreas Gohr            'core.getRecentMediaChanges' => new ApiCall([$this, 'getRecentMediaChanges'], 'media'),
656cce3332SAndreas Gohr
666cce3332SAndreas Gohr            'core.getMedia' => new ApiCall([$this, 'getMedia'], 'media'),
676cce3332SAndreas Gohr            'core.getMediaInfo' => new ApiCall([$this, 'getMediaInfo'], 'media'),
686cce3332SAndreas Gohr            // todo: implement getMediaVersions
696cce3332SAndreas Gohr            // todo: implement getMediaUsage
706cce3332SAndreas Gohr
716cce3332SAndreas Gohr            'core.saveMedia' => new ApiCall([$this, 'saveMedia'], 'media'),
726cce3332SAndreas Gohr            'core.deleteMedia' => new ApiCall([$this, 'deleteMedia'], 'media'),
73104a3b7cSAndreas Gohr        ];
74dd87735dSAndreas Gohr    }
75dd87735dSAndreas Gohr
766cce3332SAndreas Gohr    // region info
77dd87735dSAndreas Gohr
78dd87735dSAndreas Gohr    /**
798a9282a2SAndreas Gohr     * Return the API version
808a9282a2SAndreas Gohr     *
818a9282a2SAndreas Gohr     * This is the version of the DokuWiki API. It increases whenever the API definition changes.
828a9282a2SAndreas Gohr     *
838a9282a2SAndreas Gohr     * When developing a client, you should check this version and make sure you can handle it.
84dd87735dSAndreas Gohr     *
85dd87735dSAndreas Gohr     * @return int
86dd87735dSAndreas Gohr     */
87dd87735dSAndreas Gohr    public function getAPIVersion()
88dd87735dSAndreas Gohr    {
89dd87735dSAndreas Gohr        return self::API_VERSION;
90dd87735dSAndreas Gohr    }
91dd87735dSAndreas Gohr
92dd87735dSAndreas Gohr    /**
936cce3332SAndreas Gohr     * Returns the wiki title
946cce3332SAndreas Gohr     *
956cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:title
966cce3332SAndreas Gohr     * @return string
976cce3332SAndreas Gohr     */
986cce3332SAndreas Gohr    public function getWikiTitle()
996cce3332SAndreas Gohr    {
1006cce3332SAndreas Gohr        global $conf;
1016cce3332SAndreas Gohr        return $conf['title'];
1026cce3332SAndreas Gohr    }
1036cce3332SAndreas Gohr
1046cce3332SAndreas Gohr    /**
1056cce3332SAndreas Gohr     * Return the current server time
1066cce3332SAndreas Gohr     *
1076cce3332SAndreas Gohr     * Returns a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC).
1086cce3332SAndreas Gohr     *
1096cce3332SAndreas Gohr     * You can use this to compensate for differences between your client's time and the
1106cce3332SAndreas Gohr     * server's time when working with last modified timestamps (revisions).
1116cce3332SAndreas Gohr     *
1126cce3332SAndreas Gohr     * @return int A unix timestamp
1136cce3332SAndreas Gohr     */
1146cce3332SAndreas Gohr    public function getWikiTime()
1156cce3332SAndreas Gohr    {
1166cce3332SAndreas Gohr        return time();
1176cce3332SAndreas Gohr    }
1186cce3332SAndreas Gohr
1196cce3332SAndreas Gohr    // endregion
1206cce3332SAndreas Gohr
1216cce3332SAndreas Gohr    // region user
1226cce3332SAndreas Gohr
1236cce3332SAndreas Gohr    /**
124dd87735dSAndreas Gohr     * Login
125dd87735dSAndreas Gohr     *
1268a9282a2SAndreas Gohr     * This will use the given credentials and attempt to login the user. This will set the
1278a9282a2SAndreas Gohr     * appropriate cookies, which can be used for subsequent requests.
1288a9282a2SAndreas Gohr     *
129fe9f11e2SAndreas Gohr     * Use of this mechanism is discouraged. Using token authentication is preferred.
130fe9f11e2SAndreas Gohr     *
1318a9282a2SAndreas Gohr     * @param string $user The user name
1328a9282a2SAndreas Gohr     * @param string $pass The password
133fe9f11e2SAndreas Gohr     * @return int If the login was successful
134dd87735dSAndreas Gohr     */
135dd87735dSAndreas Gohr    public function login($user, $pass)
136dd87735dSAndreas Gohr    {
137dd87735dSAndreas Gohr        global $conf;
138104a3b7cSAndreas Gohr        /** @var AuthPlugin $auth */
139dd87735dSAndreas Gohr        global $auth;
140dd87735dSAndreas Gohr
141dd87735dSAndreas Gohr        if (!$conf['useacl']) return 0;
1426547cfc7SGerrit Uitslag        if (!$auth instanceof AuthPlugin) return 0;
143dd87735dSAndreas Gohr
144dd87735dSAndreas Gohr        @session_start(); // reopen session for login
14581e99965SPhy        $ok = null;
146dd87735dSAndreas Gohr        if ($auth->canDo('external')) {
147dd87735dSAndreas Gohr            $ok = $auth->trustExternal($user, $pass, false);
14881e99965SPhy        }
14981e99965SPhy        if ($ok === null) {
150104a3b7cSAndreas Gohr            $evdata = [
151dd87735dSAndreas Gohr                'user' => $user,
152dd87735dSAndreas Gohr                'password' => $pass,
153dd87735dSAndreas Gohr                'sticky' => false,
154104a3b7cSAndreas Gohr                'silent' => true
155104a3b7cSAndreas Gohr            ];
156cbb44eabSAndreas Gohr            $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
157dd87735dSAndreas Gohr        }
158dd87735dSAndreas Gohr        session_write_close(); // we're done with the session
159dd87735dSAndreas Gohr
160dd87735dSAndreas Gohr        return $ok;
161dd87735dSAndreas Gohr    }
162dd87735dSAndreas Gohr
163dd87735dSAndreas Gohr    /**
164dd87735dSAndreas Gohr     * Log off
165dd87735dSAndreas Gohr     *
1668a9282a2SAndreas Gohr     * Attempt to log out the current user, deleting the appropriate cookies
1678a9282a2SAndreas Gohr     *
1686cce3332SAndreas Gohr     * Use of this mechanism is discouraged. Using token authentication is preferred.
1696cce3332SAndreas Gohr     *
1708a9282a2SAndreas Gohr     * @return int 0 on failure, 1 on success
171dd87735dSAndreas Gohr     */
172dd87735dSAndreas Gohr    public function logoff()
173dd87735dSAndreas Gohr    {
174dd87735dSAndreas Gohr        global $conf;
175dd87735dSAndreas Gohr        global $auth;
176dd87735dSAndreas Gohr        if (!$conf['useacl']) return 0;
1776547cfc7SGerrit Uitslag        if (!$auth instanceof AuthPlugin) return 0;
178dd87735dSAndreas Gohr
179dd87735dSAndreas Gohr        auth_logoff();
180dd87735dSAndreas Gohr
181dd87735dSAndreas Gohr        return 1;
182dd87735dSAndreas Gohr    }
183dd87735dSAndreas Gohr
184dd87735dSAndreas Gohr    /**
1856cce3332SAndreas Gohr     * Info about the currently authenticated user
1866cce3332SAndreas Gohr     *
1876cce3332SAndreas Gohr     * @return User
1886cce3332SAndreas Gohr     */
1896cce3332SAndreas Gohr    public function whoAmI()
1906cce3332SAndreas Gohr    {
1916cce3332SAndreas Gohr        return new User([]);
1926cce3332SAndreas Gohr    }
1936cce3332SAndreas Gohr
1946cce3332SAndreas Gohr    /**
1956cce3332SAndreas Gohr     * Check ACL Permissions
1966cce3332SAndreas Gohr     *
1976cce3332SAndreas Gohr     * This call allows to check the permissions for a given page/media and user/group combination.
1986cce3332SAndreas Gohr     * If no user/group is given, the current user is used.
1996cce3332SAndreas Gohr     *
2006cce3332SAndreas Gohr     * Read the link below to learn more about the permission levels.
2016cce3332SAndreas Gohr     *
2026cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/acl#background_info
2036cce3332SAndreas Gohr     * @param string $page A page or media ID
2046cce3332SAndreas Gohr     * @param string $user username
2056cce3332SAndreas Gohr     * @param string[] $groups array of groups
2066cce3332SAndreas Gohr     * @return int permission level
207*902647e6SAndreas Gohr     * @throws RemoteException
2086cce3332SAndreas Gohr     */
2096cce3332SAndreas Gohr    public function aclCheck($page, $user = '', $groups = [])
2106cce3332SAndreas Gohr    {
2116cce3332SAndreas Gohr        /** @var AuthPlugin $auth */
2126cce3332SAndreas Gohr        global $auth;
2136cce3332SAndreas Gohr
214*902647e6SAndreas Gohr        $page = $this->checkPage($page, false, AUTH_NONE);
215*902647e6SAndreas Gohr
2166cce3332SAndreas Gohr        if ($user === '') {
2176cce3332SAndreas Gohr            return auth_quickaclcheck($page);
2186cce3332SAndreas Gohr        } else {
2196cce3332SAndreas Gohr            if ($groups === []) {
2206cce3332SAndreas Gohr                $userinfo = $auth->getUserData($user);
2216cce3332SAndreas Gohr                if ($userinfo === false) {
2226cce3332SAndreas Gohr                    $groups = [];
2236cce3332SAndreas Gohr                } else {
2246cce3332SAndreas Gohr                    $groups = $userinfo['grps'];
2256cce3332SAndreas Gohr                }
2266cce3332SAndreas Gohr            }
2276cce3332SAndreas Gohr            return auth_aclcheck($page, $user, $groups);
2286cce3332SAndreas Gohr        }
2296cce3332SAndreas Gohr    }
2306cce3332SAndreas Gohr
2316cce3332SAndreas Gohr    // endregion
2326cce3332SAndreas Gohr
2336cce3332SAndreas Gohr    // region pages
2346cce3332SAndreas Gohr
2356cce3332SAndreas Gohr    /**
2366cce3332SAndreas Gohr     * List all pages in the given namespace (and below)
2376cce3332SAndreas Gohr     *
2386cce3332SAndreas Gohr     * Setting the `depth` to `0` and the `namespace` to `""` will return all pages in the wiki.
2396cce3332SAndreas Gohr     *
2406cce3332SAndreas Gohr     * @param string $namespace The namespace to search. Empty string for root namespace
2416cce3332SAndreas Gohr     * @param int $depth How deep to search. 0 for all subnamespaces
2426cce3332SAndreas Gohr     * @param bool $hash Whether to include a MD5 hash of the page content
2436cce3332SAndreas Gohr     * @return Page[] A list of matching pages
2446cce3332SAndreas Gohr     */
2456cce3332SAndreas Gohr    public function listPages($namespace = '', $depth = 1, $hash = false)
2466cce3332SAndreas Gohr    {
2476cce3332SAndreas Gohr        global $conf;
2486cce3332SAndreas Gohr
2496cce3332SAndreas Gohr        $namespace = cleanID($namespace);
2506cce3332SAndreas Gohr
2516cce3332SAndreas Gohr        // shortcut for all pages
2526cce3332SAndreas Gohr        if ($namespace === '' && $depth === 0) {
2536cce3332SAndreas Gohr            return $this->getAllPages($hash);
2546cce3332SAndreas Gohr        }
2556cce3332SAndreas Gohr
2566cce3332SAndreas Gohr        // run our search iterator to get the pages
2576cce3332SAndreas Gohr        $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
2586cce3332SAndreas Gohr        $data = [];
2596cce3332SAndreas Gohr        $opts['skipacl'] = 0;
2606cce3332SAndreas Gohr        $opts['depth'] = $depth; // FIXME depth needs to be calculated relative to $dir
2616cce3332SAndreas Gohr        $opts['hash'] = $hash;
2626cce3332SAndreas Gohr        search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
2636cce3332SAndreas Gohr
2646cce3332SAndreas Gohr        return array_map(fn($item) => new Page($item), $data);
2656cce3332SAndreas Gohr    }
2666cce3332SAndreas Gohr
2676cce3332SAndreas Gohr    /**
2686cce3332SAndreas Gohr     * Get all pages at once
2696cce3332SAndreas Gohr     *
2706cce3332SAndreas Gohr     * This is uses the page index and is quicker than iterating which is done in listPages()
2716cce3332SAndreas Gohr     *
2726cce3332SAndreas Gohr     * @return Page[] A list of all pages
2736cce3332SAndreas Gohr     * @see listPages()
2746cce3332SAndreas Gohr     */
2756cce3332SAndreas Gohr    protected function getAllPages($hash = false)
2766cce3332SAndreas Gohr    {
2776cce3332SAndreas Gohr        $list = [];
2786cce3332SAndreas Gohr        $pages = idx_get_indexer()->getPages();
2796cce3332SAndreas Gohr        Sort::ksort($pages);
2806cce3332SAndreas Gohr
2816cce3332SAndreas Gohr        foreach (array_keys($pages) as $idx) {
2826cce3332SAndreas Gohr            $perm = auth_quickaclcheck($pages[$idx]);
2836cce3332SAndreas Gohr            if ($perm < AUTH_READ || isHiddenPage($pages[$idx]) || !page_exists($pages[$idx])) {
2846cce3332SAndreas Gohr                continue;
2856cce3332SAndreas Gohr            }
2866cce3332SAndreas Gohr
2876cce3332SAndreas Gohr            $page = new Page([
2886cce3332SAndreas Gohr                'id' => $pages[$idx],
2896cce3332SAndreas Gohr                'perm' => $perm,
2906cce3332SAndreas Gohr            ]);
2916cce3332SAndreas Gohr            if ($hash) $page->calculateHash();
2926cce3332SAndreas Gohr
2936cce3332SAndreas Gohr            $list[] = $page;
2946cce3332SAndreas Gohr        }
2956cce3332SAndreas Gohr
2966cce3332SAndreas Gohr        return $list;
2976cce3332SAndreas Gohr    }
2986cce3332SAndreas Gohr
2996cce3332SAndreas Gohr    /**
3006cce3332SAndreas Gohr     * Do a fulltext search
3016cce3332SAndreas Gohr     *
3026cce3332SAndreas Gohr     * This executes a full text search and returns the results. The query uses the standard
3036cce3332SAndreas Gohr     * DokuWiki search syntax.
3046cce3332SAndreas Gohr     *
3056cce3332SAndreas Gohr     * Snippets are provided for the first 15 results only. The title is either the first heading
3066cce3332SAndreas Gohr     * or the page id depending on the wiki's configuration.
3076cce3332SAndreas Gohr     *
3086cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/search#syntax
3096cce3332SAndreas Gohr     * @param string $query The search query as supported by the DokuWiki search
3106cce3332SAndreas Gohr     * @return PageHit[] A list of matching pages
3116cce3332SAndreas Gohr     */
3126cce3332SAndreas Gohr    public function searchPages($query)
3136cce3332SAndreas Gohr    {
3146cce3332SAndreas Gohr        $regex = [];
3156cce3332SAndreas Gohr        $data = ft_pageSearch($query, $regex);
3166cce3332SAndreas Gohr        $pages = [];
3176cce3332SAndreas Gohr
3186cce3332SAndreas Gohr        // prepare additional data
3196cce3332SAndreas Gohr        $idx = 0;
3206cce3332SAndreas Gohr        foreach ($data as $id => $score) {
3216cce3332SAndreas Gohr            $file = wikiFN($id);
3226cce3332SAndreas Gohr
3236cce3332SAndreas Gohr            if ($idx < FT_SNIPPET_NUMBER) {
3246cce3332SAndreas Gohr                $snippet = ft_snippet($id, $regex);
3256cce3332SAndreas Gohr                $idx++;
3266cce3332SAndreas Gohr            } else {
3276cce3332SAndreas Gohr                $snippet = '';
3286cce3332SAndreas Gohr            }
3296cce3332SAndreas Gohr
3306cce3332SAndreas Gohr            $pages[] = new PageHit([
3316cce3332SAndreas Gohr                'id' => $id,
3326cce3332SAndreas Gohr                'score' => (int)$score,
3336cce3332SAndreas Gohr                'rev' => filemtime($file),
3346cce3332SAndreas Gohr                'mtime' => filemtime($file),
3356cce3332SAndreas Gohr                'size' => filesize($file),
3366cce3332SAndreas Gohr                'snippet' => $snippet,
3376cce3332SAndreas Gohr                'title' => useHeading('navigation') ? p_get_first_heading($id) : $id
3386cce3332SAndreas Gohr            ]);
3396cce3332SAndreas Gohr        }
3406cce3332SAndreas Gohr        return $pages;
3416cce3332SAndreas Gohr    }
3426cce3332SAndreas Gohr
3436cce3332SAndreas Gohr    /**
3446cce3332SAndreas Gohr     * Get recent page changes
3456cce3332SAndreas Gohr     *
3466cce3332SAndreas Gohr     * Returns a list of recent changes to wiki pages. The results can be limited to changes newer than
3476cce3332SAndreas Gohr     * a given timestamp.
3486cce3332SAndreas Gohr     *
3496cce3332SAndreas Gohr     * Only changes within the configured `$conf['recent']` range are returned. This is the default
3506cce3332SAndreas Gohr     * when no timestamp is given.
3516cce3332SAndreas Gohr     *
3526cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:recent
3536cce3332SAndreas Gohr     * @param int $timestamp Only show changes newer than this unix timestamp
3546cce3332SAndreas Gohr     * @return PageRevision[]
3556cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
3566cce3332SAndreas Gohr     * @author Michael Hamann <michael@content-space.de>
3576cce3332SAndreas Gohr     */
3586cce3332SAndreas Gohr    public function getRecentPageChanges($timestamp = 0)
3596cce3332SAndreas Gohr    {
3606cce3332SAndreas Gohr        $recents = getRecentsSince($timestamp);
3616cce3332SAndreas Gohr
3626cce3332SAndreas Gohr        $changes = [];
3636cce3332SAndreas Gohr        foreach ($recents as $recent) {
3646cce3332SAndreas Gohr            $changes[] = new PageRevision([
3656cce3332SAndreas Gohr                'id' => $recent['id'],
3666cce3332SAndreas Gohr                'revision' => $recent['date'],
3676cce3332SAndreas Gohr                'author' => $recent['user'],
3686cce3332SAndreas Gohr                'ip' => $recent['ip'],
3696cce3332SAndreas Gohr                'summary' => $recent['sum'],
3706cce3332SAndreas Gohr                'type' => $recent['type'],
3716cce3332SAndreas Gohr                'sizechange' => $recent['sizechange'],
3726cce3332SAndreas Gohr            ]);
3736cce3332SAndreas Gohr        }
3746cce3332SAndreas Gohr
3756cce3332SAndreas Gohr        return $changes;
3766cce3332SAndreas Gohr    }
3776cce3332SAndreas Gohr
3786cce3332SAndreas Gohr    /**
3796cce3332SAndreas Gohr     * Get a wiki page's syntax
3806cce3332SAndreas Gohr     *
3816cce3332SAndreas Gohr     * Returns the syntax of the given page. When no revision is given, the current revision is returned.
3826cce3332SAndreas Gohr     *
3836cce3332SAndreas Gohr     * A non-existing page (or revision) will return an empty string usually. For the current revision
3846cce3332SAndreas Gohr     * a page template will be returned if configured.
3856cce3332SAndreas Gohr     *
3866cce3332SAndreas Gohr     * Read access is required for the page.
3876cce3332SAndreas Gohr     *
3886cce3332SAndreas Gohr     * @param string $page wiki page id
389*902647e6SAndreas Gohr     * @param string $rev Revision timestamp to access an older revision
3906cce3332SAndreas Gohr     * @return string the syntax of the page
391*902647e6SAndreas Gohr     * @throws AccessDeniedException
392*902647e6SAndreas Gohr     * @throws RemoteException
3936cce3332SAndreas Gohr     */
3946cce3332SAndreas Gohr    public function getPage($page, $rev = '')
3956cce3332SAndreas Gohr    {
396*902647e6SAndreas Gohr        $page = $this->checkPage($page, false);
397*902647e6SAndreas Gohr
3986cce3332SAndreas Gohr        $text = rawWiki($page, $rev);
3996cce3332SAndreas Gohr        if (!$text && !$rev) {
4006cce3332SAndreas Gohr            return pageTemplate($page);
4016cce3332SAndreas Gohr        } else {
4026cce3332SAndreas Gohr            return $text;
4036cce3332SAndreas Gohr        }
4046cce3332SAndreas Gohr    }
4056cce3332SAndreas Gohr
4066cce3332SAndreas Gohr    /**
4076cce3332SAndreas Gohr     * Return a wiki page rendered to HTML
4086cce3332SAndreas Gohr     *
4096cce3332SAndreas Gohr     * The page is rendered to HTML as it would be in the wiki. The HTML consist only of the data for the page
4106cce3332SAndreas Gohr     * content itself, no surrounding structural tags, header, footers, sidebars etc are returned.
4116cce3332SAndreas Gohr     *
4126cce3332SAndreas Gohr     * References in the HTML are relative to the wiki base URL unless the `canonical` configuration is set.
4136cce3332SAndreas Gohr     *
414*902647e6SAndreas Gohr     * If the page does not exist, an error is returned.
4156cce3332SAndreas Gohr     *
4166cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:canonical
4176cce3332SAndreas Gohr     * @param string $page page id
418*902647e6SAndreas Gohr     * @param string $rev revision timestamp
4196cce3332SAndreas Gohr     * @return string Rendered HTML for the page
420*902647e6SAndreas Gohr     * @throws AccessDeniedException
421*902647e6SAndreas Gohr     * @throws RemoteException
4226cce3332SAndreas Gohr     */
4236cce3332SAndreas Gohr    public function getPageHTML($page, $rev = '')
4246cce3332SAndreas Gohr    {
425*902647e6SAndreas Gohr        $page = $this->checkPage($page);
426*902647e6SAndreas Gohr
4276cce3332SAndreas Gohr        return (string)p_wiki_xhtml($page, $rev, false);
4286cce3332SAndreas Gohr    }
4296cce3332SAndreas Gohr
4306cce3332SAndreas Gohr    /**
4316cce3332SAndreas Gohr     * Return some basic data about a page
4326cce3332SAndreas Gohr     *
4336cce3332SAndreas Gohr     * The call will return an error if the requested page does not exist.
4346cce3332SAndreas Gohr     *
4356cce3332SAndreas Gohr     * Read access is required for the page.
4366cce3332SAndreas Gohr     *
4376cce3332SAndreas Gohr     * @param string $page page id
438*902647e6SAndreas Gohr     * @param string $rev revision timestamp
4396cce3332SAndreas Gohr     * @param bool $author whether to include the author information
4406cce3332SAndreas Gohr     * @param bool $hash whether to include the MD5 hash of the page content
4416cce3332SAndreas Gohr     * @return Page
442*902647e6SAndreas Gohr     * @throws AccessDeniedException
443*902647e6SAndreas Gohr     * @throws RemoteException
4446cce3332SAndreas Gohr     */
4456cce3332SAndreas Gohr    public function getPageInfo($page, $rev = '', $author = false, $hash = false)
4466cce3332SAndreas Gohr    {
447*902647e6SAndreas Gohr        $page = $this->checkPage($page);
4486cce3332SAndreas Gohr
4496cce3332SAndreas Gohr        $result = new Page(['id' => $page, 'rev' => $rev]);
4506cce3332SAndreas Gohr        if ($author) $result->retrieveAuthor();
4516cce3332SAndreas Gohr        if ($hash) $result->calculateHash();
4526cce3332SAndreas Gohr
4536cce3332SAndreas Gohr        return $result;
4546cce3332SAndreas Gohr    }
4556cce3332SAndreas Gohr
4566cce3332SAndreas Gohr    /**
4576cce3332SAndreas Gohr     * Returns a list of available revisions of a given wiki page
4586cce3332SAndreas Gohr     *
4596cce3332SAndreas Gohr     * The number of returned pages is set by `$conf['recent']`, but non accessible revisions pages
4606cce3332SAndreas Gohr     * are skipped, so less than that may be returned.
4616cce3332SAndreas Gohr     *
4626cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:recent
4636cce3332SAndreas Gohr     * @param string $page page id
4646cce3332SAndreas Gohr     * @param int $first skip the first n changelog lines, 0 starts at the current revision
4656cce3332SAndreas Gohr     * @return PageRevision[]
466*902647e6SAndreas Gohr     * @throws AccessDeniedException
467*902647e6SAndreas Gohr     * @throws RemoteException
4686cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
4696cce3332SAndreas Gohr     */
4706cce3332SAndreas Gohr    public function getPageVersions($page, $first = 0)
4716cce3332SAndreas Gohr    {
4726cce3332SAndreas Gohr        global $conf;
4736cce3332SAndreas Gohr
474*902647e6SAndreas Gohr        $page = $this->checkPage($page, false);
4756cce3332SAndreas Gohr
4766cce3332SAndreas Gohr        $pagelog = new PageChangeLog($page);
4776cce3332SAndreas Gohr        $pagelog->setChunkSize(1024);
4786cce3332SAndreas Gohr        // old revisions are counted from 0, so we need to subtract 1 for the current one
4796cce3332SAndreas Gohr        $revisions = $pagelog->getRevisions($first - 1, $conf['recent']);
4806cce3332SAndreas Gohr
4816cce3332SAndreas Gohr        $result = [];
4826cce3332SAndreas Gohr        foreach ($revisions as $rev) {
4836cce3332SAndreas Gohr            if (!page_exists($page, $rev)) continue; // skip non-existing revisions
4846cce3332SAndreas Gohr            $info = $pagelog->getRevisionInfo($rev);
4856cce3332SAndreas Gohr
4866cce3332SAndreas Gohr            $result[] = new PageRevision([
4876cce3332SAndreas Gohr                'id' => $page,
4886cce3332SAndreas Gohr                'revision' => $rev,
4896cce3332SAndreas Gohr                'author' => $info['user'],
4906cce3332SAndreas Gohr                'ip' => $info['ip'],
4916cce3332SAndreas Gohr                'summary' => $info['sum'],
4926cce3332SAndreas Gohr                'type' => $info['type'],
4936cce3332SAndreas Gohr                'sizechange' => $info['sizechange'],
4946cce3332SAndreas Gohr            ]);
4956cce3332SAndreas Gohr        }
4966cce3332SAndreas Gohr
4976cce3332SAndreas Gohr        return $result;
4986cce3332SAndreas Gohr    }
4996cce3332SAndreas Gohr
5006cce3332SAndreas Gohr    /**
5016cce3332SAndreas Gohr     * Get a page's links
5026cce3332SAndreas Gohr     *
5036cce3332SAndreas Gohr     * This returns a list of links found in the given page. This includes internal, external and interwiki links
5046cce3332SAndreas Gohr     *
505*902647e6SAndreas Gohr     * Read access for the given page is needed and page has to exist.
5066cce3332SAndreas Gohr     *
5076cce3332SAndreas Gohr     * @param string $page page id
5086cce3332SAndreas Gohr     * @return Link[] A list of links found on the given page
509*902647e6SAndreas Gohr     * @throws AccessDeniedException
510*902647e6SAndreas Gohr     * @throws RemoteException
5116cce3332SAndreas Gohr     * @todo returning link titles would be a nice addition
5126cce3332SAndreas Gohr     * @todo hash handling seems not to be correct
513*902647e6SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
5146cce3332SAndreas Gohr     */
5156cce3332SAndreas Gohr    public function getPageLinks($page)
5166cce3332SAndreas Gohr    {
517*902647e6SAndreas Gohr        $page = $this->checkPage($page);
5186cce3332SAndreas Gohr
5196cce3332SAndreas Gohr        // resolve page instructions
5206cce3332SAndreas Gohr        $ins = p_cached_instructions(wikiFN($page));
5216cce3332SAndreas Gohr
5226cce3332SAndreas Gohr        // instantiate new Renderer - needed for interwiki links
5236cce3332SAndreas Gohr        $Renderer = new Doku_Renderer_xhtml();
5246cce3332SAndreas Gohr        $Renderer->interwiki = getInterwiki();
5256cce3332SAndreas Gohr
5266cce3332SAndreas Gohr        // parse instructions
5276cce3332SAndreas Gohr        $links = [];
5286cce3332SAndreas Gohr        foreach ($ins as $in) {
5296cce3332SAndreas Gohr            switch ($in[0]) {
5306cce3332SAndreas Gohr                case 'internallink':
5316cce3332SAndreas Gohr                    $links[] = new Link([
5326cce3332SAndreas Gohr                        'type' => 'local',
5336cce3332SAndreas Gohr                        'page' => $in[1][0],
5346cce3332SAndreas Gohr                        'href' => wl($in[1][0]),
5356cce3332SAndreas Gohr                    ]);
5366cce3332SAndreas Gohr                    break;
5376cce3332SAndreas Gohr                case 'externallink':
5386cce3332SAndreas Gohr                    $links[] = new Link([
5396cce3332SAndreas Gohr                        'type' => 'extern',
5406cce3332SAndreas Gohr                        'page' => $in[1][0],
5416cce3332SAndreas Gohr                        'href' => $in[1][0],
5426cce3332SAndreas Gohr                    ]);
5436cce3332SAndreas Gohr                    break;
5446cce3332SAndreas Gohr                case 'interwikilink':
5456cce3332SAndreas Gohr                    $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]);
5466cce3332SAndreas Gohr                    $links[] = new Link([
5476cce3332SAndreas Gohr                        'type' => 'interwiki',
5486cce3332SAndreas Gohr                        'page' => $in[1][0],
5496cce3332SAndreas Gohr                        'href' => $url,
5506cce3332SAndreas Gohr                    ]);
5516cce3332SAndreas Gohr                    break;
5526cce3332SAndreas Gohr            }
5536cce3332SAndreas Gohr        }
5546cce3332SAndreas Gohr
5556cce3332SAndreas Gohr        return ($links);
5566cce3332SAndreas Gohr    }
5576cce3332SAndreas Gohr
5586cce3332SAndreas Gohr    /**
5596cce3332SAndreas Gohr     * Get a page's backlinks
5606cce3332SAndreas Gohr     *
5616cce3332SAndreas Gohr     * A backlink is a wiki link on another page that links to the given page.
5626cce3332SAndreas Gohr     *
563*902647e6SAndreas Gohr     * Only links from pages readable by the current user are returned. The page itself
564*902647e6SAndreas Gohr     * needs to be readable. Otherwise an error is returned.
5656cce3332SAndreas Gohr     *
5666cce3332SAndreas Gohr     * @param string $page page id
5676cce3332SAndreas Gohr     * @return string[] A list of pages linking to the given page
568*902647e6SAndreas Gohr     * @throws AccessDeniedException
569*902647e6SAndreas Gohr     * @throws RemoteException
5706cce3332SAndreas Gohr     */
5716cce3332SAndreas Gohr    public function getPageBackLinks($page)
5726cce3332SAndreas Gohr    {
573*902647e6SAndreas Gohr        $page = $this->checkPage($page, false);
574*902647e6SAndreas Gohr
575*902647e6SAndreas Gohr        return ft_backlinks($page);
5766cce3332SAndreas Gohr    }
5776cce3332SAndreas Gohr
5786cce3332SAndreas Gohr    /**
5796cce3332SAndreas Gohr     * Lock the given set of pages
5806cce3332SAndreas Gohr     *
5816cce3332SAndreas Gohr     * This call will try to lock all given pages. It will return a list of pages that were
5826cce3332SAndreas Gohr     * successfully locked. If a page could not be locked, eg. because a different user is
5836cce3332SAndreas Gohr     * currently holding a lock, that page will be missing from the returned list.
5846cce3332SAndreas Gohr     *
5856cce3332SAndreas Gohr     * You should always ensure that the list of returned pages matches the given list of
5866cce3332SAndreas Gohr     * pages. It's up to you to decide how to handle failed locking.
5876cce3332SAndreas Gohr     *
5886cce3332SAndreas Gohr     * Note: you can only lock pages that you have write access for. It is possible to create
5896cce3332SAndreas Gohr     * a lock for a page that does not exist, yet.
5906cce3332SAndreas Gohr     *
5916cce3332SAndreas Gohr     * Note: it is not necessary to lock a page before saving it. The `savePage()` call will
5926cce3332SAndreas Gohr     * automatically lock and unlock the page for you. However if you plan to do related
5936cce3332SAndreas Gohr     * operations on multiple pages, locking them all at once beforehand can be useful.
5946cce3332SAndreas Gohr     *
5956cce3332SAndreas Gohr     * @param string[] $pages A list of pages to lock
5966cce3332SAndreas Gohr     * @return string[] A list of pages that were successfully locked
5976cce3332SAndreas Gohr     */
5986cce3332SAndreas Gohr    public function lockPages($pages)
5996cce3332SAndreas Gohr    {
6006cce3332SAndreas Gohr        $locked = [];
6016cce3332SAndreas Gohr
6026cce3332SAndreas Gohr        foreach ($pages as $id) {
603*902647e6SAndreas Gohr            $id = cleanID($id);
6046cce3332SAndreas Gohr            if ($id === '') continue;
6056cce3332SAndreas Gohr            if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) {
6066cce3332SAndreas Gohr                continue;
6076cce3332SAndreas Gohr            }
6086cce3332SAndreas Gohr            lock($id);
6096cce3332SAndreas Gohr            $locked[] = $id;
6106cce3332SAndreas Gohr        }
6116cce3332SAndreas Gohr        return $locked;
6126cce3332SAndreas Gohr    }
6136cce3332SAndreas Gohr
6146cce3332SAndreas Gohr    /**
6156cce3332SAndreas Gohr     * Unlock the given set of pages
6166cce3332SAndreas Gohr     *
6176cce3332SAndreas Gohr     * This call will try to unlock all given pages. It will return a list of pages that were
6186cce3332SAndreas Gohr     * successfully unlocked. If a page could not be unlocked, eg. because a different user is
6196cce3332SAndreas Gohr     * currently holding a lock, that page will be missing from the returned list.
6206cce3332SAndreas Gohr     *
6216cce3332SAndreas Gohr     * You should always ensure that the list of returned pages matches the given list of
6226cce3332SAndreas Gohr     * pages. It's up to you to decide how to handle failed unlocking.
6236cce3332SAndreas Gohr     *
6246cce3332SAndreas Gohr     * Note: you can only unlock pages that you have write access for.
6256cce3332SAndreas Gohr     *
6266cce3332SAndreas Gohr     * @param string[] $pages A list of pages to unlock
6276cce3332SAndreas Gohr     * @return string[] A list of pages that were successfully unlocked
6286cce3332SAndreas Gohr     */
6296cce3332SAndreas Gohr    public function unlockPages($pages)
6306cce3332SAndreas Gohr    {
6316cce3332SAndreas Gohr        $unlocked = [];
6326cce3332SAndreas Gohr
6336cce3332SAndreas Gohr        foreach ($pages as $id) {
634*902647e6SAndreas Gohr            $id = cleanID($id);
6356cce3332SAndreas Gohr            if ($id === '') continue;
6366cce3332SAndreas Gohr            if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) {
6376cce3332SAndreas Gohr                continue;
6386cce3332SAndreas Gohr            }
6396cce3332SAndreas Gohr            $unlocked[] = $id;
6406cce3332SAndreas Gohr        }
6416cce3332SAndreas Gohr
6426cce3332SAndreas Gohr        return $unlocked;
6436cce3332SAndreas Gohr    }
6446cce3332SAndreas Gohr
6456cce3332SAndreas Gohr    /**
6466cce3332SAndreas Gohr     * Save a wiki page
6476cce3332SAndreas Gohr     *
6486cce3332SAndreas Gohr     * Saves the given wiki text to the given page. If the page does not exist, it will be created.
6496cce3332SAndreas Gohr     * Just like in the wiki, saving an empty text will delete the page.
6506cce3332SAndreas Gohr     *
6516cce3332SAndreas Gohr     * You need write permissions for the given page and the page may not be locked by another user.
6526cce3332SAndreas Gohr     *
6536cce3332SAndreas Gohr     * @param string $page page id
6546cce3332SAndreas Gohr     * @param string $text wiki text
6556cce3332SAndreas Gohr     * @param string $summary edit summary
6566cce3332SAndreas Gohr     * @param bool $isminor whether this is a minor edit
6576cce3332SAndreas Gohr     * @return bool Returns true on success
6586cce3332SAndreas Gohr     * @throws AccessDeniedException no write access for page
6596cce3332SAndreas Gohr     * @throws RemoteException no id, empty new page or locked
6606cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
6616cce3332SAndreas Gohr     */
6626cce3332SAndreas Gohr    public function savePage($page, $text, $summary = '', $isminor = false)
6636cce3332SAndreas Gohr    {
6646cce3332SAndreas Gohr        global $TEXT;
6656cce3332SAndreas Gohr        global $lang;
6666cce3332SAndreas Gohr
667*902647e6SAndreas Gohr        $page = $this->checkPage($page, false, AUTH_EDIT);
6686cce3332SAndreas Gohr        $TEXT = cleanText($text);
6696cce3332SAndreas Gohr
6706cce3332SAndreas Gohr
6716cce3332SAndreas Gohr        if (!page_exists($page) && trim($TEXT) == '') {
6726cce3332SAndreas Gohr            throw new RemoteException('Refusing to write an empty new wiki page', 132);
6736cce3332SAndreas Gohr        }
6746cce3332SAndreas Gohr
6756cce3332SAndreas Gohr        // Check, if page is locked
6766cce3332SAndreas Gohr        if (checklock($page)) {
6776cce3332SAndreas Gohr            throw new RemoteException('The page is currently locked', 133);
6786cce3332SAndreas Gohr        }
6796cce3332SAndreas Gohr
6806cce3332SAndreas Gohr        // SPAM check
6816cce3332SAndreas Gohr        if (checkwordblock()) {
6826cce3332SAndreas Gohr            throw new RemoteException('Positive wordblock check', 134);
6836cce3332SAndreas Gohr        }
6846cce3332SAndreas Gohr
6856cce3332SAndreas Gohr        // autoset summary on new pages
6866cce3332SAndreas Gohr        if (!page_exists($page) && empty($summary)) {
6876cce3332SAndreas Gohr            $summary = $lang['created'];
6886cce3332SAndreas Gohr        }
6896cce3332SAndreas Gohr
6906cce3332SAndreas Gohr        // autoset summary on deleted pages
6916cce3332SAndreas Gohr        if (page_exists($page) && empty($TEXT) && empty($summary)) {
6926cce3332SAndreas Gohr            $summary = $lang['deleted'];
6936cce3332SAndreas Gohr        }
6946cce3332SAndreas Gohr
695*902647e6SAndreas Gohr        // FIXME auto set a summary in other cases "API Edit" might be a good idea?
696*902647e6SAndreas Gohr
6976cce3332SAndreas Gohr        lock($page);
6986cce3332SAndreas Gohr        saveWikiText($page, $TEXT, $summary, $isminor);
6996cce3332SAndreas Gohr        unlock($page);
7006cce3332SAndreas Gohr
7016cce3332SAndreas Gohr        // run the indexer if page wasn't indexed yet
7026cce3332SAndreas Gohr        idx_addPage($page);
7036cce3332SAndreas Gohr
7046cce3332SAndreas Gohr        return true;
7056cce3332SAndreas Gohr    }
7066cce3332SAndreas Gohr
7076cce3332SAndreas Gohr    /**
7086cce3332SAndreas Gohr     * Appends text to the end of a wiki page
7096cce3332SAndreas Gohr     *
7106cce3332SAndreas Gohr     * If the page does not exist, it will be created. If a page template for the non-existant
7116cce3332SAndreas Gohr     * page is configured, the given text will appended to that template.
7126cce3332SAndreas Gohr     *
7136cce3332SAndreas Gohr     * The call will create a new page revision.
7146cce3332SAndreas Gohr     *
7156cce3332SAndreas Gohr     * You need write permissions for the given page.
7166cce3332SAndreas Gohr     *
7176cce3332SAndreas Gohr     * @param string $page page id
7186cce3332SAndreas Gohr     * @param string $text wiki text
7196cce3332SAndreas Gohr     * @param string $summary edit summary
7206cce3332SAndreas Gohr     * @param bool $isminor whether this is a minor edit
7216cce3332SAndreas Gohr     * @return bool Returns true on success
7226cce3332SAndreas Gohr     * @throws AccessDeniedException
7236cce3332SAndreas Gohr     * @throws RemoteException
7246cce3332SAndreas Gohr     */
7256cce3332SAndreas Gohr    public function appendPage($page, $text, $summary, $isminor)
7266cce3332SAndreas Gohr    {
7276cce3332SAndreas Gohr        $currentpage = $this->getPage($page);
7286cce3332SAndreas Gohr        if (!is_string($currentpage)) {
7296cce3332SAndreas Gohr            $currentpage = '';
7306cce3332SAndreas Gohr        }
7316cce3332SAndreas Gohr        return $this->savePage($page, $currentpage . $text, $summary, $isminor);
7326cce3332SAndreas Gohr    }
7336cce3332SAndreas Gohr
7346cce3332SAndreas Gohr    // endregion
7356cce3332SAndreas Gohr
7366cce3332SAndreas Gohr    // region media
7376cce3332SAndreas Gohr
7386cce3332SAndreas Gohr    /**
7396cce3332SAndreas Gohr     * List all media files in the given namespace (and below)
7406cce3332SAndreas Gohr     *
7416cce3332SAndreas Gohr     * Setting the `depth` to `0` and the `namespace` to `""` will return all media files in the wiki.
7426cce3332SAndreas Gohr     *
7436cce3332SAndreas Gohr     * When `pattern` is given, it needs to be a valid regular expression as understood by PHP's
7446cce3332SAndreas Gohr     * `preg_match()` including delimiters.
7456cce3332SAndreas Gohr     * The pattern is matched against the full media ID, including the namespace.
7466cce3332SAndreas Gohr     *
7476cce3332SAndreas Gohr     * @link https://www.php.net/manual/en/reference.pcre.pattern.syntax.php
7486cce3332SAndreas Gohr     * @param string $namespace The namespace to search. Empty string for root namespace
7496cce3332SAndreas Gohr     * @param string $pattern A regular expression to filter the returned files
7506cce3332SAndreas Gohr     * @param int $depth How deep to search. 0 for all subnamespaces
7516cce3332SAndreas Gohr     * @param bool $hash Whether to include a MD5 hash of the media content
7526cce3332SAndreas Gohr     * @return Media[]
7536cce3332SAndreas Gohr     * @throws AccessDeniedException no access to the media files
7546cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
7556cce3332SAndreas Gohr     */
7566cce3332SAndreas Gohr    public function listMedia($namespace = '', $pattern = '', $depth = 1, $hash = false)
7576cce3332SAndreas Gohr    {
7586cce3332SAndreas Gohr        global $conf;
7596cce3332SAndreas Gohr
7606cce3332SAndreas Gohr        $namespace = cleanID($namespace);
7616cce3332SAndreas Gohr
7626cce3332SAndreas Gohr        if (auth_quickaclcheck($namespace . ':*') < AUTH_READ) {
7636cce3332SAndreas Gohr            throw new AccessDeniedException('You are not allowed to list media files.', 215);
7646cce3332SAndreas Gohr        }
7656cce3332SAndreas Gohr
7666cce3332SAndreas Gohr        $options = [
7676cce3332SAndreas Gohr            'skipacl' => 0,
7686cce3332SAndreas Gohr            'depth' => $depth,
7696cce3332SAndreas Gohr            'hash' => $hash,
7706cce3332SAndreas Gohr            'pattern' => $pattern,
7716cce3332SAndreas Gohr        ];
7726cce3332SAndreas Gohr
7736cce3332SAndreas Gohr        $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
7746cce3332SAndreas Gohr        $data = [];
7756cce3332SAndreas Gohr        search($data, $conf['mediadir'], 'search_media', $options, $dir);
7766cce3332SAndreas Gohr        return array_map(fn($item) => new Media($item), $data);
7776cce3332SAndreas Gohr    }
7786cce3332SAndreas Gohr
7796cce3332SAndreas Gohr    /**
7806cce3332SAndreas Gohr     * Get recent media changes
7816cce3332SAndreas Gohr     *
7826cce3332SAndreas Gohr     * Returns a list of recent changes to media files. The results can be limited to changes newer than
7836cce3332SAndreas Gohr     * a given timestamp.
7846cce3332SAndreas Gohr     *
7856cce3332SAndreas Gohr     * Only changes within the configured `$conf['recent']` range are returned. This is the default
7866cce3332SAndreas Gohr     * when no timestamp is given.
7876cce3332SAndreas Gohr     *
7886cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:recent
7896cce3332SAndreas Gohr     * @param int $timestamp Only show changes newer than this unix timestamp
7906cce3332SAndreas Gohr     * @return MediaRevision[]
7916cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
7926cce3332SAndreas Gohr     * @author Michael Hamann <michael@content-space.de>
7936cce3332SAndreas Gohr     */
7946cce3332SAndreas Gohr    public function getRecentMediaChanges($timestamp = 0)
7956cce3332SAndreas Gohr    {
7966cce3332SAndreas Gohr
7976cce3332SAndreas Gohr        $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
7986cce3332SAndreas Gohr
7996cce3332SAndreas Gohr        $changes = [];
8006cce3332SAndreas Gohr        foreach ($recents as $recent) {
8016cce3332SAndreas Gohr            $changes[] = new MediaRevision([
8026cce3332SAndreas Gohr                'id' => $recent['id'],
8036cce3332SAndreas Gohr                'revision' => $recent['date'],
8046cce3332SAndreas Gohr                'author' => $recent['user'],
8056cce3332SAndreas Gohr                'ip' => $recent['ip'],
8066cce3332SAndreas Gohr                'summary' => $recent['sum'],
8076cce3332SAndreas Gohr                'type' => $recent['type'],
8086cce3332SAndreas Gohr                'sizechange' => $recent['sizechange'],
8096cce3332SAndreas Gohr            ]);
8106cce3332SAndreas Gohr        }
8116cce3332SAndreas Gohr
8126cce3332SAndreas Gohr        return $changes;
8136cce3332SAndreas Gohr    }
8146cce3332SAndreas Gohr
8156cce3332SAndreas Gohr    /**
8166cce3332SAndreas Gohr     * Get a media file's content
8176cce3332SAndreas Gohr     *
8186cce3332SAndreas Gohr     * Returns the content of the given media file. When no revision is given, the current revision is returned.
8196cce3332SAndreas Gohr     *
8206cce3332SAndreas Gohr     * @link https://en.wikipedia.org/wiki/Base64
8216cce3332SAndreas Gohr     * @param string $media file id
8226cce3332SAndreas Gohr     * @param int $rev revision timestamp
8236cce3332SAndreas Gohr     * @return string Base64 encoded media file contents
8246cce3332SAndreas Gohr     * @throws AccessDeniedException no permission for media
8256cce3332SAndreas Gohr     * @throws RemoteException not exist
8266cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
8276cce3332SAndreas Gohr     *
8286cce3332SAndreas Gohr     */
8296cce3332SAndreas Gohr    public function getMedia($media, $rev = '')
8306cce3332SAndreas Gohr    {
8316cce3332SAndreas Gohr        $media = cleanID($media);
8326cce3332SAndreas Gohr        if (auth_quickaclcheck($media) < AUTH_READ) {
8336cce3332SAndreas Gohr            throw new AccessDeniedException('You are not allowed to read this file', 211);
8346cce3332SAndreas Gohr        }
8356cce3332SAndreas Gohr
8366cce3332SAndreas Gohr        $file = mediaFN($media, $rev);
8376cce3332SAndreas Gohr        if (!@ file_exists($file)) {
8386cce3332SAndreas Gohr            throw new RemoteException('The requested file does not exist', 221);
8396cce3332SAndreas Gohr        }
8406cce3332SAndreas Gohr
8416cce3332SAndreas Gohr        $data = io_readFile($file, false);
8426cce3332SAndreas Gohr        return base64_encode($data);
8436cce3332SAndreas Gohr    }
8446cce3332SAndreas Gohr
8456cce3332SAndreas Gohr    /**
8466cce3332SAndreas Gohr     * Return info about a media file
8476cce3332SAndreas Gohr     *
8486cce3332SAndreas Gohr     * The call will return an error if the requested media file does not exist.
8496cce3332SAndreas Gohr     *
8506cce3332SAndreas Gohr     * Read access is required for the media file.
8516cce3332SAndreas Gohr     *
8526cce3332SAndreas Gohr     * @param string $media file id
8536cce3332SAndreas Gohr     * @param int $rev revision timestamp
8546cce3332SAndreas Gohr     * @param bool $hash whether to include the MD5 hash of the media content
8556cce3332SAndreas Gohr     * @return Media
8566cce3332SAndreas Gohr     * @throws AccessDeniedException no permission for media
8576cce3332SAndreas Gohr     * @throws RemoteException if not exist
8586cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
8596cce3332SAndreas Gohr     */
8606cce3332SAndreas Gohr    public function getMediaInfo($media, $rev = '', $hash = false)
8616cce3332SAndreas Gohr    {
8626cce3332SAndreas Gohr        $media = cleanID($media);
8636cce3332SAndreas Gohr        if (auth_quickaclcheck($media) < AUTH_READ) {
8646cce3332SAndreas Gohr            throw new AccessDeniedException('You are not allowed to read this file', 211);
8656cce3332SAndreas Gohr        }
8666cce3332SAndreas Gohr        if (!media_exists($media, $rev)) {
8676cce3332SAndreas Gohr            throw new RemoteException('The requested media file does not exist', 221);
8686cce3332SAndreas Gohr        }
8696cce3332SAndreas Gohr
8706cce3332SAndreas Gohr        $file = mediaFN($media, $rev);
8716cce3332SAndreas Gohr
8726cce3332SAndreas Gohr        $info = new Media([
8736cce3332SAndreas Gohr            'id' => $media,
8746cce3332SAndreas Gohr            'mtime' => filemtime($file),
8756cce3332SAndreas Gohr            'size' => filesize($file),
8766cce3332SAndreas Gohr        ]);
8776cce3332SAndreas Gohr        if ($hash) $info->calculateHash();
8786cce3332SAndreas Gohr
8796cce3332SAndreas Gohr        return $info;
8806cce3332SAndreas Gohr    }
8816cce3332SAndreas Gohr
8826cce3332SAndreas Gohr    /**
8836cce3332SAndreas Gohr     * Uploads a file to the wiki
8846cce3332SAndreas Gohr     *
8856cce3332SAndreas Gohr     * The file data has to be passed as a base64 encoded string.
8866cce3332SAndreas Gohr     *
8876cce3332SAndreas Gohr     * @link https://en.wikipedia.org/wiki/Base64
8886cce3332SAndreas Gohr     * @param string $media media id
8896cce3332SAndreas Gohr     * @param string $base64 Base64 encoded file contents
8906cce3332SAndreas Gohr     * @param bool $overwrite Should an existing file be overwritten?
8916cce3332SAndreas Gohr     * @return bool Should always be true
8926cce3332SAndreas Gohr     * @throws RemoteException
8936cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
8946cce3332SAndreas Gohr     */
8956cce3332SAndreas Gohr    public function saveMedia($media, $base64, $overwrite = false)
8966cce3332SAndreas Gohr    {
8976cce3332SAndreas Gohr        $media = cleanID($media);
8986cce3332SAndreas Gohr        $auth = auth_quickaclcheck(getNS($media) . ':*');
8996cce3332SAndreas Gohr
9006cce3332SAndreas Gohr        if ($media === '') {
9016cce3332SAndreas Gohr            throw new RemoteException('Media ID not given.', 231);
9026cce3332SAndreas Gohr        }
9036cce3332SAndreas Gohr
9046cce3332SAndreas Gohr        // clean up base64 encoded data
9056cce3332SAndreas Gohr        $base64 = strtr($base64, [
9066cce3332SAndreas Gohr            "\n" => '', // strip newlines
9076cce3332SAndreas Gohr            "\r" => '', // strip carriage returns
9086cce3332SAndreas Gohr            '-' => '+', // RFC4648 base64url
9096cce3332SAndreas Gohr            '_' => '/', // RFC4648 base64url
9106cce3332SAndreas Gohr            ' ' => '+', // JavaScript data uri
9116cce3332SAndreas Gohr        ]);
9126cce3332SAndreas Gohr
9136cce3332SAndreas Gohr        $data = base64_decode($base64, true);
9146cce3332SAndreas Gohr        if ($data === false) {
9156cce3332SAndreas Gohr            throw new RemoteException('Invalid base64 encoded data.', 231); // FIXME adjust code
9166cce3332SAndreas Gohr        }
9176cce3332SAndreas Gohr
9186cce3332SAndreas Gohr        // save temporary file
9196cce3332SAndreas Gohr        global $conf;
9206cce3332SAndreas Gohr        $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP());
9216cce3332SAndreas Gohr        @unlink($ftmp);
9226cce3332SAndreas Gohr        io_saveFile($ftmp, $data);
9236cce3332SAndreas Gohr
9246cce3332SAndreas Gohr        $res = media_save(['name' => $ftmp], $media, $overwrite, $auth, 'rename');
9256cce3332SAndreas Gohr        if (is_array($res)) {
9266cce3332SAndreas Gohr            throw new RemoteException($res[0], -$res[1]); // FIXME adjust code -1 * -1 = 1, we want a 23x code
9276cce3332SAndreas Gohr        }
9286cce3332SAndreas Gohr        return (bool)$res; // should always be true at this point
9296cce3332SAndreas Gohr    }
9306cce3332SAndreas Gohr
9316cce3332SAndreas Gohr    /**
9326cce3332SAndreas Gohr     * Deletes a file from the wiki
9336cce3332SAndreas Gohr     *
9346cce3332SAndreas Gohr     * You need to have delete permissions for the file.
9356cce3332SAndreas Gohr     *
9366cce3332SAndreas Gohr     * @param string $media media id
9376cce3332SAndreas Gohr     * @return bool Should always be true
9386cce3332SAndreas Gohr     * @throws AccessDeniedException no permissions
9396cce3332SAndreas Gohr     * @throws RemoteException file in use or not deleted
9406cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
9416cce3332SAndreas Gohr     *
9426cce3332SAndreas Gohr     */
9436cce3332SAndreas Gohr    public function deleteMedia($media)
9446cce3332SAndreas Gohr    {
9456cce3332SAndreas Gohr        $media = cleanID($media);
9466cce3332SAndreas Gohr        $auth = auth_quickaclcheck($media);
9476cce3332SAndreas Gohr        $res = media_delete($media, $auth);
9486cce3332SAndreas Gohr        if ($res & DOKU_MEDIA_DELETED) {
9496cce3332SAndreas Gohr            return true;
9506cce3332SAndreas Gohr        } elseif ($res & DOKU_MEDIA_NOT_AUTH) {
9516cce3332SAndreas Gohr            throw new AccessDeniedException('You don\'t have permissions to delete files.', 212);
9526cce3332SAndreas Gohr        } elseif ($res & DOKU_MEDIA_INUSE) {
9536cce3332SAndreas Gohr            throw new RemoteException('File is still referenced', 232);
9546cce3332SAndreas Gohr        } else {
9556cce3332SAndreas Gohr            throw new RemoteException('Could not delete file', 233);
9566cce3332SAndreas Gohr        }
9576cce3332SAndreas Gohr    }
9586cce3332SAndreas Gohr
9596cce3332SAndreas Gohr    // endregion
9606cce3332SAndreas Gohr
9616cce3332SAndreas Gohr
9626cce3332SAndreas Gohr    /**
963*902647e6SAndreas Gohr     * Convenience method for page checks
964*902647e6SAndreas Gohr     *
965*902647e6SAndreas Gohr     * This method will perform multiple tasks:
966*902647e6SAndreas Gohr     *
967*902647e6SAndreas Gohr     * - clean the given page id
968*902647e6SAndreas Gohr     * - disallow an empty page id
969*902647e6SAndreas Gohr     * - check if the page exists (unless disabled)
970*902647e6SAndreas Gohr     * - check if the user has the required access level (pass AUTH_NONE to skip)
971dd87735dSAndreas Gohr     *
972dd87735dSAndreas Gohr     * @param string $id page id
973*902647e6SAndreas Gohr     * @return string the cleaned page id
974*902647e6SAndreas Gohr     * @throws RemoteException
975*902647e6SAndreas Gohr     * @throws AccessDeniedException
976dd87735dSAndreas Gohr     */
977*902647e6SAndreas Gohr    private function checkPage($id, $existCheck = true, $minAccess = AUTH_READ)
978dd87735dSAndreas Gohr    {
979dd87735dSAndreas Gohr        $id = cleanID($id);
980*902647e6SAndreas Gohr        if ($id === '') {
981*902647e6SAndreas Gohr            throw new RemoteException('Empty or invalid page ID given', 131); // FIXME check code
982dd87735dSAndreas Gohr        }
983*902647e6SAndreas Gohr
984*902647e6SAndreas Gohr        if ($existCheck && !page_exists($id)) {
985*902647e6SAndreas Gohr            throw new RemoteException('The requested page does not exist', 121); // FIXME check code
986*902647e6SAndreas Gohr        }
987*902647e6SAndreas Gohr
988*902647e6SAndreas Gohr        if ($minAccess && auth_quickaclcheck($id) < $minAccess) {
989*902647e6SAndreas Gohr            throw new AccessDeniedException('You are not allowed to read this page', 111); // FIXME check code
990*902647e6SAndreas Gohr        }
991*902647e6SAndreas Gohr
992dd87735dSAndreas Gohr        return $id;
993dd87735dSAndreas Gohr    }
994dd87735dSAndreas Gohr}
995