1<?php 2 3namespace dokuwiki\Remote; 4 5use dokuwiki\Extension\RemotePlugin; 6use dokuwiki\Logger; 7use dokuwiki\test\Remote\Mock\ApiCore as 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 LegacyApiCore())->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 | \ArgumentCountError $e) { 139 throw new RemoteException($e->getMessage(), -32602); 140 } 141 } 142 143 /** 144 * Check that the API is generally enabled 145 * 146 * @return void 147 * @throws RemoteException thrown when the API is disabled 148 */ 149 public function ensureApiIsEnabled() 150 { 151 global $conf; 152 if (!$conf['remote'] || trim($conf['remoteuser']) == '!!not set!!') { 153 throw new AccessDeniedException('Server Error. API is not enabled in config.', -32604); 154 } 155 } 156 157 /** 158 * Check if the current user is allowed to call the given method 159 * 160 * @param ApiCall $method 161 * @return void 162 * @throws AccessDeniedException Thrown when the user is not allowed to call the method 163 */ 164 public function ensureAccessIsAllowed(ApiCall $method) 165 { 166 global $conf; 167 global $INPUT; 168 global $USERINFO; 169 170 if ($method->isPublic()) return; // public methods are always allowed 171 if (!$conf['useacl']) return; // ACL is not enabled, so we can't check users 172 if (trim($conf['remoteuser']) === '') return; // all users are allowed 173 if (auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array)($USERINFO['grps'] ?? []))) { 174 return; // user is allowed 175 } 176 177 // still here? no can do 178 throw new AccessDeniedException('server error. not authorized to call method', -32604); 179 } 180} 181