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