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 2942e66c7aSAndreas Gohr /** 3042e66c7aSAndreas Gohr * Make the given method available as an API call 3142e66c7aSAndreas Gohr * 3242e66c7aSAndreas Gohr * @param string|array $method Either [object,'method'] or 'function' 3342e66c7aSAndreas Gohr * @throws \ReflectionException 3442e66c7aSAndreas Gohr */ 3542e66c7aSAndreas Gohr public function __construct($method) 3642e66c7aSAndreas Gohr { 3742e66c7aSAndreas Gohr if (!is_callable($method)) { 3842e66c7aSAndreas Gohr throw new \InvalidArgumentException('Method is not callable'); 3942e66c7aSAndreas Gohr } 4042e66c7aSAndreas Gohr 4142e66c7aSAndreas Gohr $this->method = $method; 4242e66c7aSAndreas Gohr $this->parseData(); 4342e66c7aSAndreas Gohr } 4442e66c7aSAndreas Gohr 4542e66c7aSAndreas Gohr /** 4642e66c7aSAndreas Gohr * Call the method 4742e66c7aSAndreas Gohr * 4842e66c7aSAndreas Gohr * Important: access/authentication checks need to be done before calling this! 4942e66c7aSAndreas Gohr * 5042e66c7aSAndreas Gohr * @param array $args 5142e66c7aSAndreas Gohr * @return mixed 5242e66c7aSAndreas Gohr */ 5342e66c7aSAndreas Gohr public function __invoke($args) 5442e66c7aSAndreas Gohr { 5542e66c7aSAndreas Gohr if (!array_is_list($args)) { 5642e66c7aSAndreas Gohr $args = $this->namedArgsToPositional($args); 5742e66c7aSAndreas Gohr } 5842e66c7aSAndreas Gohr return call_user_func_array($this->method, $args); 5942e66c7aSAndreas Gohr } 6042e66c7aSAndreas Gohr 6142e66c7aSAndreas Gohr /** 6242e66c7aSAndreas Gohr * @return bool 6342e66c7aSAndreas Gohr */ 6442e66c7aSAndreas Gohr public function isPublic(): bool 6542e66c7aSAndreas Gohr { 6642e66c7aSAndreas Gohr return $this->isPublic; 6742e66c7aSAndreas Gohr } 6842e66c7aSAndreas Gohr 6942e66c7aSAndreas Gohr /** 7042e66c7aSAndreas Gohr * @param bool $isPublic 7142e66c7aSAndreas Gohr * @return $this 7242e66c7aSAndreas Gohr */ 7342e66c7aSAndreas Gohr public function setPublic(bool $isPublic = true): self 7442e66c7aSAndreas Gohr { 7542e66c7aSAndreas Gohr $this->isPublic = $isPublic; 7642e66c7aSAndreas Gohr return $this; 7742e66c7aSAndreas Gohr } 7842e66c7aSAndreas Gohr 7942e66c7aSAndreas Gohr 8042e66c7aSAndreas Gohr /** 8142e66c7aSAndreas Gohr * @return array 8242e66c7aSAndreas Gohr */ 8342e66c7aSAndreas Gohr public function getArgs(): array 8442e66c7aSAndreas Gohr { 8542e66c7aSAndreas Gohr return $this->args; 8642e66c7aSAndreas Gohr } 8742e66c7aSAndreas Gohr 8842e66c7aSAndreas Gohr /** 8942e66c7aSAndreas Gohr * Limit the arguments to the given ones 9042e66c7aSAndreas Gohr * 9142e66c7aSAndreas Gohr * @param string[] $args 9242e66c7aSAndreas Gohr * @return $this 9342e66c7aSAndreas Gohr */ 9442e66c7aSAndreas Gohr public function limitArgs($args): self 9542e66c7aSAndreas Gohr { 9642e66c7aSAndreas Gohr foreach ($args as $arg) { 9742e66c7aSAndreas Gohr if (!isset($this->args[$arg])) { 9842e66c7aSAndreas Gohr throw new \InvalidArgumentException("Unknown argument $arg"); 9942e66c7aSAndreas Gohr } 10042e66c7aSAndreas Gohr } 10142e66c7aSAndreas Gohr $this->args = array_intersect_key($this->args, array_flip($args)); 10242e66c7aSAndreas Gohr 10342e66c7aSAndreas Gohr return $this; 10442e66c7aSAndreas Gohr } 10542e66c7aSAndreas Gohr 10642e66c7aSAndreas Gohr /** 10742e66c7aSAndreas Gohr * Set the description for an argument 10842e66c7aSAndreas Gohr * 10942e66c7aSAndreas Gohr * @param string $arg 11042e66c7aSAndreas Gohr * @param string $description 11142e66c7aSAndreas Gohr * @return $this 11242e66c7aSAndreas Gohr */ 11342e66c7aSAndreas Gohr public function setArgDescription(string $arg, string $description): self 11442e66c7aSAndreas Gohr { 11542e66c7aSAndreas Gohr if (!isset($this->args[$arg])) { 11642e66c7aSAndreas Gohr throw new \InvalidArgumentException('Unknown argument'); 11742e66c7aSAndreas Gohr } 11842e66c7aSAndreas Gohr $this->args[$arg]['description'] = $description; 11942e66c7aSAndreas Gohr return $this; 12042e66c7aSAndreas Gohr } 12142e66c7aSAndreas Gohr 12242e66c7aSAndreas Gohr /** 12342e66c7aSAndreas Gohr * @return array 12442e66c7aSAndreas Gohr */ 12542e66c7aSAndreas Gohr public function getReturn(): array 12642e66c7aSAndreas Gohr { 12742e66c7aSAndreas Gohr return $this->return; 12842e66c7aSAndreas Gohr } 12942e66c7aSAndreas Gohr 13042e66c7aSAndreas Gohr /** 13142e66c7aSAndreas Gohr * Set the description for the return value 13242e66c7aSAndreas Gohr * 13342e66c7aSAndreas Gohr * @param string $description 13442e66c7aSAndreas Gohr * @return $this 13542e66c7aSAndreas Gohr */ 13642e66c7aSAndreas Gohr public function setReturnDescription(string $description): self 13742e66c7aSAndreas Gohr { 13842e66c7aSAndreas Gohr $this->return['description'] = $description; 13942e66c7aSAndreas Gohr return $this; 14042e66c7aSAndreas Gohr } 14142e66c7aSAndreas Gohr 14242e66c7aSAndreas Gohr /** 14342e66c7aSAndreas Gohr * @return string 14442e66c7aSAndreas Gohr */ 14542e66c7aSAndreas Gohr public function getSummary(): string 14642e66c7aSAndreas Gohr { 14742e66c7aSAndreas Gohr return $this->summary; 14842e66c7aSAndreas Gohr } 14942e66c7aSAndreas Gohr 15042e66c7aSAndreas Gohr /** 15142e66c7aSAndreas Gohr * @param string $summary 15242e66c7aSAndreas Gohr * @return $this 15342e66c7aSAndreas Gohr */ 15442e66c7aSAndreas Gohr public function setSummary(string $summary): self 15542e66c7aSAndreas Gohr { 15642e66c7aSAndreas Gohr $this->summary = $summary; 15742e66c7aSAndreas Gohr return $this; 15842e66c7aSAndreas Gohr } 15942e66c7aSAndreas Gohr 16042e66c7aSAndreas Gohr /** 16142e66c7aSAndreas Gohr * @return string 16242e66c7aSAndreas Gohr */ 16342e66c7aSAndreas Gohr public function getDescription(): string 16442e66c7aSAndreas Gohr { 16542e66c7aSAndreas Gohr return $this->description; 16642e66c7aSAndreas Gohr } 16742e66c7aSAndreas Gohr 16842e66c7aSAndreas Gohr /** 16942e66c7aSAndreas Gohr * @param string $description 17042e66c7aSAndreas Gohr * @return $this 17142e66c7aSAndreas Gohr */ 17242e66c7aSAndreas Gohr public function setDescription(string $description): self 17342e66c7aSAndreas Gohr { 17442e66c7aSAndreas Gohr $this->description = $description; 17542e66c7aSAndreas Gohr return $this; 17642e66c7aSAndreas Gohr } 17742e66c7aSAndreas Gohr 17842e66c7aSAndreas Gohr /** 17942e66c7aSAndreas Gohr * Fill in the metadata 18042e66c7aSAndreas Gohr * 18142e66c7aSAndreas Gohr * This uses Reflection to inspect the method signature and doc block 18242e66c7aSAndreas Gohr * 18342e66c7aSAndreas Gohr * @throws \ReflectionException 18442e66c7aSAndreas Gohr */ 18542e66c7aSAndreas Gohr protected function parseData() 18642e66c7aSAndreas Gohr { 18742e66c7aSAndreas Gohr if (is_array($this->method)) { 18842e66c7aSAndreas Gohr $reflect = new \ReflectionMethod($this->method[0], $this->method[1]); 18942e66c7aSAndreas Gohr } else { 19042e66c7aSAndreas Gohr $reflect = new \ReflectionFunction($this->method); 19142e66c7aSAndreas Gohr } 19242e66c7aSAndreas Gohr 19342e66c7aSAndreas Gohr $docInfo = $this->parseDocBlock($reflect->getDocComment()); 19442e66c7aSAndreas Gohr $this->summary = $docInfo['summary']; 19542e66c7aSAndreas Gohr $this->description = $docInfo['description']; 19642e66c7aSAndreas Gohr 19742e66c7aSAndreas Gohr foreach ($reflect->getParameters() as $parameter) { 19842e66c7aSAndreas Gohr $name = $parameter->name; 19942e66c7aSAndreas Gohr $realType = $parameter->getType(); 20042e66c7aSAndreas Gohr if ($realType) { 20142e66c7aSAndreas Gohr $type = $realType->getName(); 20242e66c7aSAndreas Gohr } elseif (isset($docInfo['args'][$name]['type'])) { 20342e66c7aSAndreas Gohr $type = $docInfo['args'][$name]['type']; 20442e66c7aSAndreas Gohr } else { 20542e66c7aSAndreas Gohr $type = 'string'; 20642e66c7aSAndreas Gohr } 20742e66c7aSAndreas Gohr 20842e66c7aSAndreas Gohr if (isset($docInfo['args'][$name]['description'])) { 20942e66c7aSAndreas Gohr $description = $docInfo['args'][$name]['description']; 21042e66c7aSAndreas Gohr } else { 21142e66c7aSAndreas Gohr $description = ''; 21242e66c7aSAndreas Gohr } 21342e66c7aSAndreas Gohr 21442e66c7aSAndreas Gohr $this->args[$name] = [ 21542e66c7aSAndreas Gohr 'type' => $type, 21642e66c7aSAndreas Gohr 'description' => trim($description), 21742e66c7aSAndreas Gohr ]; 21842e66c7aSAndreas Gohr } 21942e66c7aSAndreas Gohr 22042e66c7aSAndreas Gohr $returnType = $reflect->getReturnType(); 22142e66c7aSAndreas Gohr if ($returnType) { 22242e66c7aSAndreas Gohr $this->return['type'] = $returnType->getName(); 22342e66c7aSAndreas Gohr } elseif (isset($docInfo['return']['type'])) { 22442e66c7aSAndreas Gohr $this->return['type'] = $docInfo['return']['type']; 22542e66c7aSAndreas Gohr } else { 22642e66c7aSAndreas Gohr $this->return['type'] = 'string'; 22742e66c7aSAndreas Gohr } 22842e66c7aSAndreas Gohr 22942e66c7aSAndreas Gohr if (isset($docInfo['return']['description'])) { 23042e66c7aSAndreas Gohr $this->return['description'] = $docInfo['return']['description']; 23142e66c7aSAndreas Gohr } 23242e66c7aSAndreas Gohr } 23342e66c7aSAndreas Gohr 23442e66c7aSAndreas Gohr /** 23542e66c7aSAndreas Gohr * Parse a doc block 23642e66c7aSAndreas Gohr * 23742e66c7aSAndreas Gohr * @param string $doc 23842e66c7aSAndreas Gohr * @return array 23942e66c7aSAndreas Gohr */ 24042e66c7aSAndreas Gohr protected function parseDocBlock($doc) 24142e66c7aSAndreas Gohr { 24242e66c7aSAndreas Gohr // strip asterisks and leading spaces 24342e66c7aSAndreas Gohr $doc = preg_replace( 24442e66c7aSAndreas Gohr ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'], 24542e66c7aSAndreas Gohr ['', '', '', ''], 24642e66c7aSAndreas Gohr $doc 24742e66c7aSAndreas Gohr ); 24842e66c7aSAndreas Gohr 24942e66c7aSAndreas Gohr $doc = trim($doc); 25042e66c7aSAndreas Gohr 25142e66c7aSAndreas Gohr // get all tags 25242e66c7aSAndreas Gohr $tags = []; 25342e66c7aSAndreas Gohr if (preg_match_all('/^@(\w+)\s+(.*)$/m', $doc, $matches, PREG_SET_ORDER)) { 25442e66c7aSAndreas Gohr foreach ($matches as $match) { 25542e66c7aSAndreas Gohr $tags[$match[1]][] = trim($match[2]); 25642e66c7aSAndreas Gohr } 25742e66c7aSAndreas Gohr } 25842e66c7aSAndreas Gohr $params = $this->extractDocTags($tags); 25942e66c7aSAndreas Gohr 26042e66c7aSAndreas Gohr // strip the tags from the doc 26142e66c7aSAndreas Gohr $doc = preg_replace('/^@(\w+)\s+(.*)$/m', '', $doc); 26242e66c7aSAndreas Gohr 26342e66c7aSAndreas Gohr [$summary, $description] = sexplode("\n\n", $doc, 2, ''); 26442e66c7aSAndreas Gohr return array_merge( 26542e66c7aSAndreas Gohr [ 26642e66c7aSAndreas Gohr 'summary' => trim($summary), 26742e66c7aSAndreas Gohr 'description' => trim($description), 26842e66c7aSAndreas Gohr 'tags' => $tags, 26942e66c7aSAndreas Gohr ], 27042e66c7aSAndreas Gohr $params 27142e66c7aSAndreas Gohr ); 27242e66c7aSAndreas Gohr } 27342e66c7aSAndreas Gohr 27442e66c7aSAndreas Gohr /** 27542e66c7aSAndreas Gohr * Process the param and return tags 27642e66c7aSAndreas Gohr * 27742e66c7aSAndreas Gohr * @param array $tags 27842e66c7aSAndreas Gohr * @return array 27942e66c7aSAndreas Gohr */ 28042e66c7aSAndreas Gohr protected function extractDocTags(&$tags) 28142e66c7aSAndreas Gohr { 28242e66c7aSAndreas Gohr $result = []; 28342e66c7aSAndreas Gohr 28442e66c7aSAndreas Gohr if (isset($tags['param'])) { 28542e66c7aSAndreas Gohr foreach ($tags['param'] as $param) { 286*b05603abSAndreas Gohr [$type, $name, $description] = array_map('trim', sexplode(' ', $param, 3, '')); 287*b05603abSAndreas Gohr if ($name[0] !== '$') continue; 288*b05603abSAndreas Gohr $name = substr($name, 1); 289*b05603abSAndreas Gohr 290*b05603abSAndreas Gohr $result['args'][$name] = [ 291*b05603abSAndreas Gohr 'type' => $this->cleanTypeHint($type), 292*b05603abSAndreas Gohr 'description' => $description, 29342e66c7aSAndreas Gohr ]; 29442e66c7aSAndreas Gohr } 29542e66c7aSAndreas Gohr unset($tags['param']); 29642e66c7aSAndreas Gohr } 29742e66c7aSAndreas Gohr 29842e66c7aSAndreas Gohr if (isset($tags['return'])) { 29942e66c7aSAndreas Gohr $return = $tags['return'][0]; 300*b05603abSAndreas Gohr [$type, $description] = array_map('trim', sexplode(' ', $return, 2, '')); 30142e66c7aSAndreas Gohr $result['return'] = [ 302*b05603abSAndreas Gohr 'type' => $this->cleanTypeHint($type), 303*b05603abSAndreas Gohr 'description' => $description 30442e66c7aSAndreas Gohr ]; 30542e66c7aSAndreas Gohr unset($tags['return']); 30642e66c7aSAndreas Gohr } 30742e66c7aSAndreas Gohr 30842e66c7aSAndreas Gohr return $result; 30942e66c7aSAndreas Gohr } 31042e66c7aSAndreas Gohr 31142e66c7aSAndreas Gohr /** 31242e66c7aSAndreas Gohr * Matches the given type hint against the valid options for the remote API 31342e66c7aSAndreas Gohr * 31442e66c7aSAndreas Gohr * @param string $hint 31542e66c7aSAndreas Gohr * @return string 31642e66c7aSAndreas Gohr */ 31742e66c7aSAndreas Gohr protected function cleanTypeHint($hint) 31842e66c7aSAndreas Gohr { 31942e66c7aSAndreas Gohr $types = explode('|', $hint); 32042e66c7aSAndreas Gohr foreach ($types as $t) { 32142e66c7aSAndreas Gohr if (str_ends_with($t, '[]')) { 32242e66c7aSAndreas Gohr return 'array'; 32342e66c7aSAndreas Gohr } 32442e66c7aSAndreas Gohr if ($t === 'boolean' || $t === 'true' || $t === 'false') { 32542e66c7aSAndreas Gohr return 'bool'; 32642e66c7aSAndreas Gohr } 32742e66c7aSAndreas Gohr if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) { 32842e66c7aSAndreas Gohr return $t; 32942e66c7aSAndreas Gohr } 33042e66c7aSAndreas Gohr } 33142e66c7aSAndreas Gohr return 'string'; 33242e66c7aSAndreas Gohr } 33342e66c7aSAndreas Gohr 33442e66c7aSAndreas Gohr /** 33542e66c7aSAndreas Gohr * Converts named arguments to positional arguments 33642e66c7aSAndreas Gohr * 33742e66c7aSAndreas Gohr * @fixme with PHP 8 we can use named arguments directly using the spread operator 33842e66c7aSAndreas Gohr * @param array $params 33942e66c7aSAndreas Gohr * @return array 34042e66c7aSAndreas Gohr */ 34142e66c7aSAndreas Gohr protected function namedArgsToPositional($params) 34242e66c7aSAndreas Gohr { 34342e66c7aSAndreas Gohr $args = []; 34442e66c7aSAndreas Gohr 34542e66c7aSAndreas Gohr foreach (array_keys($this->args) as $arg) { 34642e66c7aSAndreas Gohr if (isset($params[$arg])) { 34742e66c7aSAndreas Gohr $args[] = $params[$arg]; 34842e66c7aSAndreas Gohr } else { 34942e66c7aSAndreas Gohr $args[] = null; 35042e66c7aSAndreas Gohr } 35142e66c7aSAndreas Gohr } 35242e66c7aSAndreas Gohr 35342e66c7aSAndreas Gohr return $args; 35442e66c7aSAndreas Gohr } 35542e66c7aSAndreas Gohr 35642e66c7aSAndreas Gohr} 357