4 * Increased whenever the API is changed
6 define('DOKU_API_VERSION', 10);
9 * Provides the core methods for the remote API.
10 * The methods are ordered in 'wiki.<method>' and 'dokuwiki.<method>' namespaces
17 * @param RemoteAPI $api
19 public function __construct(RemoteAPI
$api) {
24 * Returns details about the core methods
28 public function __getRemoteInfo() {
30 'dokuwiki.getVersion' => array(
33 'doc' => 'Returns the running DokuWiki version.'
34 ), 'dokuwiki.login' => array(
35 'args' => array('string', 'string'),
37 'doc' => 'Tries to login with the given credentials and sets auth cookies.',
39 ), 'dokuwiki.logoff' => array(
42 'doc' => 'Tries to logoff by expiring auth cookies and the associated PHP session.'
43 ), 'dokuwiki.getPagelist' => array(
44 'args' => array('string', 'array'),
46 'doc' => 'List all pages within the given namespace.',
47 'name' => 'readNamespace'
48 ), 'dokuwiki.search' => array(
49 'args' => array('string'),
51 'doc' => 'Perform a fulltext search and return a list of matching pages'
52 ), 'dokuwiki.getTime' => array(
55 'doc' => 'Returns the current time at the remote wiki server as Unix timestamp.',
56 ), 'dokuwiki.setLocks' => array(
57 'args' => array('array'),
59 'doc' => 'Lock or unlock pages.'
60 ), 'dokuwiki.getTitle' => array(
63 'doc' => 'Returns the wiki title.',
65 ), 'dokuwiki.appendPage' => array(
66 'args' => array('string', 'string', 'array'),
68 'doc' => 'Append text to a wiki page.'
69 ), 'wiki.getPage' => array(
70 'args' => array('string'),
72 'doc' => 'Get the raw Wiki text of page, latest version.',
74 ), 'wiki.getPageVersion' => array(
75 'args' => array('string', 'int'),
78 'doc' => 'Return a raw wiki page'
79 ), 'wiki.getPageHTML' => array(
80 'args' => array('string'),
82 'doc' => 'Return page in rendered HTML, latest version.',
84 ), 'wiki.getPageHTMLVersion' => array(
85 'args' => array('string', 'int'),
87 'doc' => 'Return page in rendered HTML.',
89 ), 'wiki.getAllPages' => array(
92 'doc' => 'Returns a list of all pages. The result is an array of utf8 pagenames.',
94 ), 'wiki.getAttachments' => array(
95 'args' => array('string', 'array'),
97 'doc' => 'Returns a list of all media files.',
98 'name' => 'listAttachments'
99 ), 'wiki.getBackLinks' => array(
100 'args' => array('string'),
102 'doc' => 'Returns the pages that link to this page.',
103 'name' => 'listBackLinks'
104 ), 'wiki.getPageInfo' => array(
105 'args' => array('string'),
107 'doc' => 'Returns a struct with info about the page, latest version.',
109 ), 'wiki.getPageInfoVersion' => array(
110 'args' => array('string', 'int'),
112 'doc' => 'Returns a struct with info about the page.',
114 ), 'wiki.getPageVersions' => array(
115 'args' => array('string', 'int'),
117 'doc' => 'Returns the available revisions of the page.',
118 'name' => 'pageVersions'
119 ), 'wiki.putPage' => array(
120 'args' => array('string', 'string', 'array'),
122 'doc' => 'Saves a wiki page.'
123 ), 'wiki.listLinks' => array(
124 'args' => array('string'),
126 'doc' => 'Lists all links contained in a wiki page.'
127 ), 'wiki.getRecentChanges' => array(
128 'args' => array('int'),
130 'Returns a struct about all recent changes since given timestamp.'
131 ), 'wiki.getRecentMediaChanges' => array(
132 'args' => array('int'),
134 'Returns a struct about all recent media changes since given timestamp.'
135 ), 'wiki.aclCheck' => array(
136 'args' => array('string', 'string', 'array'),
138 'doc' => 'Returns the permissions of a given wiki page. By default, for current user/groups'
139 ), 'wiki.putAttachment' => array(
140 'args' => array('string', 'file', 'array'),
142 'doc' => 'Upload a file to the wiki.'
143 ), 'wiki.deleteAttachment' => array(
144 'args' => array('string'),
146 'doc' => 'Delete a file from the wiki.'
147 ), 'wiki.getAttachment' => array(
148 'args' => array('string'),
149 'doc' => 'Return a media file',
151 'name' => 'getAttachment',
152 ), 'wiki.getAttachmentInfo' => array(
153 'args' => array('string'),
155 'doc' => 'Returns a struct with info about the attachment.'
156 ), 'dokuwiki.getXMLRPCAPIVersion' => array(
158 'name' => 'getAPIVersion',
160 'doc' => 'Returns the XMLRPC API version.',
162 ), 'wiki.getRPCVersionSupported' => array(
164 'name' => 'wiki_RPCVersion',
166 'doc' => 'Returns 2 with the supported RPC API version.',
176 public function getVersion() {
181 * @return int unix timestamp
183 public function getTime() {
188 * Return a raw wiki page
190 * @param string $id wiki page id
191 * @param int|string $rev revision timestamp of the page or empty string
192 * @return string page text.
193 * @throws RemoteAccessDeniedException if no permission for page
195 public function rawPage($id,$rev=''){
196 $id = $this->resolvePageId($id);
197 if(auth_quickaclcheck($id) < AUTH_READ
){
198 throw new RemoteAccessDeniedException('You are not allowed to read this file', 111);
200 $text = rawWiki($id,$rev);
202 return pageTemplate($id);
209 * Return a media file
211 * @author Gina Haeussge <osd@foosel.net>
213 * @param string $id file id
214 * @return mixed media file
215 * @throws RemoteAccessDeniedException no permission for media
216 * @throws RemoteException not exist
218 public function getAttachment($id){
220 if (auth_quickaclcheck(getNS($id).':*') < AUTH_READ
) {
221 throw new RemoteAccessDeniedException('You are not allowed to read this file', 211);
224 $file = mediaFN($id);
225 if (!@ file_exists($file)) {
226 throw new RemoteException('The requested file does not exist', 221);
229 $data = io_readFile($file, false);
230 return $this->api
->toFile($data);
234 * Return info about a media file
236 * @author Gina Haeussge <osd@foosel.net>
238 * @param string $id page id
241 public function getAttachmentInfo($id){
244 'lastModified' => $this->api
->toDate(0),
248 $file = mediaFN($id);
249 if(auth_quickaclcheck(getNS($id) . ':*') >= AUTH_READ
) {
250 if(file_exists($file)) {
251 $info['lastModified'] = $this->api
->toDate(filemtime($file));
252 $info['size'] = filesize($file);
254 //Is it deleted media with changelog?
255 $medialog = new MediaChangeLog($id);
256 $revisions = $medialog->getRevisions(0, 1);
257 if(!empty($revisions)) {
258 $info['lastModified'] = $this->api
->toDate($revisions[0]);
267 * Return a wiki page rendered to html
269 * @param string $id page id
270 * @param string|int $rev revision timestamp or empty string
271 * @return null|string html
272 * @throws RemoteAccessDeniedException no access to page
274 public function htmlPage($id,$rev=''){
275 $id = $this->resolvePageId($id);
276 if(auth_quickaclcheck($id) < AUTH_READ
){
277 throw new RemoteAccessDeniedException('You are not allowed to read this page', 111);
279 return p_wiki_xhtml($id,$rev,false);
283 * List all pages - we use the indexer list here
287 public function listPages(){
289 $pages = idx_get_indexer()->getPages();
290 $pages = array_filter(array_filter($pages,'isVisiblePage'),'page_exists');
292 foreach(array_keys($pages) as $idx) {
293 $perm = auth_quickaclcheck($pages[$idx]);
294 if($perm < AUTH_READ
) {
298 $page['id'] = trim($pages[$idx]);
299 $page['perms'] = $perm;
300 $page['size'] = @filesize
(wikiFN($pages[$idx]));
301 $page['lastModified'] = $this->api
->toDate(@filemtime
(wikiFN($pages[$idx])));
309 * List all pages in the given namespace (and below)
313 * $opts['depth'] recursion level, 0 for all
314 * $opts['hash'] do md5 sum of content?
317 public function readNamespace($ns,$opts){
320 if(!is_array($opts)) $opts=array();
323 $dir = utf8_encodeFN(str_replace(':', '/', $ns));
325 $opts['skipacl'] = 0; // no ACL skipping for XMLRPC
326 search($data, $conf['datadir'], 'search_allpages', $opts, $dir);
331 * List all pages in the given namespace (and below)
333 * @param string $query
336 public function search($query){
338 $data = ft_pageSearch($query,$regex);
341 // prepare additional data
343 foreach($data as $id => $score){
346 if($idx < FT_SNIPPET_NUMBER
){
347 $snippet = ft_snippet($id,$regex);
355 'score' => intval($score),
356 'rev' => filemtime($file),
357 'mtime' => filemtime($file),
358 'size' => filesize($file),
359 'snippet' => $snippet,
360 'title' => useHeading('navigation') ?
p_get_first_heading($id) : $id
367 * Returns the wiki title.
371 public function getTitle(){
373 return $conf['title'];
377 * List all media files.
379 * Available options are 'recursive' for also including the subnamespaces
380 * in the listing, and 'pattern' for filtering the returned files against
381 * a regular expression matching their name.
383 * @author Gina Haeussge <osd@foosel.net>
386 * @param array $options
387 * $options['depth'] recursion level, 0 for all
388 * $options['showmsg'] shows message if invalid media id is used
389 * $options['pattern'] check given pattern
390 * $options['hash'] add hashes to result list
392 * @throws RemoteAccessDeniedException no access to the media files
394 public function listAttachments($ns, $options = array()) {
399 if (!is_array($options)) $options = array();
400 $options['skipacl'] = 0; // no ACL skipping for XMLRPC
402 if(auth_quickaclcheck($ns.':*') >= AUTH_READ
) {
403 $dir = utf8_encodeFN(str_replace(':', '/', $ns));
406 search($data, $conf['mediadir'], 'search_media', $options, $dir);
408 if(!$len) return array();
410 for($i=0; $i<$len; $i++
) {
411 unset($data[$i]['meta']);
412 $data[$i]['perms'] = $data[$i]['perm'];
413 unset($data[$i]['perm']);
414 $data[$i]['lastModified'] = $this->api
->toDate($data[$i]['mtime']);
418 throw new RemoteAccessDeniedException('You are not allowed to list media files.', 215);
423 * Return a list of backlinks
425 * @param string $id page id
428 function listBackLinks($id){
429 return ft_backlinks($this->resolvePageId($id));
433 * Return some basic data about a page
435 * @param string $id page id
436 * @param string|int $rev revision timestamp or empty string
438 * @throws RemoteAccessDeniedException no access for page
439 * @throws RemoteException page not exist
441 public function pageInfo($id,$rev=''){
442 $id = $this->resolvePageId($id);
443 if(auth_quickaclcheck($id) < AUTH_READ
){
444 throw new RemoteAccessDeniedException('You are not allowed to read this page', 111);
446 $file = wikiFN($id,$rev);
447 $time = @filemtime
($file);
449 throw new RemoteException('The requested page does not exist', 121);
452 // set revision to current version if empty, use revision otherwise
453 // as the timestamps of old files are not necessarily correct
458 $pagelog = new PageChangeLog($id, 1024);
459 $info = $pagelog->getRevisionInfo($rev);
463 'lastModified' => $this->api
->toDate($rev),
464 'author' => (($info['user']) ?
$info['user'] : $info['ip']),
474 * @author Michael Klier <chi@chimeric.de>
476 * @param string $id page id
477 * @param string $text wiki text
478 * @param array $params parameters: summary, minor edit
480 * @throws RemoteAccessDeniedException no write access for page
481 * @throws RemoteException no id, empty new page or locked
483 public function putPage($id, $text, $params) {
487 $id = $this->resolvePageId($id);
488 $TEXT = cleanText($text);
489 $sum = $params['sum'];
490 $minor = $params['minor'];
493 throw new RemoteException('Empty page ID', 131);
496 if(!page_exists($id) && trim($TEXT) == '' ) {
497 throw new RemoteException('Refusing to write an empty new wiki page', 132);
500 if(auth_quickaclcheck($id) < AUTH_EDIT
) {
501 throw new RemoteAccessDeniedException('You are not allowed to edit this page', 112);
504 // Check, if page is locked
506 throw new RemoteException('The page is currently locked', 133);
510 if(checkwordblock()) {
511 throw new RemoteException('Positive wordblock check', 134);
514 // autoset summary on new pages
515 if(!page_exists($id) && empty($sum)) {
516 $sum = $lang['created'];
519 // autoset summary on deleted pages
520 if(page_exists($id) && empty($TEXT) && empty($sum)) {
521 $sum = $lang['deleted'];
526 saveWikiText($id,$TEXT,$sum,$minor);
530 // run the indexer if page wasn't indexed yet
537 * Appends text to a wiki page.
539 * @param string $id page id
540 * @param string $text wiki text
541 * @param array $params such as summary,minor
542 * @return bool|string
544 public function appendPage($id, $text, $params) {
545 $currentpage = $this->rawPage($id);
546 if (!is_string($currentpage)) {
549 return $this->putPage($id, $currentpage.$text, $params);
553 * Uploads a file to the wiki.
555 * Michael Klier <chi@chimeric.de>
557 * @param string $id page id
558 * @param string $file
559 * @param array $params such as overwrite
560 * @return false|string
561 * @throws RemoteException
563 public function putAttachment($id, $file, $params) {
565 $auth = auth_quickaclcheck(getNS($id).':*');
568 throw new RemoteException('Filename not given.', 231);
573 $ftmp = $conf['tmpdir'] . '/' . md5($id.clientIP());
575 // save temporary file
577 io_saveFile($ftmp, $file);
579 $res = media_save(array('name' => $ftmp), $id, $params['ow'], $auth, 'rename');
580 if (is_array($res)) {
581 throw new RemoteException($res[0], -$res[1]);
588 * Deletes a file from the wiki.
590 * @author Gina Haeussge <osd@foosel.net>
592 * @param string $id page id
594 * @throws RemoteAccessDeniedException no permissions
595 * @throws RemoteException file in use or not deleted
597 public function deleteAttachment($id){
599 $auth = auth_quickaclcheck(getNS($id).':*');
600 $res = media_delete($id, $auth);
601 if ($res & DOKU_MEDIA_DELETED
) {
603 } elseif ($res & DOKU_MEDIA_NOT_AUTH
) {
604 throw new RemoteAccessDeniedException('You don\'t have permissions to delete files.', 212);
605 } elseif ($res & DOKU_MEDIA_INUSE
) {
606 throw new RemoteException('File is still referenced', 232);
608 throw new RemoteException('Could not delete file', 233);
613 * Returns the permissions of a given wiki page for the current user or another user
615 * @param string $id page id
616 * @param string|null $user username
617 * @param array|null $groups array of groups
618 * @return int permission level
620 public function aclCheck($id, $user = null, $groups = null) {
621 /** @var DokuWiki_Auth_Plugin $auth */
624 $id = $this->resolvePageId($id);
626 return auth_quickaclcheck($id);
628 if($groups === null) {
629 $userinfo = $auth->getUserData($user);
630 if($userinfo === false) {
633 $groups = $userinfo['grps'];
636 return auth_aclcheck($id, $user, $groups);
641 * Lists all links contained in a wiki page
643 * @author Michael Klier <chi@chimeric.de>
645 * @param string $id page id
647 * @throws RemoteAccessDeniedException no read access for page
649 public function listLinks($id) {
650 $id = $this->resolvePageId($id);
651 if(auth_quickaclcheck($id) < AUTH_READ
){
652 throw new RemoteAccessDeniedException('You are not allowed to read this page', 111);
656 // resolve page instructions
657 $ins = p_cached_instructions(wikiFN($id));
659 // instantiate new Renderer - needed for interwiki links
660 $Renderer = new Doku_Renderer_xhtml();
661 $Renderer->interwiki
= getInterwiki();
663 // parse parse instructions
664 foreach($ins as $in) {
668 $link['type'] = 'local';
669 $link['page'] = $in[1][0];
670 $link['href'] = wl($in[1][0]);
671 array_push($links,$link);
674 $link['type'] = 'extern';
675 $link['page'] = $in[1][0];
676 $link['href'] = $in[1][0];
677 array_push($links,$link);
679 case 'interwikilink':
680 $url = $Renderer->_resolveInterWiki($in[1][2],$in[1][3]);
681 $link['type'] = 'extern';
682 $link['page'] = $url;
683 $link['href'] = $url;
684 array_push($links,$link);
693 * Returns a list of recent changes since give timestamp
695 * @author Michael Hamann <michael@content-space.de>
696 * @author Michael Klier <chi@chimeric.de>
698 * @param int $timestamp unix timestamp
700 * @throws RemoteException no valid timestamp
702 public function getRecentChanges($timestamp) {
703 if(strlen($timestamp) != 10) {
704 throw new RemoteException('The provided value is not a valid timestamp', 311);
707 $recents = getRecentsSince($timestamp);
711 foreach ($recents as $recent) {
713 $change['name'] = $recent['id'];
714 $change['lastModified'] = $this->api
->toDate($recent['date']);
715 $change['author'] = $recent['user'];
716 $change['version'] = $recent['date'];
717 $change['perms'] = $recent['perms'];
718 $change['size'] = @filesize
(wikiFN($recent['id']));
719 array_push($changes, $change);
722 if (!empty($changes)) {
725 // in case we still have nothing at this point
726 throw new RemoteException('There are no changes in the specified timeframe', 321);
731 * Returns a list of recent media changes since give timestamp
733 * @author Michael Hamann <michael@content-space.de>
734 * @author Michael Klier <chi@chimeric.de>
736 * @param int $timestamp unix timestamp
738 * @throws RemoteException no valid timestamp
740 public function getRecentMediaChanges($timestamp) {
741 if(strlen($timestamp) != 10)
742 throw new RemoteException('The provided value is not a valid timestamp', 311);
744 $recents = getRecentsSince($timestamp, null, '', RECENTS_MEDIA_CHANGES
);
748 foreach ($recents as $recent) {
750 $change['name'] = $recent['id'];
751 $change['lastModified'] = $this->api
->toDate($recent['date']);
752 $change['author'] = $recent['user'];
753 $change['version'] = $recent['date'];
754 $change['perms'] = $recent['perms'];
755 $change['size'] = @filesize
(mediaFN($recent['id']));
756 array_push($changes, $change);
759 if (!empty($changes)) {
762 // in case we still have nothing at this point
763 throw new RemoteException('There are no changes in the specified timeframe', 321);
768 * Returns a list of available revisions of a given wiki page
769 * Number of returned pages is set by $conf['recent']
770 * However not accessible pages are skipped, so less than $conf['recent'] could be returned
772 * @author Michael Klier <chi@chimeric.de>
774 * @param string $id page id
775 * @param int $first skip the first n changelog lines (0 = from current(if exists), 1 = from 1st old rev, 2 = from 2nd old rev, etc)
777 * @throws RemoteAccessDeniedException no read access for page
778 * @throws RemoteException empty id
780 public function pageVersions($id, $first) {
781 $id = $this->resolvePageId($id);
782 if(auth_quickaclcheck($id) < AUTH_READ
) {
783 throw new RemoteAccessDeniedException('You are not allowed to read this page', 111);
790 throw new RemoteException('Empty page ID', 131);
793 $first = (int) $first;
794 $first_rev = $first - 1;
795 $first_rev = $first_rev < 0 ?
0 : $first_rev;
796 $pagelog = new PageChangeLog($id);
797 $revisions = $pagelog->getRevisions($first_rev, $conf['recent']);
800 array_unshift($revisions, ''); // include current revision
801 if ( count($revisions) > $conf['recent'] ){
802 array_pop($revisions); // remove extra log entry
806 if(!empty($revisions)) {
807 foreach($revisions as $rev) {
808 $file = wikiFN($id,$rev);
809 $time = @filemtime
($file);
810 // we check if the page actually exists, if this is not the
811 // case this can lead to less pages being returned than
812 // specified via $conf['recent']
814 $pagelog->setChunkSize(1024);
815 $info = $pagelog->getRevisionInfo($rev ?
$rev : $time);
818 $data['user'] = $info['user'];
819 $data['ip'] = $info['ip'];
820 $data['type'] = $info['type'];
821 $data['sum'] = $info['sum'];
822 $data['modified'] = $this->api
->toDate($info['date']);
823 $data['version'] = $info['date'];
824 array_push($versions, $data);
835 * The version of Wiki RPC API supported
837 public function wiki_RPCVersion(){
843 * Locks or unlocks a given batch of pages
845 * Give an associative array with two keys: lock and unlock. Both should contain a
846 * list of pages to lock or unlock
848 * Returns an associative array with the keys locked, lockfail, unlocked and
849 * unlockfail, each containing lists of pages.
851 * @param array[] $set list pages with array('lock' => array, 'unlock' => array)
854 public function setLocks($set){
858 $unlockfail = array();
860 foreach((array) $set['lock'] as $id){
861 $id = $this->resolvePageId($id);
862 if(auth_quickaclcheck($id) < AUTH_EDIT ||
checklock($id)){
870 foreach((array) $set['unlock'] as $id){
871 $id = $this->resolvePageId($id);
872 if(auth_quickaclcheck($id) < AUTH_EDIT ||
!unlock($id)){
881 'lockfail' => $lockfail,
882 'unlocked' => $unlocked,
883 'unlockfail' => $unlockfail,
892 public function getAPIVersion(){
893 return DOKU_API_VERSION
;
899 * @param string $user
900 * @param string $pass
903 public function login($user,$pass){
905 /** @var DokuWiki_Auth_Plugin $auth */
908 if(!$conf['useacl']) return 0;
911 @session_start
(); // reopen session for login
912 if($auth->canDo('external')){
913 $ok = $auth->trustExternal($user,$pass,false);
921 $ok = trigger_event('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
923 session_write_close(); // we're done with the session
933 public function logoff(){
936 if(!$conf['useacl']) return 0;
947 * @param string $id page id
950 private function resolvePageId($id) {
954 $id = cleanID($conf['start']);