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