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