xref: /dokuwiki/inc/Remote/ApiCore.php (revision 83b3acccb42578eaa33f84e6b13612436320090b)
1dd87735dSAndreas Gohr<?php
2dd87735dSAndreas Gohr
3dd87735dSAndreas Gohrnamespace dokuwiki\Remote;
4dd87735dSAndreas Gohr
5dd87735dSAndreas Gohruse Doku_Renderer_xhtml;
60c3a5702SAndreas Gohruse dokuwiki\ChangeLog\PageChangeLog;
761d21e86Skuangfiouse dokuwiki\ChangeLog\MediaChangeLog;
8104a3b7cSAndreas Gohruse dokuwiki\Extension\AuthPlugin;
9cbb44eabSAndreas Gohruse dokuwiki\Extension\Event;
106cce3332SAndreas Gohruse dokuwiki\Remote\Response\Link;
116cce3332SAndreas Gohruse dokuwiki\Remote\Response\Media;
1258ae4747SAndreas Gohruse dokuwiki\Remote\Response\MediaChange;
138ddd9b69SAndreas Gohruse dokuwiki\Remote\Response\Page;
1458ae4747SAndreas Gohruse dokuwiki\Remote\Response\PageChange;
156cce3332SAndreas Gohruse dokuwiki\Remote\Response\PageHit;
166cce3332SAndreas Gohruse dokuwiki\Remote\Response\User;
174027a91aSSatoshi Saharause dokuwiki\Search\Indexer;
180cba610bSSatoshi Saharause dokuwiki\Search\FulltextSearch;
194a90f94bSSatoshi Saharause dokuwiki\Search\MetadataIndex;
202d85e841SAndreas Gohruse dokuwiki\Utf8\Sort;
210cba610bSSatoshi Sahara
22dd87735dSAndreas Gohr/**
23dd87735dSAndreas Gohr * Provides the core methods for the remote API.
24dd87735dSAndreas Gohr * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
25dd87735dSAndreas Gohr */
26dd87735dSAndreas Gohrclass ApiCore
27dd87735dSAndreas Gohr{
28dd87735dSAndreas Gohr    /** @var int Increased whenever the API is changed */
290e1bcd98SAndreas Gohr    public const API_VERSION = 14;
30dd87735dSAndreas Gohr
31dd87735dSAndreas Gohr    /**
32dd87735dSAndreas Gohr     * Returns details about the core methods
33dd87735dSAndreas Gohr     *
34dd87735dSAndreas Gohr     * @return array
35dd87735dSAndreas Gohr     */
366cce3332SAndreas Gohr    public function getMethods()
37dd87735dSAndreas Gohr    {
38104a3b7cSAndreas Gohr        return [
39093fe67eSAndreas Gohr            'core.getAPIVersion' => (new ApiCall($this->getAPIVersion(...), 'info'))->setPublic(),
40dd87735dSAndreas Gohr
416cce3332SAndreas Gohr            'core.getWikiVersion' => new ApiCall('getVersion', 'info'),
42093fe67eSAndreas Gohr            'core.getWikiTitle' => (new ApiCall($this->getWikiTitle(...), 'info'))->setPublic(),
43093fe67eSAndreas Gohr            'core.getWikiTime' => (new ApiCall($this->getWikiTime(...), 'info')),
446cce3332SAndreas Gohr
45093fe67eSAndreas Gohr            'core.login' => (new ApiCall($this->login(...), 'user'))->setPublic(),
46093fe67eSAndreas Gohr            'core.logoff' => new ApiCall($this->logoff(...), 'user'),
47093fe67eSAndreas Gohr            'core.whoAmI' => (new ApiCall($this->whoAmI(...), 'user')),
48093fe67eSAndreas Gohr            'core.aclCheck' => new ApiCall($this->aclCheck(...), 'user'),
496cce3332SAndreas Gohr
50093fe67eSAndreas Gohr            'core.listPages' => new ApiCall($this->listPages(...), 'pages'),
51093fe67eSAndreas Gohr            'core.searchPages' => new ApiCall($this->searchPages(...), 'pages'),
52093fe67eSAndreas Gohr            'core.getRecentPageChanges' => new ApiCall($this->getRecentPageChanges(...), 'pages'),
536cce3332SAndreas Gohr
54093fe67eSAndreas Gohr            'core.getPage' => (new ApiCall($this->getPage(...), 'pages')),
55093fe67eSAndreas Gohr            'core.getPageHTML' => (new ApiCall($this->getPageHTML(...), 'pages')),
56093fe67eSAndreas Gohr            'core.getPageInfo' => (new ApiCall($this->getPageInfo(...), 'pages')),
57093fe67eSAndreas Gohr            'core.getPageHistory' => new ApiCall($this->getPageHistory(...), 'pages'),
58093fe67eSAndreas Gohr            'core.getPageLinks' => new ApiCall($this->getPageLinks(...), 'pages'),
59093fe67eSAndreas Gohr            'core.getPageBackLinks' => new ApiCall($this->getPageBackLinks(...), 'pages'),
606cce3332SAndreas Gohr
61093fe67eSAndreas Gohr            'core.lockPages' => new ApiCall($this->lockPages(...), 'pages'),
62093fe67eSAndreas Gohr            'core.unlockPages' => new ApiCall($this->unlockPages(...), 'pages'),
63093fe67eSAndreas Gohr            'core.savePage' => new ApiCall($this->savePage(...), 'pages'),
64093fe67eSAndreas Gohr            'core.appendPage' => new ApiCall($this->appendPage(...), 'pages'),
656cce3332SAndreas Gohr
66093fe67eSAndreas Gohr            'core.listMedia' => new ApiCall($this->listMedia(...), 'media'),
67093fe67eSAndreas Gohr            'core.getRecentMediaChanges' => new ApiCall($this->getRecentMediaChanges(...), 'media'),
686cce3332SAndreas Gohr
69093fe67eSAndreas Gohr            'core.getMedia' => new ApiCall($this->getMedia(...), 'media'),
70093fe67eSAndreas Gohr            'core.getMediaInfo' => new ApiCall($this->getMediaInfo(...), 'media'),
71093fe67eSAndreas Gohr            'core.getMediaUsage' => new ApiCall($this->getMediaUsage(...), 'media'),
72093fe67eSAndreas Gohr            'core.getMediaHistory' => new ApiCall($this->getMediaHistory(...), 'media'),
736cce3332SAndreas Gohr
74093fe67eSAndreas Gohr            'core.saveMedia' => new ApiCall($this->saveMedia(...), 'media'),
75093fe67eSAndreas Gohr            'core.deleteMedia' => new ApiCall($this->deleteMedia(...), 'media'),
76104a3b7cSAndreas Gohr        ];
77dd87735dSAndreas Gohr    }
78dd87735dSAndreas Gohr
796cce3332SAndreas Gohr    // region info
80dd87735dSAndreas Gohr
81dd87735dSAndreas Gohr    /**
828a9282a2SAndreas Gohr     * Return the API version
83dd87735dSAndreas Gohr     *
848a9282a2SAndreas Gohr     * This is the version of the DokuWiki API. It increases whenever the API definition changes.
85dd87735dSAndreas Gohr     *
868a9282a2SAndreas Gohr     * When developing a client, you should check this version and make sure you can handle it.
87dd87735dSAndreas Gohr     *
88dd87735dSAndreas Gohr     * @return int
89dd87735dSAndreas Gohr     */
90dd87735dSAndreas Gohr    public function getAPIVersion()
91dd87735dSAndreas Gohr    {
92dd87735dSAndreas Gohr        return self::API_VERSION;
93dd87735dSAndreas Gohr    }
94dd87735dSAndreas Gohr
95dd87735dSAndreas Gohr    /**
966cce3332SAndreas Gohr     * Returns the wiki title
976cce3332SAndreas Gohr     *
986cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:title
996cce3332SAndreas Gohr     * @return string
1006cce3332SAndreas Gohr     */
1016cce3332SAndreas Gohr    public function getWikiTitle()
1026cce3332SAndreas Gohr    {
1036cce3332SAndreas Gohr        global $conf;
1046cce3332SAndreas Gohr        return $conf['title'];
1056cce3332SAndreas Gohr    }
1066cce3332SAndreas Gohr
1076cce3332SAndreas Gohr    /**
1086cce3332SAndreas Gohr     * Return the current server time
1096cce3332SAndreas Gohr     *
1106cce3332SAndreas Gohr     * Returns a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC).
1116cce3332SAndreas Gohr     *
1126cce3332SAndreas Gohr     * You can use this to compensate for differences between your client's time and the
1136cce3332SAndreas Gohr     * server's time when working with last modified timestamps (revisions).
1146cce3332SAndreas Gohr     *
1156cce3332SAndreas Gohr     * @return int A unix timestamp
1166cce3332SAndreas Gohr     */
1176cce3332SAndreas Gohr    public function getWikiTime()
1186cce3332SAndreas Gohr    {
1196cce3332SAndreas Gohr        return time();
1206cce3332SAndreas Gohr    }
1216cce3332SAndreas Gohr
1226cce3332SAndreas Gohr    // endregion
1236cce3332SAndreas Gohr
1246cce3332SAndreas Gohr    // region user
1256cce3332SAndreas Gohr
1266cce3332SAndreas Gohr    /**
127dd87735dSAndreas Gohr     * Login
128dd87735dSAndreas Gohr     *
1298a9282a2SAndreas Gohr     * This will use the given credentials and attempt to login the user. This will set the
1308a9282a2SAndreas Gohr     * appropriate cookies, which can be used for subsequent requests.
1318a9282a2SAndreas Gohr     *
132fe9f11e2SAndreas Gohr     * Use of this mechanism is discouraged. Using token authentication is preferred.
133fe9f11e2SAndreas Gohr     *
1348a9282a2SAndreas Gohr     * @param string $user The user name
1358a9282a2SAndreas Gohr     * @param string $pass The password
136fe9f11e2SAndreas Gohr     * @return int If the login was successful
137dd87735dSAndreas Gohr     */
138dd87735dSAndreas Gohr    public function login($user, $pass)
139dd87735dSAndreas Gohr    {
140dd87735dSAndreas Gohr        global $conf;
141104a3b7cSAndreas Gohr        /** @var AuthPlugin $auth */
142dd87735dSAndreas Gohr        global $auth;
143dd87735dSAndreas Gohr
144dd87735dSAndreas Gohr        if (!$conf['useacl']) return 0;
1456547cfc7SGerrit Uitslag        if (!$auth instanceof AuthPlugin) return 0;
146dd87735dSAndreas Gohr
147dd87735dSAndreas Gohr        @session_start(); // reopen session for login
14881e99965SPhy        $ok = null;
149dd87735dSAndreas Gohr        if ($auth->canDo('external')) {
150dd87735dSAndreas Gohr            $ok = $auth->trustExternal($user, $pass, false);
15181e99965SPhy        }
15281e99965SPhy        if ($ok === null) {
153104a3b7cSAndreas Gohr            $evdata = [
154dd87735dSAndreas Gohr                'user' => $user,
155dd87735dSAndreas Gohr                'password' => $pass,
156dd87735dSAndreas Gohr                'sticky' => false,
157104a3b7cSAndreas Gohr                'silent' => true
158104a3b7cSAndreas Gohr            ];
159cbb44eabSAndreas Gohr            $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
160dd87735dSAndreas Gohr        }
161dd87735dSAndreas Gohr        session_write_close(); // we're done with the session
162dd87735dSAndreas Gohr
163dd87735dSAndreas Gohr        return $ok;
164dd87735dSAndreas Gohr    }
165dd87735dSAndreas Gohr
166dd87735dSAndreas Gohr    /**
167dd87735dSAndreas Gohr     * Log off
168dd87735dSAndreas Gohr     *
1698a9282a2SAndreas Gohr     * Attempt to log out the current user, deleting the appropriate cookies
1708a9282a2SAndreas Gohr     *
1716cce3332SAndreas Gohr     * Use of this mechanism is discouraged. Using token authentication is preferred.
1726cce3332SAndreas Gohr     *
1738a9282a2SAndreas Gohr     * @return int 0 on failure, 1 on success
174dd87735dSAndreas Gohr     */
175dd87735dSAndreas Gohr    public function logoff()
176dd87735dSAndreas Gohr    {
177dd87735dSAndreas Gohr        global $conf;
178dd87735dSAndreas Gohr        global $auth;
179dd87735dSAndreas Gohr        if (!$conf['useacl']) return 0;
1806547cfc7SGerrit Uitslag        if (!$auth instanceof AuthPlugin) return 0;
181dd87735dSAndreas Gohr
182dd87735dSAndreas Gohr        auth_logoff();
183dd87735dSAndreas Gohr
184dd87735dSAndreas Gohr        return 1;
185dd87735dSAndreas Gohr    }
186dd87735dSAndreas Gohr
187dd87735dSAndreas Gohr    /**
1886cce3332SAndreas Gohr     * Info about the currently authenticated user
1896cce3332SAndreas Gohr     *
1906cce3332SAndreas Gohr     * @return User
1916cce3332SAndreas Gohr     */
1926cce3332SAndreas Gohr    public function whoAmI()
1936cce3332SAndreas Gohr    {
19458ae4747SAndreas Gohr        return new User();
1956cce3332SAndreas Gohr    }
1966cce3332SAndreas Gohr
1976cce3332SAndreas Gohr    /**
1986cce3332SAndreas Gohr     * Check ACL Permissions
1996cce3332SAndreas Gohr     *
2006cce3332SAndreas Gohr     * This call allows to check the permissions for a given page/media and user/group combination.
2016cce3332SAndreas Gohr     * If no user/group is given, the current user is used.
2026cce3332SAndreas Gohr     *
2036cce3332SAndreas Gohr     * Read the link below to learn more about the permission levels.
2046cce3332SAndreas Gohr     *
2056cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/acl#background_info
2066cce3332SAndreas Gohr     * @param string $page A page or media ID
2076cce3332SAndreas Gohr     * @param string $user username
2086cce3332SAndreas Gohr     * @param string[] $groups array of groups
2096cce3332SAndreas Gohr     * @return int permission level
210902647e6SAndreas Gohr     * @throws RemoteException
2116cce3332SAndreas Gohr     */
2126cce3332SAndreas Gohr    public function aclCheck($page, $user = '', $groups = [])
2136cce3332SAndreas Gohr    {
2146cce3332SAndreas Gohr        /** @var AuthPlugin $auth */
2156cce3332SAndreas Gohr        global $auth;
2166cce3332SAndreas Gohr
2170eb4820cSAndreas Gohr        $page = $this->checkPage($page, 0, false, AUTH_NONE);
218902647e6SAndreas Gohr
2196cce3332SAndreas Gohr        if ($user === '') {
2206cce3332SAndreas Gohr            return auth_quickaclcheck($page);
2216cce3332SAndreas Gohr        } else {
2226cce3332SAndreas Gohr            if ($groups === []) {
2236cce3332SAndreas Gohr                $userinfo = $auth->getUserData($user);
2246cce3332SAndreas Gohr                if ($userinfo === false) {
2256cce3332SAndreas Gohr                    $groups = [];
2266cce3332SAndreas Gohr                } else {
2276cce3332SAndreas Gohr                    $groups = $userinfo['grps'];
2286cce3332SAndreas Gohr                }
2296cce3332SAndreas Gohr            }
2306cce3332SAndreas Gohr            return auth_aclcheck($page, $user, $groups);
2316cce3332SAndreas Gohr        }
2326cce3332SAndreas Gohr    }
2336cce3332SAndreas Gohr
2346cce3332SAndreas Gohr    // endregion
2356cce3332SAndreas Gohr
2366cce3332SAndreas Gohr    // region pages
2376cce3332SAndreas Gohr
2386cce3332SAndreas Gohr    /**
2396cce3332SAndreas Gohr     * List all pages in the given namespace (and below)
2406cce3332SAndreas Gohr     *
2416cce3332SAndreas Gohr     * Setting the `depth` to `0` and the `namespace` to `""` will return all pages in the wiki.
2426cce3332SAndreas Gohr     *
24358ae4747SAndreas Gohr     * Note: author information is not available in this call.
24458ae4747SAndreas Gohr     *
2456cce3332SAndreas Gohr     * @param string $namespace The namespace to search. Empty string for root namespace
2466cce3332SAndreas Gohr     * @param int $depth How deep to search. 0 for all subnamespaces
2476cce3332SAndreas Gohr     * @param bool $hash Whether to include a MD5 hash of the page content
2486cce3332SAndreas Gohr     * @return Page[] A list of matching pages
2499e6b19e6SAndreas Gohr     * @todo might be a good idea to replace search_allpages with search_universal
2506cce3332SAndreas Gohr     */
2516cce3332SAndreas Gohr    public function listPages($namespace = '', $depth = 1, $hash = false)
2526cce3332SAndreas Gohr    {
2536cce3332SAndreas Gohr        global $conf;
2546cce3332SAndreas Gohr
2556cce3332SAndreas Gohr        $namespace = cleanID($namespace);
2566cce3332SAndreas Gohr
2576cce3332SAndreas Gohr        // shortcut for all pages
2586cce3332SAndreas Gohr        if ($namespace === '' && $depth === 0) {
2596cce3332SAndreas Gohr            return $this->getAllPages($hash);
2606cce3332SAndreas Gohr        }
2616cce3332SAndreas Gohr
2627288c5bdSAndreas Gohr        // search_allpages handles depth weird, we need to add the given namespace depth
2637288c5bdSAndreas Gohr        if ($depth) {
2647288c5bdSAndreas Gohr            $depth += substr_count($namespace, ':') + 1;
2657288c5bdSAndreas Gohr        }
2667288c5bdSAndreas Gohr
2676cce3332SAndreas Gohr        // run our search iterator to get the pages
2686cce3332SAndreas Gohr        $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
2696cce3332SAndreas Gohr        $data = [];
2706cce3332SAndreas Gohr        $opts['skipacl'] = 0;
2717288c5bdSAndreas Gohr        $opts['depth'] = $depth;
2726cce3332SAndreas Gohr        $opts['hash'] = $hash;
2736cce3332SAndreas Gohr        search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
2746cce3332SAndreas Gohr
275d48c2b25SAndreas Gohr        return array_map(static fn($item) => new Page(
27658ae4747SAndreas Gohr            $item['id'],
27758ae4747SAndreas Gohr            0, // we're searching current revisions only
27858ae4747SAndreas Gohr            $item['mtime'],
2799e6b19e6SAndreas Gohr            '', // not returned by search_allpages
28058ae4747SAndreas Gohr            $item['size'],
2819e6b19e6SAndreas Gohr            null, // not returned by search_allpages
28258ae4747SAndreas Gohr            $item['hash'] ?? ''
28358ae4747SAndreas Gohr        ), $data);
2846cce3332SAndreas Gohr    }
2856cce3332SAndreas Gohr
2866cce3332SAndreas Gohr    /**
2876cce3332SAndreas Gohr     * Get all pages at once
2886cce3332SAndreas Gohr     *
2896cce3332SAndreas Gohr     * This is uses the page index and is quicker than iterating which is done in listPages()
2906cce3332SAndreas Gohr     *
2916cce3332SAndreas Gohr     * @return Page[] A list of all pages
2926cce3332SAndreas Gohr     * @see listPages()
2936cce3332SAndreas Gohr     */
2946cce3332SAndreas Gohr    protected function getAllPages($hash = false)
2956cce3332SAndreas Gohr    {
2966cce3332SAndreas Gohr        $list = [];
297*83b3acccSAndreas Gohr        $pages = (new Indexer())->getAllPages();
2986cce3332SAndreas Gohr        Sort::ksort($pages);
2996cce3332SAndreas Gohr
3006cce3332SAndreas Gohr        foreach (array_keys($pages) as $idx) {
3016cce3332SAndreas Gohr            $perm = auth_quickaclcheck($pages[$idx]);
3026cce3332SAndreas Gohr            if ($perm < AUTH_READ || isHiddenPage($pages[$idx]) || !page_exists($pages[$idx])) {
3036cce3332SAndreas Gohr                continue;
3046cce3332SAndreas Gohr            }
3056cce3332SAndreas Gohr
30658ae4747SAndreas Gohr            $page = new Page($pages[$idx], 0, 0, '', null, $perm);
3076cce3332SAndreas Gohr            if ($hash) $page->calculateHash();
3086cce3332SAndreas Gohr
3096cce3332SAndreas Gohr            $list[] = $page;
3106cce3332SAndreas Gohr        }
3116cce3332SAndreas Gohr
3126cce3332SAndreas Gohr        return $list;
3136cce3332SAndreas Gohr    }
3146cce3332SAndreas Gohr
3156cce3332SAndreas Gohr    /**
3166cce3332SAndreas Gohr     * Do a fulltext search
3176cce3332SAndreas Gohr     *
3186cce3332SAndreas Gohr     * This executes a full text search and returns the results. The query uses the standard
3196cce3332SAndreas Gohr     * DokuWiki search syntax.
3206cce3332SAndreas Gohr     *
3216cce3332SAndreas Gohr     * Snippets are provided for the first 15 results only. The title is either the first heading
3226cce3332SAndreas Gohr     * or the page id depending on the wiki's configuration.
3236cce3332SAndreas Gohr     *
3246cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/search#syntax
3256cce3332SAndreas Gohr     * @param string $query The search query as supported by the DokuWiki search
3266cce3332SAndreas Gohr     * @return PageHit[] A list of matching pages
3276cce3332SAndreas Gohr     */
3286cce3332SAndreas Gohr    public function searchPages($query)
3296cce3332SAndreas Gohr    {
3306cce3332SAndreas Gohr        $regex = [];
3319df9f0c8SAndreas Gohr        $FulltextSearch = new FulltextSearch();
3329df9f0c8SAndreas Gohr        $data = $FulltextSearch->pageSearch($query, $regex);
3336cce3332SAndreas Gohr        $pages = [];
3346cce3332SAndreas Gohr
3356cce3332SAndreas Gohr        // prepare additional data
3366cce3332SAndreas Gohr        $idx = 0;
3376cce3332SAndreas Gohr        foreach ($data as $id => $score) {
3386cce3332SAndreas Gohr            if ($idx < FT_SNIPPET_NUMBER) {
3399df9f0c8SAndreas Gohr                $snippet = $FulltextSearch->snippet($id, $regex);
3406cce3332SAndreas Gohr                $idx++;
3416cce3332SAndreas Gohr            } else {
3426cce3332SAndreas Gohr                $snippet = '';
3436cce3332SAndreas Gohr            }
3446cce3332SAndreas Gohr
34558ae4747SAndreas Gohr            $pages[] = new PageHit(
34658ae4747SAndreas Gohr                $id,
34758ae4747SAndreas Gohr                $snippet,
34858ae4747SAndreas Gohr                $score,
34958ae4747SAndreas Gohr                useHeading('navigation') ? p_get_first_heading($id) : $id
35058ae4747SAndreas Gohr            );
3516cce3332SAndreas Gohr        }
3526cce3332SAndreas Gohr        return $pages;
3536cce3332SAndreas Gohr    }
3546cce3332SAndreas Gohr
3556cce3332SAndreas Gohr    /**
3566cce3332SAndreas Gohr     * Get recent page changes
3576cce3332SAndreas Gohr     *
3586cce3332SAndreas Gohr     * Returns a list of recent changes to wiki pages. The results can be limited to changes newer than
3596cce3332SAndreas Gohr     * a given timestamp.
3606cce3332SAndreas Gohr     *
3616cce3332SAndreas Gohr     * Only changes within the configured `$conf['recent']` range are returned. This is the default
3626cce3332SAndreas Gohr     * when no timestamp is given.
3636cce3332SAndreas Gohr     *
3646cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:recent
3656cce3332SAndreas Gohr     * @param int $timestamp Only show changes newer than this unix timestamp
36658ae4747SAndreas Gohr     * @return PageChange[]
3676cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
3686cce3332SAndreas Gohr     * @author Michael Hamann <michael@content-space.de>
3696cce3332SAndreas Gohr     */
3706cce3332SAndreas Gohr    public function getRecentPageChanges($timestamp = 0)
3716cce3332SAndreas Gohr    {
3726cce3332SAndreas Gohr        $recents = getRecentsSince($timestamp);
3736cce3332SAndreas Gohr
3746cce3332SAndreas Gohr        $changes = [];
3756cce3332SAndreas Gohr        foreach ($recents as $recent) {
37658ae4747SAndreas Gohr            $changes[] = new PageChange(
37758ae4747SAndreas Gohr                $recent['id'],
37858ae4747SAndreas Gohr                $recent['date'],
37958ae4747SAndreas Gohr                $recent['user'],
38058ae4747SAndreas Gohr                $recent['ip'],
38158ae4747SAndreas Gohr                $recent['sum'],
38258ae4747SAndreas Gohr                $recent['type'],
38358ae4747SAndreas Gohr                $recent['sizechange']
38458ae4747SAndreas Gohr            );
3856cce3332SAndreas Gohr        }
3866cce3332SAndreas Gohr
3876cce3332SAndreas Gohr        return $changes;
3886cce3332SAndreas Gohr    }
3896cce3332SAndreas Gohr
3906cce3332SAndreas Gohr    /**
3916cce3332SAndreas Gohr     * Get a wiki page's syntax
3926cce3332SAndreas Gohr     *
3936cce3332SAndreas Gohr     * Returns the syntax of the given page. When no revision is given, the current revision is returned.
3946cce3332SAndreas Gohr     *
3956cce3332SAndreas Gohr     * A non-existing page (or revision) will return an empty string usually. For the current revision
3966cce3332SAndreas Gohr     * a page template will be returned if configured.
3976cce3332SAndreas Gohr     *
3986cce3332SAndreas Gohr     * Read access is required for the page.
3996cce3332SAndreas Gohr     *
4006cce3332SAndreas Gohr     * @param string $page wiki page id
401b115d6dbSAndreas Gohr     * @param int $rev Revision timestamp to access an older revision
4026cce3332SAndreas Gohr     * @return string the syntax of the page
403902647e6SAndreas Gohr     * @throws AccessDeniedException
404902647e6SAndreas Gohr     * @throws RemoteException
4056cce3332SAndreas Gohr     */
406b115d6dbSAndreas Gohr    public function getPage($page, $rev = 0)
4076cce3332SAndreas Gohr    {
4080eb4820cSAndreas Gohr        $page = $this->checkPage($page, $rev, false);
409902647e6SAndreas Gohr
4106cce3332SAndreas Gohr        $text = rawWiki($page, $rev);
4116cce3332SAndreas Gohr        if (!$text && !$rev) {
4126cce3332SAndreas Gohr            return pageTemplate($page);
4136cce3332SAndreas Gohr        } else {
4146cce3332SAndreas Gohr            return $text;
4156cce3332SAndreas Gohr        }
4166cce3332SAndreas Gohr    }
4176cce3332SAndreas Gohr
4186cce3332SAndreas Gohr    /**
4196cce3332SAndreas Gohr     * Return a wiki page rendered to HTML
4206cce3332SAndreas Gohr     *
4216cce3332SAndreas Gohr     * The page is rendered to HTML as it would be in the wiki. The HTML consist only of the data for the page
4226cce3332SAndreas Gohr     * content itself, no surrounding structural tags, header, footers, sidebars etc are returned.
4236cce3332SAndreas Gohr     *
4246cce3332SAndreas Gohr     * References in the HTML are relative to the wiki base URL unless the `canonical` configuration is set.
4256cce3332SAndreas Gohr     *
426902647e6SAndreas Gohr     * If the page does not exist, an error is returned.
4276cce3332SAndreas Gohr     *
4286cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:canonical
4296cce3332SAndreas Gohr     * @param string $page page id
430b115d6dbSAndreas Gohr     * @param int $rev revision timestamp
4316cce3332SAndreas Gohr     * @return string Rendered HTML for the page
432902647e6SAndreas Gohr     * @throws AccessDeniedException
433902647e6SAndreas Gohr     * @throws RemoteException
4346cce3332SAndreas Gohr     */
435b115d6dbSAndreas Gohr    public function getPageHTML($page, $rev = 0)
4366cce3332SAndreas Gohr    {
4370eb4820cSAndreas Gohr        $page = $this->checkPage($page, $rev);
438902647e6SAndreas Gohr
4396cce3332SAndreas Gohr        return (string)p_wiki_xhtml($page, $rev, false);
4406cce3332SAndreas Gohr    }
4416cce3332SAndreas Gohr
4426cce3332SAndreas Gohr    /**
4436cce3332SAndreas Gohr     * Return some basic data about a page
4446cce3332SAndreas Gohr     *
4456cce3332SAndreas Gohr     * The call will return an error if the requested page does not exist.
4466cce3332SAndreas Gohr     *
4476cce3332SAndreas Gohr     * Read access is required for the page.
4486cce3332SAndreas Gohr     *
4496cce3332SAndreas Gohr     * @param string $page page id
450b115d6dbSAndreas Gohr     * @param int $rev revision timestamp
4516cce3332SAndreas Gohr     * @param bool $author whether to include the author information
4526cce3332SAndreas Gohr     * @param bool $hash whether to include the MD5 hash of the page content
4536cce3332SAndreas Gohr     * @return Page
454902647e6SAndreas Gohr     * @throws AccessDeniedException
455902647e6SAndreas Gohr     * @throws RemoteException
4566cce3332SAndreas Gohr     */
45758ae4747SAndreas Gohr    public function getPageInfo($page, $rev = 0, $author = false, $hash = false)
4586cce3332SAndreas Gohr    {
4590eb4820cSAndreas Gohr        $page = $this->checkPage($page, $rev);
4606cce3332SAndreas Gohr
46158ae4747SAndreas Gohr        $result = new Page($page, $rev);
4626cce3332SAndreas Gohr        if ($author) $result->retrieveAuthor();
4636cce3332SAndreas Gohr        if ($hash) $result->calculateHash();
4646cce3332SAndreas Gohr
4656cce3332SAndreas Gohr        return $result;
4666cce3332SAndreas Gohr    }
4676cce3332SAndreas Gohr
4686cce3332SAndreas Gohr    /**
4696cce3332SAndreas Gohr     * Returns a list of available revisions of a given wiki page
4706cce3332SAndreas Gohr     *
471a8f218d4SAndreas Gohr     * The number of returned pages is set by `$conf['recent']`, but non accessible revisions
4726cce3332SAndreas Gohr     * are skipped, so less than that may be returned.
4736cce3332SAndreas Gohr     *
4746cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:recent
4756cce3332SAndreas Gohr     * @param string $page page id
4766cce3332SAndreas Gohr     * @param int $first skip the first n changelog lines, 0 starts at the current revision
47758ae4747SAndreas Gohr     * @return PageChange[]
478902647e6SAndreas Gohr     * @throws AccessDeniedException
479902647e6SAndreas Gohr     * @throws RemoteException
4806cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
4816cce3332SAndreas Gohr     */
4825bef72beSAndreas Gohr    public function getPageHistory($page, $first = 0)
4836cce3332SAndreas Gohr    {
4846cce3332SAndreas Gohr        global $conf;
4856cce3332SAndreas Gohr
4860eb4820cSAndreas Gohr        $page = $this->checkPage($page, 0, false);
4876cce3332SAndreas Gohr
4886cce3332SAndreas Gohr        $pagelog = new PageChangeLog($page);
4896cce3332SAndreas Gohr        $pagelog->setChunkSize(1024);
4906cce3332SAndreas Gohr        // old revisions are counted from 0, so we need to subtract 1 for the current one
4916cce3332SAndreas Gohr        $revisions = $pagelog->getRevisions($first - 1, $conf['recent']);
4926cce3332SAndreas Gohr
4936cce3332SAndreas Gohr        $result = [];
4946cce3332SAndreas Gohr        foreach ($revisions as $rev) {
4956cce3332SAndreas Gohr            if (!page_exists($page, $rev)) continue; // skip non-existing revisions
4966cce3332SAndreas Gohr            $info = $pagelog->getRevisionInfo($rev);
4976cce3332SAndreas Gohr
49858ae4747SAndreas Gohr            $result[] = new PageChange(
49958ae4747SAndreas Gohr                $page,
50058ae4747SAndreas Gohr                $rev,
50158ae4747SAndreas Gohr                $info['user'],
50258ae4747SAndreas Gohr                $info['ip'],
50358ae4747SAndreas Gohr                $info['sum'],
50458ae4747SAndreas Gohr                $info['type'],
50558ae4747SAndreas Gohr                $info['sizechange']
50658ae4747SAndreas Gohr            );
5076cce3332SAndreas Gohr        }
5086cce3332SAndreas Gohr
5096cce3332SAndreas Gohr        return $result;
5106cce3332SAndreas Gohr    }
5116cce3332SAndreas Gohr
5126cce3332SAndreas Gohr    /**
5136cce3332SAndreas Gohr     * Get a page's links
5146cce3332SAndreas Gohr     *
5156cce3332SAndreas Gohr     * This returns a list of links found in the given page. This includes internal, external and interwiki links
5166cce3332SAndreas Gohr     *
517d1f06eb4SAndreas Gohr     * If a link occurs multiple times on the page, it will be returned multiple times.
518d1f06eb4SAndreas Gohr     *
519902647e6SAndreas Gohr     * Read access for the given page is needed and page has to exist.
5206cce3332SAndreas Gohr     *
5216cce3332SAndreas Gohr     * @param string $page page id
5226cce3332SAndreas Gohr     * @return Link[] A list of links found on the given page
523902647e6SAndreas Gohr     * @throws AccessDeniedException
524902647e6SAndreas Gohr     * @throws RemoteException
5256cce3332SAndreas Gohr     * @todo returning link titles would be a nice addition
5266cce3332SAndreas Gohr     * @todo hash handling seems not to be correct
527d1f06eb4SAndreas Gohr     * @todo maybe return the same link only once?
528902647e6SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
5296cce3332SAndreas Gohr     */
5306cce3332SAndreas Gohr    public function getPageLinks($page)
5316cce3332SAndreas Gohr    {
532902647e6SAndreas Gohr        $page = $this->checkPage($page);
5336cce3332SAndreas Gohr
5346cce3332SAndreas Gohr        // resolve page instructions
53556bbc10dSAndreas Gohr        $ins = p_cached_instructions(wikiFN($page), false, $page);
5366cce3332SAndreas Gohr
5376cce3332SAndreas Gohr        // instantiate new Renderer - needed for interwiki links
5386cce3332SAndreas Gohr        $Renderer = new Doku_Renderer_xhtml();
5396cce3332SAndreas Gohr        $Renderer->interwiki = getInterwiki();
5406cce3332SAndreas Gohr
5416cce3332SAndreas Gohr        // parse instructions
5426cce3332SAndreas Gohr        $links = [];
5436cce3332SAndreas Gohr        foreach ($ins as $in) {
5446cce3332SAndreas Gohr            switch ($in[0]) {
5456cce3332SAndreas Gohr                case 'internallink':
54658ae4747SAndreas Gohr                    $links[] = new Link('local', $in[1][0], wl($in[1][0]));
5476cce3332SAndreas Gohr                    break;
5486cce3332SAndreas Gohr                case 'externallink':
54958ae4747SAndreas Gohr                    $links[] = new Link('extern', $in[1][0], $in[1][0]);
5506cce3332SAndreas Gohr                    break;
5516cce3332SAndreas Gohr                case 'interwikilink':
5526cce3332SAndreas Gohr                    $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]);
55358ae4747SAndreas Gohr                    $links[] = new Link('interwiki', $in[1][0], $url);
5546cce3332SAndreas Gohr                    break;
5556cce3332SAndreas Gohr            }
5566cce3332SAndreas Gohr        }
5576cce3332SAndreas Gohr
5586cce3332SAndreas Gohr        return ($links);
5596cce3332SAndreas Gohr    }
5606cce3332SAndreas Gohr
5616cce3332SAndreas Gohr    /**
5626cce3332SAndreas Gohr     * Get a page's backlinks
5636cce3332SAndreas Gohr     *
5646cce3332SAndreas Gohr     * A backlink is a wiki link on another page that links to the given page.
5656cce3332SAndreas Gohr     *
566902647e6SAndreas Gohr     * Only links from pages readable by the current user are returned. The page itself
567902647e6SAndreas Gohr     * needs to be readable. Otherwise an error is returned.
5686cce3332SAndreas Gohr     *
5696cce3332SAndreas Gohr     * @param string $page page id
5706cce3332SAndreas Gohr     * @return string[] A list of pages linking to the given page
571902647e6SAndreas Gohr     * @throws AccessDeniedException
572902647e6SAndreas Gohr     * @throws RemoteException
5736cce3332SAndreas Gohr     */
5746cce3332SAndreas Gohr    public function getPageBackLinks($page)
5756cce3332SAndreas Gohr    {
5760eb4820cSAndreas Gohr        $page = $this->checkPage($page, 0, false);
5779df9f0c8SAndreas Gohr        return (new MetadataIndex())->backlinks($page);
5786cce3332SAndreas Gohr   }
5796cce3332SAndreas Gohr
5806cce3332SAndreas Gohr    /**
5816cce3332SAndreas Gohr     * Lock the given set of pages
5826cce3332SAndreas Gohr     *
5836cce3332SAndreas Gohr     * This call will try to lock all given pages. It will return a list of pages that were
5846cce3332SAndreas Gohr     * successfully locked. If a page could not be locked, eg. because a different user is
5856cce3332SAndreas Gohr     * currently holding a lock, that page will be missing from the returned list.
5866cce3332SAndreas Gohr     *
5876cce3332SAndreas Gohr     * You should always ensure that the list of returned pages matches the given list of
5886cce3332SAndreas Gohr     * pages. It's up to you to decide how to handle failed locking.
5896cce3332SAndreas Gohr     *
5906cce3332SAndreas Gohr     * Note: you can only lock pages that you have write access for. It is possible to create
5916cce3332SAndreas Gohr     * a lock for a page that does not exist, yet.
5926cce3332SAndreas Gohr     *
5936cce3332SAndreas Gohr     * Note: it is not necessary to lock a page before saving it. The `savePage()` call will
5946cce3332SAndreas Gohr     * automatically lock and unlock the page for you. However if you plan to do related
5956cce3332SAndreas Gohr     * operations on multiple pages, locking them all at once beforehand can be useful.
5966cce3332SAndreas Gohr     *
5976cce3332SAndreas Gohr     * @param string[] $pages A list of pages to lock
5986cce3332SAndreas Gohr     * @return string[] A list of pages that were successfully locked
5996cce3332SAndreas Gohr     */
6006cce3332SAndreas Gohr    public function lockPages($pages)
6016cce3332SAndreas Gohr    {
6026cce3332SAndreas Gohr        $locked = [];
6036cce3332SAndreas Gohr
6046cce3332SAndreas Gohr        foreach ($pages as $id) {
605902647e6SAndreas Gohr            $id = cleanID($id);
6066cce3332SAndreas Gohr            if ($id === '') continue;
6076cce3332SAndreas Gohr            if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) {
6086cce3332SAndreas Gohr                continue;
6096cce3332SAndreas Gohr            }
6106cce3332SAndreas Gohr            lock($id);
6116cce3332SAndreas Gohr            $locked[] = $id;
6126cce3332SAndreas Gohr        }
6136cce3332SAndreas Gohr        return $locked;
6146cce3332SAndreas Gohr    }
6156cce3332SAndreas Gohr
6166cce3332SAndreas Gohr    /**
6176cce3332SAndreas Gohr     * Unlock the given set of pages
6186cce3332SAndreas Gohr     *
6196cce3332SAndreas Gohr     * This call will try to unlock all given pages. It will return a list of pages that were
6206cce3332SAndreas Gohr     * successfully unlocked. If a page could not be unlocked, eg. because a different user is
6216cce3332SAndreas Gohr     * currently holding a lock, that page will be missing from the returned list.
6226cce3332SAndreas Gohr     *
6236cce3332SAndreas Gohr     * You should always ensure that the list of returned pages matches the given list of
6246cce3332SAndreas Gohr     * pages. It's up to you to decide how to handle failed unlocking.
6256cce3332SAndreas Gohr     *
6266cce3332SAndreas Gohr     * Note: you can only unlock pages that you have write access for.
6276cce3332SAndreas Gohr     *
6286cce3332SAndreas Gohr     * @param string[] $pages A list of pages to unlock
6296cce3332SAndreas Gohr     * @return string[] A list of pages that were successfully unlocked
6306cce3332SAndreas Gohr     */
6316cce3332SAndreas Gohr    public function unlockPages($pages)
6326cce3332SAndreas Gohr    {
6336cce3332SAndreas Gohr        $unlocked = [];
6346cce3332SAndreas Gohr
6356cce3332SAndreas Gohr        foreach ($pages as $id) {
636902647e6SAndreas Gohr            $id = cleanID($id);
6376cce3332SAndreas Gohr            if ($id === '') continue;
6386cce3332SAndreas Gohr            if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) {
6396cce3332SAndreas Gohr                continue;
6406cce3332SAndreas Gohr            }
6416cce3332SAndreas Gohr            $unlocked[] = $id;
6426cce3332SAndreas Gohr        }
6436cce3332SAndreas Gohr
6446cce3332SAndreas Gohr        return $unlocked;
6456cce3332SAndreas Gohr    }
6466cce3332SAndreas Gohr
6476cce3332SAndreas Gohr    /**
6486cce3332SAndreas Gohr     * Save a wiki page
6496cce3332SAndreas Gohr     *
6506cce3332SAndreas Gohr     * Saves the given wiki text to the given page. If the page does not exist, it will be created.
6516cce3332SAndreas Gohr     * Just like in the wiki, saving an empty text will delete the page.
6526cce3332SAndreas Gohr     *
6536cce3332SAndreas Gohr     * You need write permissions for the given page and the page may not be locked by another user.
6546cce3332SAndreas Gohr     *
6556cce3332SAndreas Gohr     * @param string $page page id
6566cce3332SAndreas Gohr     * @param string $text wiki text
6576cce3332SAndreas Gohr     * @param string $summary edit summary
6586cce3332SAndreas Gohr     * @param bool $isminor whether this is a minor edit
6596cce3332SAndreas Gohr     * @return bool Returns true on success
6606cce3332SAndreas Gohr     * @throws AccessDeniedException no write access for page
6616cce3332SAndreas Gohr     * @throws RemoteException no id, empty new page or locked
6626cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
6636cce3332SAndreas Gohr     */
6646cce3332SAndreas Gohr    public function savePage($page, $text, $summary = '', $isminor = false)
6656cce3332SAndreas Gohr    {
6666cce3332SAndreas Gohr        global $TEXT;
6676cce3332SAndreas Gohr        global $lang;
6686cce3332SAndreas Gohr
6690eb4820cSAndreas Gohr        $page = $this->checkPage($page, 0, false, AUTH_EDIT);
6706cce3332SAndreas Gohr        $TEXT = cleanText($text);
6716cce3332SAndreas Gohr
6726cce3332SAndreas Gohr
6736cce3332SAndreas Gohr        if (!page_exists($page) && trim($TEXT) == '') {
6746cce3332SAndreas Gohr            throw new RemoteException('Refusing to write an empty new wiki page', 132);
6756cce3332SAndreas Gohr        }
6766cce3332SAndreas Gohr
6776cce3332SAndreas Gohr        // Check, if page is locked
6786cce3332SAndreas Gohr        if (checklock($page)) {
6796cce3332SAndreas Gohr            throw new RemoteException('The page is currently locked', 133);
6806cce3332SAndreas Gohr        }
6816cce3332SAndreas Gohr
6826cce3332SAndreas Gohr        // SPAM check
6836cce3332SAndreas Gohr        if (checkwordblock()) {
684d3856637SAndreas Gohr            throw new RemoteException('The page content was blocked', 134);
6856cce3332SAndreas Gohr        }
6866cce3332SAndreas Gohr
6876cce3332SAndreas Gohr        // autoset summary on new pages
6886cce3332SAndreas Gohr        if (!page_exists($page) && empty($summary)) {
6896cce3332SAndreas Gohr            $summary = $lang['created'];
6906cce3332SAndreas Gohr        }
6916cce3332SAndreas Gohr
6926cce3332SAndreas Gohr        // autoset summary on deleted pages
6936cce3332SAndreas Gohr        if (page_exists($page) && empty($TEXT) && empty($summary)) {
6946cce3332SAndreas Gohr            $summary = $lang['deleted'];
6956cce3332SAndreas Gohr        }
6966cce3332SAndreas Gohr
697902647e6SAndreas Gohr        // FIXME auto set a summary in other cases "API Edit" might be a good idea?
698902647e6SAndreas Gohr
6996cce3332SAndreas Gohr        lock($page);
7006cce3332SAndreas Gohr        saveWikiText($page, $TEXT, $summary, $isminor);
7016cce3332SAndreas Gohr        unlock($page);
7026cce3332SAndreas Gohr
7036cce3332SAndreas Gohr        // run the indexer if page wasn't indexed yet
704*83b3acccSAndreas Gohr        try {
705*83b3acccSAndreas Gohr            (new Indexer())->addPage($page);
706*83b3acccSAndreas Gohr        } catch (\Exception $e) {
707*83b3acccSAndreas Gohr            // indexing failure is non-fatal, the page was saved successfully
708*83b3acccSAndreas Gohr        }
7096cce3332SAndreas Gohr
7106cce3332SAndreas Gohr        return true;
7116cce3332SAndreas Gohr    }
7126cce3332SAndreas Gohr
7136cce3332SAndreas Gohr    /**
7146cce3332SAndreas Gohr     * Appends text to the end of a wiki page
7156cce3332SAndreas Gohr     *
7166cce3332SAndreas Gohr     * If the page does not exist, it will be created. If a page template for the non-existant
7176cce3332SAndreas Gohr     * page is configured, the given text will appended to that template.
7186cce3332SAndreas Gohr     *
7196cce3332SAndreas Gohr     * The call will create a new page revision.
7206cce3332SAndreas Gohr     *
7216cce3332SAndreas Gohr     * You need write permissions for the given page.
7226cce3332SAndreas Gohr     *
7236cce3332SAndreas Gohr     * @param string $page page id
7246cce3332SAndreas Gohr     * @param string $text wiki text
7256cce3332SAndreas Gohr     * @param string $summary edit summary
7266cce3332SAndreas Gohr     * @param bool $isminor whether this is a minor edit
7276cce3332SAndreas Gohr     * @return bool Returns true on success
7286cce3332SAndreas Gohr     * @throws AccessDeniedException
7296cce3332SAndreas Gohr     * @throws RemoteException
7306cce3332SAndreas Gohr     */
731d1f06eb4SAndreas Gohr    public function appendPage($page, $text, $summary = '', $isminor = false)
7326cce3332SAndreas Gohr    {
7336cce3332SAndreas Gohr        $currentpage = $this->getPage($page);
7346cce3332SAndreas Gohr        if (!is_string($currentpage)) {
7356cce3332SAndreas Gohr            $currentpage = '';
7366cce3332SAndreas Gohr        }
7376cce3332SAndreas Gohr        return $this->savePage($page, $currentpage . $text, $summary, $isminor);
7386cce3332SAndreas Gohr    }
7396cce3332SAndreas Gohr
7406cce3332SAndreas Gohr    // endregion
7416cce3332SAndreas Gohr
7426cce3332SAndreas Gohr    // region media
7436cce3332SAndreas Gohr
7446cce3332SAndreas Gohr    /**
7456cce3332SAndreas Gohr     * List all media files in the given namespace (and below)
7466cce3332SAndreas Gohr     *
7476cce3332SAndreas Gohr     * Setting the `depth` to `0` and the `namespace` to `""` will return all media files in the wiki.
7486cce3332SAndreas Gohr     *
7496cce3332SAndreas Gohr     * When `pattern` is given, it needs to be a valid regular expression as understood by PHP's
7506cce3332SAndreas Gohr     * `preg_match()` including delimiters.
7516cce3332SAndreas Gohr     * The pattern is matched against the full media ID, including the namespace.
7526cce3332SAndreas Gohr     *
7536cce3332SAndreas Gohr     * @link https://www.php.net/manual/en/reference.pcre.pattern.syntax.php
7546cce3332SAndreas Gohr     * @param string $namespace The namespace to search. Empty string for root namespace
7556cce3332SAndreas Gohr     * @param string $pattern A regular expression to filter the returned files
7566cce3332SAndreas Gohr     * @param int $depth How deep to search. 0 for all subnamespaces
7576cce3332SAndreas Gohr     * @param bool $hash Whether to include a MD5 hash of the media content
7586cce3332SAndreas Gohr     * @return Media[]
7596cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
7606cce3332SAndreas Gohr     */
7616cce3332SAndreas Gohr    public function listMedia($namespace = '', $pattern = '', $depth = 1, $hash = false)
7626cce3332SAndreas Gohr    {
7636cce3332SAndreas Gohr        global $conf;
7646cce3332SAndreas Gohr
7656cce3332SAndreas Gohr        $namespace = cleanID($namespace);
7666cce3332SAndreas Gohr
7676cce3332SAndreas Gohr        $options = [
7686cce3332SAndreas Gohr            'skipacl' => 0,
7696cce3332SAndreas Gohr            'depth' => $depth,
7706cce3332SAndreas Gohr            'hash' => $hash,
7716cce3332SAndreas Gohr            'pattern' => $pattern,
7726cce3332SAndreas Gohr        ];
7736cce3332SAndreas Gohr
7746cce3332SAndreas Gohr        $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
7756cce3332SAndreas Gohr        $data = [];
7766cce3332SAndreas Gohr        search($data, $conf['mediadir'], 'search_media', $options, $dir);
777d48c2b25SAndreas Gohr        return array_map(static fn($item) => new Media(
77858ae4747SAndreas Gohr            $item['id'],
77958ae4747SAndreas Gohr            0, // we're searching current revisions only
78058ae4747SAndreas Gohr            $item['mtime'],
78158ae4747SAndreas Gohr            $item['size'],
78258ae4747SAndreas Gohr            $item['perm'],
78358ae4747SAndreas Gohr            $item['isimg'],
78458ae4747SAndreas Gohr            $item['hash'] ?? ''
78558ae4747SAndreas Gohr        ), $data);
7866cce3332SAndreas Gohr    }
7876cce3332SAndreas Gohr
7886cce3332SAndreas Gohr    /**
7896cce3332SAndreas Gohr     * Get recent media changes
7906cce3332SAndreas Gohr     *
7916cce3332SAndreas Gohr     * Returns a list of recent changes to media files. The results can be limited to changes newer than
7926cce3332SAndreas Gohr     * a given timestamp.
7936cce3332SAndreas Gohr     *
7946cce3332SAndreas Gohr     * Only changes within the configured `$conf['recent']` range are returned. This is the default
7956cce3332SAndreas Gohr     * when no timestamp is given.
7966cce3332SAndreas Gohr     *
7976cce3332SAndreas Gohr     * @link https://www.dokuwiki.org/config:recent
7986cce3332SAndreas Gohr     * @param int $timestamp Only show changes newer than this unix timestamp
79958ae4747SAndreas Gohr     * @return MediaChange[]
8006cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
8016cce3332SAndreas Gohr     * @author Michael Hamann <michael@content-space.de>
8026cce3332SAndreas Gohr     */
8036cce3332SAndreas Gohr    public function getRecentMediaChanges($timestamp = 0)
8046cce3332SAndreas Gohr    {
8056cce3332SAndreas Gohr
8066cce3332SAndreas Gohr        $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
8076cce3332SAndreas Gohr
8086cce3332SAndreas Gohr        $changes = [];
8096cce3332SAndreas Gohr        foreach ($recents as $recent) {
81058ae4747SAndreas Gohr            $changes[] = new MediaChange(
81158ae4747SAndreas Gohr                $recent['id'],
81258ae4747SAndreas Gohr                $recent['date'],
81358ae4747SAndreas Gohr                $recent['user'],
81458ae4747SAndreas Gohr                $recent['ip'],
81558ae4747SAndreas Gohr                $recent['sum'],
81658ae4747SAndreas Gohr                $recent['type'],
81758ae4747SAndreas Gohr                $recent['sizechange']
81858ae4747SAndreas Gohr            );
8196cce3332SAndreas Gohr        }
8206cce3332SAndreas Gohr
8216cce3332SAndreas Gohr        return $changes;
8226cce3332SAndreas Gohr    }
8236cce3332SAndreas Gohr
8246cce3332SAndreas Gohr    /**
8256cce3332SAndreas Gohr     * Get a media file's content
8266cce3332SAndreas Gohr     *
8276cce3332SAndreas Gohr     * Returns the content of the given media file. When no revision is given, the current revision is returned.
8286cce3332SAndreas Gohr     *
8296cce3332SAndreas Gohr     * @link https://en.wikipedia.org/wiki/Base64
8306cce3332SAndreas Gohr     * @param string $media file id
8316cce3332SAndreas Gohr     * @param int $rev revision timestamp
8326cce3332SAndreas Gohr     * @return string Base64 encoded media file contents
8336cce3332SAndreas Gohr     * @throws AccessDeniedException no permission for media
8346cce3332SAndreas Gohr     * @throws RemoteException not exist
8356cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
8366cce3332SAndreas Gohr     *
8376cce3332SAndreas Gohr     */
838b115d6dbSAndreas Gohr    public function getMedia($media, $rev = 0)
8396cce3332SAndreas Gohr    {
8406cce3332SAndreas Gohr        $media = cleanID($media);
8416cce3332SAndreas Gohr        if (auth_quickaclcheck($media) < AUTH_READ) {
842d3856637SAndreas Gohr            throw new AccessDeniedException('You are not allowed to read this media file', 211);
8436cce3332SAndreas Gohr        }
8446cce3332SAndreas Gohr
84572b0e523SAndreas Gohr        // was the current revision requested?
84672b0e523SAndreas Gohr        if ($this->isCurrentMediaRev($media, $rev)) {
84772b0e523SAndreas Gohr            $rev = 0;
84872b0e523SAndreas Gohr        }
84972b0e523SAndreas Gohr
8506cce3332SAndreas Gohr        $file = mediaFN($media, $rev);
8516cce3332SAndreas Gohr        if (!@ file_exists($file)) {
852d1f06eb4SAndreas Gohr            throw new RemoteException('The requested media file (revision) does not exist', 221);
8536cce3332SAndreas Gohr        }
8546cce3332SAndreas Gohr
8556cce3332SAndreas Gohr        $data = io_readFile($file, false);
8566cce3332SAndreas Gohr        return base64_encode($data);
8576cce3332SAndreas Gohr    }
8586cce3332SAndreas Gohr
8596cce3332SAndreas Gohr    /**
8606cce3332SAndreas Gohr     * Return info about a media file
8616cce3332SAndreas Gohr     *
8626cce3332SAndreas Gohr     * The call will return an error if the requested media file does not exist.
8636cce3332SAndreas Gohr     *
8646cce3332SAndreas Gohr     * Read access is required for the media file.
8656cce3332SAndreas Gohr     *
8666cce3332SAndreas Gohr     * @param string $media file id
8676cce3332SAndreas Gohr     * @param int $rev revision timestamp
868d1f06eb4SAndreas Gohr     * @param bool $author whether to include the author information
8696cce3332SAndreas Gohr     * @param bool $hash whether to include the MD5 hash of the media content
8706cce3332SAndreas Gohr     * @return Media
8716cce3332SAndreas Gohr     * @throws AccessDeniedException no permission for media
8726cce3332SAndreas Gohr     * @throws RemoteException if not exist
8736cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
8746cce3332SAndreas Gohr     */
875d1f06eb4SAndreas Gohr    public function getMediaInfo($media, $rev = 0, $author = false, $hash = false)
8766cce3332SAndreas Gohr    {
8776cce3332SAndreas Gohr        $media = cleanID($media);
8786cce3332SAndreas Gohr        if (auth_quickaclcheck($media) < AUTH_READ) {
879d3856637SAndreas Gohr            throw new AccessDeniedException('You are not allowed to read this media file', 211);
8806cce3332SAndreas Gohr        }
88172b0e523SAndreas Gohr
88272b0e523SAndreas Gohr        // was the current revision requested?
88372b0e523SAndreas Gohr        if ($this->isCurrentMediaRev($media, $rev)) {
88472b0e523SAndreas Gohr            $rev = 0;
88572b0e523SAndreas Gohr        }
88672b0e523SAndreas Gohr
8876cce3332SAndreas Gohr        if (!media_exists($media, $rev)) {
8886cce3332SAndreas Gohr            throw new RemoteException('The requested media file does not exist', 221);
8896cce3332SAndreas Gohr        }
8906cce3332SAndreas Gohr
89158ae4747SAndreas Gohr        $info = new Media($media, $rev);
8926cce3332SAndreas Gohr        if ($hash) $info->calculateHash();
893d1f06eb4SAndreas Gohr        if ($author) $info->retrieveAuthor();
8946cce3332SAndreas Gohr
8956cce3332SAndreas Gohr        return $info;
8966cce3332SAndreas Gohr    }
8976cce3332SAndreas Gohr
8986cce3332SAndreas Gohr    /**
899885b0fb0SAnushka Trivedi     * Returns the pages that use a given media file
900885b0fb0SAnushka Trivedi     *
901885b0fb0SAnushka Trivedi     * The call will return an error if the requested media file does not exist.
902885b0fb0SAnushka Trivedi     *
903885b0fb0SAnushka Trivedi     * Read access is required for the media file.
904885b0fb0SAnushka Trivedi     *
90599a3dafaSAndreas Gohr     * Since API Version 13
90699a3dafaSAndreas Gohr     *
907885b0fb0SAnushka Trivedi     * @param string $media file id
908885b0fb0SAnushka Trivedi     * @return string[] A list of pages linking to the given page
909885b0fb0SAnushka Trivedi     * @throws AccessDeniedException no permission for media
910885b0fb0SAnushka Trivedi     * @throws RemoteException if not exist
911885b0fb0SAnushka Trivedi     */
912885b0fb0SAnushka Trivedi    public function getMediaUsage($media)
913885b0fb0SAnushka Trivedi    {
914885b0fb0SAnushka Trivedi        $media = cleanID($media);
915885b0fb0SAnushka Trivedi        if (auth_quickaclcheck($media) < AUTH_READ) {
916885b0fb0SAnushka Trivedi            throw new AccessDeniedException('You are not allowed to read this media file', 211);
917885b0fb0SAnushka Trivedi        }
91899a3dafaSAndreas Gohr        if (!media_exists($media)) {
919885b0fb0SAnushka Trivedi            throw new RemoteException('The requested media file does not exist', 221);
920885b0fb0SAnushka Trivedi        }
921885b0fb0SAnushka Trivedi
922885b0fb0SAnushka Trivedi        return ft_mediause($media);
923885b0fb0SAnushka Trivedi    }
924885b0fb0SAnushka Trivedi
925885b0fb0SAnushka Trivedi    /**
9260e1bcd98SAndreas Gohr     * Returns a list of available revisions of a given media file
92761d21e86Skuangfio     *
928a8f218d4SAndreas Gohr     * The number of returned files is set by `$conf['recent']`, but non accessible revisions
929a8f218d4SAndreas Gohr     * are skipped, so less than that may be returned.
930a8f218d4SAndreas Gohr     *
9310e1bcd98SAndreas Gohr     * Since API Version 14
93261d21e86Skuangfio     *
93361d21e86Skuangfio     * @link https://www.dokuwiki.org/config:recent
93461d21e86Skuangfio     * @param string $media file id
93561d21e86Skuangfio     * @param int $first skip the first n changelog lines, 0 starts at the current revision
93661d21e86Skuangfio     * @return MediaChange[]
93761d21e86Skuangfio     * @throws AccessDeniedException
93861d21e86Skuangfio     * @throws RemoteException
93961d21e86Skuangfio     * @author
94061d21e86Skuangfio     */
94161d21e86Skuangfio    public function getMediaHistory($media, $first = 0)
94261d21e86Skuangfio    {
94361d21e86Skuangfio        global $conf;
94461d21e86Skuangfio
94561d21e86Skuangfio        $media = cleanID($media);
94661d21e86Skuangfio        // check that this media exists
94761d21e86Skuangfio        if (auth_quickaclcheck($media) < AUTH_READ) {
94861d21e86Skuangfio            throw new AccessDeniedException('You are not allowed to read this media file', 211);
94961d21e86Skuangfio        }
95061d21e86Skuangfio        if (!media_exists($media, 0)) {
95161d21e86Skuangfio            throw new RemoteException('The requested media file does not exist', 221);
95261d21e86Skuangfio        }
95361d21e86Skuangfio
95461d21e86Skuangfio        $medialog = new MediaChangeLog($media);
95561d21e86Skuangfio        $medialog->setChunkSize(1024);
95661d21e86Skuangfio        // old revisions are counted from 0, so we need to subtract 1 for the current one
95761d21e86Skuangfio        $revisions = $medialog->getRevisions($first - 1, $conf['recent']);
95861d21e86Skuangfio
95961d21e86Skuangfio        $result = [];
96061d21e86Skuangfio        foreach ($revisions as $rev) {
96172b0e523SAndreas Gohr            // the current revision needs to be checked against the current file path
96272b0e523SAndreas Gohr            $check = $this->isCurrentMediaRev($media, $rev) ? '' : $rev;
96372b0e523SAndreas Gohr            if (!media_exists($media, $check)) continue; // skip non-existing revisions
96472b0e523SAndreas Gohr
96561d21e86Skuangfio            $info = $medialog->getRevisionInfo($rev);
96661d21e86Skuangfio
96761d21e86Skuangfio            $result[] = new MediaChange(
96861d21e86Skuangfio                $media,
96961d21e86Skuangfio                $rev,
97061d21e86Skuangfio                $info['user'],
97161d21e86Skuangfio                $info['ip'],
97261d21e86Skuangfio                $info['sum'],
97361d21e86Skuangfio                $info['type'],
97461d21e86Skuangfio                $info['sizechange']
97561d21e86Skuangfio            );
97661d21e86Skuangfio        }
97761d21e86Skuangfio
97861d21e86Skuangfio        return $result;
97961d21e86Skuangfio    }
98061d21e86Skuangfio
98161d21e86Skuangfio    /**
9826cce3332SAndreas Gohr     * Uploads a file to the wiki
9836cce3332SAndreas Gohr     *
9846cce3332SAndreas Gohr     * The file data has to be passed as a base64 encoded string.
9856cce3332SAndreas Gohr     *
9866cce3332SAndreas Gohr     * @link https://en.wikipedia.org/wiki/Base64
9876cce3332SAndreas Gohr     * @param string $media media id
9886cce3332SAndreas Gohr     * @param string $base64 Base64 encoded file contents
9896cce3332SAndreas Gohr     * @param bool $overwrite Should an existing file be overwritten?
9906cce3332SAndreas Gohr     * @return bool Should always be true
9916cce3332SAndreas Gohr     * @throws RemoteException
9926cce3332SAndreas Gohr     * @author Michael Klier <chi@chimeric.de>
9936cce3332SAndreas Gohr     */
9946cce3332SAndreas Gohr    public function saveMedia($media, $base64, $overwrite = false)
9956cce3332SAndreas Gohr    {
9966cce3332SAndreas Gohr        $media = cleanID($media);
9976cce3332SAndreas Gohr        $auth = auth_quickaclcheck(getNS($media) . ':*');
9986cce3332SAndreas Gohr
9996cce3332SAndreas Gohr        if ($media === '') {
1000d3856637SAndreas Gohr            throw new RemoteException('Empty or invalid media ID given', 231);
10016cce3332SAndreas Gohr        }
10026cce3332SAndreas Gohr
10036cce3332SAndreas Gohr        // clean up base64 encoded data
10046cce3332SAndreas Gohr        $base64 = strtr($base64, [
10056cce3332SAndreas Gohr            "\n" => '', // strip newlines
10066cce3332SAndreas Gohr            "\r" => '', // strip carriage returns
10076cce3332SAndreas Gohr            '-' => '+', // RFC4648 base64url
10086cce3332SAndreas Gohr            '_' => '/', // RFC4648 base64url
10096cce3332SAndreas Gohr            ' ' => '+', // JavaScript data uri
10106cce3332SAndreas Gohr        ]);
10116cce3332SAndreas Gohr
10126cce3332SAndreas Gohr        $data = base64_decode($base64, true);
10136cce3332SAndreas Gohr        if ($data === false) {
1014d3856637SAndreas Gohr            throw new RemoteException('Invalid base64 encoded data', 234);
10156cce3332SAndreas Gohr        }
10166cce3332SAndreas Gohr
1017d1f06eb4SAndreas Gohr        if ($data === '') {
1018d1f06eb4SAndreas Gohr            throw new RemoteException('Empty file given', 235);
1019d1f06eb4SAndreas Gohr        }
1020d1f06eb4SAndreas Gohr
10216cce3332SAndreas Gohr        // save temporary file
10226cce3332SAndreas Gohr        global $conf;
10236cce3332SAndreas Gohr        $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP());
10246cce3332SAndreas Gohr        @unlink($ftmp);
10256cce3332SAndreas Gohr        io_saveFile($ftmp, $data);
10266cce3332SAndreas Gohr
10276cce3332SAndreas Gohr        $res = media_save(['name' => $ftmp], $media, $overwrite, $auth, 'rename');
10286cce3332SAndreas Gohr        if (is_array($res)) {
1029d1f06eb4SAndreas Gohr            throw new RemoteException('Failed to save media: ' . $res[0], 236);
10306cce3332SAndreas Gohr        }
10316cce3332SAndreas Gohr        return (bool)$res; // should always be true at this point
10326cce3332SAndreas Gohr    }
10336cce3332SAndreas Gohr
10346cce3332SAndreas Gohr    /**
10356cce3332SAndreas Gohr     * Deletes a file from the wiki
10366cce3332SAndreas Gohr     *
10376cce3332SAndreas Gohr     * You need to have delete permissions for the file.
10386cce3332SAndreas Gohr     *
10396cce3332SAndreas Gohr     * @param string $media media id
10406cce3332SAndreas Gohr     * @return bool Should always be true
10416cce3332SAndreas Gohr     * @throws AccessDeniedException no permissions
10426cce3332SAndreas Gohr     * @throws RemoteException file in use or not deleted
10436cce3332SAndreas Gohr     * @author Gina Haeussge <osd@foosel.net>
10446cce3332SAndreas Gohr     *
10456cce3332SAndreas Gohr     */
10466cce3332SAndreas Gohr    public function deleteMedia($media)
10476cce3332SAndreas Gohr    {
10486cce3332SAndreas Gohr        $media = cleanID($media);
1049d1f06eb4SAndreas Gohr
10506cce3332SAndreas Gohr        $auth = auth_quickaclcheck($media);
10516cce3332SAndreas Gohr        $res = media_delete($media, $auth);
10526cce3332SAndreas Gohr        if ($res & DOKU_MEDIA_DELETED) {
10536cce3332SAndreas Gohr            return true;
10546cce3332SAndreas Gohr        } elseif ($res & DOKU_MEDIA_NOT_AUTH) {
1055d3856637SAndreas Gohr            throw new AccessDeniedException('You are not allowed to delete this media file', 212);
10566cce3332SAndreas Gohr        } elseif ($res & DOKU_MEDIA_INUSE) {
1057d3856637SAndreas Gohr            throw new RemoteException('Media file is still referenced', 232);
1058d1f06eb4SAndreas Gohr        } elseif (!media_exists($media)) {
1059d1f06eb4SAndreas Gohr            throw new RemoteException('The media file requested to delete does not exist', 221);
10606cce3332SAndreas Gohr        } else {
1061d3856637SAndreas Gohr            throw new RemoteException('Failed to delete media file', 233);
10626cce3332SAndreas Gohr        }
10636cce3332SAndreas Gohr    }
10646cce3332SAndreas Gohr
106572b0e523SAndreas Gohr    /**
106672b0e523SAndreas Gohr     * Check if the given revision is the current revision of this file
106772b0e523SAndreas Gohr     *
106872b0e523SAndreas Gohr     * @param string $id
106972b0e523SAndreas Gohr     * @param int $rev
107072b0e523SAndreas Gohr     * @return bool
107172b0e523SAndreas Gohr     */
107272b0e523SAndreas Gohr    protected function isCurrentMediaRev(string $id, int $rev)
107372b0e523SAndreas Gohr    {
107472b0e523SAndreas Gohr        $current = @filemtime(mediaFN($id));
107572b0e523SAndreas Gohr        if ($current === $rev) return true;
107672b0e523SAndreas Gohr        return false;
107772b0e523SAndreas Gohr    }
107872b0e523SAndreas Gohr
10796cce3332SAndreas Gohr    // endregion
10806cce3332SAndreas Gohr
10816cce3332SAndreas Gohr
10826cce3332SAndreas Gohr    /**
1083902647e6SAndreas Gohr     * Convenience method for page checks
1084902647e6SAndreas Gohr     *
1085902647e6SAndreas Gohr     * This method will perform multiple tasks:
1086902647e6SAndreas Gohr     *
1087902647e6SAndreas Gohr     * - clean the given page id
1088902647e6SAndreas Gohr     * - disallow an empty page id
1089902647e6SAndreas Gohr     * - check if the page exists (unless disabled)
1090902647e6SAndreas Gohr     * - check if the user has the required access level (pass AUTH_NONE to skip)
1091dd87735dSAndreas Gohr     *
1092dd87735dSAndreas Gohr     * @param string $id page id
10930eb4820cSAndreas Gohr     * @param int $rev page revision
10940eb4820cSAndreas Gohr     * @param bool $existCheck
10950eb4820cSAndreas Gohr     * @param int $minAccess
1096902647e6SAndreas Gohr     * @return string the cleaned page id
1097902647e6SAndreas Gohr     * @throws AccessDeniedException
10980eb4820cSAndreas Gohr     * @throws RemoteException
1099dd87735dSAndreas Gohr     */
11000eb4820cSAndreas Gohr    private function checkPage($id, $rev = 0, $existCheck = true, $minAccess = AUTH_READ)
1101dd87735dSAndreas Gohr    {
1102dd87735dSAndreas Gohr        $id = cleanID($id);
1103902647e6SAndreas Gohr        if ($id === '') {
1104d3856637SAndreas Gohr            throw new RemoteException('Empty or invalid page ID given', 131);
1105dd87735dSAndreas Gohr        }
1106902647e6SAndreas Gohr
11070eb4820cSAndreas Gohr        if ($existCheck && !page_exists($id, $rev)) {
11080eb4820cSAndreas Gohr            throw new RemoteException('The requested page (revision) does not exist', 121);
1109902647e6SAndreas Gohr        }
1110902647e6SAndreas Gohr
1111902647e6SAndreas Gohr        if ($minAccess && auth_quickaclcheck($id) < $minAccess) {
1112d3856637SAndreas Gohr            throw new AccessDeniedException('You are not allowed to read this page', 111);
1113902647e6SAndreas Gohr        }
1114902647e6SAndreas Gohr
1115dd87735dSAndreas Gohr        return $id;
1116dd87735dSAndreas Gohr    }
1117dd87735dSAndreas Gohr}
1118