142e66c7aSAndreas Gohr<?php 242e66c7aSAndreas Gohr 342e66c7aSAndreas Gohrnamespace dokuwiki\Remote; 442e66c7aSAndreas Gohr 542e66c7aSAndreas Gohr 642e66c7aSAndreas Gohrclass ApiCall 742e66c7aSAndreas Gohr{ 842e66c7aSAndreas Gohr /** @var callable The method to be called for this endpoint */ 942e66c7aSAndreas Gohr protected $method; 1042e66c7aSAndreas Gohr 1142e66c7aSAndreas Gohr /** @var bool Whether this call can be called without authentication */ 1242e66c7aSAndreas Gohr protected bool $isPublic = false; 1342e66c7aSAndreas Gohr 1442e66c7aSAndreas Gohr /** @var array Metadata on the accepted parameters */ 1542e66c7aSAndreas Gohr protected array $args = []; 1642e66c7aSAndreas Gohr 1742e66c7aSAndreas Gohr /** @var array Metadata on the return value */ 1842e66c7aSAndreas Gohr protected array $return = [ 1942e66c7aSAndreas Gohr 'type' => 'string', 2042e66c7aSAndreas Gohr 'description' => '', 2142e66c7aSAndreas Gohr ]; 2242e66c7aSAndreas Gohr 2342e66c7aSAndreas Gohr /** @var string The summary of the method */ 2442e66c7aSAndreas Gohr protected string $summary = ''; 2542e66c7aSAndreas Gohr 2642e66c7aSAndreas Gohr /** @var string The description of the method */ 2742e66c7aSAndreas Gohr protected string $description = ''; 2842e66c7aSAndreas Gohr 29*66f07661SAndreas Gohr /** @var array[] The parsed tags */ 30*66f07661SAndreas Gohr protected $tags; 31*66f07661SAndreas Gohr 3242e66c7aSAndreas Gohr /** 3342e66c7aSAndreas Gohr * Make the given method available as an API call 3442e66c7aSAndreas Gohr * 3542e66c7aSAndreas Gohr * @param string|array $method Either [object,'method'] or 'function' 3642e66c7aSAndreas Gohr * @throws \ReflectionException 3742e66c7aSAndreas Gohr */ 3842e66c7aSAndreas Gohr public function __construct($method) 3942e66c7aSAndreas Gohr { 4042e66c7aSAndreas Gohr if (!is_callable($method)) { 4142e66c7aSAndreas Gohr throw new \InvalidArgumentException('Method is not callable'); 4242e66c7aSAndreas Gohr } 4342e66c7aSAndreas Gohr 4442e66c7aSAndreas Gohr $this->method = $method; 4542e66c7aSAndreas Gohr $this->parseData(); 4642e66c7aSAndreas Gohr } 4742e66c7aSAndreas Gohr 4842e66c7aSAndreas Gohr /** 4942e66c7aSAndreas Gohr * Call the method 5042e66c7aSAndreas Gohr * 5142e66c7aSAndreas Gohr * Important: access/authentication checks need to be done before calling this! 5242e66c7aSAndreas Gohr * 5342e66c7aSAndreas Gohr * @param array $args 5442e66c7aSAndreas Gohr * @return mixed 5542e66c7aSAndreas Gohr */ 5642e66c7aSAndreas Gohr public function __invoke($args) 5742e66c7aSAndreas Gohr { 5842e66c7aSAndreas Gohr if (!array_is_list($args)) { 5942e66c7aSAndreas Gohr $args = $this->namedArgsToPositional($args); 6042e66c7aSAndreas Gohr } 6142e66c7aSAndreas Gohr return call_user_func_array($this->method, $args); 6242e66c7aSAndreas Gohr } 6342e66c7aSAndreas Gohr 6442e66c7aSAndreas Gohr /** 6542e66c7aSAndreas Gohr * @return bool 6642e66c7aSAndreas Gohr */ 6742e66c7aSAndreas Gohr public function isPublic(): bool 6842e66c7aSAndreas Gohr { 6942e66c7aSAndreas Gohr return $this->isPublic; 7042e66c7aSAndreas Gohr } 7142e66c7aSAndreas Gohr 7242e66c7aSAndreas Gohr /** 7342e66c7aSAndreas Gohr * @param bool $isPublic 7442e66c7aSAndreas Gohr * @return $this 7542e66c7aSAndreas Gohr */ 7642e66c7aSAndreas Gohr public function setPublic(bool $isPublic = true): self 7742e66c7aSAndreas Gohr { 7842e66c7aSAndreas Gohr $this->isPublic = $isPublic; 7942e66c7aSAndreas Gohr return $this; 8042e66c7aSAndreas Gohr } 8142e66c7aSAndreas Gohr 8242e66c7aSAndreas Gohr 8342e66c7aSAndreas Gohr /** 8442e66c7aSAndreas Gohr * @return array 8542e66c7aSAndreas Gohr */ 8642e66c7aSAndreas Gohr public function getArgs(): array 8742e66c7aSAndreas Gohr { 8842e66c7aSAndreas Gohr return $this->args; 8942e66c7aSAndreas Gohr } 9042e66c7aSAndreas Gohr 9142e66c7aSAndreas Gohr /** 9242e66c7aSAndreas Gohr * Limit the arguments to the given ones 9342e66c7aSAndreas Gohr * 9442e66c7aSAndreas Gohr * @param string[] $args 9542e66c7aSAndreas Gohr * @return $this 9642e66c7aSAndreas Gohr */ 9742e66c7aSAndreas Gohr public function limitArgs($args): self 9842e66c7aSAndreas Gohr { 9942e66c7aSAndreas Gohr foreach ($args as $arg) { 10042e66c7aSAndreas Gohr if (!isset($this->args[$arg])) { 10142e66c7aSAndreas Gohr throw new \InvalidArgumentException("Unknown argument $arg"); 10242e66c7aSAndreas Gohr } 10342e66c7aSAndreas Gohr } 10442e66c7aSAndreas Gohr $this->args = array_intersect_key($this->args, array_flip($args)); 10542e66c7aSAndreas Gohr 10642e66c7aSAndreas Gohr return $this; 10742e66c7aSAndreas Gohr } 10842e66c7aSAndreas Gohr 10942e66c7aSAndreas Gohr /** 11042e66c7aSAndreas Gohr * Set the description for an argument 11142e66c7aSAndreas Gohr * 11242e66c7aSAndreas Gohr * @param string $arg 11342e66c7aSAndreas Gohr * @param string $description 11442e66c7aSAndreas Gohr * @return $this 11542e66c7aSAndreas Gohr */ 11642e66c7aSAndreas Gohr public function setArgDescription(string $arg, string $description): self 11742e66c7aSAndreas Gohr { 11842e66c7aSAndreas Gohr if (!isset($this->args[$arg])) { 11942e66c7aSAndreas Gohr throw new \InvalidArgumentException('Unknown argument'); 12042e66c7aSAndreas Gohr } 12142e66c7aSAndreas Gohr $this->args[$arg]['description'] = $description; 12242e66c7aSAndreas Gohr return $this; 12342e66c7aSAndreas Gohr } 12442e66c7aSAndreas Gohr 12542e66c7aSAndreas Gohr /** 12642e66c7aSAndreas Gohr * @return array 12742e66c7aSAndreas Gohr */ 12842e66c7aSAndreas Gohr public function getReturn(): array 12942e66c7aSAndreas Gohr { 13042e66c7aSAndreas Gohr return $this->return; 13142e66c7aSAndreas Gohr } 13242e66c7aSAndreas Gohr 13342e66c7aSAndreas Gohr /** 13442e66c7aSAndreas Gohr * Set the description for the return value 13542e66c7aSAndreas Gohr * 13642e66c7aSAndreas Gohr * @param string $description 13742e66c7aSAndreas Gohr * @return $this 13842e66c7aSAndreas Gohr */ 13942e66c7aSAndreas Gohr public function setReturnDescription(string $description): self 14042e66c7aSAndreas Gohr { 14142e66c7aSAndreas Gohr $this->return['description'] = $description; 14242e66c7aSAndreas Gohr return $this; 14342e66c7aSAndreas Gohr } 14442e66c7aSAndreas Gohr 14542e66c7aSAndreas Gohr /** 14642e66c7aSAndreas Gohr * @return string 14742e66c7aSAndreas Gohr */ 14842e66c7aSAndreas Gohr public function getSummary(): string 14942e66c7aSAndreas Gohr { 15042e66c7aSAndreas Gohr return $this->summary; 15142e66c7aSAndreas Gohr } 15242e66c7aSAndreas Gohr 15342e66c7aSAndreas Gohr /** 15442e66c7aSAndreas Gohr * @param string $summary 15542e66c7aSAndreas Gohr * @return $this 15642e66c7aSAndreas Gohr */ 15742e66c7aSAndreas Gohr public function setSummary(string $summary): self 15842e66c7aSAndreas Gohr { 15942e66c7aSAndreas Gohr $this->summary = $summary; 16042e66c7aSAndreas Gohr return $this; 16142e66c7aSAndreas Gohr } 16242e66c7aSAndreas Gohr 16342e66c7aSAndreas Gohr /** 16442e66c7aSAndreas Gohr * @return string 16542e66c7aSAndreas Gohr */ 16642e66c7aSAndreas Gohr public function getDescription(): string 16742e66c7aSAndreas Gohr { 16842e66c7aSAndreas Gohr return $this->description; 16942e66c7aSAndreas Gohr } 17042e66c7aSAndreas Gohr 17142e66c7aSAndreas Gohr /** 17242e66c7aSAndreas Gohr * @param string $description 17342e66c7aSAndreas Gohr * @return $this 17442e66c7aSAndreas Gohr */ 17542e66c7aSAndreas Gohr public function setDescription(string $description): self 17642e66c7aSAndreas Gohr { 17742e66c7aSAndreas Gohr $this->description = $description; 17842e66c7aSAndreas Gohr return $this; 17942e66c7aSAndreas Gohr } 18042e66c7aSAndreas Gohr 18142e66c7aSAndreas Gohr /** 182*66f07661SAndreas Gohr * Returns the docblock tags that have not been processed specially 183*66f07661SAndreas Gohr * 184*66f07661SAndreas Gohr * @return array[] 185*66f07661SAndreas Gohr */ 186*66f07661SAndreas Gohr public function getTags() 187*66f07661SAndreas Gohr { 188*66f07661SAndreas Gohr return $this->tags; 189*66f07661SAndreas Gohr } 190*66f07661SAndreas Gohr 191*66f07661SAndreas Gohr /** 192*66f07661SAndreas Gohr * Returns any data that is available in the given docblock tag 193*66f07661SAndreas Gohr * 194*66f07661SAndreas Gohr * @param string $tag 195*66f07661SAndreas Gohr * @return string[] returns an empty array if no such tags exists 196*66f07661SAndreas Gohr */ 197*66f07661SAndreas Gohr public function getTag($tag) 198*66f07661SAndreas Gohr { 199*66f07661SAndreas Gohr if(isset($this->tags[$tag])) { 200*66f07661SAndreas Gohr return $this->tags[$tag]; 201*66f07661SAndreas Gohr } 202*66f07661SAndreas Gohr return []; 203*66f07661SAndreas Gohr } 204*66f07661SAndreas Gohr 205*66f07661SAndreas Gohr /** 20642e66c7aSAndreas Gohr * Fill in the metadata 20742e66c7aSAndreas Gohr * 20842e66c7aSAndreas Gohr * This uses Reflection to inspect the method signature and doc block 20942e66c7aSAndreas Gohr * 21042e66c7aSAndreas Gohr * @throws \ReflectionException 21142e66c7aSAndreas Gohr */ 21242e66c7aSAndreas Gohr protected function parseData() 21342e66c7aSAndreas Gohr { 21442e66c7aSAndreas Gohr if (is_array($this->method)) { 21542e66c7aSAndreas Gohr $reflect = new \ReflectionMethod($this->method[0], $this->method[1]); 21642e66c7aSAndreas Gohr } else { 21742e66c7aSAndreas Gohr $reflect = new \ReflectionFunction($this->method); 21842e66c7aSAndreas Gohr } 21942e66c7aSAndreas Gohr 22042e66c7aSAndreas Gohr $docInfo = $this->parseDocBlock($reflect->getDocComment()); 22142e66c7aSAndreas Gohr $this->summary = $docInfo['summary']; 22242e66c7aSAndreas Gohr $this->description = $docInfo['description']; 223*66f07661SAndreas Gohr $this->tags = $docInfo['tags']; 22442e66c7aSAndreas Gohr 22542e66c7aSAndreas Gohr foreach ($reflect->getParameters() as $parameter) { 22642e66c7aSAndreas Gohr $name = $parameter->name; 22742e66c7aSAndreas Gohr $realType = $parameter->getType(); 22842e66c7aSAndreas Gohr if ($realType) { 22942e66c7aSAndreas Gohr $type = $realType->getName(); 23042e66c7aSAndreas Gohr } elseif (isset($docInfo['args'][$name]['type'])) { 23142e66c7aSAndreas Gohr $type = $docInfo['args'][$name]['type']; 23242e66c7aSAndreas Gohr } else { 23342e66c7aSAndreas Gohr $type = 'string'; 23442e66c7aSAndreas Gohr } 23542e66c7aSAndreas Gohr 23642e66c7aSAndreas Gohr if (isset($docInfo['args'][$name]['description'])) { 23742e66c7aSAndreas Gohr $description = $docInfo['args'][$name]['description']; 23842e66c7aSAndreas Gohr } else { 23942e66c7aSAndreas Gohr $description = ''; 24042e66c7aSAndreas Gohr } 24142e66c7aSAndreas Gohr 24242e66c7aSAndreas Gohr $this->args[$name] = [ 24342e66c7aSAndreas Gohr 'type' => $type, 24442e66c7aSAndreas Gohr 'description' => trim($description), 24542e66c7aSAndreas Gohr ]; 24642e66c7aSAndreas Gohr } 24742e66c7aSAndreas Gohr 24842e66c7aSAndreas Gohr $returnType = $reflect->getReturnType(); 24942e66c7aSAndreas Gohr if ($returnType) { 25042e66c7aSAndreas Gohr $this->return['type'] = $returnType->getName(); 25142e66c7aSAndreas Gohr } elseif (isset($docInfo['return']['type'])) { 25242e66c7aSAndreas Gohr $this->return['type'] = $docInfo['return']['type']; 25342e66c7aSAndreas Gohr } else { 25442e66c7aSAndreas Gohr $this->return['type'] = 'string'; 25542e66c7aSAndreas Gohr } 25642e66c7aSAndreas Gohr 25742e66c7aSAndreas Gohr if (isset($docInfo['return']['description'])) { 25842e66c7aSAndreas Gohr $this->return['description'] = $docInfo['return']['description']; 25942e66c7aSAndreas Gohr } 26042e66c7aSAndreas Gohr } 26142e66c7aSAndreas Gohr 26242e66c7aSAndreas Gohr /** 26342e66c7aSAndreas Gohr * Parse a doc block 26442e66c7aSAndreas Gohr * 26542e66c7aSAndreas Gohr * @param string $doc 26642e66c7aSAndreas Gohr * @return array 26742e66c7aSAndreas Gohr */ 26842e66c7aSAndreas Gohr protected function parseDocBlock($doc) 26942e66c7aSAndreas Gohr { 27042e66c7aSAndreas Gohr // strip asterisks and leading spaces 27142e66c7aSAndreas Gohr $doc = preg_replace( 27242e66c7aSAndreas Gohr ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'], 27342e66c7aSAndreas Gohr ['', '', '', ''], 27442e66c7aSAndreas Gohr $doc 27542e66c7aSAndreas Gohr ); 27642e66c7aSAndreas Gohr 27742e66c7aSAndreas Gohr $doc = trim($doc); 27842e66c7aSAndreas Gohr 27942e66c7aSAndreas Gohr // get all tags 28042e66c7aSAndreas Gohr $tags = []; 28142e66c7aSAndreas Gohr if (preg_match_all('/^@(\w+)\s+(.*)$/m', $doc, $matches, PREG_SET_ORDER)) { 28242e66c7aSAndreas Gohr foreach ($matches as $match) { 28342e66c7aSAndreas Gohr $tags[$match[1]][] = trim($match[2]); 28442e66c7aSAndreas Gohr } 28542e66c7aSAndreas Gohr } 28642e66c7aSAndreas Gohr $params = $this->extractDocTags($tags); 28742e66c7aSAndreas Gohr 28842e66c7aSAndreas Gohr // strip the tags from the doc 28942e66c7aSAndreas Gohr $doc = preg_replace('/^@(\w+)\s+(.*)$/m', '', $doc); 29042e66c7aSAndreas Gohr 29142e66c7aSAndreas Gohr [$summary, $description] = sexplode("\n\n", $doc, 2, ''); 29242e66c7aSAndreas Gohr return array_merge( 29342e66c7aSAndreas Gohr [ 29442e66c7aSAndreas Gohr 'summary' => trim($summary), 29542e66c7aSAndreas Gohr 'description' => trim($description), 29642e66c7aSAndreas Gohr 'tags' => $tags, 29742e66c7aSAndreas Gohr ], 29842e66c7aSAndreas Gohr $params 29942e66c7aSAndreas Gohr ); 30042e66c7aSAndreas Gohr } 30142e66c7aSAndreas Gohr 30242e66c7aSAndreas Gohr /** 30342e66c7aSAndreas Gohr * Process the param and return tags 30442e66c7aSAndreas Gohr * 30542e66c7aSAndreas Gohr * @param array $tags 30642e66c7aSAndreas Gohr * @return array 30742e66c7aSAndreas Gohr */ 30842e66c7aSAndreas Gohr protected function extractDocTags(&$tags) 30942e66c7aSAndreas Gohr { 31042e66c7aSAndreas Gohr $result = []; 31142e66c7aSAndreas Gohr 31242e66c7aSAndreas Gohr if (isset($tags['param'])) { 31342e66c7aSAndreas Gohr foreach ($tags['param'] as $param) { 314b05603abSAndreas Gohr [$type, $name, $description] = array_map('trim', sexplode(' ', $param, 3, '')); 315b05603abSAndreas Gohr if ($name[0] !== '$') continue; 316b05603abSAndreas Gohr $name = substr($name, 1); 317b05603abSAndreas Gohr 318b05603abSAndreas Gohr $result['args'][$name] = [ 319b05603abSAndreas Gohr 'type' => $this->cleanTypeHint($type), 320b05603abSAndreas Gohr 'description' => $description, 32142e66c7aSAndreas Gohr ]; 32242e66c7aSAndreas Gohr } 32342e66c7aSAndreas Gohr unset($tags['param']); 32442e66c7aSAndreas Gohr } 32542e66c7aSAndreas Gohr 32642e66c7aSAndreas Gohr if (isset($tags['return'])) { 32742e66c7aSAndreas Gohr $return = $tags['return'][0]; 328b05603abSAndreas Gohr [$type, $description] = array_map('trim', sexplode(' ', $return, 2, '')); 32942e66c7aSAndreas Gohr $result['return'] = [ 330b05603abSAndreas Gohr 'type' => $this->cleanTypeHint($type), 331b05603abSAndreas Gohr 'description' => $description 33242e66c7aSAndreas Gohr ]; 33342e66c7aSAndreas Gohr unset($tags['return']); 33442e66c7aSAndreas Gohr } 33542e66c7aSAndreas Gohr 33642e66c7aSAndreas Gohr return $result; 33742e66c7aSAndreas Gohr } 33842e66c7aSAndreas Gohr 33942e66c7aSAndreas Gohr /** 34042e66c7aSAndreas Gohr * Matches the given type hint against the valid options for the remote API 34142e66c7aSAndreas Gohr * 34242e66c7aSAndreas Gohr * @param string $hint 34342e66c7aSAndreas Gohr * @return string 34442e66c7aSAndreas Gohr */ 34542e66c7aSAndreas Gohr protected function cleanTypeHint($hint) 34642e66c7aSAndreas Gohr { 34742e66c7aSAndreas Gohr $types = explode('|', $hint); 34842e66c7aSAndreas Gohr foreach ($types as $t) { 34942e66c7aSAndreas Gohr if (str_ends_with($t, '[]')) { 35042e66c7aSAndreas Gohr return 'array'; 35142e66c7aSAndreas Gohr } 35242e66c7aSAndreas Gohr if ($t === 'boolean' || $t === 'true' || $t === 'false') { 35342e66c7aSAndreas Gohr return 'bool'; 35442e66c7aSAndreas Gohr } 35542e66c7aSAndreas Gohr if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) { 35642e66c7aSAndreas Gohr return $t; 35742e66c7aSAndreas Gohr } 35842e66c7aSAndreas Gohr } 35942e66c7aSAndreas Gohr return 'string'; 36042e66c7aSAndreas Gohr } 36142e66c7aSAndreas Gohr 36242e66c7aSAndreas Gohr /** 36342e66c7aSAndreas Gohr * Converts named arguments to positional arguments 36442e66c7aSAndreas Gohr * 36542e66c7aSAndreas Gohr * @fixme with PHP 8 we can use named arguments directly using the spread operator 36642e66c7aSAndreas Gohr * @param array $params 36742e66c7aSAndreas Gohr * @return array 36842e66c7aSAndreas Gohr */ 36942e66c7aSAndreas Gohr protected function namedArgsToPositional($params) 37042e66c7aSAndreas Gohr { 37142e66c7aSAndreas Gohr $args = []; 37242e66c7aSAndreas Gohr 37342e66c7aSAndreas Gohr foreach (array_keys($this->args) as $arg) { 37442e66c7aSAndreas Gohr if (isset($params[$arg])) { 37542e66c7aSAndreas Gohr $args[] = $params[$arg]; 37642e66c7aSAndreas Gohr } else { 37742e66c7aSAndreas Gohr $args[] = null; 37842e66c7aSAndreas Gohr } 37942e66c7aSAndreas Gohr } 38042e66c7aSAndreas Gohr 38142e66c7aSAndreas Gohr return $args; 38242e66c7aSAndreas Gohr } 38342e66c7aSAndreas Gohr 38442e66c7aSAndreas Gohr} 385