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