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 = 13; 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 'core.getMediaUsage' => new ApiCall([$this, 'getMediaUsage'], 'media'), 68 // todo: implement getMediaHistory 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(static 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 * If a link occurs multiple times on the page, it will be returned multiple times. 513 * 514 * Read access for the given page is needed and page has to exist. 515 * 516 * @param string $page page id 517 * @return Link[] A list of links found on the given page 518 * @throws AccessDeniedException 519 * @throws RemoteException 520 * @todo returning link titles would be a nice addition 521 * @todo hash handling seems not to be correct 522 * @todo maybe return the same link only once? 523 * @author Michael Klier <chi@chimeric.de> 524 */ 525 public function getPageLinks($page) 526 { 527 $page = $this->checkPage($page); 528 529 // resolve page instructions 530 $ins = p_cached_instructions(wikiFN($page), false, $page); 531 532 // instantiate new Renderer - needed for interwiki links 533 $Renderer = new Doku_Renderer_xhtml(); 534 $Renderer->interwiki = getInterwiki(); 535 536 // parse instructions 537 $links = []; 538 foreach ($ins as $in) { 539 switch ($in[0]) { 540 case 'internallink': 541 $links[] = new Link('local', $in[1][0], wl($in[1][0])); 542 break; 543 case 'externallink': 544 $links[] = new Link('extern', $in[1][0], $in[1][0]); 545 break; 546 case 'interwikilink': 547 $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]); 548 $links[] = new Link('interwiki', $in[1][0], $url); 549 break; 550 } 551 } 552 553 return ($links); 554 } 555 556 /** 557 * Get a page's backlinks 558 * 559 * A backlink is a wiki link on another page that links to the given page. 560 * 561 * Only links from pages readable by the current user are returned. The page itself 562 * needs to be readable. Otherwise an error is returned. 563 * 564 * @param string $page page id 565 * @return string[] A list of pages linking to the given page 566 * @throws AccessDeniedException 567 * @throws RemoteException 568 */ 569 public function getPageBackLinks($page) 570 { 571 $page = $this->checkPage($page, 0, false); 572 573 return ft_backlinks($page); 574 } 575 576 /** 577 * Lock the given set of pages 578 * 579 * This call will try to lock all given pages. It will return a list of pages that were 580 * successfully locked. If a page could not be locked, eg. because a different user is 581 * currently holding a lock, that page will be missing from the returned list. 582 * 583 * You should always ensure that the list of returned pages matches the given list of 584 * pages. It's up to you to decide how to handle failed locking. 585 * 586 * Note: you can only lock pages that you have write access for. It is possible to create 587 * a lock for a page that does not exist, yet. 588 * 589 * Note: it is not necessary to lock a page before saving it. The `savePage()` call will 590 * automatically lock and unlock the page for you. However if you plan to do related 591 * operations on multiple pages, locking them all at once beforehand can be useful. 592 * 593 * @param string[] $pages A list of pages to lock 594 * @return string[] A list of pages that were successfully locked 595 */ 596 public function lockPages($pages) 597 { 598 $locked = []; 599 600 foreach ($pages as $id) { 601 $id = cleanID($id); 602 if ($id === '') continue; 603 if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) { 604 continue; 605 } 606 lock($id); 607 $locked[] = $id; 608 } 609 return $locked; 610 } 611 612 /** 613 * Unlock the given set of pages 614 * 615 * This call will try to unlock all given pages. It will return a list of pages that were 616 * successfully unlocked. If a page could not be unlocked, eg. because a different user is 617 * currently holding a lock, that page will be missing from the returned list. 618 * 619 * You should always ensure that the list of returned pages matches the given list of 620 * pages. It's up to you to decide how to handle failed unlocking. 621 * 622 * Note: you can only unlock pages that you have write access for. 623 * 624 * @param string[] $pages A list of pages to unlock 625 * @return string[] A list of pages that were successfully unlocked 626 */ 627 public function unlockPages($pages) 628 { 629 $unlocked = []; 630 631 foreach ($pages as $id) { 632 $id = cleanID($id); 633 if ($id === '') continue; 634 if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) { 635 continue; 636 } 637 $unlocked[] = $id; 638 } 639 640 return $unlocked; 641 } 642 643 /** 644 * Save a wiki page 645 * 646 * Saves the given wiki text to the given page. If the page does not exist, it will be created. 647 * Just like in the wiki, saving an empty text will delete the page. 648 * 649 * You need write permissions for the given page and the page may not be locked by another user. 650 * 651 * @param string $page page id 652 * @param string $text wiki text 653 * @param string $summary edit summary 654 * @param bool $isminor whether this is a minor edit 655 * @return bool Returns true on success 656 * @throws AccessDeniedException no write access for page 657 * @throws RemoteException no id, empty new page or locked 658 * @author Michael Klier <chi@chimeric.de> 659 */ 660 public function savePage($page, $text, $summary = '', $isminor = false) 661 { 662 global $TEXT; 663 global $lang; 664 665 $page = $this->checkPage($page, 0, false, AUTH_EDIT); 666 $TEXT = cleanText($text); 667 668 669 if (!page_exists($page) && trim($TEXT) == '') { 670 throw new RemoteException('Refusing to write an empty new wiki page', 132); 671 } 672 673 // Check, if page is locked 674 if (checklock($page)) { 675 throw new RemoteException('The page is currently locked', 133); 676 } 677 678 // SPAM check 679 if (checkwordblock()) { 680 throw new RemoteException('The page content was blocked', 134); 681 } 682 683 // autoset summary on new pages 684 if (!page_exists($page) && empty($summary)) { 685 $summary = $lang['created']; 686 } 687 688 // autoset summary on deleted pages 689 if (page_exists($page) && empty($TEXT) && empty($summary)) { 690 $summary = $lang['deleted']; 691 } 692 693 // FIXME auto set a summary in other cases "API Edit" might be a good idea? 694 695 lock($page); 696 saveWikiText($page, $TEXT, $summary, $isminor); 697 unlock($page); 698 699 // run the indexer if page wasn't indexed yet 700 idx_addPage($page); 701 702 return true; 703 } 704 705 /** 706 * Appends text to the end of a wiki page 707 * 708 * If the page does not exist, it will be created. If a page template for the non-existant 709 * page is configured, the given text will appended to that template. 710 * 711 * The call will create a new page revision. 712 * 713 * You need write permissions for the given page. 714 * 715 * @param string $page page id 716 * @param string $text wiki text 717 * @param string $summary edit summary 718 * @param bool $isminor whether this is a minor edit 719 * @return bool Returns true on success 720 * @throws AccessDeniedException 721 * @throws RemoteException 722 */ 723 public function appendPage($page, $text, $summary = '', $isminor = false) 724 { 725 $currentpage = $this->getPage($page); 726 if (!is_string($currentpage)) { 727 $currentpage = ''; 728 } 729 return $this->savePage($page, $currentpage . $text, $summary, $isminor); 730 } 731 732 // endregion 733 734 // region media 735 736 /** 737 * List all media files in the given namespace (and below) 738 * 739 * Setting the `depth` to `0` and the `namespace` to `""` will return all media files in the wiki. 740 * 741 * When `pattern` is given, it needs to be a valid regular expression as understood by PHP's 742 * `preg_match()` including delimiters. 743 * The pattern is matched against the full media ID, including the namespace. 744 * 745 * @link https://www.php.net/manual/en/reference.pcre.pattern.syntax.php 746 * @param string $namespace The namespace to search. Empty string for root namespace 747 * @param string $pattern A regular expression to filter the returned files 748 * @param int $depth How deep to search. 0 for all subnamespaces 749 * @param bool $hash Whether to include a MD5 hash of the media content 750 * @return Media[] 751 * @author Gina Haeussge <osd@foosel.net> 752 */ 753 public function listMedia($namespace = '', $pattern = '', $depth = 1, $hash = false) 754 { 755 global $conf; 756 757 $namespace = cleanID($namespace); 758 759 $options = [ 760 'skipacl' => 0, 761 'depth' => $depth, 762 'hash' => $hash, 763 'pattern' => $pattern, 764 ]; 765 766 $dir = utf8_encodeFN(str_replace(':', '/', $namespace)); 767 $data = []; 768 search($data, $conf['mediadir'], 'search_media', $options, $dir); 769 return array_map(static fn($item) => new Media( 770 $item['id'], 771 0, // we're searching current revisions only 772 $item['mtime'], 773 $item['size'], 774 $item['perm'], 775 $item['isimg'], 776 $item['hash'] ?? '' 777 ), $data); 778 } 779 780 /** 781 * Get recent media changes 782 * 783 * Returns a list of recent changes to media files. The results can be limited to changes newer than 784 * a given timestamp. 785 * 786 * Only changes within the configured `$conf['recent']` range are returned. This is the default 787 * when no timestamp is given. 788 * 789 * @link https://www.dokuwiki.org/config:recent 790 * @param int $timestamp Only show changes newer than this unix timestamp 791 * @return MediaChange[] 792 * @author Michael Klier <chi@chimeric.de> 793 * @author Michael Hamann <michael@content-space.de> 794 */ 795 public function getRecentMediaChanges($timestamp = 0) 796 { 797 798 $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES); 799 800 $changes = []; 801 foreach ($recents as $recent) { 802 $changes[] = new MediaChange( 803 $recent['id'], 804 $recent['date'], 805 $recent['user'], 806 $recent['ip'], 807 $recent['sum'], 808 $recent['type'], 809 $recent['sizechange'] 810 ); 811 } 812 813 return $changes; 814 } 815 816 /** 817 * Get a media file's content 818 * 819 * Returns the content of the given media file. When no revision is given, the current revision is returned. 820 * 821 * @link https://en.wikipedia.org/wiki/Base64 822 * @param string $media file id 823 * @param int $rev revision timestamp 824 * @return string Base64 encoded media file contents 825 * @throws AccessDeniedException no permission for media 826 * @throws RemoteException not exist 827 * @author Gina Haeussge <osd@foosel.net> 828 * 829 */ 830 public function getMedia($media, $rev = 0) 831 { 832 $media = cleanID($media); 833 if (auth_quickaclcheck($media) < AUTH_READ) { 834 throw new AccessDeniedException('You are not allowed to read this media file', 211); 835 } 836 837 $file = mediaFN($media, $rev); 838 if (!@ file_exists($file)) { 839 throw new RemoteException('The requested media file (revision) does not exist', 221); 840 } 841 842 $data = io_readFile($file, false); 843 return base64_encode($data); 844 } 845 846 /** 847 * Return info about a media file 848 * 849 * The call will return an error if the requested media file does not exist. 850 * 851 * Read access is required for the media file. 852 * 853 * @param string $media file id 854 * @param int $rev revision timestamp 855 * @param bool $author whether to include the author information 856 * @param bool $hash whether to include the MD5 hash of the media content 857 * @return Media 858 * @throws AccessDeniedException no permission for media 859 * @throws RemoteException if not exist 860 * @author Gina Haeussge <osd@foosel.net> 861 */ 862 public function getMediaInfo($media, $rev = 0, $author = false, $hash = false) 863 { 864 $media = cleanID($media); 865 if (auth_quickaclcheck($media) < AUTH_READ) { 866 throw new AccessDeniedException('You are not allowed to read this media file', 211); 867 } 868 if (!media_exists($media, $rev)) { 869 throw new RemoteException('The requested media file does not exist', 221); 870 } 871 872 $info = new Media($media, $rev); 873 if ($hash) $info->calculateHash(); 874 if ($author) $info->retrieveAuthor(); 875 876 return $info; 877 } 878 879 /** 880 * Returns the pages that use a given media file 881 * 882 * The call will return an error if the requested media file does not exist. 883 * 884 * Read access is required for the media file. 885 * 886 * Since API Version 13 887 * 888 * @param string $media file id 889 * @return string[] A list of pages linking to the given page 890 * @throws AccessDeniedException no permission for media 891 * @throws RemoteException if not exist 892 */ 893 public function getMediaUsage($media) 894 { 895 $media = cleanID($media); 896 if (auth_quickaclcheck($media) < AUTH_READ) { 897 throw new AccessDeniedException('You are not allowed to read this media file', 211); 898 } 899 if (!media_exists($media)) { 900 throw new RemoteException('The requested media file does not exist', 221); 901 } 902 903 return ft_mediause($media); 904 } 905 906 /** 907 * Uploads a file to the wiki 908 * 909 * The file data has to be passed as a base64 encoded string. 910 * 911 * @link https://en.wikipedia.org/wiki/Base64 912 * @param string $media media id 913 * @param string $base64 Base64 encoded file contents 914 * @param bool $overwrite Should an existing file be overwritten? 915 * @return bool Should always be true 916 * @throws RemoteException 917 * @author Michael Klier <chi@chimeric.de> 918 */ 919 public function saveMedia($media, $base64, $overwrite = false) 920 { 921 $media = cleanID($media); 922 $auth = auth_quickaclcheck(getNS($media) . ':*'); 923 924 if ($media === '') { 925 throw new RemoteException('Empty or invalid media ID given', 231); 926 } 927 928 // clean up base64 encoded data 929 $base64 = strtr($base64, [ 930 "\n" => '', // strip newlines 931 "\r" => '', // strip carriage returns 932 '-' => '+', // RFC4648 base64url 933 '_' => '/', // RFC4648 base64url 934 ' ' => '+', // JavaScript data uri 935 ]); 936 937 $data = base64_decode($base64, true); 938 if ($data === false) { 939 throw new RemoteException('Invalid base64 encoded data', 234); 940 } 941 942 if ($data === '') { 943 throw new RemoteException('Empty file given', 235); 944 } 945 946 // save temporary file 947 global $conf; 948 $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP()); 949 @unlink($ftmp); 950 io_saveFile($ftmp, $data); 951 952 $res = media_save(['name' => $ftmp], $media, $overwrite, $auth, 'rename'); 953 if (is_array($res)) { 954 throw new RemoteException('Failed to save media: ' . $res[0], 236); 955 } 956 return (bool)$res; // should always be true at this point 957 } 958 959 /** 960 * Deletes a file from the wiki 961 * 962 * You need to have delete permissions for the file. 963 * 964 * @param string $media media id 965 * @return bool Should always be true 966 * @throws AccessDeniedException no permissions 967 * @throws RemoteException file in use or not deleted 968 * @author Gina Haeussge <osd@foosel.net> 969 * 970 */ 971 public function deleteMedia($media) 972 { 973 $media = cleanID($media); 974 975 $auth = auth_quickaclcheck($media); 976 $res = media_delete($media, $auth); 977 if ($res & DOKU_MEDIA_DELETED) { 978 return true; 979 } elseif ($res & DOKU_MEDIA_NOT_AUTH) { 980 throw new AccessDeniedException('You are not allowed to delete this media file', 212); 981 } elseif ($res & DOKU_MEDIA_INUSE) { 982 throw new RemoteException('Media file is still referenced', 232); 983 } elseif (!media_exists($media)) { 984 throw new RemoteException('The media file requested to delete does not exist', 221); 985 } else { 986 throw new RemoteException('Failed to delete media file', 233); 987 } 988 } 989 990 // endregion 991 992 993 /** 994 * Convenience method for page checks 995 * 996 * This method will perform multiple tasks: 997 * 998 * - clean the given page id 999 * - disallow an empty page id 1000 * - check if the page exists (unless disabled) 1001 * - check if the user has the required access level (pass AUTH_NONE to skip) 1002 * 1003 * @param string $id page id 1004 * @param int $rev page revision 1005 * @param bool $existCheck 1006 * @param int $minAccess 1007 * @return string the cleaned page id 1008 * @throws AccessDeniedException 1009 * @throws RemoteException 1010 */ 1011 private function checkPage($id, $rev = 0, $existCheck = true, $minAccess = AUTH_READ) 1012 { 1013 $id = cleanID($id); 1014 if ($id === '') { 1015 throw new RemoteException('Empty or invalid page ID given', 131); 1016 } 1017 1018 if ($existCheck && !page_exists($id, $rev)) { 1019 throw new RemoteException('The requested page (revision) does not exist', 121); 1020 } 1021 1022 if ($minAccess && auth_quickaclcheck($id) < $minAccess) { 1023 throw new AccessDeniedException('You are not allowed to read this page', 111); 1024 } 1025 1026 return $id; 1027 } 1028} 1029