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), 450e8fe812SAndreas 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 /** 56*d3856637SAndreas Gohr * Read all error codes used in ApiCore.php 57*d3856637SAndreas Gohr * 58*d3856637SAndreas Gohr * This is useful for the documentation, but also for checking if the error codes are unique 59*d3856637SAndreas Gohr * 60*d3856637SAndreas Gohr * @return array 61*d3856637SAndreas Gohr * @todo Getting all classes/methods registered with the API and reading their error codes would be even better 62*d3856637SAndreas Gohr * @todo This is super crude. Using the PHP Tokenizer would be more sensible 63*d3856637SAndreas Gohr */ 64*d3856637SAndreas Gohr public function getErrorCodes() 65*d3856637SAndreas Gohr { 66*d3856637SAndreas Gohr $lines = file(DOKU_INC . 'inc/Remote/ApiCore.php'); 67*d3856637SAndreas Gohr 68*d3856637SAndreas Gohr $codes = []; 69*d3856637SAndreas Gohr $method = ''; 70*d3856637SAndreas Gohr 71*d3856637SAndreas Gohr foreach ($lines as $no => $line) { 72*d3856637SAndreas Gohr if (preg_match('/ *function (\w+)/', $line, $match)) { 73*d3856637SAndreas Gohr $method = $match[1]; 74*d3856637SAndreas Gohr } 75*d3856637SAndreas Gohr if (preg_match('/^ *throw new RemoteException\(\'([^\']+)\'.*?, (\d+)/', $line, $match)) { 76*d3856637SAndreas Gohr $codes[] = [ 77*d3856637SAndreas Gohr 'line' => $no, 78*d3856637SAndreas Gohr 'exception' => 'RemoteException', 79*d3856637SAndreas Gohr 'method' => $method, 80*d3856637SAndreas Gohr 'code' => $match[2], 81*d3856637SAndreas Gohr 'message' => $match[1], 82*d3856637SAndreas Gohr ]; 83*d3856637SAndreas Gohr } 84*d3856637SAndreas Gohr if (preg_match('/^ *throw new AccessDeniedException\(\'([^\']+)\'.*?, (\d+)/', $line, $match)) { 85*d3856637SAndreas Gohr $codes[] = [ 86*d3856637SAndreas Gohr 'line' => $no, 87*d3856637SAndreas Gohr 'exception' => 'AccessDeniedException', 88*d3856637SAndreas Gohr 'method' => $method, 89*d3856637SAndreas Gohr 'code' => $match[2], 90*d3856637SAndreas Gohr 'message' => $match[1], 91*d3856637SAndreas Gohr ]; 92*d3856637SAndreas Gohr } 93*d3856637SAndreas Gohr } 94*d3856637SAndreas Gohr 95*d3856637SAndreas Gohr usort($codes, function ($a, $b) { 96*d3856637SAndreas Gohr return $a['code'] <=> $b['code']; 97*d3856637SAndreas Gohr }); 98*d3856637SAndreas Gohr 99*d3856637SAndreas Gohr return $codes; 100*d3856637SAndreas Gohr } 101*d3856637SAndreas Gohr 102*d3856637SAndreas Gohr 103*d3856637SAndreas Gohr /** 104e4e3d439SAndreas Gohr * Add the current DokuWiki instance as a server 105e4e3d439SAndreas Gohr * 106e4e3d439SAndreas Gohr * @return void 107e4e3d439SAndreas Gohr */ 108e4e3d439SAndreas Gohr protected function addServers() 109e4e3d439SAndreas Gohr { 110e4e3d439SAndreas Gohr $this->documentation['servers'] = [ 111e4e3d439SAndreas Gohr [ 112e4e3d439SAndreas Gohr 'url' => DOKU_URL . 'lib/exe/jsonrpc.php', 113e4e3d439SAndreas Gohr ], 114e4e3d439SAndreas Gohr ]; 115e4e3d439SAndreas Gohr } 116e4e3d439SAndreas Gohr 117e4e3d439SAndreas Gohr /** 118e4e3d439SAndreas Gohr * Define the default security schemes 119e4e3d439SAndreas Gohr * 120e4e3d439SAndreas Gohr * @return void 121e4e3d439SAndreas Gohr */ 122e4e3d439SAndreas Gohr protected function addSecurity() 123e4e3d439SAndreas Gohr { 124e4e3d439SAndreas Gohr $this->documentation['components']['securitySchemes'] = [ 125e4e3d439SAndreas Gohr 'basicAuth' => [ 126e4e3d439SAndreas Gohr 'type' => 'http', 127e4e3d439SAndreas Gohr 'scheme' => 'basic', 128e4e3d439SAndreas Gohr ], 129e4e3d439SAndreas Gohr 'jwt' => [ 130e4e3d439SAndreas Gohr 'type' => 'http', 131e4e3d439SAndreas Gohr 'scheme' => 'bearer', 132e4e3d439SAndreas Gohr 'bearerFormat' => 'JWT', 133e4e3d439SAndreas Gohr ] 134e4e3d439SAndreas Gohr ]; 135e4e3d439SAndreas Gohr $this->documentation['security'] = [ 136e4e3d439SAndreas Gohr [ 137e4e3d439SAndreas Gohr 'basicAuth' => [], 138e4e3d439SAndreas Gohr ], 139e4e3d439SAndreas Gohr [ 140e4e3d439SAndreas Gohr 'jwt' => [], 141e4e3d439SAndreas Gohr ], 142e4e3d439SAndreas Gohr ]; 143e4e3d439SAndreas Gohr } 144e4e3d439SAndreas Gohr 145e4e3d439SAndreas Gohr /** 146e4e3d439SAndreas Gohr * Add all methods available in the API to the documentation 147e4e3d439SAndreas Gohr * 148e4e3d439SAndreas Gohr * @return void 149e4e3d439SAndreas Gohr */ 150e4e3d439SAndreas Gohr protected function addMethods() 151e4e3d439SAndreas Gohr { 152e4e3d439SAndreas Gohr $methods = $this->api->getMethods(); 153e4e3d439SAndreas Gohr 154e4e3d439SAndreas Gohr $this->documentation['paths'] = []; 155e4e3d439SAndreas Gohr foreach ($methods as $method => $call) { 156e4e3d439SAndreas Gohr $this->documentation['paths']['/' . $method] = [ 157e4e3d439SAndreas Gohr 'post' => $this->getMethodDefinition($method, $call), 158e4e3d439SAndreas Gohr ]; 159e4e3d439SAndreas Gohr } 160e4e3d439SAndreas Gohr } 161e4e3d439SAndreas Gohr 162e4e3d439SAndreas Gohr /** 163e4e3d439SAndreas Gohr * Create the schema definition for a single API method 164e4e3d439SAndreas Gohr * 165e4e3d439SAndreas Gohr * @param string $method API method name 166e4e3d439SAndreas Gohr * @param ApiCall $call The call definition 167e4e3d439SAndreas Gohr * @return array 168e4e3d439SAndreas Gohr */ 169e4e3d439SAndreas Gohr protected function getMethodDefinition(string $method, ApiCall $call) 170e4e3d439SAndreas Gohr { 171e4e3d439SAndreas Gohr $description = $call->getDescription(); 172e4e3d439SAndreas Gohr $links = $call->getDocs()->getTag('link'); 173e4e3d439SAndreas Gohr if ($links) { 174e4e3d439SAndreas Gohr $description .= "\n\n**See also:**"; 175e4e3d439SAndreas Gohr foreach ($links as $link) { 176e4e3d439SAndreas Gohr $description .= "\n\n* " . $this->generateLink($link); 177e4e3d439SAndreas Gohr } 178e4e3d439SAndreas Gohr } 179e4e3d439SAndreas Gohr 180e4e3d439SAndreas Gohr $retType = $call->getReturn()['type']; 181e4e3d439SAndreas Gohr $result = array_merge( 182e4e3d439SAndreas Gohr [ 183e4e3d439SAndreas Gohr 'description' => $call->getReturn()['description'], 184e4e3d439SAndreas Gohr 'examples' => [$this->generateExample('result', $retType->getOpenApiType())], 185e4e3d439SAndreas Gohr ], 186e4e3d439SAndreas Gohr $this->typeToSchema($retType) 187e4e3d439SAndreas Gohr ); 188e4e3d439SAndreas Gohr 189e4e3d439SAndreas Gohr $definition = [ 190e4e3d439SAndreas Gohr 'operationId' => $method, 191e4e3d439SAndreas Gohr 'summary' => $call->getSummary(), 192e4e3d439SAndreas Gohr 'description' => $description, 193e4e3d439SAndreas Gohr 'tags' => [PhpString::ucwords($call->getCategory())], 194e4e3d439SAndreas Gohr 'requestBody' => [ 195e4e3d439SAndreas Gohr 'required' => true, 196e4e3d439SAndreas Gohr 'content' => [ 197e4e3d439SAndreas Gohr 'application/json' => $this->getMethodArguments($call->getArgs()), 198e4e3d439SAndreas Gohr ] 199e4e3d439SAndreas Gohr ], 200e4e3d439SAndreas Gohr 'responses' => [ 201e4e3d439SAndreas Gohr 200 => [ 202e4e3d439SAndreas Gohr 'description' => 'Result', 203e4e3d439SAndreas Gohr 'content' => [ 204e4e3d439SAndreas Gohr 'application/json' => [ 205e4e3d439SAndreas Gohr 'schema' => [ 206e4e3d439SAndreas Gohr 'type' => 'object', 207e4e3d439SAndreas Gohr 'properties' => [ 208e4e3d439SAndreas Gohr 'result' => $result, 209e4e3d439SAndreas Gohr 'error' => [ 210e4e3d439SAndreas Gohr 'type' => 'object', 211e4e3d439SAndreas Gohr 'description' => 'Error object in case of an error', 212e4e3d439SAndreas Gohr 'properties' => [ 213e4e3d439SAndreas Gohr 'code' => [ 214e4e3d439SAndreas Gohr 'type' => 'integer', 215e4e3d439SAndreas Gohr 'description' => 'The error code', 216e4e3d439SAndreas Gohr 'examples' => [0], 217e4e3d439SAndreas Gohr ], 218e4e3d439SAndreas Gohr 'message' => [ 219e4e3d439SAndreas Gohr 'type' => 'string', 220e4e3d439SAndreas Gohr 'description' => 'The error message', 221e4e3d439SAndreas Gohr 'examples' => ['Success'], 222e4e3d439SAndreas Gohr ], 223e4e3d439SAndreas Gohr ], 224e4e3d439SAndreas Gohr ], 225e4e3d439SAndreas Gohr ], 226e4e3d439SAndreas Gohr ], 227e4e3d439SAndreas Gohr ], 228e4e3d439SAndreas Gohr ], 229e4e3d439SAndreas Gohr ], 230e4e3d439SAndreas Gohr ] 231e4e3d439SAndreas Gohr ]; 232e4e3d439SAndreas Gohr 233e4e3d439SAndreas Gohr if ($call->isPublic()) { 234e4e3d439SAndreas Gohr $definition['security'] = [ 235e4e3d439SAndreas Gohr new stdClass(), 236e4e3d439SAndreas Gohr ]; 237e4e3d439SAndreas Gohr $definition['description'] = 'This method is public and does not require authentication. ' . 238e4e3d439SAndreas Gohr "\n\n" . $definition['description']; 239e4e3d439SAndreas Gohr } 240e4e3d439SAndreas Gohr 241e4e3d439SAndreas Gohr if ($call->getDocs()->getTag('deprecated')) { 242e4e3d439SAndreas Gohr $definition['deprecated'] = true; 243e4e3d439SAndreas Gohr $definition['description'] = '**This method is deprecated.** ' . 244e4e3d439SAndreas Gohr $call->getDocs()->getTag('deprecated')[0] . 245e4e3d439SAndreas Gohr "\n\n" . $definition['description']; 246e4e3d439SAndreas Gohr } 247e4e3d439SAndreas Gohr 248e4e3d439SAndreas Gohr return $definition; 249e4e3d439SAndreas Gohr } 250e4e3d439SAndreas Gohr 251e4e3d439SAndreas Gohr /** 252e4e3d439SAndreas Gohr * Create the schema definition for the arguments of a single API method 253e4e3d439SAndreas Gohr * 254e4e3d439SAndreas Gohr * @param array $args The arguments of the method as returned by ApiCall::getArgs() 255e4e3d439SAndreas Gohr * @return array 256e4e3d439SAndreas Gohr */ 257e4e3d439SAndreas Gohr protected function getMethodArguments($args) 258e4e3d439SAndreas Gohr { 259e4e3d439SAndreas Gohr if (!$args) { 260e4e3d439SAndreas Gohr // even if no arguments are needed, we need to define a body 261e4e3d439SAndreas Gohr // this is to ensure the openapi spec knows that a application/json header is needed 262e4e3d439SAndreas Gohr return ['schema' => ['type' => 'null']]; 263e4e3d439SAndreas Gohr } 264e4e3d439SAndreas Gohr 265e4e3d439SAndreas Gohr $props = []; 266e4e3d439SAndreas Gohr $reqs = []; 267e4e3d439SAndreas Gohr $schema = [ 268e4e3d439SAndreas Gohr 'schema' => [ 269e4e3d439SAndreas Gohr 'type' => 'object', 270e4e3d439SAndreas Gohr 'required' => &$reqs, 271e4e3d439SAndreas Gohr 'properties' => &$props 272e4e3d439SAndreas Gohr ] 273e4e3d439SAndreas Gohr ]; 274e4e3d439SAndreas Gohr 275e4e3d439SAndreas Gohr foreach ($args as $name => $info) { 276e4e3d439SAndreas Gohr $example = $this->generateExample($name, $info['type']->getOpenApiType()); 277e4e3d439SAndreas Gohr 278e4e3d439SAndreas Gohr $description = $info['description']; 279e4e3d439SAndreas Gohr if ($info['optional'] && isset($info['default'])) { 280e4e3d439SAndreas Gohr $description .= ' [_default: `' . json_encode($info['default']) . '`_]'; 281e4e3d439SAndreas Gohr } 282e4e3d439SAndreas Gohr 283e4e3d439SAndreas Gohr $props[$name] = array_merge( 284e4e3d439SAndreas Gohr [ 285e4e3d439SAndreas Gohr 'description' => $description, 286e4e3d439SAndreas Gohr 'examples' => [$example], 287e4e3d439SAndreas Gohr ], 288e4e3d439SAndreas Gohr $this->typeToSchema($info['type']) 289e4e3d439SAndreas Gohr ); 290e4e3d439SAndreas Gohr if (!$info['optional']) $reqs[] = $name; 291e4e3d439SAndreas Gohr } 292e4e3d439SAndreas Gohr 293e4e3d439SAndreas Gohr 294e4e3d439SAndreas Gohr return $schema; 295e4e3d439SAndreas Gohr } 296e4e3d439SAndreas Gohr 297e4e3d439SAndreas Gohr /** 298e4e3d439SAndreas Gohr * Generate an example value for the given parameter 299e4e3d439SAndreas Gohr * 300e4e3d439SAndreas Gohr * @param string $name The parameter's name 301e4e3d439SAndreas Gohr * @param string $type The parameter's type 302e4e3d439SAndreas Gohr * @return mixed 303e4e3d439SAndreas Gohr */ 304e4e3d439SAndreas Gohr protected function generateExample($name, $type) 305e4e3d439SAndreas Gohr { 306e4e3d439SAndreas Gohr switch ($type) { 307e4e3d439SAndreas Gohr case 'integer': 308e4e3d439SAndreas Gohr if ($name === 'rev') return 0; 309e4e3d439SAndreas Gohr if ($name === 'revision') return 0; 310e4e3d439SAndreas Gohr if ($name === 'timestamp') return time() - 60 * 24 * 30 * 2; 311e4e3d439SAndreas Gohr return 42; 312e4e3d439SAndreas Gohr case 'boolean': 313e4e3d439SAndreas Gohr return true; 314e4e3d439SAndreas Gohr case 'string': 315e4e3d439SAndreas Gohr if ($name === 'page') return 'playground:playground'; 316e4e3d439SAndreas Gohr if ($name === 'media') return 'wiki:dokuwiki-128.png'; 317e4e3d439SAndreas Gohr return 'some-' . $name; 318e4e3d439SAndreas Gohr case 'array': 319e4e3d439SAndreas Gohr return ['some-' . $name, 'other-' . $name]; 320e4e3d439SAndreas Gohr default: 321e4e3d439SAndreas Gohr return new stdClass(); 322e4e3d439SAndreas Gohr } 323e4e3d439SAndreas Gohr } 324e4e3d439SAndreas Gohr 325e4e3d439SAndreas Gohr /** 326e4e3d439SAndreas Gohr * Generates a markdown link from a dokuwiki.org URL 327e4e3d439SAndreas Gohr * 328e4e3d439SAndreas Gohr * @param $url 329e4e3d439SAndreas Gohr * @return mixed|string 330e4e3d439SAndreas Gohr */ 331e4e3d439SAndreas Gohr protected function generateLink($url) 332e4e3d439SAndreas Gohr { 333e4e3d439SAndreas Gohr if (preg_match('/^https?:\/\/(www\.)?dokuwiki\.org\/(.+)$/', $url, $match)) { 334e4e3d439SAndreas Gohr $name = $match[2]; 335e4e3d439SAndreas Gohr 336e4e3d439SAndreas Gohr $name = str_replace(['_', '#', ':'], [' ', ' ', ' '], $name); 337e4e3d439SAndreas Gohr $name = PhpString::ucwords($name); 338e4e3d439SAndreas Gohr 339e4e3d439SAndreas Gohr return "[$name]($url)"; 340e4e3d439SAndreas Gohr } else { 341e4e3d439SAndreas Gohr return $url; 342e4e3d439SAndreas Gohr } 343e4e3d439SAndreas Gohr } 344e4e3d439SAndreas Gohr 345e4e3d439SAndreas Gohr 346e4e3d439SAndreas Gohr /** 347e4e3d439SAndreas Gohr * Generate the OpenAPI schema for the given type 348e4e3d439SAndreas Gohr * 349e4e3d439SAndreas Gohr * @param Type $type 350e4e3d439SAndreas Gohr * @return array 351e4e3d439SAndreas Gohr * @todo add example generation here 352e4e3d439SAndreas Gohr */ 353e4e3d439SAndreas Gohr public function typeToSchema(Type $type) 354e4e3d439SAndreas Gohr { 355e4e3d439SAndreas Gohr $schema = [ 356e4e3d439SAndreas Gohr 'type' => $type->getOpenApiType(), 357e4e3d439SAndreas Gohr ]; 358e4e3d439SAndreas Gohr 359e4e3d439SAndreas Gohr // if a sub type is known, define the items 360e4e3d439SAndreas Gohr if ($schema['type'] === 'array' && $type->getSubType()) { 361e4e3d439SAndreas Gohr $schema['items'] = $this->typeToSchema($type->getSubType()); 362e4e3d439SAndreas Gohr } 363e4e3d439SAndreas Gohr 364e4e3d439SAndreas Gohr // if this is an object, define the properties 365e4e3d439SAndreas Gohr if ($schema['type'] === 'object') { 366e4e3d439SAndreas Gohr try { 367e4e3d439SAndreas Gohr $baseType = $type->getBaseType(); 368e4e3d439SAndreas Gohr $doc = new DocBlockClass(new ReflectionClass($baseType)); 369e4e3d439SAndreas Gohr $schema['properties'] = []; 370e4e3d439SAndreas Gohr foreach ($doc->getPropertyDocs() as $property => $propertyDoc) { 371e4e3d439SAndreas Gohr $schema['properties'][$property] = array_merge( 372e4e3d439SAndreas Gohr [ 373e4e3d439SAndreas Gohr 'description' => $propertyDoc->getSummary(), 374e4e3d439SAndreas Gohr ], 375e4e3d439SAndreas Gohr $this->typeToSchema($propertyDoc->getType()) 376e4e3d439SAndreas Gohr ); 377e4e3d439SAndreas Gohr } 378e4e3d439SAndreas Gohr } catch (ReflectionException $e) { 379e4e3d439SAndreas Gohr // The class is not available, so we cannot generate a schema 380e4e3d439SAndreas Gohr } 381e4e3d439SAndreas Gohr } 382e4e3d439SAndreas Gohr 383e4e3d439SAndreas Gohr return $schema; 384e4e3d439SAndreas Gohr } 385e4e3d439SAndreas Gohr 386e4e3d439SAndreas Gohr} 387