1*42e66c7aSAndreas Gohr<?php 2*42e66c7aSAndreas Gohr 3*42e66c7aSAndreas Gohrnamespace dokuwiki\Remote; 4*42e66c7aSAndreas Gohr 5*42e66c7aSAndreas Gohr 6*42e66c7aSAndreas Gohrclass ApiCall 7*42e66c7aSAndreas Gohr{ 8*42e66c7aSAndreas Gohr /** @var callable The method to be called for this endpoint */ 9*42e66c7aSAndreas Gohr protected $method; 10*42e66c7aSAndreas Gohr 11*42e66c7aSAndreas Gohr /** @var bool Whether this call can be called without authentication */ 12*42e66c7aSAndreas Gohr protected bool $isPublic = false; 13*42e66c7aSAndreas Gohr 14*42e66c7aSAndreas Gohr /** @var array Metadata on the accepted parameters */ 15*42e66c7aSAndreas Gohr protected array $args = []; 16*42e66c7aSAndreas Gohr 17*42e66c7aSAndreas Gohr /** @var array Metadata on the return value */ 18*42e66c7aSAndreas Gohr protected array $return = [ 19*42e66c7aSAndreas Gohr 'type' => 'string', 20*42e66c7aSAndreas Gohr 'description' => '', 21*42e66c7aSAndreas Gohr ]; 22*42e66c7aSAndreas Gohr 23*42e66c7aSAndreas Gohr /** @var string The summary of the method */ 24*42e66c7aSAndreas Gohr protected string $summary = ''; 25*42e66c7aSAndreas Gohr 26*42e66c7aSAndreas Gohr /** @var string The description of the method */ 27*42e66c7aSAndreas Gohr protected string $description = ''; 28*42e66c7aSAndreas Gohr 29*42e66c7aSAndreas Gohr /** 30*42e66c7aSAndreas Gohr * Make the given method available as an API call 31*42e66c7aSAndreas Gohr * 32*42e66c7aSAndreas Gohr * @param string|array $method Either [object,'method'] or 'function' 33*42e66c7aSAndreas Gohr * @throws \ReflectionException 34*42e66c7aSAndreas Gohr */ 35*42e66c7aSAndreas Gohr public function __construct($method) 36*42e66c7aSAndreas Gohr { 37*42e66c7aSAndreas Gohr if (!is_callable($method)) { 38*42e66c7aSAndreas Gohr throw new \InvalidArgumentException('Method is not callable'); 39*42e66c7aSAndreas Gohr } 40*42e66c7aSAndreas Gohr 41*42e66c7aSAndreas Gohr $this->method = $method; 42*42e66c7aSAndreas Gohr $this->parseData(); 43*42e66c7aSAndreas Gohr } 44*42e66c7aSAndreas Gohr 45*42e66c7aSAndreas Gohr /** 46*42e66c7aSAndreas Gohr * Call the method 47*42e66c7aSAndreas Gohr * 48*42e66c7aSAndreas Gohr * Important: access/authentication checks need to be done before calling this! 49*42e66c7aSAndreas Gohr * 50*42e66c7aSAndreas Gohr * @param array $args 51*42e66c7aSAndreas Gohr * @return mixed 52*42e66c7aSAndreas Gohr */ 53*42e66c7aSAndreas Gohr public function __invoke($args) 54*42e66c7aSAndreas Gohr { 55*42e66c7aSAndreas Gohr if (!array_is_list($args)) { 56*42e66c7aSAndreas Gohr $args = $this->namedArgsToPositional($args); 57*42e66c7aSAndreas Gohr } 58*42e66c7aSAndreas Gohr return call_user_func_array($this->method, $args); 59*42e66c7aSAndreas Gohr } 60*42e66c7aSAndreas Gohr 61*42e66c7aSAndreas Gohr /** 62*42e66c7aSAndreas Gohr * @return bool 63*42e66c7aSAndreas Gohr */ 64*42e66c7aSAndreas Gohr public function isPublic(): bool 65*42e66c7aSAndreas Gohr { 66*42e66c7aSAndreas Gohr return $this->isPublic; 67*42e66c7aSAndreas Gohr } 68*42e66c7aSAndreas Gohr 69*42e66c7aSAndreas Gohr /** 70*42e66c7aSAndreas Gohr * @param bool $isPublic 71*42e66c7aSAndreas Gohr * @return $this 72*42e66c7aSAndreas Gohr */ 73*42e66c7aSAndreas Gohr public function setPublic(bool $isPublic = true): self 74*42e66c7aSAndreas Gohr { 75*42e66c7aSAndreas Gohr $this->isPublic = $isPublic; 76*42e66c7aSAndreas Gohr return $this; 77*42e66c7aSAndreas Gohr } 78*42e66c7aSAndreas Gohr 79*42e66c7aSAndreas Gohr 80*42e66c7aSAndreas Gohr /** 81*42e66c7aSAndreas Gohr * @return array 82*42e66c7aSAndreas Gohr */ 83*42e66c7aSAndreas Gohr public function getArgs(): array 84*42e66c7aSAndreas Gohr { 85*42e66c7aSAndreas Gohr return $this->args; 86*42e66c7aSAndreas Gohr } 87*42e66c7aSAndreas Gohr 88*42e66c7aSAndreas Gohr /** 89*42e66c7aSAndreas Gohr * Limit the arguments to the given ones 90*42e66c7aSAndreas Gohr * 91*42e66c7aSAndreas Gohr * @param string[] $args 92*42e66c7aSAndreas Gohr * @return $this 93*42e66c7aSAndreas Gohr */ 94*42e66c7aSAndreas Gohr public function limitArgs($args): self 95*42e66c7aSAndreas Gohr { 96*42e66c7aSAndreas Gohr foreach ($args as $arg) { 97*42e66c7aSAndreas Gohr if (!isset($this->args[$arg])) { 98*42e66c7aSAndreas Gohr throw new \InvalidArgumentException("Unknown argument $arg"); 99*42e66c7aSAndreas Gohr } 100*42e66c7aSAndreas Gohr } 101*42e66c7aSAndreas Gohr $this->args = array_intersect_key($this->args, array_flip($args)); 102*42e66c7aSAndreas Gohr 103*42e66c7aSAndreas Gohr return $this; 104*42e66c7aSAndreas Gohr } 105*42e66c7aSAndreas Gohr 106*42e66c7aSAndreas Gohr /** 107*42e66c7aSAndreas Gohr * Set the description for an argument 108*42e66c7aSAndreas Gohr * 109*42e66c7aSAndreas Gohr * @param string $arg 110*42e66c7aSAndreas Gohr * @param string $description 111*42e66c7aSAndreas Gohr * @return $this 112*42e66c7aSAndreas Gohr */ 113*42e66c7aSAndreas Gohr public function setArgDescription(string $arg, string $description): self 114*42e66c7aSAndreas Gohr { 115*42e66c7aSAndreas Gohr if (!isset($this->args[$arg])) { 116*42e66c7aSAndreas Gohr throw new \InvalidArgumentException('Unknown argument'); 117*42e66c7aSAndreas Gohr } 118*42e66c7aSAndreas Gohr $this->args[$arg]['description'] = $description; 119*42e66c7aSAndreas Gohr return $this; 120*42e66c7aSAndreas Gohr } 121*42e66c7aSAndreas Gohr 122*42e66c7aSAndreas Gohr /** 123*42e66c7aSAndreas Gohr * @return array 124*42e66c7aSAndreas Gohr */ 125*42e66c7aSAndreas Gohr public function getReturn(): array 126*42e66c7aSAndreas Gohr { 127*42e66c7aSAndreas Gohr return $this->return; 128*42e66c7aSAndreas Gohr } 129*42e66c7aSAndreas Gohr 130*42e66c7aSAndreas Gohr /** 131*42e66c7aSAndreas Gohr * Set the description for the return value 132*42e66c7aSAndreas Gohr * 133*42e66c7aSAndreas Gohr * @param string $description 134*42e66c7aSAndreas Gohr * @return $this 135*42e66c7aSAndreas Gohr */ 136*42e66c7aSAndreas Gohr public function setReturnDescription(string $description): self 137*42e66c7aSAndreas Gohr { 138*42e66c7aSAndreas Gohr $this->return['description'] = $description; 139*42e66c7aSAndreas Gohr return $this; 140*42e66c7aSAndreas Gohr } 141*42e66c7aSAndreas Gohr 142*42e66c7aSAndreas Gohr /** 143*42e66c7aSAndreas Gohr * @return string 144*42e66c7aSAndreas Gohr */ 145*42e66c7aSAndreas Gohr public function getSummary(): string 146*42e66c7aSAndreas Gohr { 147*42e66c7aSAndreas Gohr return $this->summary; 148*42e66c7aSAndreas Gohr } 149*42e66c7aSAndreas Gohr 150*42e66c7aSAndreas Gohr /** 151*42e66c7aSAndreas Gohr * @param string $summary 152*42e66c7aSAndreas Gohr * @return $this 153*42e66c7aSAndreas Gohr */ 154*42e66c7aSAndreas Gohr public function setSummary(string $summary): self 155*42e66c7aSAndreas Gohr { 156*42e66c7aSAndreas Gohr $this->summary = $summary; 157*42e66c7aSAndreas Gohr return $this; 158*42e66c7aSAndreas Gohr } 159*42e66c7aSAndreas Gohr 160*42e66c7aSAndreas Gohr /** 161*42e66c7aSAndreas Gohr * @return string 162*42e66c7aSAndreas Gohr */ 163*42e66c7aSAndreas Gohr public function getDescription(): string 164*42e66c7aSAndreas Gohr { 165*42e66c7aSAndreas Gohr return $this->description; 166*42e66c7aSAndreas Gohr } 167*42e66c7aSAndreas Gohr 168*42e66c7aSAndreas Gohr /** 169*42e66c7aSAndreas Gohr * @param string $description 170*42e66c7aSAndreas Gohr * @return $this 171*42e66c7aSAndreas Gohr */ 172*42e66c7aSAndreas Gohr public function setDescription(string $description): self 173*42e66c7aSAndreas Gohr { 174*42e66c7aSAndreas Gohr $this->description = $description; 175*42e66c7aSAndreas Gohr return $this; 176*42e66c7aSAndreas Gohr } 177*42e66c7aSAndreas Gohr 178*42e66c7aSAndreas Gohr /** 179*42e66c7aSAndreas Gohr * Fill in the metadata 180*42e66c7aSAndreas Gohr * 181*42e66c7aSAndreas Gohr * This uses Reflection to inspect the method signature and doc block 182*42e66c7aSAndreas Gohr * 183*42e66c7aSAndreas Gohr * @throws \ReflectionException 184*42e66c7aSAndreas Gohr */ 185*42e66c7aSAndreas Gohr protected function parseData() 186*42e66c7aSAndreas Gohr { 187*42e66c7aSAndreas Gohr if (is_array($this->method)) { 188*42e66c7aSAndreas Gohr $reflect = new \ReflectionMethod($this->method[0], $this->method[1]); 189*42e66c7aSAndreas Gohr } else { 190*42e66c7aSAndreas Gohr $reflect = new \ReflectionFunction($this->method); 191*42e66c7aSAndreas Gohr } 192*42e66c7aSAndreas Gohr 193*42e66c7aSAndreas Gohr $docInfo = $this->parseDocBlock($reflect->getDocComment()); 194*42e66c7aSAndreas Gohr $this->summary = $docInfo['summary']; 195*42e66c7aSAndreas Gohr $this->description = $docInfo['description']; 196*42e66c7aSAndreas Gohr 197*42e66c7aSAndreas Gohr foreach ($reflect->getParameters() as $parameter) { 198*42e66c7aSAndreas Gohr $name = $parameter->name; 199*42e66c7aSAndreas Gohr $realType = $parameter->getType(); 200*42e66c7aSAndreas Gohr if ($realType) { 201*42e66c7aSAndreas Gohr $type = $realType->getName(); 202*42e66c7aSAndreas Gohr } elseif (isset($docInfo['args'][$name]['type'])) { 203*42e66c7aSAndreas Gohr $type = $docInfo['args'][$name]['type']; 204*42e66c7aSAndreas Gohr } else { 205*42e66c7aSAndreas Gohr $type = 'string'; 206*42e66c7aSAndreas Gohr } 207*42e66c7aSAndreas Gohr 208*42e66c7aSAndreas Gohr if (isset($docInfo['args'][$name]['description'])) { 209*42e66c7aSAndreas Gohr $description = $docInfo['args'][$name]['description']; 210*42e66c7aSAndreas Gohr } else { 211*42e66c7aSAndreas Gohr $description = ''; 212*42e66c7aSAndreas Gohr } 213*42e66c7aSAndreas Gohr 214*42e66c7aSAndreas Gohr $this->args[$name] = [ 215*42e66c7aSAndreas Gohr 'type' => $type, 216*42e66c7aSAndreas Gohr 'description' => trim($description), 217*42e66c7aSAndreas Gohr ]; 218*42e66c7aSAndreas Gohr } 219*42e66c7aSAndreas Gohr 220*42e66c7aSAndreas Gohr $returnType = $reflect->getReturnType(); 221*42e66c7aSAndreas Gohr if ($returnType) { 222*42e66c7aSAndreas Gohr $this->return['type'] = $returnType->getName(); 223*42e66c7aSAndreas Gohr } elseif (isset($docInfo['return']['type'])) { 224*42e66c7aSAndreas Gohr $this->return['type'] = $docInfo['return']['type']; 225*42e66c7aSAndreas Gohr } else { 226*42e66c7aSAndreas Gohr $this->return['type'] = 'string'; 227*42e66c7aSAndreas Gohr } 228*42e66c7aSAndreas Gohr 229*42e66c7aSAndreas Gohr if (isset($docInfo['return']['description'])) { 230*42e66c7aSAndreas Gohr $this->return['description'] = $docInfo['return']['description']; 231*42e66c7aSAndreas Gohr } 232*42e66c7aSAndreas Gohr } 233*42e66c7aSAndreas Gohr 234*42e66c7aSAndreas Gohr /** 235*42e66c7aSAndreas Gohr * Parse a doc block 236*42e66c7aSAndreas Gohr * 237*42e66c7aSAndreas Gohr * @param string $doc 238*42e66c7aSAndreas Gohr * @return array 239*42e66c7aSAndreas Gohr */ 240*42e66c7aSAndreas Gohr protected function parseDocBlock($doc) 241*42e66c7aSAndreas Gohr { 242*42e66c7aSAndreas Gohr // strip asterisks and leading spaces 243*42e66c7aSAndreas Gohr $doc = preg_replace( 244*42e66c7aSAndreas Gohr ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'], 245*42e66c7aSAndreas Gohr ['', '', '', ''], 246*42e66c7aSAndreas Gohr $doc 247*42e66c7aSAndreas Gohr ); 248*42e66c7aSAndreas Gohr 249*42e66c7aSAndreas Gohr $doc = trim($doc); 250*42e66c7aSAndreas Gohr 251*42e66c7aSAndreas Gohr // get all tags 252*42e66c7aSAndreas Gohr $tags = []; 253*42e66c7aSAndreas Gohr if (preg_match_all('/^@(\w+)\s+(.*)$/m', $doc, $matches, PREG_SET_ORDER)) { 254*42e66c7aSAndreas Gohr foreach ($matches as $match) { 255*42e66c7aSAndreas Gohr $tags[$match[1]][] = trim($match[2]); 256*42e66c7aSAndreas Gohr } 257*42e66c7aSAndreas Gohr } 258*42e66c7aSAndreas Gohr $params = $this->extractDocTags($tags); 259*42e66c7aSAndreas Gohr 260*42e66c7aSAndreas Gohr // strip the tags from the doc 261*42e66c7aSAndreas Gohr $doc = preg_replace('/^@(\w+)\s+(.*)$/m', '', $doc); 262*42e66c7aSAndreas Gohr 263*42e66c7aSAndreas Gohr [$summary, $description] = sexplode("\n\n", $doc, 2, ''); 264*42e66c7aSAndreas Gohr return array_merge( 265*42e66c7aSAndreas Gohr [ 266*42e66c7aSAndreas Gohr 'summary' => trim($summary), 267*42e66c7aSAndreas Gohr 'description' => trim($description), 268*42e66c7aSAndreas Gohr 'tags' => $tags, 269*42e66c7aSAndreas Gohr ], 270*42e66c7aSAndreas Gohr $params 271*42e66c7aSAndreas Gohr ); 272*42e66c7aSAndreas Gohr } 273*42e66c7aSAndreas Gohr 274*42e66c7aSAndreas Gohr /** 275*42e66c7aSAndreas Gohr * Process the param and return tags 276*42e66c7aSAndreas Gohr * 277*42e66c7aSAndreas Gohr * @param array $tags 278*42e66c7aSAndreas Gohr * @return array 279*42e66c7aSAndreas Gohr */ 280*42e66c7aSAndreas Gohr protected function extractDocTags(&$tags) 281*42e66c7aSAndreas Gohr { 282*42e66c7aSAndreas Gohr $result = []; 283*42e66c7aSAndreas Gohr 284*42e66c7aSAndreas Gohr if (isset($tags['param'])) { 285*42e66c7aSAndreas Gohr foreach ($tags['param'] as $param) { 286*42e66c7aSAndreas Gohr if (preg_match('/^(\w+)\s+\$(\w+)(\s+(.*))?$/m', $param, $m)) { 287*42e66c7aSAndreas Gohr $result['args'][$m[2]] = [ 288*42e66c7aSAndreas Gohr 'type' => $this->cleanTypeHint($m[1]), 289*42e66c7aSAndreas Gohr 'description' => trim($m[3] ?? ''), 290*42e66c7aSAndreas Gohr ]; 291*42e66c7aSAndreas Gohr } 292*42e66c7aSAndreas Gohr } 293*42e66c7aSAndreas Gohr unset($tags['param']); 294*42e66c7aSAndreas Gohr } 295*42e66c7aSAndreas Gohr 296*42e66c7aSAndreas Gohr 297*42e66c7aSAndreas Gohr if (isset($tags['return'])) { 298*42e66c7aSAndreas Gohr $return = $tags['return'][0]; 299*42e66c7aSAndreas Gohr if (preg_match('/^(\w+)(\s+(.*))$/m', $return, $m)) { 300*42e66c7aSAndreas Gohr $result['return'] = [ 301*42e66c7aSAndreas Gohr 'type' => $this->cleanTypeHint($m[1]), 302*42e66c7aSAndreas Gohr 'description' => trim($m[2] ?? '') 303*42e66c7aSAndreas Gohr ]; 304*42e66c7aSAndreas Gohr } 305*42e66c7aSAndreas Gohr unset($tags['return']); 306*42e66c7aSAndreas Gohr } 307*42e66c7aSAndreas Gohr 308*42e66c7aSAndreas Gohr return $result; 309*42e66c7aSAndreas Gohr } 310*42e66c7aSAndreas Gohr 311*42e66c7aSAndreas Gohr /** 312*42e66c7aSAndreas Gohr * Matches the given type hint against the valid options for the remote API 313*42e66c7aSAndreas Gohr * 314*42e66c7aSAndreas Gohr * @param string $hint 315*42e66c7aSAndreas Gohr * @return string 316*42e66c7aSAndreas Gohr */ 317*42e66c7aSAndreas Gohr protected function cleanTypeHint($hint) 318*42e66c7aSAndreas Gohr { 319*42e66c7aSAndreas Gohr $types = explode('|', $hint); 320*42e66c7aSAndreas Gohr foreach ($types as $t) { 321*42e66c7aSAndreas Gohr if (str_ends_with($t, '[]')) { 322*42e66c7aSAndreas Gohr return 'array'; 323*42e66c7aSAndreas Gohr } 324*42e66c7aSAndreas Gohr if ($t === 'boolean' || $t === 'true' || $t === 'false') { 325*42e66c7aSAndreas Gohr return 'bool'; 326*42e66c7aSAndreas Gohr } 327*42e66c7aSAndreas Gohr if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) { 328*42e66c7aSAndreas Gohr return $t; 329*42e66c7aSAndreas Gohr } 330*42e66c7aSAndreas Gohr } 331*42e66c7aSAndreas Gohr return 'string'; 332*42e66c7aSAndreas Gohr } 333*42e66c7aSAndreas Gohr 334*42e66c7aSAndreas Gohr /** 335*42e66c7aSAndreas Gohr * Converts named arguments to positional arguments 336*42e66c7aSAndreas Gohr * 337*42e66c7aSAndreas Gohr * @fixme with PHP 8 we can use named arguments directly using the spread operator 338*42e66c7aSAndreas Gohr * @param array $params 339*42e66c7aSAndreas Gohr * @return array 340*42e66c7aSAndreas Gohr */ 341*42e66c7aSAndreas Gohr protected function namedArgsToPositional($params) 342*42e66c7aSAndreas Gohr { 343*42e66c7aSAndreas Gohr $args = []; 344*42e66c7aSAndreas Gohr 345*42e66c7aSAndreas Gohr foreach (array_keys($this->args) as $arg) { 346*42e66c7aSAndreas Gohr if (isset($params[$arg])) { 347*42e66c7aSAndreas Gohr $args[] = $params[$arg]; 348*42e66c7aSAndreas Gohr } else { 349*42e66c7aSAndreas Gohr $args[] = null; 350*42e66c7aSAndreas Gohr } 351*42e66c7aSAndreas Gohr } 352*42e66c7aSAndreas Gohr 353*42e66c7aSAndreas Gohr return $args; 354*42e66c7aSAndreas Gohr } 355*42e66c7aSAndreas Gohr 356*42e66c7aSAndreas Gohr} 357