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