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