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