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