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