1dd87735dSAndreas Gohr<?php 2dd87735dSAndreas Gohr 3dd87735dSAndreas Gohrnamespace dokuwiki\Remote; 4dd87735dSAndreas Gohr 5e1d9dcc8SAndreas Gohruse dokuwiki\Extension\RemotePlugin; 642e66c7aSAndreas Gohruse dokuwiki\Logger; 7b433b69eSAndreas Gohruse dokuwiki\test\Remote\MockApiCore; 8dd87735dSAndreas Gohr 9dd87735dSAndreas Gohr/** 10dd87735dSAndreas Gohr * This class provides information about remote access to the wiki. 11dd87735dSAndreas Gohr * 12dd87735dSAndreas Gohr * == Types of methods == 13dd87735dSAndreas Gohr * There are two types of remote methods. The first is the core methods. 14dd87735dSAndreas Gohr * These are always available and provided by dokuwiki. 15dd87735dSAndreas Gohr * The other is plugin methods. These are provided by remote plugins. 16dd87735dSAndreas Gohr * 17dd87735dSAndreas Gohr * == Information structure == 18dd87735dSAndreas Gohr * The information about methods will be given in an array with the following structure: 19dd87735dSAndreas Gohr * array( 20dd87735dSAndreas Gohr * 'method.remoteName' => array( 21dd87735dSAndreas Gohr * 'args' => array( 22dd87735dSAndreas Gohr * 'type eg. string|int|...|date|file', 23dd87735dSAndreas Gohr * ) 24dd87735dSAndreas Gohr * 'name' => 'method name in class', 25dd87735dSAndreas Gohr * 'return' => 'type', 26dd87735dSAndreas Gohr * 'public' => 1/0 - method bypass default group check (used by login) 27dd87735dSAndreas Gohr * ['doc' = 'method documentation'], 28dd87735dSAndreas Gohr * ) 29dd87735dSAndreas Gohr * ) 30dd87735dSAndreas Gohr * 31dd87735dSAndreas Gohr * plugin names are formed the following: 32dd87735dSAndreas Gohr * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself. 33dd87735dSAndreas Gohr * i.e.: dokuwiki.version or wiki.getPage 34dd87735dSAndreas Gohr * 35dd87735dSAndreas Gohr * plugin methods are formed like 'plugin.<plugin name>.<method name>'. 36dd87735dSAndreas Gohr * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime 37dd87735dSAndreas Gohr */ 38dd87735dSAndreas Gohrclass Api 39dd87735dSAndreas Gohr{ 4042e66c7aSAndreas Gohr /** @var ApiCall[] core methods provided by dokuwiki */ 4142e66c7aSAndreas Gohr protected $coreMethods; 42dd87735dSAndreas Gohr 4342e66c7aSAndreas Gohr /** @var ApiCall[] remote methods provided by dokuwiki plugins */ 4442e66c7aSAndreas Gohr protected $pluginMethods; 45dd87735dSAndreas Gohr 46dd87735dSAndreas Gohr /** 47dd87735dSAndreas Gohr * Get all available methods with remote access. 48dd87735dSAndreas Gohr * 4942e66c7aSAndreas Gohr * @return ApiCall[] with information to all available methods 50dd87735dSAndreas Gohr */ 51dd87735dSAndreas Gohr public function getMethods() 52dd87735dSAndreas Gohr { 53dd87735dSAndreas Gohr return array_merge($this->getCoreMethods(), $this->getPluginMethods()); 54dd87735dSAndreas Gohr } 55dd87735dSAndreas Gohr 56dd87735dSAndreas Gohr /** 5742e66c7aSAndreas Gohr * Collects all the core methods 5842e66c7aSAndreas Gohr * 59b433b69eSAndreas Gohr * @param ApiCore|MockApiCore $apiCore this parameter is used for testing. 6042e66c7aSAndreas Gohr * Here you can pass a non-default RemoteAPICore instance. (for mocking) 6142e66c7aSAndreas Gohr * @return ApiCall[] all core methods. 6242e66c7aSAndreas Gohr */ 6342e66c7aSAndreas Gohr public function getCoreMethods($apiCore = null) 6442e66c7aSAndreas Gohr { 6542e66c7aSAndreas Gohr if (!$this->coreMethods) { 6642e66c7aSAndreas Gohr if ($apiCore === null) { 67e4e3d439SAndreas Gohr $this->coreMethods = (new ApiCore())->getMethods(); 6842e66c7aSAndreas Gohr } else { 696cce3332SAndreas Gohr $this->coreMethods = $apiCore->getMethods(); 7042e66c7aSAndreas Gohr } 7142e66c7aSAndreas Gohr } 7242e66c7aSAndreas Gohr return $this->coreMethods; 7342e66c7aSAndreas Gohr } 7442e66c7aSAndreas Gohr 7542e66c7aSAndreas Gohr /** 7642e66c7aSAndreas Gohr * Collects all the methods of the enabled Remote Plugins 7742e66c7aSAndreas Gohr * 7842e66c7aSAndreas Gohr * @return ApiCall[] all plugin methods. 7942e66c7aSAndreas Gohr */ 8042e66c7aSAndreas Gohr public function getPluginMethods() 8142e66c7aSAndreas Gohr { 8242e66c7aSAndreas Gohr if ($this->pluginMethods) return $this->pluginMethods; 8342e66c7aSAndreas Gohr 8442e66c7aSAndreas Gohr $plugins = plugin_list('remote'); 8542e66c7aSAndreas Gohr foreach ($plugins as $pluginName) { 8642e66c7aSAndreas Gohr /** @var RemotePlugin $plugin */ 8742e66c7aSAndreas Gohr $plugin = plugin_load('remote', $pluginName); 8842e66c7aSAndreas Gohr if (!is_subclass_of($plugin, RemotePlugin::class)) { 8942e66c7aSAndreas Gohr Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin"); 9042e66c7aSAndreas Gohr continue; 9142e66c7aSAndreas Gohr } 9242e66c7aSAndreas Gohr 9342e66c7aSAndreas Gohr try { 9442e66c7aSAndreas Gohr $methods = $plugin->getMethods(); 9542e66c7aSAndreas Gohr } catch (\ReflectionException $e) { 9642e66c7aSAndreas Gohr Logger::error( 9742e66c7aSAndreas Gohr "Remote Plugin $pluginName failed to return methods", 9842e66c7aSAndreas Gohr $e->getMessage(), 9942e66c7aSAndreas Gohr $e->getFile(), 10042e66c7aSAndreas Gohr $e->getLine() 10142e66c7aSAndreas Gohr ); 10242e66c7aSAndreas Gohr continue; 10342e66c7aSAndreas Gohr } 10442e66c7aSAndreas Gohr 10542e66c7aSAndreas Gohr foreach ($methods as $method => $call) { 10642e66c7aSAndreas Gohr $this->pluginMethods["plugin.$pluginName.$method"] = $call; 10742e66c7aSAndreas Gohr } 10842e66c7aSAndreas Gohr } 10942e66c7aSAndreas Gohr 11042e66c7aSAndreas Gohr return $this->pluginMethods; 11142e66c7aSAndreas Gohr } 11242e66c7aSAndreas Gohr 11342e66c7aSAndreas Gohr /** 114dd87735dSAndreas Gohr * Call a method via remote api. 115dd87735dSAndreas Gohr * 116dd87735dSAndreas Gohr * @param string $method name of the method to call. 117dd87735dSAndreas Gohr * @param array $args arguments to pass to the given method 118dd87735dSAndreas Gohr * @return mixed result of method call, must be a primitive type. 119dd87735dSAndreas Gohr * @throws RemoteException 120dd87735dSAndreas Gohr */ 121104a3b7cSAndreas Gohr public function call($method, $args = []) 122dd87735dSAndreas Gohr { 123dd87735dSAndreas Gohr if ($args === null) { 124104a3b7cSAndreas Gohr $args = []; 125dd87735dSAndreas Gohr } 126dd87735dSAndreas Gohr 12742e66c7aSAndreas Gohr // pre-flight checks 12842e66c7aSAndreas Gohr $this->ensureApiIsEnabled(); 12942e66c7aSAndreas Gohr $methods = $this->getMethods(); 13042e66c7aSAndreas Gohr if (!isset($methods[$method])) { 1311cdd0090SAndreas Gohr throw new RemoteException('Method does not exist', -32603); 132dd87735dSAndreas Gohr } 13342e66c7aSAndreas Gohr $this->ensureAccessIsAllowed($methods[$method]); 134dd87735dSAndreas Gohr 13542e66c7aSAndreas Gohr // invoke the ApiCall 136e1215f13SPhy try { 13742e66c7aSAndreas Gohr return $methods[$method]($args); 138*d1f06eb4SAndreas Gohr } catch (\InvalidArgumentException $e) { 139*d1f06eb4SAndreas Gohr throw new RemoteException($e->getMessage(), -32602); 140*d1f06eb4SAndreas Gohr } catch (\ArgumentCountError $e) { 141*d1f06eb4SAndreas Gohr throw new RemoteException($e->getMessage(), -32602); 142e1215f13SPhy } 143dd87735dSAndreas Gohr } 144dd87735dSAndreas Gohr 145dd87735dSAndreas Gohr /** 14642e66c7aSAndreas Gohr * Check that the API is generally enabled 147dd87735dSAndreas Gohr * 148dd87735dSAndreas Gohr * @return void 14942e66c7aSAndreas Gohr * @throws RemoteException thrown when the API is disabled 150dd87735dSAndreas Gohr */ 1511468a128SAndreas Gohr public function ensureApiIsEnabled() 152dd87735dSAndreas Gohr { 15342e66c7aSAndreas Gohr global $conf; 15442e66c7aSAndreas Gohr if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') { 1551468a128SAndreas Gohr throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604); 15642e66c7aSAndreas Gohr } 15742e66c7aSAndreas Gohr } 15842e66c7aSAndreas Gohr 15942e66c7aSAndreas Gohr /** 16042e66c7aSAndreas Gohr * Check if the current user is allowed to call the given method 16142e66c7aSAndreas Gohr * 16242e66c7aSAndreas Gohr * @param ApiCall $method 16342e66c7aSAndreas Gohr * @return void 16442e66c7aSAndreas Gohr * @throws AccessDeniedException Thrown when the user is not allowed to call the method 16542e66c7aSAndreas Gohr */ 1661468a128SAndreas Gohr public function ensureAccessIsAllowed(ApiCall $method) 16742e66c7aSAndreas Gohr { 16842e66c7aSAndreas Gohr global $conf; 16942e66c7aSAndreas Gohr global $INPUT; 17042e66c7aSAndreas Gohr global $USERINFO; 17142e66c7aSAndreas Gohr 17242e66c7aSAndreas Gohr if ($method->isPublic()) return; // public methods are always allowed 17342e66c7aSAndreas Gohr if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users 17442e66c7aSAndreas Gohr if (trim($conf['remoteuser']) === '') return; // all users are allowed 17542e66c7aSAndreas Gohr if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) { 17642e66c7aSAndreas Gohr return; // user is allowed 17742e66c7aSAndreas Gohr } 17842e66c7aSAndreas Gohr 17942e66c7aSAndreas Gohr // still here? no can do 180dd87735dSAndreas Gohr throw new AccessDeniedException('server error. not authorized to call method', -32604); 181dd87735dSAndreas Gohr } 182dd87735dSAndreas Gohr} 183