[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

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

   1  <?php
   2  
   3  namespace dokuwiki\Remote;
   4  
   5  use dokuwiki\Extension\PluginInterface;
   6  use dokuwiki\Input\Input;
   7  use dokuwiki\Extension\Event;
   8  use dokuwiki\Extension\RemotePlugin;
   9  
  10  /**
  11   * This class provides information about remote access to the wiki.
  12   *
  13   * == Types of methods ==
  14   * There are two types of remote methods. The first is the core methods.
  15   * These are always available and provided by dokuwiki.
  16   * The other is plugin methods. These are provided by remote plugins.
  17   *
  18   * == Information structure ==
  19   * The information about methods will be given in an array with the following structure:
  20   * array(
  21   *     'method.remoteName' => array(
  22   *          'args' => array(
  23   *              'type eg. string|int|...|date|file',
  24   *          )
  25   *          'name' => 'method name in class',
  26   *          'return' => 'type',
  27   *          'public' => 1/0 - method bypass default group check (used by login)
  28   *          ['doc' = 'method documentation'],
  29   *     )
  30   * )
  31   *
  32   * plugin names are formed the following:
  33   *   core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
  34   *   i.e.: dokuwiki.version or wiki.getPage
  35   *
  36   * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
  37   * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
  38   */
  39  class Api
  40  {
  41      /**
  42       * @var ApiCore|\RemoteAPICoreTest
  43       */
  44      private $coreMethods;
  45  
  46      /**
  47       * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
  48       * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
  49       */
  50      private $pluginMethods;
  51  
  52      /**
  53       * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
  54       * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
  55       *
  56       * The remote method name is the same as in the remote name returned by _getMethods().
  57       */
  58      private $pluginCustomCalls;
  59  
  60      private $dateTransformation;
  61      private $fileTransformation;
  62  
  63      /**
  64       * constructor
  65       */
  66      public function __construct()
  67      {
  68          $this->dateTransformation = [$this, 'dummyTransformation'];
  69          $this->fileTransformation = [$this, 'dummyTransformation'];
  70      }
  71  
  72      /**
  73       * Get all available methods with remote access.
  74       *
  75       * @return array with information to all available methods
  76       * @throws RemoteException
  77       */
  78      public function getMethods()
  79      {
  80          return array_merge($this->getCoreMethods(), $this->getPluginMethods());
  81      }
  82  
  83      /**
  84       * Call a method via remote api.
  85       *
  86       * @param string $method name of the method to call.
  87       * @param array $args arguments to pass to the given method
  88       * @return mixed result of method call, must be a primitive type.
  89       * @throws RemoteException
  90       */
  91      public function call($method, $args = [])
  92      {
  93          if ($args === null) {
  94              $args = [];
  95          }
  96          // Ensure we have at least one '.' in $method
  97          [$type, $pluginName, /* call */] = sexplode('.', $method . '.', 3, '');
  98          if ($type === 'plugin') {
  99              return $this->callPlugin($pluginName, $method, $args);
 100          }
 101          if ($this->coreMethodExist($method)) {
 102              return $this->callCoreMethod($method, $args);
 103          }
 104          return $this->callCustomCallPlugin($method, $args);
 105      }
 106  
 107      /**
 108       * Check existance of core methods
 109       *
 110       * @param string $name name of the method
 111       * @return bool if method exists
 112       */
 113      private function coreMethodExist($name)
 114      {
 115          $coreMethods = $this->getCoreMethods();
 116          return array_key_exists($name, $coreMethods);
 117      }
 118  
 119      /**
 120       * Try to call custom methods provided by plugins
 121       *
 122       * @param string $method name of method
 123       * @param array $args
 124       * @return mixed
 125       * @throws RemoteException if method not exists
 126       */
 127      private function callCustomCallPlugin($method, $args)
 128      {
 129          $customCalls = $this->getCustomCallPlugins();
 130          if (!array_key_exists($method, $customCalls)) {
 131              throw new RemoteException('Method does not exist', -32603);
 132          }
 133          [$plugin, $method] = $customCalls[$method];
 134          $fullMethod = "plugin.$plugin.$method";
 135          return $this->callPlugin($plugin, $fullMethod, $args);
 136      }
 137  
 138      /**
 139       * Returns plugin calls that are registered via RPC_CALL_ADD action
 140       *
 141       * @return array with pairs of custom plugin calls
 142       * @triggers RPC_CALL_ADD
 143       */
 144      private function getCustomCallPlugins()
 145      {
 146          if ($this->pluginCustomCalls === null) {
 147              $data = [];
 148              Event::createAndTrigger('RPC_CALL_ADD', $data);
 149              $this->pluginCustomCalls = $data;
 150          }
 151          return $this->pluginCustomCalls;
 152      }
 153  
 154      /**
 155       * Call a plugin method
 156       *
 157       * @param string $pluginName
 158       * @param string $method method name
 159       * @param array $args
 160       * @return mixed return of custom method
 161       * @throws RemoteException
 162       */
 163      private function callPlugin($pluginName, $method, $args)
 164      {
 165          $plugin = plugin_load('remote', $pluginName);
 166          $methods = $this->getPluginMethods();
 167          if (!$plugin instanceof PluginInterface) {
 168              throw new RemoteException('Method does not exist', -32603);
 169          }
 170          $this->checkAccess($methods[$method]);
 171          $name = $this->getMethodName($methods, $method);
 172          try {
 173              set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
 174              return call_user_func_array([$plugin, $name], $args);
 175          } catch (\ArgumentCountError $th) {
 176              throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
 177          } finally {
 178              restore_error_handler();
 179          }
 180      }
 181  
 182      /**
 183       * Call a core method
 184       *
 185       * @param string $method name of method
 186       * @param array $args
 187       * @return mixed
 188       * @throws RemoteException if method not exist
 189       */
 190      private function callCoreMethod($method, $args)
 191      {
 192          $coreMethods = $this->getCoreMethods();
 193          $this->checkAccess($coreMethods[$method]);
 194          if (!isset($coreMethods[$method])) {
 195              throw new RemoteException('Method does not exist', -32603);
 196          }
 197          $this->checkArgumentLength($coreMethods[$method], $args);
 198          try {
 199              set_error_handler([$this, "argumentWarningHandler"], E_WARNING); // for PHP <7.1
 200              return call_user_func_array([$this->coreMethods, $this->getMethodName($coreMethods, $method)], $args);
 201          } catch (\ArgumentCountError $th) {
 202              throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
 203          } finally {
 204              restore_error_handler();
 205          }
 206      }
 207  
 208      /**
 209       * Check if access should be checked
 210       *
 211       * @param array $methodMeta data about the method
 212       * @throws AccessDeniedException
 213       */
 214      private function checkAccess($methodMeta)
 215      {
 216          if (!isset($methodMeta['public'])) {
 217              $this->forceAccess();
 218          } elseif ($methodMeta['public'] == '0') {
 219              $this->forceAccess();
 220          }
 221      }
 222  
 223      /**
 224       * Check the number of parameters
 225       *
 226       * @param array $methodMeta data about the method
 227       * @param array $args
 228       * @throws RemoteException if wrong parameter count
 229       */
 230      private function checkArgumentLength($methodMeta, $args)
 231      {
 232          if (count($methodMeta['args']) < count($args)) {
 233              throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
 234          }
 235      }
 236  
 237      /**
 238       * Determine the name of the real method
 239       *
 240       * @param array $methodMeta list of data of the methods
 241       * @param string $method name of method
 242       * @return string
 243       */
 244      private function getMethodName($methodMeta, $method)
 245      {
 246          if (isset($methodMeta[$method]['name'])) {
 247              return $methodMeta[$method]['name'];
 248          }
 249          $method = explode('.', $method);
 250          return $method[count($method) - 1];
 251      }
 252  
 253      /**
 254       * Perform access check for current user
 255       *
 256       * @return bool true if the current user has access to remote api.
 257       * @throws AccessDeniedException If remote access disabled
 258       */
 259      public function hasAccess()
 260      {
 261          global $conf;
 262          global $USERINFO;
 263          /** @var Input $INPUT */
 264          global $INPUT;
 265  
 266          if (!$conf['remote']) {
 267              throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
 268          }
 269          if (trim($conf['remoteuser']) == '!!not set!!') {
 270              return false;
 271          }
 272          if (!$conf['useacl']) {
 273              return true;
 274          }
 275          if (trim($conf['remoteuser']) == '') {
 276              return true;
 277          }
 278  
 279          return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
 280      }
 281  
 282      /**
 283       * Requests access
 284       *
 285       * @return void
 286       * @throws AccessDeniedException On denied access.
 287       */
 288      public function forceAccess()
 289      {
 290          if (!$this->hasAccess()) {
 291              throw new AccessDeniedException('server error. not authorized to call method', -32604);
 292          }
 293      }
 294  
 295      /**
 296       * Collects all the methods of the enabled Remote Plugins
 297       *
 298       * @return array all plugin methods.
 299       * @throws RemoteException if not implemented
 300       */
 301      public function getPluginMethods()
 302      {
 303          if ($this->pluginMethods === null) {
 304              $this->pluginMethods = [];
 305              $plugins = plugin_list('remote');
 306  
 307              foreach ($plugins as $pluginName) {
 308                  /** @var RemotePlugin $plugin */
 309                  $plugin = plugin_load('remote', $pluginName);
 310                  if (!is_subclass_of($plugin, 'dokuwiki\Extension\RemotePlugin')) {
 311                      throw new RemoteException(
 312                          "Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin"
 313                      );
 314                  }
 315  
 316                  try {
 317                      $methods = $plugin->_getMethods();
 318                  } catch (\ReflectionException $e) {
 319                      throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
 320                  }
 321  
 322                  foreach ($methods as $method => $meta) {
 323                      $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
 324                  }
 325              }
 326          }
 327          return $this->pluginMethods;
 328      }
 329  
 330      /**
 331       * Collects all the core methods
 332       *
 333       * @param ApiCore|\RemoteAPICoreTest $apiCore this parameter is used for testing.
 334       *        Here you can pass a non-default RemoteAPICore instance. (for mocking)
 335       * @return array all core methods.
 336       */
 337      public function getCoreMethods($apiCore = null)
 338      {
 339          if ($this->coreMethods === null) {
 340              if ($apiCore === null) {
 341                  $this->coreMethods = new ApiCore($this);
 342              } else {
 343                  $this->coreMethods = $apiCore;
 344              }
 345          }
 346          return $this->coreMethods->getRemoteInfo();
 347      }
 348  
 349      /**
 350       * Transform file to xml
 351       *
 352       * @param mixed $data
 353       * @return mixed
 354       */
 355      public function toFile($data)
 356      {
 357          return call_user_func($this->fileTransformation, $data);
 358      }
 359  
 360      /**
 361       * Transform date to xml
 362       *
 363       * @param mixed $data
 364       * @return mixed
 365       */
 366      public function toDate($data)
 367      {
 368          return call_user_func($this->dateTransformation, $data);
 369      }
 370  
 371      /**
 372       * A simple transformation
 373       *
 374       * @param mixed $data
 375       * @return mixed
 376       */
 377      public function dummyTransformation($data)
 378      {
 379          return $data;
 380      }
 381  
 382      /**
 383       * Set the transformer function
 384       *
 385       * @param callback $dateTransformation
 386       */
 387      public function setDateTransformation($dateTransformation)
 388      {
 389          $this->dateTransformation = $dateTransformation;
 390      }
 391  
 392      /**
 393       * Set the transformer function
 394       *
 395       * @param callback $fileTransformation
 396       */
 397      public function setFileTransformation($fileTransformation)
 398      {
 399          $this->fileTransformation = $fileTransformation;
 400      }
 401  
 402      /**
 403       * The error handler that catches argument-related warnings
 404       */
 405      public function argumentWarningHandler($errno, $errstr)
 406      {
 407          if (str_starts_with($errstr, 'Missing argument ')) {
 408              throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
 409          }
 410      }
 411  }