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