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