[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/Remote/ -> ApiCore.php (source)

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