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