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