1<?php 2 3namespace dokuwiki\Remote; 4 5use Doku_Renderer_xhtml; 6use dokuwiki\ChangeLog\MediaChangeLog; 7use dokuwiki\ChangeLog\PageChangeLog; 8use dokuwiki\Extension\AuthPlugin; 9use dokuwiki\Extension\Event; 10use dokuwiki\Remote\Response\Page; 11use dokuwiki\Utf8\Sort; 12 13/** 14 * Provides the core methods for the remote API. 15 * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces 16 */ 17class ApiCore 18{ 19 /** @var int Increased whenever the API is changed */ 20 public const API_VERSION = 11; 21 22 23 /** @var Api */ 24 private $api; 25 26 /** 27 * @param Api $api 28 */ 29 public function __construct(Api $api) 30 { 31 $this->api = $api; 32 } 33 34 /** 35 * Returns details about the core methods 36 * 37 * @return array 38 */ 39 public function getRemoteInfo() 40 { 41 return [ 42 'dokuwiki.getVersion' => new ApiCall('getVersion'), 43 'dokuwiki.login' => (new ApiCall([$this, 'login'])) 44 ->setPublic(), 45 'dokuwiki.logoff' => new ApiCall([$this, 'logoff']), 46 'dokuwiki.getPagelist' => new ApiCall([$this, 'readNamespace']), 47 'dokuwiki.search' => new ApiCall([$this, 'search']), 48 'dokuwiki.getTime' => (new ApiCall([$this, 'time'])), 49 'dokuwiki.setLocks' => new ApiCall([$this, 'setLocks']), 50 'dokuwiki.getTitle' => (new ApiCall([$this, 'getTitle'])) 51 ->setPublic(), 52 'dokuwiki.appendPage' => new ApiCall([$this, 'appendPage']), 53 'dokuwiki.createUser' => new ApiCall([$this, 'createUser']), 54 'dokuwiki.deleteUsers' => new ApiCall([$this, 'deleteUsers']), 55// 'wiki.getPage' => (new ApiCall([$this, 'rawPage'])) 56// ->limitArgs(['page']), 57// 'wiki.getPageVersion' => (new ApiCall([$this, 'rawPage'])) 58// ->setSummary('Get a specific revision of a wiki page'), 59// 'wiki.getPageHTML' => (new ApiCall([$this, 'htmlPage'])) 60// ->limitArgs(['page']), 61// 'wiki.getPageHTMLVersion' => (new ApiCall([$this, 'htmlPage'])) 62// ->setSummary('Get the HTML for a specific revision of a wiki page'), 63 'wiki.getAllPages' => new ApiCall([$this, 'listPages']), 64 'wiki.getAttachments' => new ApiCall([$this, 'listAttachments']), 65 'wiki.getBackLinks' => new ApiCall([$this, 'listBackLinks']), 66// 'wiki.getPageInfo' => (new ApiCall([$this, 'pageInfo'])) 67// ->limitArgs(['page']), 68// 'wiki.getPageInfoVersion' => (new ApiCall([$this, 'pageInfo'])) 69// ->setSummary('Get some basic data about a specific revison of a wiki page'), 70 'wiki.getPageVersions' => new ApiCall([$this, 'pageVersions']), 71 'wiki.putPage' => new ApiCall([$this, 'putPage']), 72 'wiki.listLinks' => new ApiCall([$this, 'listLinks']), 73 'wiki.getRecentChanges' => new ApiCall([$this, 'getRecentChanges']), 74 'wiki.getRecentMediaChanges' => new ApiCall([$this, 'getRecentMediaChanges']), 75 'wiki.aclCheck' => new ApiCall([$this, 'aclCheck']), 76 'wiki.putAttachment' => new ApiCall([$this, 'putAttachment']), 77 'wiki.deleteAttachment' => new ApiCall([$this, 'deleteAttachment']), 78 'wiki.getAttachment' => new ApiCall([$this, 'getAttachment']), 79 'wiki.getAttachmentInfo' => new ApiCall([$this, 'getAttachmentInfo']), 80 'dokuwiki.getXMLRPCAPIVersion' => (new ApiCall([$this, 'getAPIVersion']))->setPublic(), 81 'wiki.getRPCVersionSupported' => (new ApiCall([$this, 'wikiRpcVersion']))->setPublic(), 82 ]; 83 } 84 85 /** 86 * Return the current server time 87 * 88 * Returns a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC). 89 * 90 * You can use this to compensate for differences between your client's time and the 91 * server's time when working with last modified timestamps (revisions). 92 * 93 * @return int A unix timestamp 94 */ 95 public function time() 96 { 97 return time(); 98 } 99 100 /** 101 * Return a raw wiki page 102 * 103 * @param string $page wiki page id 104 * @param int $rev revision timestamp of the page 105 * @return string the syntax of the page 106 * @throws AccessDeniedException if no permission for page 107 */ 108 public function rawPage($page, $rev = '') 109 { 110 $page = $this->resolvePageId($page); 111 if (auth_quickaclcheck($page) < AUTH_READ) { 112 throw new AccessDeniedException('You are not allowed to read this file', 111); 113 } 114 $text = rawWiki($page, $rev); 115 if (!$text) { 116 return pageTemplate($page); 117 } else { 118 return $text; 119 } 120 } 121 122 /** 123 * Return a media file 124 * 125 * @param string $media file id 126 * @return string media file contents 127 * @throws AccessDeniedException no permission for media 128 * @throws RemoteException not exist 129 * @author Gina Haeussge <osd@foosel.net> 130 * 131 */ 132 public function getAttachment($media) 133 { 134 $media = cleanID($media); 135 if (auth_quickaclcheck(getNS($media) . ':*') < AUTH_READ) { 136 throw new AccessDeniedException('You are not allowed to read this file', 211); 137 } 138 139 $file = mediaFN($media); 140 if (!@ file_exists($file)) { 141 throw new RemoteException('The requested file does not exist', 221); 142 } 143 144 $data = io_readFile($file, false); 145 return $this->api->toFile($data); 146 } 147 148 /** 149 * Return info about a media file 150 * 151 * @param string $media file id 152 * @return array 153 * @author Gina Haeussge <osd@foosel.net> 154 * 155 */ 156 public function getAttachmentInfo($media) 157 { 158 $media = cleanID($media); 159 $info = ['lastModified' => $this->api->toDate(0), 'size' => 0]; 160 161 $file = mediaFN($media); 162 if (auth_quickaclcheck(getNS($media) . ':*') >= AUTH_READ) { 163 if (file_exists($file)) { 164 $info['lastModified'] = $this->api->toDate(filemtime($file)); 165 $info['size'] = filesize($file); 166 } else { 167 //Is it deleted media with changelog? 168 $medialog = new MediaChangeLog($media); 169 $revisions = $medialog->getRevisions(0, 1); 170 if (!empty($revisions)) { 171 $info['lastModified'] = $this->api->toDate($revisions[0]); 172 } 173 } 174 } 175 176 return $info; 177 } 178 179 /** 180 * Return a wiki page rendered to HTML 181 * 182 * @param string $page page id 183 * @param string $rev revision timestamp 184 * @return string Rendered HTML for the page 185 * @throws AccessDeniedException no access to page 186 */ 187 public function htmlPage($page, $rev = '') 188 { 189 $page = $this->resolvePageId($page); 190 if (auth_quickaclcheck($page) < AUTH_READ) { 191 throw new AccessDeniedException('You are not allowed to read this page', 111); 192 } 193 return p_wiki_xhtml($page, $rev, false); 194 } 195 196 /** 197 * List all pages 198 * 199 * This uses the search index and only returns pages that have been indexed already 200 * 201 * @return Page[] A list of all pages with id, perms, size, lastModified 202 */ 203 public function listPages() 204 { 205 $list = []; 206 $pages = idx_get_indexer()->getPages(); 207 $pages = array_filter(array_filter($pages, 'isVisiblePage'), 'page_exists'); 208 Sort::ksort($pages); 209 210 foreach (array_keys($pages) as $idx) { 211 $perm = auth_quickaclcheck($pages[$idx]); 212 if ($perm < AUTH_READ) { 213 continue; 214 } 215 $list[] = new Page(['id' => $pages[$idx], 'perm' => $perm]); 216 } 217 218 return $list; 219 } 220 221 /** 222 * List all pages in the given namespace (and below) 223 * 224 * @param string $ns 225 * @param array $opts 226 * $opts['depth'] recursion level, 0 for all 227 * $opts['hash'] do md5 sum of content? 228 * @return Page[] A list of matching pages with id, rev, mtime, size, (hash) 229 */ 230 public function readNamespace($ns, $opts = []) 231 { 232 global $conf; 233 234 if (!is_array($opts)) $opts = []; 235 236 $ns = cleanID($ns); 237 $dir = utf8_encodeFN(str_replace(':', '/', $ns)); 238 $data = []; 239 $opts['skipacl'] = 0; // no ACL skipping for XMLRPC 240 search($data, $conf['datadir'], 'search_allpages', $opts, $dir); 241 242 $result = array_map(fn($item) => new Page($item), $data); 243 244 245 return $result; 246 } 247 248 /** 249 * Do a fulltext search 250 * 251 * This executes a full text search and returns the results. The query uses the standard 252 * DokuWiki search syntax. 253 * 254 * Snippets are provided for the first 15 results only. The title is either the first heading 255 * or the page id depending on the wiki's configuration. 256 * 257 * @link https://www.dokuwiki.org/search#syntax 258 * @param string $query The search query as supported by the DokuWiki search 259 * @return array[] A list of matching pages with id, score, rev, mtime, size, snippet, title 260 */ 261 public function search($query) 262 { 263 $regex = []; 264 $data = ft_pageSearch($query, $regex); 265 $pages = []; 266 267 // prepare additional data 268 $idx = 0; 269 foreach ($data as $id => $score) { 270 $file = wikiFN($id); 271 272 if ($idx < FT_SNIPPET_NUMBER) { 273 $snippet = ft_snippet($id, $regex); 274 $idx++; 275 } else { 276 $snippet = ''; 277 } 278 279 $pages[] = [ 280 'id' => $id, 281 'score' => (int)$score, 282 'rev' => filemtime($file), 283 'mtime' => filemtime($file), 284 'size' => filesize($file), 285 'snippet' => $snippet, 286 'title' => useHeading('navigation') ? p_get_first_heading($id) : $id 287 ]; 288 } 289 return $pages; 290 } 291 292 /** 293 * Returns the wiki title 294 * 295 * @link https://www.dokuwiki.org/config:title 296 * @return string 297 */ 298 public function getTitle() 299 { 300 global $conf; 301 return $conf['title']; 302 } 303 304 /** 305 * List all media files. 306 * 307 * Available options are 'recursive' for also including the subnamespaces 308 * in the listing, and 'pattern' for filtering the returned files against 309 * a regular expression matching their name. 310 * 311 * @param string $ns 312 * @param array $options 313 * $options['depth'] recursion level, 0 for all 314 * $options['showmsg'] shows message if invalid media id is used 315 * $options['pattern'] check given pattern 316 * $options['hash'] add hashes to result list 317 * @return array 318 * @throws AccessDeniedException no access to the media files 319 * @author Gina Haeussge <osd@foosel.net> 320 * 321 */ 322 public function listAttachments($ns, $options = []) 323 { 324 global $conf; 325 326 $ns = cleanID($ns); 327 328 if (!is_array($options)) $options = []; 329 $options['skipacl'] = 0; // no ACL skipping for XMLRPC 330 331 if (auth_quickaclcheck($ns . ':*') >= AUTH_READ) { 332 $dir = utf8_encodeFN(str_replace(':', '/', $ns)); 333 334 $data = []; 335 search($data, $conf['mediadir'], 'search_media', $options, $dir); 336 $len = count($data); 337 if (!$len) return []; 338 339 for ($i = 0; $i < $len; $i++) { 340 unset($data[$i]['meta']); 341 $data[$i]['perms'] = $data[$i]['perm']; 342 unset($data[$i]['perm']); 343 $data[$i]['lastModified'] = $this->api->toDate($data[$i]['mtime']); 344 } 345 return $data; 346 } else { 347 throw new AccessDeniedException('You are not allowed to list media files.', 215); 348 } 349 } 350 351 /** 352 * Return a list of backlinks 353 * 354 * @param string $page page id 355 * @return string[] 356 */ 357 public function listBackLinks($page) 358 { 359 return ft_backlinks($this->resolvePageId($page)); 360 } 361 362 /** 363 * Return some basic data about a page 364 * 365 * @param string $page page id 366 * @param string|int $rev revision timestamp or empty string 367 * @return array 368 * @throws AccessDeniedException no access for page 369 * @throws RemoteException page not exist 370 */ 371 public function pageInfo($page, $rev = '') 372 { 373 $page = $this->resolvePageId($page); 374 if (auth_quickaclcheck($page) < AUTH_READ) { 375 throw new AccessDeniedException('You are not allowed to read this page', 111); 376 } 377 $file = wikiFN($page, $rev); 378 $time = @filemtime($file); 379 if (!$time) { 380 throw new RemoteException('The requested page does not exist', 121); 381 } 382 383 // set revision to current version if empty, use revision otherwise 384 // as the timestamps of old files are not necessarily correct 385 if ($rev === '') { 386 $rev = $time; 387 } 388 389 $pagelog = new PageChangeLog($page, 1024); 390 $info = $pagelog->getRevisionInfo($rev); 391 392 $data = [ 393 'name' => $page, 394 'lastModified' => $this->api->toDate($rev), 395 'author' => is_array($info) ? ($info['user'] ?: $info['ip']) : null, 396 'version' => $rev 397 ]; 398 399 return ($data); 400 } 401 402 /** 403 * Save a wiki page 404 * 405 * Saves the given wiki text to the given page. If the page does not exist, it will be created. 406 * 407 * You need write permissions for the given page. 408 * 409 * @param string $page page id 410 * @param string $text wiki text 411 * @param array $params parameters: summary, minor edit 412 * @return bool 413 * @throws AccessDeniedException no write access for page 414 * @throws RemoteException no id, empty new page or locked 415 * @author Michael Klier <chi@chimeric.de> 416 */ 417 public function putPage($page, $text, $params = []) 418 { 419 global $TEXT; 420 global $lang; 421 422 $page = $this->resolvePageId($page); 423 $TEXT = cleanText($text); 424 $sum = $params['sum'] ?? ''; 425 $minor = $params['minor'] ?? false; 426 427 if (empty($page)) { 428 throw new RemoteException('Empty page ID', 131); 429 } 430 431 if (!page_exists($page) && trim($TEXT) == '') { 432 throw new RemoteException('Refusing to write an empty new wiki page', 132); 433 } 434 435 if (auth_quickaclcheck($page) < AUTH_EDIT) { 436 throw new AccessDeniedException('You are not allowed to edit this page', 112); 437 } 438 439 // Check, if page is locked 440 if (checklock($page)) { 441 throw new RemoteException('The page is currently locked', 133); 442 } 443 444 // SPAM check 445 if (checkwordblock()) { 446 throw new RemoteException('Positive wordblock check', 134); 447 } 448 449 // autoset summary on new pages 450 if (!page_exists($page) && empty($sum)) { 451 $sum = $lang['created']; 452 } 453 454 // autoset summary on deleted pages 455 if (page_exists($page) && empty($TEXT) && empty($sum)) { 456 $sum = $lang['deleted']; 457 } 458 459 lock($page); 460 461 saveWikiText($page, $TEXT, $sum, $minor); 462 463 unlock($page); 464 465 // run the indexer if page wasn't indexed yet 466 idx_addPage($page); 467 468 return true; 469 } 470 471 /** 472 * Appends text to the end of a wiki page 473 * 474 * If the page does not exist, it will be created. The call will create a new page revision. 475 * 476 * You need write permissions for the given page. 477 * 478 * @param string $page page id 479 * @param string $text wiki text 480 * @param array $params such as summary,minor 481 * @return bool|string 482 * @throws RemoteException 483 */ 484 public function appendPage($page, $text, $params = []) 485 { 486 $currentpage = $this->rawPage($page); 487 if (!is_string($currentpage)) { 488 return $currentpage; 489 } 490 return $this->putPage($page, $currentpage . $text, $params); 491 } 492 493 /** 494 * Create a new user 495 * 496 * If no password is provided, a password is auto generated. 497 * 498 * You need to be a superuser to create users. 499 * 500 * @param array[] $userStruct User struct with user, password, name, mail, groups, notify 501 * @return boolean Was the user successfully created? 502 * @throws AccessDeniedException 503 * @throws RemoteException 504 */ 505 public function createUser($userStruct) 506 { 507 if (!auth_isadmin()) { 508 throw new AccessDeniedException('Only admins are allowed to create users', 114); 509 } 510 511 /** @var AuthPlugin $auth */ 512 global $auth; 513 514 if (!$auth->canDo('addUser')) { 515 throw new AccessDeniedException( 516 sprintf('Authentication backend %s can\'t do addUser', $auth->getPluginName()), 517 114 518 ); 519 } 520 521 $user = trim($auth->cleanUser($userStruct['user'] ?? '')); 522 $password = $userStruct['password'] ?? ''; 523 $name = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $userStruct['name'] ?? '')); 524 $mail = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $userStruct['mail'] ?? '')); 525 $groups = $userStruct['groups'] ?? []; 526 527 $notify = (bool)($userStruct['notify'] ?? false); 528 529 if ($user === '') throw new RemoteException('empty or invalid user', 401); 530 if ($name === '') throw new RemoteException('empty or invalid user name', 402); 531 if (!mail_isvalid($mail)) throw new RemoteException('empty or invalid mail address', 403); 532 533 if ((string)$password === '') { 534 $password = auth_pwgen($user); 535 } 536 537 if (!is_array($groups) || $groups === []) { 538 $groups = null; 539 } 540 541 $ok = $auth->triggerUserMod('create', [$user, $password, $name, $mail, $groups]); 542 543 if ($ok !== false && $ok !== null) { 544 $ok = true; 545 } 546 547 if ($ok) { 548 if ($notify) { 549 auth_sendPassword($user, $password); 550 } 551 } 552 553 return $ok; 554 } 555 556 557 /** 558 * Remove one or more users from the list of registered users 559 * 560 * You need to be a superuser to delete users. 561 * 562 * @param string[] $usernames List of usernames to remove 563 * @return bool if the users were successfully deleted 564 * @throws AccessDeniedException 565 */ 566 public function deleteUsers($usernames) 567 { 568 if (!auth_isadmin()) { 569 throw new AccessDeniedException('Only admins are allowed to delete users', 114); 570 } 571 /** @var AuthPlugin $auth */ 572 global $auth; 573 return (bool)$auth->triggerUserMod('delete', [$usernames]); 574 } 575 576 /** 577 * Uploads a file to the wiki 578 * 579 * @param string $media media id 580 * @param string $data file contents 581 * @param array $params such as overwrite 582 * @return false|string 583 * @throws RemoteException 584 * @author Michael Klier <chi@chimeric.de> 585 */ 586 public function putAttachment($media, $data, $params = []) 587 { 588 $media = cleanID($media); 589 $auth = auth_quickaclcheck(getNS($media) . ':*'); 590 591 if (!isset($media)) { 592 throw new RemoteException('Filename not given.', 231); 593 } 594 595 global $conf; 596 597 $ftmp = $conf['tmpdir'] . '/' . md5($media . clientIP()); 598 599 // save temporary file 600 @unlink($ftmp); 601 io_saveFile($ftmp, $data); 602 603 $res = media_save(['name' => $ftmp], $media, $params['ow'], $auth, 'rename'); 604 if (is_array($res)) { 605 throw new RemoteException($res[0], -$res[1]); 606 } else { 607 return $res; 608 } 609 } 610 611 /** 612 * Deletes a file from the wiki 613 * 614 * You need to have delete permissions for the file. 615 * 616 * @param string $media media id 617 * @return int 618 * @throws AccessDeniedException no permissions 619 * @throws RemoteException file in use or not deleted 620 * @author Gina Haeussge <osd@foosel.net> 621 * 622 */ 623 public function deleteAttachment($media) 624 { 625 $media = cleanID($media); 626 $auth = auth_quickaclcheck(getNS($media) . ':*'); 627 $res = media_delete($media, $auth); 628 if ($res & DOKU_MEDIA_DELETED) { 629 return 0; 630 } elseif ($res & DOKU_MEDIA_NOT_AUTH) { 631 throw new AccessDeniedException('You don\'t have permissions to delete files.', 212); 632 } elseif ($res & DOKU_MEDIA_INUSE) { 633 throw new RemoteException('File is still referenced', 232); 634 } else { 635 throw new RemoteException('Could not delete file', 233); 636 } 637 } 638 639 /** 640 * Returns the permissions of a given wiki page for the current user or another user 641 * 642 * @param string $page page id 643 * @param string|null $user username 644 * @param array|null $groups array of groups 645 * @return int permission level 646 */ 647 public function aclCheck($page, $user = null, $groups = null) 648 { 649 /** @var AuthPlugin $auth */ 650 global $auth; 651 652 $page = $this->resolvePageId($page); 653 if ($user === null) { 654 return auth_quickaclcheck($page); 655 } else { 656 if ($groups === null) { 657 $userinfo = $auth->getUserData($user); 658 if ($userinfo === false) { 659 $groups = []; 660 } else { 661 $groups = $userinfo['grps']; 662 } 663 } 664 return auth_aclcheck($page, $user, $groups); 665 } 666 } 667 668 /** 669 * Lists all links contained in a wiki page 670 * 671 * @param string $page page id 672 * @return array 673 * @throws AccessDeniedException no read access for page 674 * @author Michael Klier <chi@chimeric.de> 675 * 676 */ 677 public function listLinks($page) 678 { 679 $page = $this->resolvePageId($page); 680 if (auth_quickaclcheck($page) < AUTH_READ) { 681 throw new AccessDeniedException('You are not allowed to read this page', 111); 682 } 683 $links = []; 684 685 // resolve page instructions 686 $ins = p_cached_instructions(wikiFN($page)); 687 688 // instantiate new Renderer - needed for interwiki links 689 $Renderer = new Doku_Renderer_xhtml(); 690 $Renderer->interwiki = getInterwiki(); 691 692 // parse parse instructions 693 foreach ($ins as $in) { 694 $link = []; 695 switch ($in[0]) { 696 case 'internallink': 697 $link['type'] = 'local'; 698 $link['page'] = $in[1][0]; 699 $link['href'] = wl($in[1][0]); 700 $links[] = $link; 701 break; 702 case 'externallink': 703 $link['type'] = 'extern'; 704 $link['page'] = $in[1][0]; 705 $link['href'] = $in[1][0]; 706 $links[] = $link; 707 break; 708 case 'interwikilink': 709 $url = $Renderer->_resolveInterWiki($in[1][2], $in[1][3]); 710 $link['type'] = 'extern'; 711 $link['page'] = $url; 712 $link['href'] = $url; 713 $links[] = $link; 714 break; 715 } 716 } 717 718 return ($links); 719 } 720 721 /** 722 * Returns a list of recent changes since given timestamp 723 * 724 * The results are limited to date range configured in $conf['recent'] 725 * 726 * @link https://www.dokuwiki.org/config:recent 727 * @param int $timestamp unix timestamp 728 * @return array 729 * @throws RemoteException no valid timestamp 730 * @author Michael Klier <chi@chimeric.de> 731 * 732 * @author Michael Hamann <michael@content-space.de> 733 */ 734 public function getRecentChanges($timestamp) 735 { 736 if (strlen($timestamp) != 10) { 737 throw new RemoteException('The provided value is not a valid timestamp', 311); 738 } 739 740 $recents = getRecentsSince($timestamp); 741 742 $changes = []; 743 744 foreach ($recents as $recent) { 745 $change = []; 746 $change['name'] = $recent['id']; 747 $change['lastModified'] = $this->api->toDate($recent['date']); 748 $change['author'] = $recent['user']; 749 $change['version'] = $recent['date']; 750 $change['perms'] = $recent['perms']; 751 $change['size'] = @filesize(wikiFN($recent['id'])); 752 $changes[] = $change; 753 } 754 755 if ($changes !== []) { 756 return $changes; 757 } else { 758 // in case we still have nothing at this point 759 throw new RemoteException('There are no changes in the specified timeframe', 321); 760 } 761 } 762 763 /** 764 * Returns a list of recent media changes since given timestamp 765 * 766 * @param int $timestamp unix timestamp 767 * @return array 768 * @throws RemoteException no valid timestamp 769 * @author Michael Klier <chi@chimeric.de> 770 * 771 * @author Michael Hamann <michael@content-space.de> 772 */ 773 public function getRecentMediaChanges($timestamp) 774 { 775 if (strlen($timestamp) != 10) 776 throw new RemoteException('The provided value is not a valid timestamp', 311); 777 778 $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES); 779 780 $changes = []; 781 782 foreach ($recents as $recent) { 783 $change = []; 784 $change['name'] = $recent['id']; 785 $change['lastModified'] = $this->api->toDate($recent['date']); 786 $change['author'] = $recent['user']; 787 $change['version'] = $recent['date']; 788 $change['perms'] = $recent['perms']; 789 $change['size'] = @filesize(mediaFN($recent['id'])); 790 $changes[] = $change; 791 } 792 793 if ($changes !== []) { 794 return $changes; 795 } else { 796 // in case we still have nothing at this point 797 throw new RemoteException('There are no changes in the specified timeframe', 321); 798 } 799 } 800 801 /** 802 * Returns a list of available revisions of a given wiki page 803 * Number of returned pages is set by $conf['recent'] 804 * However not accessible pages are skipped, so less than $conf['recent'] could be returned 805 * 806 * @param string $page page id 807 * @param int $first skip the first n changelog lines 808 * 0 = from current(if exists) 809 * 1 = from 1st old rev 810 * 2 = from 2nd old rev, etc 811 * @return array 812 * @throws AccessDeniedException no read access for page 813 * @throws RemoteException empty id 814 * @author Michael Klier <chi@chimeric.de> 815 * 816 */ 817 public function pageVersions($page, $first = 0) 818 { 819 $page = $this->resolvePageId($page); 820 if (auth_quickaclcheck($page) < AUTH_READ) { 821 throw new AccessDeniedException('You are not allowed to read this page', 111); 822 } 823 global $conf; 824 825 $versions = []; 826 827 if (empty($page)) { 828 throw new RemoteException('Empty page ID', 131); 829 } 830 831 $first = (int)$first; 832 $first_rev = $first - 1; 833 $first_rev = max(0, $first_rev); 834 835 $pagelog = new PageChangeLog($page); 836 $revisions = $pagelog->getRevisions($first_rev, $conf['recent']); 837 838 if ($first == 0) { 839 array_unshift($revisions, ''); // include current revision 840 if (count($revisions) > $conf['recent']) { 841 array_pop($revisions); // remove extra log entry 842 } 843 } 844 845 if (!empty($revisions)) { 846 foreach ($revisions as $rev) { 847 $file = wikiFN($page, $rev); 848 $time = @filemtime($file); 849 // we check if the page actually exists, if this is not the 850 // case this can lead to less pages being returned than 851 // specified via $conf['recent'] 852 if ($time) { 853 $pagelog->setChunkSize(1024); 854 $info = $pagelog->getRevisionInfo($rev ?: $time); 855 if (!empty($info)) { 856 $data = []; 857 $data['user'] = $info['user']; 858 $data['ip'] = $info['ip']; 859 $data['type'] = $info['type']; 860 $data['sum'] = $info['sum']; 861 $data['modified'] = $this->api->toDate($info['date']); 862 $data['version'] = $info['date']; 863 $versions[] = $data; 864 } 865 } 866 } 867 return $versions; 868 } else { 869 return []; 870 } 871 } 872 873 /** 874 * The version of Wiki RPC API supported 875 * 876 * This is the version of the Wiki RPC specification implemented. Since that specification 877 * is no longer maintained, this will always return 2 878 * 879 * You probably want to look at dokuwiki.getXMLRPCAPIVersion instead 880 * 881 * @return int 882 */ 883 public function wikiRpcVersion() 884 { 885 return 2; 886 } 887 888 /** 889 * Locks or unlocks a given batch of pages 890 * 891 * Give an associative array with two keys: lock and unlock. Both should contain a 892 * list of pages to lock or unlock 893 * 894 * Returns an associative array with the keys locked, lockfail, unlocked and 895 * unlockfail, each containing lists of pages. 896 * 897 * @param array[] $set list pages with ['lock' => [], 'unlock' => []] 898 * @return array[] list of pages with ['locked' => [], 'lockfail' => [], 'unlocked' => [], 'unlockfail' => []] 899 */ 900 public function setLocks($set) 901 { 902 $locked = []; 903 $lockfail = []; 904 $unlocked = []; 905 $unlockfail = []; 906 907 foreach ($set['lock'] as $id) { 908 $id = $this->resolvePageId($id); 909 if (auth_quickaclcheck($id) < AUTH_EDIT || checklock($id)) { 910 $lockfail[] = $id; 911 } else { 912 lock($id); 913 $locked[] = $id; 914 } 915 } 916 917 foreach ($set['unlock'] as $id) { 918 $id = $this->resolvePageId($id); 919 if (auth_quickaclcheck($id) < AUTH_EDIT || !unlock($id)) { 920 $unlockfail[] = $id; 921 } else { 922 $unlocked[] = $id; 923 } 924 } 925 926 return [ 927 'locked' => $locked, 928 'lockfail' => $lockfail, 929 'unlocked' => $unlocked, 930 'unlockfail' => $unlockfail 931 ]; 932 } 933 934 /** 935 * Return the API version 936 * 937 * This is the version of the DokuWiki API. It increases whenever the API definition changes. 938 * 939 * When developing a client, you should check this version and make sure you can handle it. 940 * 941 * @return int 942 */ 943 public function getAPIVersion() 944 { 945 return self::API_VERSION; 946 } 947 948 /** 949 * Login 950 * 951 * This will use the given credentials and attempt to login the user. This will set the 952 * appropriate cookies, which can be used for subsequent requests. 953 * 954 * Use of this mechanism is discouraged. Using token authentication is preferred. 955 * 956 * @param string $user The user name 957 * @param string $pass The password 958 * @return int If the login was successful 959 */ 960 public function login($user, $pass) 961 { 962 global $conf; 963 /** @var AuthPlugin $auth */ 964 global $auth; 965 966 if (!$conf['useacl']) return 0; 967 if (!$auth instanceof AuthPlugin) return 0; 968 969 @session_start(); // reopen session for login 970 $ok = null; 971 if ($auth->canDo('external')) { 972 $ok = $auth->trustExternal($user, $pass, false); 973 } 974 if ($ok === null) { 975 $evdata = [ 976 'user' => $user, 977 'password' => $pass, 978 'sticky' => false, 979 'silent' => true 980 ]; 981 $ok = Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper'); 982 } 983 session_write_close(); // we're done with the session 984 985 return $ok; 986 } 987 988 /** 989 * Log off 990 * 991 * Attempt to log out the current user, deleting the appropriate cookies 992 * 993 * @return int 0 on failure, 1 on success 994 */ 995 public function logoff() 996 { 997 global $conf; 998 global $auth; 999 if (!$conf['useacl']) return 0; 1000 if (!$auth instanceof AuthPlugin) return 0; 1001 1002 auth_logoff(); 1003 1004 return 1; 1005 } 1006 1007 /** 1008 * Resolve page id 1009 * 1010 * @param string $id page id 1011 * @return string 1012 */ 1013 private function resolvePageId($id) 1014 { 1015 $id = cleanID($id); 1016 if (empty($id)) { 1017 global $conf; 1018 $id = cleanID($conf['start']); 1019 } 1020 return $id; 1021 } 1022} 1023