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