Reworked API definition

This cleans up the API:

* no more compatibility with obsolete wiki API
* no more difference between wiki.* and dokuwiki.* calls -> core.*
* use of optional parameters avoids double definitions
* use Response objects for complex results
* always use named primitives as input
* major cleanup of docblock descriptions
This commit is contained in:
Andreas Gohr 2024-01-05 13:19:42 +01:00
parent dd7472d3f8
commit 6cce3332fb
13 changed files with 1273 additions and 924 deletions

View File

@ -53,7 +53,7 @@ abstract class RemotePlugin extends Plugin
}
// add to result
$result[$method_name] = new ApiCall([$this, $method_name]);
$result[$method_name] = new ApiCall([$this, $method_name], 'plugins');
}
return $result;

View File

@ -76,9 +76,9 @@ class Api
{
if (!$this->coreMethods) {
if ($apiCore === null) {
$this->coreMethods = (new ApiCore($this))->getRemoteInfo();
$this->coreMethods = (new ApiCore($this))->getMethods();
} else {
$this->coreMethods = $apiCore->getRemoteInfo();
$this->coreMethods = $apiCore->getMethods();
}
}
return $this->coreMethods;

View File

@ -4,6 +4,11 @@ namespace dokuwiki\Remote;
use dokuwiki\Remote\OpenApiDoc\DocBlockMethod;
use InvalidArgumentException;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use RuntimeException;
class ApiCall
{
@ -13,6 +18,9 @@ class ApiCall
/** @var bool Whether this call can be called without authentication */
protected bool $isPublic = false;
/** @var string The category this call belongs to */
protected string $category;
/** @var DocBlockMethod The meta data of this call as parsed from its doc block */
protected $docs;
@ -20,14 +28,16 @@ class ApiCall
* Make the given method available as an API call
*
* @param string|array $method Either [object,'method'] or 'function'
* @param string $category The category this call belongs to
*/
public function __construct($method)
public function __construct($method, $category = '')
{
if (!is_callable($method)) {
throw new \InvalidArgumentException('Method is not callable');
throw new InvalidArgumentException('Method is not callable');
}
$this->method = $method;
$this->category = $category;
}
/**
@ -58,13 +68,13 @@ class ApiCall
if ($this->docs === null) {
try {
if (is_array($this->method)) {
$reflect = new \ReflectionMethod($this->method[0], $this->method[1]);
$reflect = new ReflectionMethod($this->method[0], $this->method[1]);
} else {
$reflect = new \ReflectionFunction($this->method);
$reflect = new ReflectionFunction($this->method);
}
$this->docs = new DocBlockMethod($reflect);
} catch (\ReflectionException $e) {
throw new \RuntimeException('Failed to parse API method documentation', 0, $e);
} catch (ReflectionException $e) {
throw new RuntimeException('Failed to parse API method documentation', 0, $e);
}
}
@ -123,6 +133,14 @@ class ApiCall
return $this->getDocs()->getDescription();
}
/**
* @return string
*/
public function getCategory(): string
{
return $this->category;
}
/**
* Converts named arguments to positional arguments
*
@ -134,7 +152,7 @@ class ApiCall
{
$args = [];
foreach (array_keys($this->docs->getParameters()) as $arg) {
foreach (array_keys($this->getDocs()->getParameters()) as $arg) {
if (isset($params[$arg])) {
$args[] = $params[$arg];
} else {

File diff suppressed because it is too large Load Diff

View File

@ -100,10 +100,11 @@ class OpenAPIGenerator
$this->typeToSchema($retType)
);
return [
$definition = [
'operationId' => $method,
'summary' => $call->getSummary(),
'description' => $description,
'tags' => [PhpString::ucwords($call->getCategory())],
'requestBody' => [
'required' => true,
'content' => [
@ -142,6 +143,22 @@ class OpenAPIGenerator
],
]
];
if ($call->isPublic()) {
$definition['security'] = [
new \stdClass(),
];
$definition['description'] = 'This method is public and does not require authentication. ' .
"\n\n" . $definition['description'];
}
if ($call->getDocs()->getTag('deprecated')) {
$definition['deprecated'] = true;
$definition['description'] = '**This method is deprecated.** ' . $call->getDocs()->getTag('deprecated')[0] .
"\n\n" . $definition['description'];
}
return $definition;
}
protected function getMethodArguments($args)
@ -153,23 +170,35 @@ class OpenAPIGenerator
}
$props = [];
$reqs = [];
$schema = [
'schema' => [
'type' => 'object',
'required' => &$reqs,
'properties' => &$props
]
];
foreach ($args as $name => $info) {
$example = $this->generateExample($name, $info['type']->getOpenApiType());
$description = $info['description'];
if ($info['optional'] && isset($info['default'])) {
$description .= ' [_default: `' . json_encode($info['default']) . '`_]';
}
$props[$name] = array_merge(
[
'description' => $info['description'],
'description' => $description,
'examples' => [$example],
'required' => !$info['optional'],
],
$this->typeToSchema($info['type'])
);
if (!$info['optional']) $reqs[] = $name;
}
return $schema;
}
@ -177,12 +206,15 @@ class OpenAPIGenerator
{
switch ($type) {
case 'integer':
if ($name === 'rev') return 0;
if ($name === 'revision') return 0;
if ($name === 'timestamp') return time() - 60 * 24 * 30 * 2;
return 42;
case 'boolean':
return true;
case 'string':
if($name === 'page') return 'playground:playground';
if($name === 'media') return 'wiki:dokuwiki-128.png';
if ($name === 'page') return 'playground:playground';
if ($name === 'media') return 'wiki:dokuwiki-128.png';
return 'some-' . $name;
case 'array':
return ['some-' . $name, 'other-' . $name];
@ -226,12 +258,12 @@ class OpenAPIGenerator
];
// if a sub type is known, define the items
if($schema['type'] === 'array' && $type->getSubType()) {
if ($schema['type'] === 'array' && $type->getSubType()) {
$schema['items'] = $this->typeToSchema($type->getSubType());
}
// if this is an object, define the properties
if($schema['type'] === 'object') {
if ($schema['type'] === 'object') {
try {
$baseType = $type->getBaseType();
$doc = new DocBlockClass(new \ReflectionClass($baseType));

View File

@ -0,0 +1,22 @@
<?php
namespace dokuwiki\Remote\Response;
class Link extends ApiResponse
{
/** @var string The type of this link: `internal`, `external` or `interwiki` */
public $type;
/** @var string The wiki page this link points to, same as `href` for external links */
public $page;
/** @var string A hyperlink pointing to the linked target */
public $href;
public function __construct($data)
{
$this->type = $data['type'] ?? '';
$this->page = $data['page'] ?? '';
$this->href = $data['href'] ?? '';
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace dokuwiki\Remote\Response;
/**
* Represents a single media revision in the wiki.
*/
class Media extends ApiResponse
{
/** @var string The media ID */
public $id;
/** @var int The media revision aka last modified timestamp */
public $revision;
/** @var int The page size in bytes */
public $size;
/** @var int The current user's permissions for this file */
public $perms;
/** @var bool Wether this is an image file */
public $isimage;
/** @var string MD5 sum over the file's content (if available and requested) */
public $hash;
/** @inheritdoc */
public function __construct($data)
{
$this->id = cleanID($data['id'] ?? '');
if ($this->id === '') {
throw new \InvalidArgumentException('Missing id');
}
if (!media_exists($this->id)) {
throw new \InvalidArgumentException('Media does not exist');
}
// FIXME this isn't really managing the difference between old and current revs correctly
$this->revision = (int)($data['rev'] ?? $data['mtime'] ?? @filemtime(mediaFN($this->id)));
$this->size = (int)($data['size'] ?? @filesize(mediaFN($this->id)));
$this->perms = $data['perm'] ?? auth_quickaclcheck($this->id);
$this->isimage = (bool)($data['isimg'] ?? false);
$this->hash = $data['hash'] ?? '';
}
/**
* Calculate the hash for this page
*
* This is a heavy operation and should only be called when needed.
*/
public function calculateHash()
{
if (!media_exists($this->id)) return;
$this->hash = md5(io_readFile(mediaFN($this->id)));
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace dokuwiki\Remote\Response;
class MediaRevision extends ApiResponse
{
/** @var string The media ID */
public $id;
/** @var int The revision (timestamp) of this change */
public $revision;
/** @var string The author of this change */
public $author;
/** @var string The IP address from where this change was made */
public $ip;
/** @var string The summary of this change */
public $summary;
/** @var string The type of this change */
public $type;
/** @var int The change in bytes */
public $sizechange;
/** @inheritdoc */
public function __construct($data)
{
$this->id = $data['id'];
$this->revision = (int)($data['revision'] ?? 0);
$this->author = $data['author'] ?? '';
$this->ip = $data['ip'] ?? '';
$this->summary = $data['summary'] ?? '';
$this->type = $data['type'] ?? '';
$this->sizechange = (int)($data['sizechange'] ?? 0);
}
}

View File

@ -2,6 +2,8 @@
namespace dokuwiki\Remote\Response;
use dokuwiki\ChangeLog\PageChangeLog;
/**
* Represents a single page revision in the wiki.
*/
@ -17,8 +19,10 @@ class Page extends ApiResponse
public $title;
/** @var int The current user's permissions for this page */
public $perms;
/** @var string MD5 sum over the page's content (only if requested) */
/** @var string MD5 sum over the page's content (if available and requested) */
public $hash;
/** @var string The author of this page revision (if available and requested) */
public $author;
/** @inheritdoc */
public function __construct($data)
@ -27,12 +31,18 @@ class Page extends ApiResponse
if ($this->id === '') {
throw new \InvalidArgumentException('Missing id');
}
if (!page_exists($this->id)) {
throw new \InvalidArgumentException('Page does not exist');
}
$this->revision = (int)($data['rev'] ?? $data['lastModified'] ?? @filemtime(wikiFN($this->id)));
// FIXME this isn't really managing the difference between old and current revs correctly
$this->revision = (int)($data['rev'] ?? @filemtime(wikiFN($this->id)));
$this->size = (int)($data['size'] ?? @filesize(wikiFN($this->id)));
$this->title = $data['title'] ?? $this->retrieveTitle();
$this->perms = $data['perm'] ?? auth_quickaclcheck($this->id);
$this->hash = $data['hash'] ?? '';
$this->author = $data['author'] ?? '';
}
/**
@ -55,4 +65,26 @@ class Page extends ApiResponse
return $this->id;
}
/**
* Calculate the hash for this page
*
* This is a heavy operation and should only be called when needed.
*/
public function calculateHash()
{
if (!page_exists($this->id)) return;
$this->hash = md5(io_readFile(wikiFN($this->id)));
}
/**
* Retrieve the author of this page
*/
public function retrieveAuthor()
{
if (!page_exists($this->id)) return;
$pagelog = new PageChangeLog($this->id, 1024);
$info = $pagelog->getRevisionInfo($this->revision);
$this->author = is_array($info) ? ($info['user'] ?: $info['ip']) : null;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace dokuwiki\Remote\Response;
/**
* Represents a page found by a search
*/
class PageHit extends Page
{
/** @var int The number of hits this result got */
public $score;
/** @var string The HTML formatted snippet in which the search term was found (if available) */
public $snippet;
/** @var string Not available for search results */
public $hash;
/** @var string Not available for search results */
public $author;
/** @inheritdoc */
public function __construct($data)
{
parent::__construct($data);
$this->snippet = $data['snippet'] ?? '';
$this->score = (int)($data['score'] ?? 0);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace dokuwiki\Remote\Response;
class PageRevision extends ApiResponse
{
/** @var string The page ID */
public $id;
/** @var int The revision (timestamp) of this change */
public $revision;
/** @var string The author of this change */
public $author;
/** @var string The IP address from where this change was made */
public $ip;
/** @var string The summary of this change */
public $summary;
/** @var string The type of this change */
public $type;
/** @var int The change in bytes */
public $sizechange;
/** @inheritdoc */
public function __construct($data)
{
$this->id = $data['id'];
$this->revision = (int)($data['revision'] ?? 0);
$this->author = $data['author'] ?? '';
$this->ip = $data['ip'] ?? '';
$this->summary = $data['summary'] ?? '';
$this->type = $data['type'] ?? '';
$this->sizechange = (int)($data['sizechange'] ?? 0);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace dokuwiki\Remote\Response;
/**
* Represents a user
*/
class User extends ApiResponse
{
/** @var string The login name of the user */
public $login;
/** @var string The full name of the user */
public $name;
/** @var string The email address of the user */
public $mail;
/** @var array The groups the user is in */
public $groups;
/** @var bool Whether the user is a super user */
public bool $isAdmin;
/** @var bool Whether the user is a manager */
public bool $isManager;
/** @inheritdoc */
public function __construct($data)
{
global $USERINFO;
global $INPUT;
$this->login = $INPUT->server->str('REMOTE_USER');
$this->name = $USERINFO['name'];
$this->mail = $USERINFO['mail'];
$this->groups = $USERINFO['grps'];
$this->isAdmin = auth_isAdmin($this->login, $this->groups);
$this->isManager = auth_isManager($this->login, $this->groups);
}
}

View File

@ -154,8 +154,6 @@ function getVersionData()
* If no version can be determined "snapshot? update version XX" is returned.
* Where XX represents the update version number set in doku.php.
*
* For checking API compatibility, you should rather rely on dokuwiki.getXMLRPCAPIVersion
*
* @author Anika Henke <anika@selfthinker.org>
* @return string The version string e.g. "Release 2023-04-04a"
*/