You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1002 lines
32 KiB

  1. <?php
  2. namespace dokuwiki\Remote;
  3. use Doku_Renderer_xhtml;
  4. use dokuwiki\ChangeLog\PageChangeLog;
  5. use dokuwiki\Extension\AuthPlugin;
  6. use dokuwiki\Extension\Event;
  7. use dokuwiki\Remote\Response\Link;
  8. use dokuwiki\Remote\Response\Media;
  9. use dokuwiki\Remote\Response\MediaChange;
  10. use dokuwiki\Remote\Response\Page;
  11. use dokuwiki\Remote\Response\PageChange;
  12. use dokuwiki\Remote\Response\PageHit;
  13. use dokuwiki\Remote\Response\User;
  14. use dokuwiki\Utf8\Sort;
  15. /**
  16. * Provides the core methods for the remote API.
  17. * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
  18. */
  19. class ApiCore
  20. {
  21. /** @var int Increased whenever the API is changed */
  22. public const API_VERSION = 12;
  23. /**
  24. * Returns details about the core methods
  25. *
  26. * @return array
  27. */
  28. public function getMethods()
  29. {
  30. return [
  31. 'core.getAPIVersion' => (new ApiCall([$this, 'getAPIVersion'], 'info'))->setPublic(),
  32. 'core.getWikiVersion' => new ApiCall('getVersion', 'info'),
  33. 'core.getWikiTitle' => (new ApiCall([$this, 'getWikiTitle'], 'info'))->setPublic(),
  34. 'core.getWikiTime' => (new ApiCall([$this, 'getWikiTime'], 'info')),
  35. 'core.login' => (new ApiCall([$this, 'login'], 'user'))->setPublic(),
  36. 'core.logoff' => new ApiCall([$this, 'logoff'], 'user'),
  37. 'core.whoAmI' => (new ApiCall([$this, 'whoAmI'], 'user')),
  38. 'core.aclCheck' => new ApiCall([$this, 'aclCheck'], 'user'),
  39. 'core.listPages' => new ApiCall([$this, 'listPages'], 'pages'),
  40. 'core.searchPages' => new ApiCall([$this, 'searchPages'], 'pages'),
  41. 'core.getRecentPageChanges' => new ApiCall([$this, 'getRecentPageChanges'], 'pages'),
  42. 'core.getPage' => (new ApiCall([$this, 'getPage'], 'pages')),
  43. 'core.getPageHTML' => (new ApiCall([$this, 'getPageHTML'], 'pages')),
  44. 'core.getPageInfo' => (new ApiCall([$this, 'getPageInfo'], 'pages')),
  45. 'core.getPageHistory' => new ApiCall([$this, 'getPageHistory'], 'pages'),
  46. 'core.getPageLinks' => new ApiCall([$this, 'getPageLinks'], 'pages'),
  47. 'core.getPageBackLinks' => new ApiCall([$this, 'getPageBackLinks'], 'pages'),
  48. 'core.lockPages' => new ApiCall([$this, 'lockPages'], 'pages'),
  49. 'core.unlockPages' => new ApiCall([$this, 'unlockPages'], 'pages'),
  50. 'core.savePage' => new ApiCall([$this, 'savePage'], 'pages'),
  51. 'core.appendPage' => new ApiCall([$this, 'appendPage'], 'pages'),
  52. 'core.listMedia' => new ApiCall([$this, 'listMedia'], 'media'),
  53. 'core.getRecentMediaChanges' => new ApiCall([$this, 'getRecentMediaChanges'], 'media'),
  54. 'core.getMedia' => new ApiCall([$this, 'getMedia'], 'media'),
  55. 'core.getMediaInfo' => new ApiCall([$this, 'getMediaInfo'], 'media'),
  56. // todo: implement getMediaHistory
  57. // todo: implement getMediaUsage
  58. 'core.saveMedia' => new ApiCall([$this, 'saveMedia'], 'media'),
  59. 'core.deleteMedia' => new ApiCall([$this, 'deleteMedia'], 'media'),
  60. ];
  61. }
  62. // region info
  63. /**
  64. * Return the API version
  65. *
  66. * This is the version of the DokuWiki API. It increases whenever the API definition changes.
  67. *
  68. * When developing a client, you should check this version and make sure you can handle it.
  69. *
  70. * @return int
  71. */
  72. public function getAPIVersion()
  73. {
  74. return self::API_VERSION;
  75. }
  76. /**
  77. * Returns the wiki title
  78. *
  79. * @link https://www.dokuwiki.org/config:title
  80. * @return string
  81. */
  82. public function getWikiTitle()
  83. {
  84. global $conf;
  85. return $conf['title'];
  86. }
  87. /**
  88. * Return the current server time
  89. *
  90. * Returns a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC).
  91. *
  92. * You can use this to compensate for differences between your client's time and the
  93. * server's time when working with last modified timestamps (revisions).
  94. *
  95. * @return int A unix timestamp
  96. */
  97. public function getWikiTime()
  98. {
  99. return time();
  100. }
  101. // endregion
  102. // region user
  103. /**
  104. * Login
  105. *
  106. * This will use the given credentials and attempt to login the user. This will set the
  107. * appropriate cookies, which can be used for subsequent requests.
  108. *
  109. * Use of this mechanism is discouraged. Using token authentication is preferred.
  110. *
  111. * @param string $user The user name
  112. * @param string $pass The password
  113. * @return int If the login was successful
  114. */
  115. public function login($user, $pass)
  116. {
  117. global $conf;
  118. /** @var AuthPlugin $auth */
  119. global $auth;
  120. if (!$conf['useacl']) return 0;
  121. if (!$auth instanceof AuthPlugin) return 0;
  122. @session_start(); // reopen session for login
  123. $ok = null;
  124. if ($auth->canDo('external')) {
  125. $ok = $auth->trustExternal($user, $pass, false);
  126. }
  127. if ($ok === null) {
  128. $evdata = [
  129. 'user' => $user,
  130. 'password' => $pass,
  131. 'sticky' => false,
  132. 'silent' => true
  133. ];
  134. $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
  135. }
  136. session_write_close(); // we're done with the session
  137. return $ok;
  138. }
  139. /**
  140. * Log off
  141. *
  142. * Attempt to log out the current user, deleting the appropriate cookies
  143. *
  144. * Use of this mechanism is discouraged. Using token authentication is preferred.
  145. *
  146. * @return int 0 on failure, 1 on success
  147. */
  148. public function logoff()
  149. {
  150. global $conf;
  151. global $auth;
  152. if (!$conf['useacl']) return 0;
  153. if (!$auth instanceof AuthPlugin) return 0;
  154. auth_logoff();
  155. return 1;
  156. }
  157. /**
  158. * Info about the currently authenticated user
  159. *
  160. * @return User
  161. */
  162. public function whoAmI()
  163. {
  164. return new User();
  165. }
  166. /**
  167. * Check ACL Permissions
  168. *
  169. * This call allows to check the permissions for a given page/media and user/group combination.
  170. * If no user/group is given, the current user is used.
  171. *
  172. * Read the link below to learn more about the permission levels.
  173. *
  174. * @link https://www.dokuwiki.org/acl#background_info
  175. * @param string $page A page or media ID
  176. * @param string $user username
  177. * @param string[] $groups array of groups
  178. * @return int permission level
  179. * @throws RemoteException
  180. */
  181. public function aclCheck($page, $user = '', $groups = [])
  182. {
  183. /** @var AuthPlugin $auth */
  184. global $auth;
  185. $page = $this->checkPage($page, 0, false, AUTH_NONE);
  186. if ($user === '') {
  187. return auth_quickaclcheck($page);
  188. } else {
  189. if ($groups === []) {
  190. $userinfo = $auth->getUserData($user);
  191. if ($userinfo === false) {
  192. $groups = [];
  193. } else {
  194. $groups = $userinfo['grps'];
  195. }
  196. }
  197. return auth_aclcheck($page, $user, $groups);
  198. }
  199. }
  200. // endregion
  201. // region pages
  202. /**
  203. * List all pages in the given namespace (and below)
  204. *
  205. * Setting the `depth` to `0` and the `namespace` to `""` will return all pages in the wiki.
  206. *
  207. * Note: author information is not available in this call.
  208. *
  209. * @param string $namespace The namespace to search. Empty string for root namespace
  210. * @param int $depth How deep to search. 0 for all subnamespaces
  211. * @param bool $hash Whether to include a MD5 hash of the page content
  212. * @return Page[] A list of matching pages
  213. * @todo might be a good idea to replace search_allpages with search_universal
  214. */
  215. public function listPages($namespace = '', $depth = 1, $hash = false)
  216. {
  217. global $conf;
  218. $namespace = cleanID($namespace);
  219. // shortcut for all pages
  220. if ($namespace === '' && $depth === 0) {
  221. return $this->getAllPages($hash);
  222. }
  223. // search_allpages handles depth weird, we need to add the given namespace depth
  224. if ($depth) {
  225. $depth += substr_count($namespace, ':') + 1;
  226. }
  227. // run our search iterator to get the pages
  228. $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
  229. $data = [];
  230. $opts['skipacl'] = 0;
  231. $opts['depth'] = $depth;
  232. $opts['hash'] = $hash;
  233. search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
  234. return array_map(static fn($item) => new Page(
  235. $item['id'],
  236. 0, // we're searching current revisions only
  237. $item['mtime'],
  238. '', // not returned by search_allpages
  239. $item['size'],
  240. null, // not returned by search_allpages
  241. $item['hash'] ?? ''
  242. ), $data);
  243. }
  244. /**
  245. * Get all pages at once
  246. *
  247. * This is uses the page index and is quicker than iterating which is done in listPages()
  248. *
  249. * @return Page[] A list of all pages
  250. * @see listPages()
  251. */
  252. protected function getAllPages($hash = false)
  253. {
  254. $list = [];
  255. $pages = idx_get_indexer()->getPages();
  256. Sort::ksort($pages);
  257. foreach (array_keys($pages) as $idx) {
  258. $perm = auth_quickaclcheck($pages[$idx]);
  259. if ($perm < AUTH_READ || isHiddenPage($pages[$idx]) || !page_exists($pages[$idx])) {
  260. continue;
  261. }
  262. $page = new Page($pages[$idx], 0, 0, '', null, $perm);
  263. if ($hash) $page->calculateHash();
  264. $list[] = $page;
  265. }
  266. return $list;
  267. }
  268. /**
  269. * Do a fulltext search
  270. *
  271. * This executes a full text search and returns the results. The query uses the standard
  272. * DokuWiki search syntax.
  273. *
  274. * Snippets are provided for the first 15 results only. The title is either the first heading
  275. * or the page id depending on the wiki's configuration.
  276. *
  277. * @link https://www.dokuwiki.org/search#syntax
  278. * @param string $query The search query as supported by the DokuWiki search
  279. * @return PageHit[] A list of matching pages
  280. */
  281. public function searchPages($query)
  282. {
  283. $regex = [];
  284. $data = ft_pageSearch($query, $regex);
  285. $pages = [];
  286. // prepare additional data
  287. $idx = 0;
  288. foreach ($data as $id => $score) {
  289. if ($idx < FT_SNIPPET_NUMBER) {
  290. $snippet = ft_snippet($id, $regex);
  291. $idx++;
  292. } else {
  293. $snippet = '';
  294. }
  295. $pages[] = new PageHit(
  296. $id,
  297. $snippet,
  298. $score,
  299. useHeading('navigation') ? p_get_first_heading($id) : $id
  300. );
  301. }
  302. return $pages;
  303. }
  304. /**
  305. * Get recent page changes
  306. *
  307. * Returns a list of recent changes to wiki pages. The results can be limited to changes newer than
  308. * a given timestamp.
  309. *
  310. * Only changes within the configured `$conf['recent']` range are returned. This is the default
  311. * when no timestamp is given.
  312. *
  313. * @link https://www.dokuwiki.org/config:recent
  314. * @param int $timestamp Only show changes newer than this unix timestamp
  315. * @return PageChange[]
  316. * @author Michael Klier <chi@chimeric.de>
  317. * @author Michael Hamann <michael@content-space.de>
  318. */
  319. public function getRecentPageChanges($timestamp = 0)
  320. {
  321. $recents = getRecentsSince($timestamp);
  322. $changes = [];
  323. foreach ($recents as $recent) {
  324. $changes[] = new PageChange(
  325. $recent['id'],
  326. $recent['date'],
  327. $recent['user'],
  328. $recent['ip'],
  329. $recent['sum'],
  330. $recent['type'],
  331. $recent['sizechange']
  332. );
  333. }
  334. return $changes;
  335. }
  336. /**
  337. * Get a wiki page's syntax
  338. *
  339. * Returns the syntax of the given page. When no revision is given, the current revision is returned.
  340. *
  341. * A non-existing page (or revision) will return an empty string usually. For the current revision
  342. * a page template will be returned if configured.
  343. *
  344. * Read access is required for the page.
  345. *
  346. * @param string $page wiki page id
  347. * @param int $rev Revision timestamp to access an older revision
  348. * @return string the syntax of the page
  349. * @throws AccessDeniedException
  350. * @throws RemoteException
  351. */
  352. public function getPage($page, $rev = 0)
  353. {
  354. $page = $this->checkPage($page, $rev, false);
  355. $text = rawWiki($page, $rev);
  356. if (!$text && !$rev) {
  357. return pageTemplate($page);
  358. } else {
  359. return $text;
  360. }
  361. }
  362. /**
  363. * Return a wiki page rendered to HTML
  364. *
  365. * The page is rendered to HTML as it would be in the wiki. The HTML consist only of the data for the page
  366. * content itself, no surrounding structural tags, header, footers, sidebars etc are returned.
  367. *
  368. * References in the HTML are relative to the wiki base URL unless the `canonical` configuration is set.
  369. *
  370. * If the page does not exist, an error is returned.
  371. *
  372. * @link https://www.dokuwiki.org/config:canonical
  373. * @param string $page page id
  374. * @param int $rev revision timestamp
  375. * @return string Rendered HTML for the page
  376. * @throws AccessDeniedException
  377. * @throws RemoteException
  378. */
  379. public function getPageHTML($page, $rev = 0)
  380. {
  381. $page = $this->checkPage($page, $rev);
  382. return (string)p_wiki_xhtml($page, $rev, false);
  383. }
  384. /**
  385. * Return some basic data about a page
  386. *
  387. * The call will return an error if the requested page does not exist.
  388. *
  389. * Read access is required for the page.
  390. *
  391. * @param string $page page id
  392. * @param int $rev revision timestamp
  393. * @param bool $author whether to include the author information
  394. * @param bool $hash whether to include the MD5 hash of the page content
  395. * @return Page
  396. * @throws AccessDeniedException
  397. * @throws RemoteException
  398. */
  399. public function getPageInfo($page, $rev = 0, $author = false, $hash = false)
  400. {
  401. $page = $this->checkPage($page, $rev);
  402. $result = new Page($page, $rev);
  403. if ($author) $result->retrieveAuthor();
  404. if ($hash) $result->calculateHash();
  405. return $result;
  406. }
  407. /**
  408. * Returns a list of available revisions of a given wiki page
  409. *
  410. * The number of returned pages is set by `$conf['recent']`, but non accessible revisions pages
  411. * are skipped, so less than that may be returned.
  412. *
  413. * @link https://www.dokuwiki.org/config:recent
  414. * @param string $page page id
  415. * @param int $first skip the first n changelog lines, 0 starts at the current revision
  416. * @return PageChange[]
  417. * @throws AccessDeniedException
  418. * @throws RemoteException
  419. * @author Michael Klier <chi@chimeric.de>
  420. */
  421. public function getPageHistory($page, $first = 0)
  422. {
  423. global $conf;
  424. $page = $this->checkPage($page, 0, false);
  425. $pagelog = new PageChangeLog($page);
  426. $pagelog->setChunkSize(1024);
  427. // old revisions are counted from 0, so we need to subtract 1 for the current one
  428. $revisions = $pagelog->getRevisions($first - 1, $conf['recent']);
  429. $result = [];
  430. foreach ($revisions as $rev) {
  431. if (!page_exists($page, $rev)) continue; // skip non-existing revisions
  432. $info = $pagelog->getRevisionInfo($rev);
  433. $result[] = new PageChange(
  434. $page,
  435. $rev,
  436. $info['user'],
  437. $info['ip'],
  438. $info['sum'],
  439. $info['type'],
  440. $info['sizechange']
  441. );
  442. }
  443. return $result;
  444. }
  445. /**
  446. * Get a page's links
  447. *
  448. * This returns a list of links found in the given page. This includes internal, external and interwiki links
  449. *
  450. * If a link occurs multiple times on the page, it will be returned multiple times.
  451. *
  452. * Read access for the given page is needed and page has to exist.
  453. *
  454. * @param string $page page id
  455. * @return Link[] A list of links found on the given page
  456. * @throws AccessDeniedException
  457. * @throws RemoteException
  458. * @todo returning link titles would be a nice addition
  459. * @todo hash handling seems not to be correct
  460. * @todo maybe return the same link only once?
  461. * @author Michael Klier <chi@chimeric.de>
  462. */
  463. public function getPageLinks($page)
  464. {
  465. $page = $this->checkPage($page);
  466. // resolve page instructions
  467. $ins = p_cached_instructions(wikiFN($page));
  468. // instantiate new Renderer - needed for interwiki links
  469. $Renderer = new Doku_Renderer_xhtml();
  470. $Renderer->interwiki = getInterwiki();
  471. // parse instructions
  472. $links = [];
  473. foreach ($ins as $in) {
  474. switch ($in[0]) {
  475. case 'internallink':
  476. $links[] = new Link('local', $in[1][0], wl($in[1][0]));
  477. break;
  478. case 'externallink':
  479. $links[] = new Link('extern', $in[1][0], $in[1][0]);
  480. break;
  481. case 'interwikilink':
  482. $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]);
  483. $links[] = new Link('interwiki', $in[1][0], $url);
  484. break;
  485. }
  486. }
  487. return ($links);
  488. }
  489. /**
  490. * Get a page's backlinks
  491. *
  492. * A backlink is a wiki link on another page that links to the given page.
  493. *
  494. * Only links from pages readable by the current user are returned. The page itself
  495. * needs to be readable. Otherwise an error is returned.
  496. *
  497. * @param string $page page id
  498. * @return string[] A list of pages linking to the given page
  499. * @throws AccessDeniedException
  500. * @throws RemoteException
  501. */
  502. public function getPageBackLinks($page)
  503. {
  504. $page = $this->checkPage($page, 0, false);
  505. return ft_backlinks($page);
  506. }
  507. /**
  508. * Lock the given set of pages
  509. *
  510. * This call will try to lock all given pages. It will return a list of pages that were
  511. * successfully locked. If a page could not be locked, eg. because a different user is
  512. * currently holding a lock, that page will be missing from the returned list.
  513. *
  514. * You should always ensure that the list of returned pages matches the given list of
  515. * pages. It's up to you to decide how to handle failed locking.
  516. *
  517. * Note: you can only lock pages that you have write access for. It is possible to create
  518. * a lock for a page that does not exist, yet.
  519. *
  520. * Note: it is not necessary to lock a page before saving it. The `savePage()` call will
  521. * automatically lock and unlock the page for you. However if you plan to do related
  522. * operations on multiple pages, locking them all at once beforehand can be useful.
  523. *
  524. * @param string[] $pages A list of pages to lock
  525. * @return string[] A list of pages that were successfully locked
  526. */
  527. public function lockPages($pages)
  528. {
  529. $locked = [];
  530. foreach ($pages as $id) {
  531. $id = cleanID($id);
  532. if ($id === '') continue;
  533. if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) {
  534. continue;
  535. }
  536. lock($id);
  537. $locked[] = $id;
  538. }
  539. return $locked;
  540. }
  541. /**
  542. * Unlock the given set of pages
  543. *
  544. * This call will try to unlock all given pages. It will return a list of pages that were
  545. * successfully unlocked. If a page could not be unlocked, eg. because a different user is
  546. * currently holding a lock, that page will be missing from the returned list.
  547. *
  548. * You should always ensure that the list of returned pages matches the given list of
  549. * pages. It's up to you to decide how to handle failed unlocking.
  550. *
  551. * Note: you can only unlock pages that you have write access for.
  552. *
  553. * @param string[] $pages A list of pages to unlock
  554. * @return string[] A list of pages that were successfully unlocked
  555. */
  556. public function unlockPages($pages)
  557. {
  558. $unlocked = [];
  559. foreach ($pages as $id) {
  560. $id = cleanID($id);
  561. if ($id === '') continue;
  562. if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) {
  563. continue;
  564. }
  565. $unlocked[] = $id;
  566. }
  567. return $unlocked;
  568. }
  569. /**
  570. * Save a wiki page
  571. *
  572. * Saves the given wiki text to the given page. If the page does not exist, it will be created.
  573. * Just like in the wiki, saving an empty text will delete the page.
  574. *
  575. * You need write permissions for the given page and the page may not be locked by another user.
  576. *
  577. * @param string $page page id
  578. * @param string $text wiki text
  579. * @param string $summary edit summary
  580. * @param bool $isminor whether this is a minor edit
  581. * @return bool Returns true on success
  582. * @throws AccessDeniedException no write access for page
  583. * @throws RemoteException no id, empty new page or locked
  584. * @author Michael Klier <chi@chimeric.de>
  585. */
  586. public function savePage($page, $text, $summary = '', $isminor = false)
  587. {
  588. global $TEXT;
  589. global $lang;
  590. $page = $this->checkPage($page, 0, false, AUTH_EDIT);
  591. $TEXT = cleanText($text);
  592. if (!page_exists($page) && trim($TEXT) == '') {
  593. throw new RemoteException('Refusing to write an empty new wiki page', 132);
  594. }
  595. // Check, if page is locked
  596. if (checklock($page)) {
  597. throw new RemoteException('The page is currently locked', 133);
  598. }
  599. // SPAM check
  600. if (checkwordblock()) {
  601. throw new RemoteException('The page content was blocked', 134);
  602. }
  603. // autoset summary on new pages
  604. if (!page_exists($page) && empty($summary)) {
  605. $summary = $lang['created'];
  606. }
  607. // autoset summary on deleted pages
  608. if (page_exists($page) && empty($TEXT) && empty($summary)) {
  609. $summary = $lang['deleted'];
  610. }
  611. // FIXME auto set a summary in other cases "API Edit" might be a good idea?
  612. lock($page);
  613. saveWikiText($page, $TEXT, $summary, $isminor);
  614. unlock($page);
  615. // run the indexer if page wasn't indexed yet
  616. idx_addPage($page);
  617. return true;
  618. }
  619. /**
  620. * Appends text to the end of a wiki page
  621. *
  622. * If the page does not exist, it will be created. If a page template for the non-existant
  623. * page is configured, the given text will appended to that template.
  624. *
  625. * The call will create a new page revision.
  626. *
  627. * You need write permissions for the given page.
  628. *
  629. * @param string $page page id
  630. * @param string $text wiki text
  631. * @param string $summary edit summary
  632. * @param bool $isminor whether this is a minor edit
  633. * @return bool Returns true on success
  634. * @throws AccessDeniedException
  635. * @throws RemoteException
  636. */
  637. public function appendPage($page, $text, $summary = '', $isminor = false)
  638. {
  639. $currentpage = $this->getPage($page);
  640. if (!is_string($currentpage)) {
  641. $currentpage = '';
  642. }
  643. return $this->savePage($page, $currentpage . $text, $summary, $isminor);
  644. }
  645. // endregion
  646. // region media
  647. /**
  648. * List all media files in the given namespace (and below)
  649. *
  650. * Setting the `depth` to `0` and the `namespace` to `""` will return all media files in the wiki.
  651. *
  652. * When `pattern` is given, it needs to be a valid regular expression as understood by PHP's
  653. * `preg_match()` including delimiters.
  654. * The pattern is matched against the full media ID, including the namespace.
  655. *
  656. * @link https://www.php.net/manual/en/reference.pcre.pattern.syntax.php
  657. * @param string $namespace The namespace to search. Empty string for root namespace
  658. * @param string $pattern A regular expression to filter the returned files
  659. * @param int $depth How deep to search. 0 for all subnamespaces
  660. * @param bool $hash Whether to include a MD5 hash of the media content
  661. * @return Media[]
  662. * @author Gina Haeussge <osd@foosel.net>
  663. */
  664. public function listMedia($namespace = '', $pattern = '', $depth = 1, $hash = false)
  665. {
  666. global $conf;
  667. $namespace = cleanID($namespace);
  668. $options = [
  669. 'skipacl' => 0,
  670. 'depth' => $depth,
  671. 'hash' => $hash,
  672. 'pattern' => $pattern,
  673. ];
  674. $dir = utf8_encodeFN(str_replace(':', '/', $namespace));
  675. $data = [];
  676. search($data, $conf['mediadir'], 'search_media', $options, $dir);
  677. return array_map(static fn($item) => new Media(
  678. $item['id'],
  679. 0, // we're searching current revisions only
  680. $item['mtime'],
  681. $item['size'],
  682. $item['perm'],
  683. $item['isimg'],
  684. $item['hash'] ?? ''
  685. ), $data);
  686. }
  687. /**
  688. * Get recent media changes
  689. *
  690. * Returns a list of recent changes to media files. The results can be limited to changes newer than
  691. * a given timestamp.
  692. *
  693. * Only changes within the configured `$conf['recent']` range are returned. This is the default
  694. * when no timestamp is given.
  695. *
  696. * @link https://www.dokuwiki.org/config:recent
  697. * @param int $timestamp Only show changes newer than this unix timestamp
  698. * @return MediaChange[]
  699. * @author Michael Klier <chi@chimeric.de>
  700. * @author Michael Hamann <michael@content-space.de>
  701. */
  702. public function getRecentMediaChanges($timestamp = 0)
  703. {
  704. $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES);
  705. $changes = [];
  706. foreach ($recents as $recent) {
  707. $changes[] = new MediaChange(
  708. $recent['id'],
  709. $recent['date'],
  710. $recent['user'],
  711. $recent['ip'],
  712. $recent['sum'],
  713. $recent['type'],
  714. $recent['sizechange']
  715. );
  716. }
  717. return $changes;
  718. }
  719. /**
  720. * Get a media file's content
  721. *
  722. * Returns the content of the given media file. When no revision is given, the current revision is returned.
  723. *
  724. * @link https://en.wikipedia.org/wiki/Base64
  725. * @param string $media file id
  726. * @param int $rev revision timestamp
  727. * @return string Base64 encoded media file contents
  728. * @throws AccessDeniedException no permission for media
  729. * @throws RemoteException not exist
  730. * @author Gina Haeussge <osd@foosel.net>
  731. *
  732. */
  733. public function getMedia($media, $rev = 0)
  734. {
  735. $media = cleanID($media);
  736. if (auth_quickaclcheck($media) < AUTH_READ) {
  737. throw new AccessDeniedException('You are not allowed to read this media file', 211);
  738. }
  739. $file = mediaFN($media, $rev);
  740. if (!@ file_exists($file)) {
  741. throw new RemoteException('The requested media file (revision) does not exist', 221);
  742. }
  743. $data = io_readFile($file, false);
  744. return base64_encode($data);
  745. }
  746. /**
  747. * Return info about a media file
  748. *
  749. * The call will return an error if the requested media file does not exist.
  750. *
  751. * Read access is required for the media file.
  752. *
  753. * @param string $media file id
  754. * @param int $rev revision timestamp
  755. * @param bool $author whether to include the author information
  756. * @param bool $hash whether to include the MD5 hash of the media content
  757. * @return Media
  758. * @throws AccessDeniedException no permission for media
  759. * @throws RemoteException if not exist
  760. * @author Gina Haeussge <osd@foosel.net>
  761. */
  762. public function getMediaInfo($media, $rev = 0, $author = false, $hash = false)
  763. {
  764. $media = cleanID($media);
  765. if (auth_quickaclcheck($media) < AUTH_READ) {
  766. throw new AccessDeniedException('You are not allowed to read this media file', 211);
  767. }
  768. if (!media_exists($media, $rev)) {
  769. throw new RemoteException('The requested media file does not exist', 221);
  770. }
  771. $info = new Media($media, $rev);
  772. if ($hash) $info->calculateHash();
  773. if ($author) $info->retrieveAuthor();
  774. return $info;
  775. }
  776. /**
  777. * Uploads a file to the wiki
  778. *
  779. * The file data has to be passed as a base64 encoded string.
  780. *
  781. * @link https://en.wikipedia.org/wiki/Base64
  782. * @param string $media media id
  783. * @param string $base64 Base64 encoded file contents
  784. * @param bool $overwrite Should an existing file be overwritten?
  785. * @return bool Should always be true
  786. * @throws RemoteException
  787. * @author Michael Klier <chi@chimeric.de>
  788. */
  789. public function saveMedia($media, $base64, $overwrite = false)
  790. {
  791. $media = cleanID($media);
  792. $auth = auth_quickaclcheck(getNS($media) . ':*');
  793. if ($media === '') {
  794. throw new RemoteException('Empty or invalid media ID given', 231);
  795. }
  796. // clean up base64 encoded data
  797. $base64 = strtr($base64, [
  798. "\n" => '', // strip newlines
  799. "\r" => '', // strip carriage returns
  800. '-' => '+', // RFC4648 base64url
  801. '_' => '/', // RFC4648 base64url
  802. ' ' => '+', // JavaScript data uri
  803. ]);
  804. $data = base64_decode($base64, true);
  805. if ($data === false) {
  806. throw new RemoteException('Invalid base64 encoded data', 234);
  807. }
  808. if ($data === '') {
  809. throw new RemoteException('Empty file given', 235);
  810. }
  811. // save temporary file
  812. global $conf;
  813. $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP());
  814. @unlink($ftmp);
  815. io_saveFile($ftmp, $data);
  816. $res = media_save(['name' => $ftmp], $media, $overwrite, $auth, 'rename');
  817. if (is_array($res)) {
  818. throw new RemoteException('Failed to save media: ' . $res[0], 236);
  819. }
  820. return (bool)$res; // should always be true at this point
  821. }
  822. /**
  823. * Deletes a file from the wiki
  824. *
  825. * You need to have delete permissions for the file.
  826. *
  827. * @param string $media media id
  828. * @return bool Should always be true
  829. * @throws AccessDeniedException no permissions
  830. * @throws RemoteException file in use or not deleted
  831. * @author Gina Haeussge <osd@foosel.net>
  832. *
  833. */
  834. public function deleteMedia($media)
  835. {
  836. $media = cleanID($media);
  837. $auth = auth_quickaclcheck($media);
  838. $res = media_delete($media, $auth);
  839. if ($res & DOKU_MEDIA_DELETED) {
  840. return true;
  841. } elseif ($res & DOKU_MEDIA_NOT_AUTH) {
  842. throw new AccessDeniedException('You are not allowed to delete this media file', 212);
  843. } elseif ($res & DOKU_MEDIA_INUSE) {
  844. throw new RemoteException('Media file is still referenced', 232);
  845. } elseif (!media_exists($media)) {
  846. throw new RemoteException('The media file requested to delete does not exist', 221);
  847. } else {
  848. throw new RemoteException('Failed to delete media file', 233);
  849. }
  850. }
  851. // endregion
  852. /**
  853. * Convenience method for page checks
  854. *
  855. * This method will perform multiple tasks:
  856. *
  857. * - clean the given page id
  858. * - disallow an empty page id
  859. * - check if the page exists (unless disabled)
  860. * - check if the user has the required access level (pass AUTH_NONE to skip)
  861. *
  862. * @param string $id page id
  863. * @param int $rev page revision
  864. * @param bool $existCheck
  865. * @param int $minAccess
  866. * @return string the cleaned page id
  867. * @throws AccessDeniedException
  868. * @throws RemoteException
  869. */
  870. private function checkPage($id, $rev = 0, $existCheck = true, $minAccess = AUTH_READ)
  871. {
  872. $id = cleanID($id);
  873. if ($id === '') {
  874. throw new RemoteException('Empty or invalid page ID given', 131);
  875. }
  876. if ($existCheck && !page_exists($id, $rev)) {
  877. throw new RemoteException('The requested page (revision) does not exist', 121);
  878. }
  879. if ($minAccess && auth_quickaclcheck($id) < $minAccess) {
  880. throw new AccessDeniedException('You are not allowed to read this page', 111);
  881. }
  882. return $id;
  883. }
  884. }