1<?php 2 3namespace dokuwiki\Remote; 4 5use dokuwiki\Extension\RemotePlugin; 6use dokuwiki\Logger; 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 */ 37class Api 38{ 39 /** @var ApiCall[] core methods provided by dokuwiki */ 40 protected $coreMethods; 41 42 /** @var ApiCall[] remote methods provided by dokuwiki plugins */ 43 protected $pluginMethods; 44 45 private $dateTransformation; 46 private $fileTransformation; 47 48 /** 49 * constructor 50 */ 51 public function __construct() 52 { 53 $this->dateTransformation = [$this, 'dummyTransformation']; 54 $this->fileTransformation = [$this, 'dummyTransformation']; 55 } 56 57 /** 58 * Get all available methods with remote access. 59 * 60 * @return ApiCall[] with information to all available methods 61 * @throws RemoteException 62 */ 63 public function getMethods() 64 { 65 return array_merge($this->getCoreMethods(), $this->getPluginMethods()); 66 } 67 68 /** 69 * Collects all the core methods 70 * 71 * @param ApiCore|\RemoteAPICoreTest $apiCore this parameter is used for testing. 72 * Here you can pass a non-default RemoteAPICore instance. (for mocking) 73 * @return ApiCall[] all core methods. 74 */ 75 public function getCoreMethods($apiCore = null) 76 { 77 if (!$this->coreMethods) { 78 if ($apiCore === null) { 79 $this->coreMethods = (new ApiCore($this))->getRemoteInfo(); 80 } else { 81 $this->coreMethods = $apiCore->getRemoteInfo(); 82 } 83 } 84 return $this->coreMethods; 85 } 86 87 /** 88 * Collects all the methods of the enabled Remote Plugins 89 * 90 * @return ApiCall[] all plugin methods. 91 */ 92 public function getPluginMethods() 93 { 94 if ($this->pluginMethods) return $this->pluginMethods; 95 96 $plugins = plugin_list('remote'); 97 foreach ($plugins as $pluginName) { 98 /** @var RemotePlugin $plugin */ 99 $plugin = plugin_load('remote', $pluginName); 100 if (!is_subclass_of($plugin, RemotePlugin::class)) { 101 Logger::error("Remote Plugin $pluginName does not implement dokuwiki\Extension\RemotePlugin"); 102 continue; 103 } 104 105 try { 106 $methods = $plugin->getMethods(); 107 } catch (\ReflectionException $e) { 108 Logger::error( 109 "Remote Plugin $pluginName failed to return methods", 110 $e->getMessage(), 111 $e->getFile(), 112 $e->getLine() 113 ); 114 continue; 115 } 116 117 foreach ($methods as $method => $call) { 118 $this->pluginMethods["plugin.$pluginName.$method"] = $call; 119 } 120 } 121 122 return $this->pluginMethods; 123 } 124 125 /** 126 * Call a method via remote api. 127 * 128 * @param string $method name of the method to call. 129 * @param array $args arguments to pass to the given method 130 * @return mixed result of method call, must be a primitive type. 131 * @throws RemoteException 132 */ 133 public function call($method, $args = []) 134 { 135 if ($args === null) { 136 $args = []; 137 } 138 139 // pre-flight checks 140 $this->ensureApiIsEnabled(); 141 $methods = $this->getMethods(); 142 if (!isset($methods[$method])) { 143 throw new RemoteException('Method does not exist', -32603); 144 } 145 $this->ensureAccessIsAllowed($methods[$method]); 146 147 // invoke the ApiCall 148 try { 149 return $methods[$method]($args); 150 } catch (\ArgumentCountError $th) { 151 throw new RemoteException('Method does not exist - wrong parameter count.', -32603); 152 } 153 } 154 155 /** 156 * Check that the API is generally enabled 157 * 158 * @return void 159 * @throws RemoteException thrown when the API is disabled 160 */ 161 public function ensureApiIsEnabled() 162 { 163 global $conf; 164 if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') { 165 throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604); 166 } 167 } 168 169 /** 170 * Check if the current user is allowed to call the given method 171 * 172 * @param ApiCall $method 173 * @return void 174 * @throws AccessDeniedException Thrown when the user is not allowed to call the method 175 */ 176 public function ensureAccessIsAllowed(ApiCall $method) 177 { 178 global $conf; 179 global $INPUT; 180 global $USERINFO; 181 182 if ($method->isPublic()) return; // public methods are always allowed 183 if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users 184 if (trim($conf['remoteuser']) === '') return; // all users are allowed 185 if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) { 186 return; // user is allowed 187 } 188 189 // still here? no can do 190 throw new AccessDeniedException('server error. not authorized to call method', -32604); 191 } 192 193 /** 194 * Transform file to xml 195 * 196 * @param mixed $data 197 * @return mixed 198 */ 199 public function toFile($data) 200 { 201 return call_user_func($this->fileTransformation, $data); 202 } 203 204 /** 205 * Transform date to xml 206 * 207 * @param mixed $data 208 * @return mixed 209 */ 210 public function toDate($data) 211 { 212 return call_user_func($this->dateTransformation, $data); 213 } 214 215 /** 216 * A simple transformation 217 * 218 * @param mixed $data 219 * @return mixed 220 */ 221 public function dummyTransformation($data) 222 { 223 return $data; 224 } 225 226 /** 227 * Set the transformer function 228 * 229 * @param callback $dateTransformation 230 */ 231 public function setDateTransformation($dateTransformation) 232 { 233 $this->dateTransformation = $dateTransformation; 234 } 235 236 /** 237 * Set the transformer function 238 * 239 * @param callback $fileTransformation 240 */ 241 public function setFileTransformation($fileTransformation) 242 { 243 $this->fileTransformation = $fileTransformation; 244 } 245 246} 247