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