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