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