ApiCore.php 34 KB

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