1<?php 2 3namespace dokuwiki\Remote; 4 5use dokuwiki\Extension\RemotePlugin; 6use dokuwiki\Logger; 7use dokuwiki\test\Remote\MockApiCore; 8 9/** 10 * This class provides information about remote access to the wiki. 11 * 12 * == Types of methods == 13 * There are two types of remote methods. The first is the core methods. 14 * These are always available and provided by dokuwiki. 15 * The other is plugin methods. These are provided by remote plugins. 16 * 17 * == Information structure == 18 * The information about methods will be given in an array with the following structure: 19 * array( 20 * 'method.remoteName' => array( 21 * 'args' => array( 22 * 'type eg. string|int|...|date|file', 23 * ) 24 * 'name' => 'method name in class', 25 * 'return' => 'type', 26 * 'public' => 1/0 - method bypass default group check (used by login) 27 * ['doc' = 'method documentation'], 28 * ) 29 * ) 30 * 31 * plugin names are formed the following: 32 * core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself. 33 * i.e.: dokuwiki.version or wiki.getPage 34 * 35 * plugin methods are formed like 'plugin.<plugin name>.<method name>'. 36 * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime 37 */ 38class Api 39{ 40 /** @var ApiCall[] core methods provided by dokuwiki */ 41 protected $coreMethods; 42 43 /** @var ApiCall[] remote methods provided by dokuwiki plugins */ 44 protected $pluginMethods; 45 46 /** 47 * Get all available methods with remote access. 48 * 49 * @return ApiCall[] with information to all available methods 50 */ 51 public function getMethods() 52 { 53 return array_merge($this->getCoreMethods(), $this->getPluginMethods()); 54 } 55 56 /** 57 * Collects all the core methods 58 * 59 * @param ApiCore|MockApiCore $apiCore this parameter is used for testing. 60 * Here you can pass a non-default RemoteAPICore instance. (for mocking) 61 * @return ApiCall[] all core methods. 62 */ 63 public function getCoreMethods($apiCore = null) 64 { 65 if (!$this->coreMethods) { 66 if ($apiCore === null) { 67 $this->coreMethods = (new ApiCore())->getMethods(); 68 } else { 69 $this->coreMethods = $apiCore->getMethods(); 70 } 71 } 72 return $this->coreMethods; 73 } 74 75 /** 76 * Collects all the methods of the enabled Remote Plugins 77 * 78 * @return ApiCall[] all plugin methods. 79 */ 80 public function getPluginMethods() 81 { 82 if ($this->pluginMethods) return $this->pluginMethods; 83 84 $plugins = plugin_list('remote'); 85 foreach ($plugins as $pluginName) { 86 /** @var RemotePlugin $plugin */ 87 $plugin = plugin_load('remote', $pluginName); 88 if (!is_subclass_of($plugin, RemotePlugin::class)) { 89 Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin"); 90 continue; 91 } 92 93 try { 94 $methods = $plugin->getMethods(); 95 } catch (\ReflectionException $e) { 96 Logger::error( 97 "Remote Plugin $pluginName failed to return methods", 98 $e->getMessage(), 99 $e->getFile(), 100 $e->getLine() 101 ); 102 continue; 103 } 104 105 foreach ($methods as $method => $call) { 106 $this->pluginMethods["plugin.$pluginName.$method"] = $call; 107 } 108 } 109 110 return $this->pluginMethods; 111 } 112 113 /** 114 * Call a method via remote api. 115 * 116 * @param string $method name of the method to call. 117 * @param array $args arguments to pass to the given method 118 * @return mixed result of method call, must be a primitive type. 119 * @throws RemoteException 120 */ 121 public function call($method, $args = []) 122 { 123 if ($args === null) { 124 $args = []; 125 } 126 127 // pre-flight checks 128 $this->ensureApiIsEnabled(); 129 $methods = $this->getMethods(); 130 if (!isset($methods[$method])) { 131 throw new RemoteException('Method does not exist', -32603); 132 } 133 $this->ensureAccessIsAllowed($methods[$method]); 134 135 // invoke the ApiCall 136 try { 137 return $methods[$method]($args); 138 } catch (\InvalidArgumentException $e) { 139 throw new RemoteException($e->getMessage(), -32602); 140 } catch (\ArgumentCountError $e) { 141 throw new RemoteException($e->getMessage(), -32602); 142 } 143 } 144 145 /** 146 * Check that the API is generally enabled 147 * 148 * @return void 149 * @throws RemoteException thrown when the API is disabled 150 */ 151 public function ensureApiIsEnabled() 152 { 153 global $conf; 154 if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') { 155 throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604); 156 } 157 } 158 159 /** 160 * Check if the current user is allowed to call the given method 161 * 162 * @param ApiCall $method 163 * @return void 164 * @throws AccessDeniedException Thrown when the user is not allowed to call the method 165 */ 166 public function ensureAccessIsAllowed(ApiCall $method) 167 { 168 global $conf; 169 global $INPUT; 170 global $USERINFO; 171 172 if ($method->isPublic()) return; // public methods are always allowed 173 if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users 174 if (trim($conf['remoteuser']) === '') return; // all users are allowed 175 if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) { 176 return; // user is allowed 177 } 178 179 // still here? no can do 180 throw new AccessDeniedException('server error. not authorized to call method', -32604); 181 } 182} 183