xref: /dokuwiki/inc/Remote/ApiCall.php (revision 66f07661cd824ab6f523f24178dd048002075505)
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