1e4e3d439SAndreas Gohr<?php 2e4e3d439SAndreas Gohr 3e4e3d439SAndreas Gohrnamespace dokuwiki\Remote\OpenApiDoc; 4e4e3d439SAndreas Gohr 5e4e3d439SAndreas Gohruse dokuwiki\Remote\Api; 6e4e3d439SAndreas Gohruse dokuwiki\Remote\ApiCall; 7e4e3d439SAndreas Gohruse dokuwiki\Remote\ApiCore; 8e4e3d439SAndreas Gohruse dokuwiki\Utf8\PhpString; 9e4e3d439SAndreas Gohruse ReflectionClass; 10e4e3d439SAndreas Gohruse ReflectionException; 11e4e3d439SAndreas Gohruse stdClass; 12e4e3d439SAndreas Gohr 13e4e3d439SAndreas Gohr/** 14e4e3d439SAndreas Gohr * Generates the OpenAPI documentation for the DokuWiki API 15e4e3d439SAndreas Gohr */ 16e4e3d439SAndreas Gohrclass OpenAPIGenerator 17e4e3d439SAndreas Gohr{ 18e4e3d439SAndreas Gohr /** @var Api */ 19e4e3d439SAndreas Gohr protected $api; 20e4e3d439SAndreas Gohr 21e4e3d439SAndreas Gohr /** @var array Holds the documentation tree while building */ 22e4e3d439SAndreas Gohr protected $documentation = []; 23e4e3d439SAndreas Gohr 24e4e3d439SAndreas Gohr /** 25e4e3d439SAndreas Gohr * OpenAPIGenerator constructor. 26e4e3d439SAndreas Gohr */ 27e4e3d439SAndreas Gohr public function __construct() 28e4e3d439SAndreas Gohr { 29e4e3d439SAndreas Gohr $this->api = new Api(); 30e4e3d439SAndreas Gohr } 31e4e3d439SAndreas Gohr 32e4e3d439SAndreas Gohr /** 33e4e3d439SAndreas Gohr * Generate the OpenAPI documentation 34e4e3d439SAndreas Gohr * 35e4e3d439SAndreas Gohr * @return string JSON encoded OpenAPI specification 36e4e3d439SAndreas Gohr */ 37e4e3d439SAndreas Gohr public function generate() 38e4e3d439SAndreas Gohr { 39e4e3d439SAndreas Gohr $this->documentation = []; 40e4e3d439SAndreas Gohr $this->documentation['openapi'] = '3.1.0'; 41e4e3d439SAndreas Gohr $this->documentation['info'] = [ 42e4e3d439SAndreas Gohr 'title' => 'DokuWiki API', 43e4e3d439SAndreas Gohr 'description' => 'The DokuWiki API OpenAPI specification', 44e4e3d439SAndreas Gohr 'version' => ((string)ApiCore::API_VERSION), 45*0e8fe812SAndreas Gohr 'x-locale' => 'en-US', 46e4e3d439SAndreas Gohr ]; 47e4e3d439SAndreas Gohr 48e4e3d439SAndreas Gohr $this->addServers(); 49e4e3d439SAndreas Gohr $this->addSecurity(); 50e4e3d439SAndreas Gohr $this->addMethods(); 51e4e3d439SAndreas Gohr 52e4e3d439SAndreas Gohr return json_encode($this->documentation, JSON_PRETTY_PRINT); 53e4e3d439SAndreas Gohr } 54e4e3d439SAndreas Gohr 55e4e3d439SAndreas Gohr /** 56e4e3d439SAndreas Gohr * Add the current DokuWiki instance as a server 57e4e3d439SAndreas Gohr * 58e4e3d439SAndreas Gohr * @return void 59e4e3d439SAndreas Gohr */ 60e4e3d439SAndreas Gohr protected function addServers() 61e4e3d439SAndreas Gohr { 62e4e3d439SAndreas Gohr $this->documentation['servers'] = [ 63e4e3d439SAndreas Gohr [ 64e4e3d439SAndreas Gohr 'url' => DOKU_URL . 'lib/exe/jsonrpc.php', 65e4e3d439SAndreas Gohr ], 66e4e3d439SAndreas Gohr ]; 67e4e3d439SAndreas Gohr } 68e4e3d439SAndreas Gohr 69e4e3d439SAndreas Gohr /** 70e4e3d439SAndreas Gohr * Define the default security schemes 71e4e3d439SAndreas Gohr * 72e4e3d439SAndreas Gohr * @return void 73e4e3d439SAndreas Gohr */ 74e4e3d439SAndreas Gohr protected function addSecurity() 75e4e3d439SAndreas Gohr { 76e4e3d439SAndreas Gohr $this->documentation['components']['securitySchemes'] = [ 77e4e3d439SAndreas Gohr 'basicAuth' => [ 78e4e3d439SAndreas Gohr 'type' => 'http', 79e4e3d439SAndreas Gohr 'scheme' => 'basic', 80e4e3d439SAndreas Gohr ], 81e4e3d439SAndreas Gohr 'jwt' => [ 82e4e3d439SAndreas Gohr 'type' => 'http', 83e4e3d439SAndreas Gohr 'scheme' => 'bearer', 84e4e3d439SAndreas Gohr 'bearerFormat' => 'JWT', 85e4e3d439SAndreas Gohr ] 86e4e3d439SAndreas Gohr ]; 87e4e3d439SAndreas Gohr $this->documentation['security'] = [ 88e4e3d439SAndreas Gohr [ 89e4e3d439SAndreas Gohr 'basicAuth' => [], 90e4e3d439SAndreas Gohr ], 91e4e3d439SAndreas Gohr [ 92e4e3d439SAndreas Gohr 'jwt' => [], 93e4e3d439SAndreas Gohr ], 94e4e3d439SAndreas Gohr ]; 95e4e3d439SAndreas Gohr } 96e4e3d439SAndreas Gohr 97e4e3d439SAndreas Gohr /** 98e4e3d439SAndreas Gohr * Add all methods available in the API to the documentation 99e4e3d439SAndreas Gohr * 100e4e3d439SAndreas Gohr * @return void 101e4e3d439SAndreas Gohr */ 102e4e3d439SAndreas Gohr protected function addMethods() 103e4e3d439SAndreas Gohr { 104e4e3d439SAndreas Gohr $methods = $this->api->getMethods(); 105e4e3d439SAndreas Gohr 106e4e3d439SAndreas Gohr $this->documentation['paths'] = []; 107e4e3d439SAndreas Gohr foreach ($methods as $method => $call) { 108e4e3d439SAndreas Gohr $this->documentation['paths']['/' . $method] = [ 109e4e3d439SAndreas Gohr 'post' => $this->getMethodDefinition($method, $call), 110e4e3d439SAndreas Gohr ]; 111e4e3d439SAndreas Gohr } 112e4e3d439SAndreas Gohr } 113e4e3d439SAndreas Gohr 114e4e3d439SAndreas Gohr /** 115e4e3d439SAndreas Gohr * Create the schema definition for a single API method 116e4e3d439SAndreas Gohr * 117e4e3d439SAndreas Gohr * @param string $method API method name 118e4e3d439SAndreas Gohr * @param ApiCall $call The call definition 119e4e3d439SAndreas Gohr * @return array 120e4e3d439SAndreas Gohr */ 121e4e3d439SAndreas Gohr protected function getMethodDefinition(string $method, ApiCall $call) 122e4e3d439SAndreas Gohr { 123e4e3d439SAndreas Gohr $description = $call->getDescription(); 124e4e3d439SAndreas Gohr $links = $call->getDocs()->getTag('link'); 125e4e3d439SAndreas Gohr if ($links) { 126e4e3d439SAndreas Gohr $description .= "\n\n**See also:**"; 127e4e3d439SAndreas Gohr foreach ($links as $link) { 128e4e3d439SAndreas Gohr $description .= "\n\n* " . $this->generateLink($link); 129e4e3d439SAndreas Gohr } 130e4e3d439SAndreas Gohr } 131e4e3d439SAndreas Gohr 132e4e3d439SAndreas Gohr $retType = $call->getReturn()['type']; 133e4e3d439SAndreas Gohr $result = array_merge( 134e4e3d439SAndreas Gohr [ 135e4e3d439SAndreas Gohr 'description' => $call->getReturn()['description'], 136e4e3d439SAndreas Gohr 'examples' => [$this->generateExample('result', $retType->getOpenApiType())], 137e4e3d439SAndreas Gohr ], 138e4e3d439SAndreas Gohr $this->typeToSchema($retType) 139e4e3d439SAndreas Gohr ); 140e4e3d439SAndreas Gohr 141e4e3d439SAndreas Gohr $definition = [ 142e4e3d439SAndreas Gohr 'operationId' => $method, 143e4e3d439SAndreas Gohr 'summary' => $call->getSummary(), 144e4e3d439SAndreas Gohr 'description' => $description, 145e4e3d439SAndreas Gohr 'tags' => [PhpString::ucwords($call->getCategory())], 146e4e3d439SAndreas Gohr 'requestBody' => [ 147e4e3d439SAndreas Gohr 'required' => true, 148e4e3d439SAndreas Gohr 'content' => [ 149e4e3d439SAndreas Gohr 'application/json' => $this->getMethodArguments($call->getArgs()), 150e4e3d439SAndreas Gohr ] 151e4e3d439SAndreas Gohr ], 152e4e3d439SAndreas Gohr 'responses' => [ 153e4e3d439SAndreas Gohr 200 => [ 154e4e3d439SAndreas Gohr 'description' => 'Result', 155e4e3d439SAndreas Gohr 'content' => [ 156e4e3d439SAndreas Gohr 'application/json' => [ 157e4e3d439SAndreas Gohr 'schema' => [ 158e4e3d439SAndreas Gohr 'type' => 'object', 159e4e3d439SAndreas Gohr 'properties' => [ 160e4e3d439SAndreas Gohr 'result' => $result, 161e4e3d439SAndreas Gohr 'error' => [ 162e4e3d439SAndreas Gohr 'type' => 'object', 163e4e3d439SAndreas Gohr 'description' => 'Error object in case of an error', 164e4e3d439SAndreas Gohr 'properties' => [ 165e4e3d439SAndreas Gohr 'code' => [ 166e4e3d439SAndreas Gohr 'type' => 'integer', 167e4e3d439SAndreas Gohr 'description' => 'The error code', 168e4e3d439SAndreas Gohr 'examples' => [0], 169e4e3d439SAndreas Gohr ], 170e4e3d439SAndreas Gohr 'message' => [ 171e4e3d439SAndreas Gohr 'type' => 'string', 172e4e3d439SAndreas Gohr 'description' => 'The error message', 173e4e3d439SAndreas Gohr 'examples' => ['Success'], 174e4e3d439SAndreas Gohr ], 175e4e3d439SAndreas Gohr ], 176e4e3d439SAndreas Gohr ], 177e4e3d439SAndreas Gohr ], 178e4e3d439SAndreas Gohr ], 179e4e3d439SAndreas Gohr ], 180e4e3d439SAndreas Gohr ], 181e4e3d439SAndreas Gohr ], 182e4e3d439SAndreas Gohr ] 183e4e3d439SAndreas Gohr ]; 184e4e3d439SAndreas Gohr 185e4e3d439SAndreas Gohr if ($call->isPublic()) { 186e4e3d439SAndreas Gohr $definition['security'] = [ 187e4e3d439SAndreas Gohr new stdClass(), 188e4e3d439SAndreas Gohr ]; 189e4e3d439SAndreas Gohr $definition['description'] = 'This method is public and does not require authentication. ' . 190e4e3d439SAndreas Gohr "\n\n" . $definition['description']; 191e4e3d439SAndreas Gohr } 192e4e3d439SAndreas Gohr 193e4e3d439SAndreas Gohr if ($call->getDocs()->getTag('deprecated')) { 194e4e3d439SAndreas Gohr $definition['deprecated'] = true; 195e4e3d439SAndreas Gohr $definition['description'] = '**This method is deprecated.** ' . 196e4e3d439SAndreas Gohr $call->getDocs()->getTag('deprecated')[0] . 197e4e3d439SAndreas Gohr "\n\n" . $definition['description']; 198e4e3d439SAndreas Gohr } 199e4e3d439SAndreas Gohr 200e4e3d439SAndreas Gohr return $definition; 201e4e3d439SAndreas Gohr } 202e4e3d439SAndreas Gohr 203e4e3d439SAndreas Gohr /** 204e4e3d439SAndreas Gohr * Create the schema definition for the arguments of a single API method 205e4e3d439SAndreas Gohr * 206e4e3d439SAndreas Gohr * @param array $args The arguments of the method as returned by ApiCall::getArgs() 207e4e3d439SAndreas Gohr * @return array 208e4e3d439SAndreas Gohr */ 209e4e3d439SAndreas Gohr protected function getMethodArguments($args) 210e4e3d439SAndreas Gohr { 211e4e3d439SAndreas Gohr if (!$args) { 212e4e3d439SAndreas Gohr // even if no arguments are needed, we need to define a body 213e4e3d439SAndreas Gohr // this is to ensure the openapi spec knows that a application/json header is needed 214e4e3d439SAndreas Gohr return ['schema' => ['type' => 'null']]; 215e4e3d439SAndreas Gohr } 216e4e3d439SAndreas Gohr 217e4e3d439SAndreas Gohr $props = []; 218e4e3d439SAndreas Gohr $reqs = []; 219e4e3d439SAndreas Gohr $schema = [ 220e4e3d439SAndreas Gohr 'schema' => [ 221e4e3d439SAndreas Gohr 'type' => 'object', 222e4e3d439SAndreas Gohr 'required' => &$reqs, 223e4e3d439SAndreas Gohr 'properties' => &$props 224e4e3d439SAndreas Gohr ] 225e4e3d439SAndreas Gohr ]; 226e4e3d439SAndreas Gohr 227e4e3d439SAndreas Gohr foreach ($args as $name => $info) { 228e4e3d439SAndreas Gohr $example = $this->generateExample($name, $info['type']->getOpenApiType()); 229e4e3d439SAndreas Gohr 230e4e3d439SAndreas Gohr $description = $info['description']; 231e4e3d439SAndreas Gohr if ($info['optional'] && isset($info['default'])) { 232e4e3d439SAndreas Gohr $description .= ' [_default: `' . json_encode($info['default']) . '`_]'; 233e4e3d439SAndreas Gohr } 234e4e3d439SAndreas Gohr 235e4e3d439SAndreas Gohr $props[$name] = array_merge( 236e4e3d439SAndreas Gohr [ 237e4e3d439SAndreas Gohr 'description' => $description, 238e4e3d439SAndreas Gohr 'examples' => [$example], 239e4e3d439SAndreas Gohr ], 240e4e3d439SAndreas Gohr $this->typeToSchema($info['type']) 241e4e3d439SAndreas Gohr ); 242e4e3d439SAndreas Gohr if (!$info['optional']) $reqs[] = $name; 243e4e3d439SAndreas Gohr } 244e4e3d439SAndreas Gohr 245e4e3d439SAndreas Gohr 246e4e3d439SAndreas Gohr return $schema; 247e4e3d439SAndreas Gohr } 248e4e3d439SAndreas Gohr 249e4e3d439SAndreas Gohr /** 250e4e3d439SAndreas Gohr * Generate an example value for the given parameter 251e4e3d439SAndreas Gohr * 252e4e3d439SAndreas Gohr * @param string $name The parameter's name 253e4e3d439SAndreas Gohr * @param string $type The parameter's type 254e4e3d439SAndreas Gohr * @return mixed 255e4e3d439SAndreas Gohr */ 256e4e3d439SAndreas Gohr protected function generateExample($name, $type) 257e4e3d439SAndreas Gohr { 258e4e3d439SAndreas Gohr switch ($type) { 259e4e3d439SAndreas Gohr case 'integer': 260e4e3d439SAndreas Gohr if ($name === 'rev') return 0; 261e4e3d439SAndreas Gohr if ($name === 'revision') return 0; 262e4e3d439SAndreas Gohr if ($name === 'timestamp') return time() - 60 * 24 * 30 * 2; 263e4e3d439SAndreas Gohr return 42; 264e4e3d439SAndreas Gohr case 'boolean': 265e4e3d439SAndreas Gohr return true; 266e4e3d439SAndreas Gohr case 'string': 267e4e3d439SAndreas Gohr if ($name === 'page') return 'playground:playground'; 268e4e3d439SAndreas Gohr if ($name === 'media') return 'wiki:dokuwiki-128.png'; 269e4e3d439SAndreas Gohr return 'some-' . $name; 270e4e3d439SAndreas Gohr case 'array': 271e4e3d439SAndreas Gohr return ['some-' . $name, 'other-' . $name]; 272e4e3d439SAndreas Gohr default: 273e4e3d439SAndreas Gohr return new stdClass(); 274e4e3d439SAndreas Gohr } 275e4e3d439SAndreas Gohr } 276e4e3d439SAndreas Gohr 277e4e3d439SAndreas Gohr /** 278e4e3d439SAndreas Gohr * Generates a markdown link from a dokuwiki.org URL 279e4e3d439SAndreas Gohr * 280e4e3d439SAndreas Gohr * @param $url 281e4e3d439SAndreas Gohr * @return mixed|string 282e4e3d439SAndreas Gohr */ 283e4e3d439SAndreas Gohr protected function generateLink($url) 284e4e3d439SAndreas Gohr { 285e4e3d439SAndreas Gohr if (preg_match('/^https?:\/\/(www\.)?dokuwiki\.org\/(.+)$/', $url, $match)) { 286e4e3d439SAndreas Gohr $name = $match[2]; 287e4e3d439SAndreas Gohr 288e4e3d439SAndreas Gohr $name = str_replace(['_', '#', ':'], [' ', ' ', ' '], $name); 289e4e3d439SAndreas Gohr $name = PhpString::ucwords($name); 290e4e3d439SAndreas Gohr 291e4e3d439SAndreas Gohr return "[$name]($url)"; 292e4e3d439SAndreas Gohr } else { 293e4e3d439SAndreas Gohr return $url; 294e4e3d439SAndreas Gohr } 295e4e3d439SAndreas Gohr } 296e4e3d439SAndreas Gohr 297e4e3d439SAndreas Gohr 298e4e3d439SAndreas Gohr /** 299e4e3d439SAndreas Gohr * Generate the OpenAPI schema for the given type 300e4e3d439SAndreas Gohr * 301e4e3d439SAndreas Gohr * @param Type $type 302e4e3d439SAndreas Gohr * @return array 303e4e3d439SAndreas Gohr * @todo add example generation here 304e4e3d439SAndreas Gohr */ 305e4e3d439SAndreas Gohr public function typeToSchema(Type $type) 306e4e3d439SAndreas Gohr { 307e4e3d439SAndreas Gohr $schema = [ 308e4e3d439SAndreas Gohr 'type' => $type->getOpenApiType(), 309e4e3d439SAndreas Gohr ]; 310e4e3d439SAndreas Gohr 311e4e3d439SAndreas Gohr // if a sub type is known, define the items 312e4e3d439SAndreas Gohr if ($schema['type'] === 'array' && $type->getSubType()) { 313e4e3d439SAndreas Gohr $schema['items'] = $this->typeToSchema($type->getSubType()); 314e4e3d439SAndreas Gohr } 315e4e3d439SAndreas Gohr 316e4e3d439SAndreas Gohr // if this is an object, define the properties 317e4e3d439SAndreas Gohr if ($schema['type'] === 'object') { 318e4e3d439SAndreas Gohr try { 319e4e3d439SAndreas Gohr $baseType = $type->getBaseType(); 320e4e3d439SAndreas Gohr $doc = new DocBlockClass(new ReflectionClass($baseType)); 321e4e3d439SAndreas Gohr $schema['properties'] = []; 322e4e3d439SAndreas Gohr foreach ($doc->getPropertyDocs() as $property => $propertyDoc) { 323e4e3d439SAndreas Gohr $schema['properties'][$property] = array_merge( 324e4e3d439SAndreas Gohr [ 325e4e3d439SAndreas Gohr 'description' => $propertyDoc->getSummary(), 326e4e3d439SAndreas Gohr ], 327e4e3d439SAndreas Gohr $this->typeToSchema($propertyDoc->getType()) 328e4e3d439SAndreas Gohr ); 329e4e3d439SAndreas Gohr } 330e4e3d439SAndreas Gohr } catch (ReflectionException $e) { 331e4e3d439SAndreas Gohr // The class is not available, so we cannot generate a schema 332e4e3d439SAndreas Gohr } 333e4e3d439SAndreas Gohr } 334e4e3d439SAndreas Gohr 335e4e3d439SAndreas Gohr return $schema; 336e4e3d439SAndreas Gohr } 337e4e3d439SAndreas Gohr 338e4e3d439SAndreas Gohr} 339