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