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