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\MediaRevision; 12use dokuwiki\Remote\Response\Page; 13use dokuwiki\Remote\Response\PageHit; 14use dokuwiki\Remote\Response\PageRevision; 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 * @param string $namespace The namespace to search. Empty string for root namespace 240 * @param int $depth How deep to search. 0 for all subnamespaces 241 * @param bool $hash Whether to include a MD5 hash of the page content 242 * @return Page[] A list of matching pages 243 */ 244 public function listPages($namespace = '', $depth = 1, $hash = false) 245 { 246 global $conf; 247 248 $namespace = cleanID($namespace); 249 250 // shortcut for all pages 251 if ($namespace === '' && $depth === 0) { 252 return $this->getAllPages($hash); 253 } 254 255 // run our search iterator to get the pages 256 $dir = utf8_encodeFN(str_replace(':', '/', $namespace)); 257 $data = []; 258 $opts['skipacl'] = 0; 259 $opts['depth'] = $depth; // FIXME depth needs to be calculated relative to $dir 260 $opts['hash'] = $hash; 261 search($data, $conf['datadir'], 'search_allpages', $opts, $dir); 262 263 return array_map(fn($item) => new Page($item), $data); 264 } 265 266 /** 267 * Get all pages at once 268 * 269 * This is uses the page index and is quicker than iterating which is done in listPages() 270 * 271 * @return Page[] A list of all pages 272 * @see listPages() 273 */ 274 protected function getAllPages($hash = false) 275 { 276 $list = []; 277 $pages = idx_get_indexer()->getPages(); 278 Sort::ksort($pages); 279 280 foreach (array_keys($pages) as $idx) { 281 $perm = auth_quickaclcheck($pages[$idx]); 282 if ($perm < AUTH_READ || isHiddenPage($pages[$idx]) || !page_exists($pages[$idx])) { 283 continue; 284 } 285 286 $page = new Page([ 287 'id' => $pages[$idx], 288 'perm' => $perm, 289 ]); 290 if ($hash) $page->calculateHash(); 291 292 $list[] = $page; 293 } 294 295 return $list; 296 } 297 298 /** 299 * Do a fulltext search 300 * 301 * This executes a full text search and returns the results. The query uses the standard 302 * DokuWiki search syntax. 303 * 304 * Snippets are provided for the first 15 results only. The title is either the first heading 305 * or the page id depending on the wiki's configuration. 306 * 307 * @link https://www.dokuwiki.org/search#syntax 308 * @param string $query The search query as supported by the DokuWiki search 309 * @return PageHit[] A list of matching pages 310 */ 311 public function searchPages($query) 312 { 313 $regex = []; 314 $data = ft_pageSearch($query, $regex); 315 $pages = []; 316 317 // prepare additional data 318 $idx = 0; 319 foreach ($data as $id => $score) { 320 $file = wikiFN($id); 321 322 if ($idx < FT_SNIPPET_NUMBER) { 323 $snippet = ft_snippet($id, $regex); 324 $idx++; 325 } else { 326 $snippet = ''; 327 } 328 329 $pages[] = new PageHit([ 330 'id' => $id, 331 'score' => (int)$score, 332 'rev' => filemtime($file), 333 'mtime' => filemtime($file), 334 'size' => filesize($file), 335 'snippet' => $snippet, 336 'title' => useHeading('navigation') ? p_get_first_heading($id) : $id 337 ]); 338 } 339 return $pages; 340 } 341 342 /** 343 * Get recent page changes 344 * 345 * Returns a list of recent changes to wiki pages. The results can be limited to changes newer than 346 * a given timestamp. 347 * 348 * Only changes within the configured `$conf['recent']` range are returned. This is the default 349 * when no timestamp is given. 350 * 351 * @link https://www.dokuwiki.org/config:recent 352 * @param int $timestamp Only show changes newer than this unix timestamp 353 * @return PageRevision[] 354 * @author Michael Klier <chi@chimeric.de> 355 * @author Michael Hamann <michael@content-space.de> 356 */ 357 public function getRecentPageChanges($timestamp = 0) 358 { 359 $recents = getRecentsSince($timestamp); 360 361 $changes = []; 362 foreach ($recents as $recent) { 363 $changes[] = new PageRevision([ 364 'id' => $recent['id'], 365 'revision' => $recent['date'], 366 'author' => $recent['user'], 367 'ip' => $recent['ip'], 368 'summary' => $recent['sum'], 369 'type' => $recent['type'], 370 'sizechange' => $recent['sizechange'], 371 ]); 372 } 373 374 return $changes; 375 } 376 377 /** 378 * Get a wiki page's syntax 379 * 380 * Returns the syntax of the given page. When no revision is given, the current revision is returned. 381 * 382 * A non-existing page (or revision) will return an empty string usually. For the current revision 383 * a page template will be returned if configured. 384 * 385 * Read access is required for the page. 386 * 387 * @param string $page wiki page id 388 * @param string $rev Revision timestamp to access an older revision 389 * @return string the syntax of the page 390 * @throws AccessDeniedException 391 * @throws RemoteException 392 */ 393 public function getPage($page, $rev = '') 394 { 395 $page = $this->checkPage($page, false); 396 397 $text = rawWiki($page, $rev); 398 if (!$text && !$rev) { 399 return pageTemplate($page); 400 } else { 401 return $text; 402 } 403 } 404 405 /** 406 * Return a wiki page rendered to HTML 407 * 408 * The page is rendered to HTML as it would be in the wiki. The HTML consist only of the data for the page 409 * content itself, no surrounding structural tags, header, footers, sidebars etc are returned. 410 * 411 * References in the HTML are relative to the wiki base URL unless the `canonical` configuration is set. 412 * 413 * If the page does not exist, an error is returned. 414 * 415 * @link https://www.dokuwiki.org/config:canonical 416 * @param string $page page id 417 * @param string $rev revision timestamp 418 * @return string Rendered HTML for the page 419 * @throws AccessDeniedException 420 * @throws RemoteException 421 */ 422 public function getPageHTML($page, $rev = '') 423 { 424 $page = $this->checkPage($page); 425 426 return (string)p_wiki_xhtml($page, $rev, false); 427 } 428 429 /** 430 * Return some basic data about a page 431 * 432 * The call will return an error if the requested page does not exist. 433 * 434 * Read access is required for the page. 435 * 436 * @param string $page page id 437 * @param string $rev revision timestamp 438 * @param bool $author whether to include the author information 439 * @param bool $hash whether to include the MD5 hash of the page content 440 * @return Page 441 * @throws AccessDeniedException 442 * @throws RemoteException 443 */ 444 public function getPageInfo($page, $rev = '', $author = false, $hash = false) 445 { 446 $page = $this->checkPage($page); 447 448 $result = new Page(['id' => $page, 'rev' => $rev]); 449 if ($author) $result->retrieveAuthor(); 450 if ($hash) $result->calculateHash(); 451 452 return $result; 453 } 454 455 /** 456 * Returns a list of available revisions of a given wiki page 457 * 458 * The number of returned pages is set by `$conf['recent']`, but non accessible revisions pages 459 * are skipped, so less than that may be returned. 460 * 461 * @link https://www.dokuwiki.org/config:recent 462 * @param string $page page id 463 * @param int $first skip the first n changelog lines, 0 starts at the current revision 464 * @return PageRevision[] 465 * @throws AccessDeniedException 466 * @throws RemoteException 467 * @author Michael Klier <chi@chimeric.de> 468 */ 469 public function getPageVersions($page, $first = 0) 470 { 471 global $conf; 472 473 $page = $this->checkPage($page, false); 474 475 $pagelog = new PageChangeLog($page); 476 $pagelog->setChunkSize(1024); 477 // old revisions are counted from 0, so we need to subtract 1 for the current one 478 $revisions = $pagelog->getRevisions($first - 1, $conf['recent']); 479 480 $result = []; 481 foreach ($revisions as $rev) { 482 if (!page_exists($page, $rev)) continue; // skip non-existing revisions 483 $info = $pagelog->getRevisionInfo($rev); 484 485 $result[] = new PageRevision([ 486 'id' => $page, 487 'revision' => $rev, 488 'author' => $info['user'], 489 'ip' => $info['ip'], 490 'summary' => $info['sum'], 491 'type' => $info['type'], 492 'sizechange' => $info['sizechange'], 493 ]); 494 } 495 496 return $result; 497 } 498 499 /** 500 * Get a page's links 501 * 502 * This returns a list of links found in the given page. This includes internal, external and interwiki links 503 * 504 * Read access for the given page is needed and page has to exist. 505 * 506 * @param string $page page id 507 * @return Link[] A list of links found on the given page 508 * @throws AccessDeniedException 509 * @throws RemoteException 510 * @todo returning link titles would be a nice addition 511 * @todo hash handling seems not to be correct 512 * @author Michael Klier <chi@chimeric.de> 513 */ 514 public function getPageLinks($page) 515 { 516 $page = $this->checkPage($page); 517 518 // resolve page instructions 519 $ins = p_cached_instructions(wikiFN($page)); 520 521 // instantiate new Renderer - needed for interwiki links 522 $Renderer = new Doku_Renderer_xhtml(); 523 $Renderer->interwiki = getInterwiki(); 524 525 // parse instructions 526 $links = []; 527 foreach ($ins as $in) { 528 switch ($in[0]) { 529 case 'internallink': 530 $links[] = new Link([ 531 'type' => 'local', 532 'page' => $in[1][0], 533 'href' => wl($in[1][0]), 534 ]); 535 break; 536 case 'externallink': 537 $links[] = new Link([ 538 'type' => 'extern', 539 'page' => $in[1][0], 540 'href' => $in[1][0], 541 ]); 542 break; 543 case 'interwikilink': 544 $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]); 545 $links[] = new Link([ 546 'type' => 'interwiki', 547 'page' => $in[1][0], 548 'href' => $url, 549 ]); 550 break; 551 } 552 } 553 554 return ($links); 555 } 556 557 /** 558 * Get a page's backlinks 559 * 560 * A backlink is a wiki link on another page that links to the given page. 561 * 562 * Only links from pages readable by the current user are returned. The page itself 563 * needs to be readable. Otherwise an error is returned. 564 * 565 * @param string $page page id 566 * @return string[] A list of pages linking to the given page 567 * @throws AccessDeniedException 568 * @throws RemoteException 569 */ 570 public function getPageBackLinks($page) 571 { 572 $page = $this->checkPage($page, false); 573 574 return ft_backlinks($page); 575 } 576 577 /** 578 * Lock the given set of pages 579 * 580 * This call will try to lock all given pages. It will return a list of pages that were 581 * successfully locked. If a page could not be locked, eg. because a different user is 582 * currently holding a lock, that page will be missing from the returned list. 583 * 584 * You should always ensure that the list of returned pages matches the given list of 585 * pages. It's up to you to decide how to handle failed locking. 586 * 587 * Note: you can only lock pages that you have write access for. It is possible to create 588 * a lock for a page that does not exist, yet. 589 * 590 * Note: it is not necessary to lock a page before saving it. The `savePage()` call will 591 * automatically lock and unlock the page for you. However if you plan to do related 592 * operations on multiple pages, locking them all at once beforehand can be useful. 593 * 594 * @param string[] $pages A list of pages to lock 595 * @return string[] A list of pages that were successfully locked 596 */ 597 public function lockPages($pages) 598 { 599 $locked = []; 600 601 foreach ($pages as $id) { 602 $id = cleanID($id); 603 if ($id === '') continue; 604 if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) { 605 continue; 606 } 607 lock($id); 608 $locked[] = $id; 609 } 610 return $locked; 611 } 612 613 /** 614 * Unlock the given set of pages 615 * 616 * This call will try to unlock all given pages. It will return a list of pages that were 617 * successfully unlocked. If a page could not be unlocked, eg. because a different user is 618 * currently holding a lock, that page will be missing from the returned list. 619 * 620 * You should always ensure that the list of returned pages matches the given list of 621 * pages. It's up to you to decide how to handle failed unlocking. 622 * 623 * Note: you can only unlock pages that you have write access for. 624 * 625 * @param string[] $pages A list of pages to unlock 626 * @return string[] A list of pages that were successfully unlocked 627 */ 628 public function unlockPages($pages) 629 { 630 $unlocked = []; 631 632 foreach ($pages as $id) { 633 $id = cleanID($id); 634 if ($id === '') continue; 635 if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) { 636 continue; 637 } 638 $unlocked[] = $id; 639 } 640 641 return $unlocked; 642 } 643 644 /** 645 * Save a wiki page 646 * 647 * Saves the given wiki text to the given page. If the page does not exist, it will be created. 648 * Just like in the wiki, saving an empty text will delete the page. 649 * 650 * You need write permissions for the given page and the page may not be locked by another user. 651 * 652 * @param string $page page id 653 * @param string $text wiki text 654 * @param string $summary edit summary 655 * @param bool $isminor whether this is a minor edit 656 * @return bool Returns true on success 657 * @throws AccessDeniedException no write access for page 658 * @throws RemoteException no id, empty new page or locked 659 * @author Michael Klier <chi@chimeric.de> 660 */ 661 public function savePage($page, $text, $summary = '', $isminor = false) 662 { 663 global $TEXT; 664 global $lang; 665 666 $page = $this->checkPage($page, false, AUTH_EDIT); 667 $TEXT = cleanText($text); 668 669 670 if (!page_exists($page) && trim($TEXT) == '') { 671 throw new RemoteException('Refusing to write an empty new wiki page', 132); 672 } 673 674 // Check, if page is locked 675 if (checklock($page)) { 676 throw new RemoteException('The page is currently locked', 133); 677 } 678 679 // SPAM check 680 if (checkwordblock()) { 681 throw new RemoteException('The page content was blocked', 134); 682 } 683 684 // autoset summary on new pages 685 if (!page_exists($page) && empty($summary)) { 686 $summary = $lang['created']; 687 } 688 689 // autoset summary on deleted pages 690 if (page_exists($page) && empty($TEXT) && empty($summary)) { 691 $summary = $lang['deleted']; 692 } 693 694 // FIXME auto set a summary in other cases "API Edit" might be a good idea? 695 696 lock($page); 697 saveWikiText($page, $TEXT, $summary, $isminor); 698 unlock($page); 699 700 // run the indexer if page wasn't indexed yet 701 idx_addPage($page); 702 703 return true; 704 } 705 706 /** 707 * Appends text to the end of a wiki page 708 * 709 * If the page does not exist, it will be created. If a page template for the non-existant 710 * page is configured, the given text will appended to that template. 711 * 712 * The call will create a new page revision. 713 * 714 * You need write permissions for the given page. 715 * 716 * @param string $page page id 717 * @param string $text wiki text 718 * @param string $summary edit summary 719 * @param bool $isminor whether this is a minor edit 720 * @return bool Returns true on success 721 * @throws AccessDeniedException 722 * @throws RemoteException 723 */ 724 public function appendPage($page, $text, $summary, $isminor) 725 { 726 $currentpage = $this->getPage($page); 727 if (!is_string($currentpage)) { 728 $currentpage = ''; 729 } 730 return $this->savePage($page, $currentpage . $text, $summary, $isminor); 731 } 732 733 // endregion 734 735 // region media 736 737 /** 738 * List all media files in the given namespace (and below) 739 * 740 * Setting the `depth` to `0` and the `namespace` to `""` will return all media files in the wiki. 741 * 742 * When `pattern` is given, it needs to be a valid regular expression as understood by PHP's 743 * `preg_match()` including delimiters. 744 * The pattern is matched against the full media ID, including the namespace. 745 * 746 * @link https://www.php.net/manual/en/reference.pcre.pattern.syntax.php 747 * @param string $namespace The namespace to search. Empty string for root namespace 748 * @param string $pattern A regular expression to filter the returned files 749 * @param int $depth How deep to search. 0 for all subnamespaces 750 * @param bool $hash Whether to include a MD5 hash of the media content 751 * @return Media[] 752 * @throws AccessDeniedException no access to the media files 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(fn($item) => new Media($item), $data); 772 } 773 774 /** 775 * Get recent media changes 776 * 777 * Returns a list of recent changes to media files. The results can be limited to changes newer than 778 * a given timestamp. 779 * 780 * Only changes within the configured `$conf['recent']` range are returned. This is the default 781 * when no timestamp is given. 782 * 783 * @link https://www.dokuwiki.org/config:recent 784 * @param int $timestamp Only show changes newer than this unix timestamp 785 * @return MediaRevision[] 786 * @author Michael Klier <chi@chimeric.de> 787 * @author Michael Hamann <michael@content-space.de> 788 */ 789 public function getRecentMediaChanges($timestamp = 0) 790 { 791 792 $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES); 793 794 $changes = []; 795 foreach ($recents as $recent) { 796 $changes[] = new MediaRevision([ 797 'id' => $recent['id'], 798 'revision' => $recent['date'], 799 'author' => $recent['user'], 800 'ip' => $recent['ip'], 801 'summary' => $recent['sum'], 802 'type' => $recent['type'], 803 'sizechange' => $recent['sizechange'], 804 ]); 805 } 806 807 return $changes; 808 } 809 810 /** 811 * Get a media file's content 812 * 813 * Returns the content of the given media file. When no revision is given, the current revision is returned. 814 * 815 * @link https://en.wikipedia.org/wiki/Base64 816 * @param string $media file id 817 * @param int $rev revision timestamp 818 * @return string Base64 encoded media file contents 819 * @throws AccessDeniedException no permission for media 820 * @throws RemoteException not exist 821 * @author Gina Haeussge <osd@foosel.net> 822 * 823 */ 824 public function getMedia($media, $rev = '') 825 { 826 $media = cleanID($media); 827 if (auth_quickaclcheck($media) < AUTH_READ) { 828 throw new AccessDeniedException('You are not allowed to read this media file', 211); 829 } 830 831 $file = mediaFN($media, $rev); 832 if (!@ file_exists($file)) { 833 throw new RemoteException('The requested media file does not exist', 221); 834 } 835 836 $data = io_readFile($file, false); 837 return base64_encode($data); 838 } 839 840 /** 841 * Return info about a media file 842 * 843 * The call will return an error if the requested media file does not exist. 844 * 845 * Read access is required for the media file. 846 * 847 * @param string $media file id 848 * @param int $rev revision timestamp 849 * @param bool $hash whether to include the MD5 hash of the media content 850 * @return Media 851 * @throws AccessDeniedException no permission for media 852 * @throws RemoteException if not exist 853 * @author Gina Haeussge <osd@foosel.net> 854 */ 855 public function getMediaInfo($media, $rev = '', $hash = false) 856 { 857 $media = cleanID($media); 858 if (auth_quickaclcheck($media) < AUTH_READ) { 859 throw new AccessDeniedException('You are not allowed to read this media file', 211); 860 } 861 if (!media_exists($media, $rev)) { 862 throw new RemoteException('The requested media file does not exist', 221); 863 } 864 865 $file = mediaFN($media, $rev); 866 867 $info = new Media([ 868 'id' => $media, 869 'mtime' => filemtime($file), 870 'size' => filesize($file), 871 ]); 872 if ($hash) $info->calculateHash(); 873 874 return $info; 875 } 876 877 /** 878 * Uploads a file to the wiki 879 * 880 * The file data has to be passed as a base64 encoded string. 881 * 882 * @link https://en.wikipedia.org/wiki/Base64 883 * @param string $media media id 884 * @param string $base64 Base64 encoded file contents 885 * @param bool $overwrite Should an existing file be overwritten? 886 * @return bool Should always be true 887 * @throws RemoteException 888 * @author Michael Klier <chi@chimeric.de> 889 */ 890 public function saveMedia($media, $base64, $overwrite = false) 891 { 892 $media = cleanID($media); 893 $auth = auth_quickaclcheck(getNS($media) . ':*'); 894 895 if ($media === '') { 896 throw new RemoteException('Empty or invalid media ID given', 231); 897 } 898 899 // clean up base64 encoded data 900 $base64 = strtr($base64, [ 901 "\n" => '', // strip newlines 902 "\r" => '', // strip carriage returns 903 '-' => '+', // RFC4648 base64url 904 '_' => '/', // RFC4648 base64url 905 ' ' => '+', // JavaScript data uri 906 ]); 907 908 $data = base64_decode($base64, true); 909 if ($data === false) { 910 throw new RemoteException('Invalid base64 encoded data', 234); 911 } 912 913 // save temporary file 914 global $conf; 915 $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP()); 916 @unlink($ftmp); 917 io_saveFile($ftmp, $data); 918 919 $res = media_save(['name' => $ftmp], $media, $overwrite, $auth, 'rename'); 920 if (is_array($res)) { 921 throw new RemoteException('Failed to save media: ' . $res[0], 235); 922 } 923 return (bool)$res; // should always be true at this point 924 } 925 926 /** 927 * Deletes a file from the wiki 928 * 929 * You need to have delete permissions for the file. 930 * 931 * @param string $media media id 932 * @return bool Should always be true 933 * @throws AccessDeniedException no permissions 934 * @throws RemoteException file in use or not deleted 935 * @author Gina Haeussge <osd@foosel.net> 936 * 937 */ 938 public function deleteMedia($media) 939 { 940 $media = cleanID($media); 941 $auth = auth_quickaclcheck($media); 942 $res = media_delete($media, $auth); 943 if ($res & DOKU_MEDIA_DELETED) { 944 return true; 945 } elseif ($res & DOKU_MEDIA_NOT_AUTH) { 946 throw new AccessDeniedException('You are not allowed to delete this media file', 212); 947 } elseif ($res & DOKU_MEDIA_INUSE) { 948 throw new RemoteException('Media file is still referenced', 232); 949 } else { 950 throw new RemoteException('Failed to delete media file', 233); 951 } 952 } 953 954 // endregion 955 956 957 /** 958 * Convenience method for page checks 959 * 960 * This method will perform multiple tasks: 961 * 962 * - clean the given page id 963 * - disallow an empty page id 964 * - check if the page exists (unless disabled) 965 * - check if the user has the required access level (pass AUTH_NONE to skip) 966 * 967 * @param string $id page id 968 * @return string the cleaned page id 969 * @throws RemoteException 970 * @throws AccessDeniedException 971 */ 972 private function checkPage($id, $existCheck = true, $minAccess = AUTH_READ) 973 { 974 $id = cleanID($id); 975 if ($id === '') { 976 throw new RemoteException('Empty or invalid page ID given', 131); 977 } 978 979 if ($existCheck && !page_exists($id)) { 980 throw new RemoteException('The requested page does not exist', 121); 981 } 982 983 if ($minAccess && auth_quickaclcheck($id) < $minAccess) { 984 throw new AccessDeniedException('You are not allowed to read this page', 111); 985 } 986 987 return $id; 988 } 989} 990