1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
|
<?php
namespace MediaWiki\Rest\Module;
use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
use MediaWiki\Rest\Handler\RedirectHandler;
use MediaWiki\Rest\PathTemplateMatcher\ModuleConfigurationException;
use MediaWiki\Rest\Reporter\ErrorReporter;
use MediaWiki\Rest\ResponseFactory;
use MediaWiki\Rest\RouteDefinitionException;
use MediaWiki\Rest\Router;
use MediaWiki\Rest\Validator\Validator;
use Wikimedia\ObjectFactory\ObjectFactory;
/**
* A Module that is based on a module definition file similar to an OpenAPI spec.
* @see docs/rest/mwapi-1.0.json for the schema of module definition files.
*
* Just like an OpenAPI spec, the module definition file contains a "paths"
* section that maps paths and HTTP methods to operations. Each operation
* then specifies the PHP class that will handle the request under the "handler"
* key. The value of the "handler" key is an object spec for use with
* ObjectFactory::createObject.
*
* The following fields are supported as a shorthand notation:
* - "redirect": the route represents a redirect and will be handled by
* the RedirectHandler class. The redirect is specified as a JSON object
* that specifies the target "path", and optional the redirect "code".
* If a redirect is defined, the "handler" key must be omitted.
*
* More shorthands may be added in the future.
*
* Route definitions can contain additional fields to configure the handler.
* The handler can access the route definition by calling getConfig().
*
* @internal
* @since 1.43
*/
class SpecBasedModule extends MatcherBasedModule {
private string $definitionFile;
private ?array $moduleDef = null;
private ?int $routeFileTimestamp = null;
private ?string $configHash = null;
/**
* @internal
*/
public function __construct(
string $definitionFile,
Router $router,
string $pathPrefix,
ResponseFactory $responseFactory,
BasicAuthorizerInterface $basicAuth,
ObjectFactory $objectFactory,
Validator $restValidator,
ErrorReporter $errorReporter
) {
parent::__construct(
$router,
$pathPrefix,
$responseFactory,
$basicAuth,
$objectFactory,
$restValidator,
$errorReporter
);
$this->definitionFile = $definitionFile;
}
/**
* Get a config version hash for cache invalidation
*
* @return string
*/
protected function getConfigHash(): string {
if ( $this->configHash === null ) {
$this->configHash = md5( json_encode( [
'class' => __CLASS__,
'version' => 1,
'fileTimestamps' => $this->getRouteFileTimestamp()
] ) );
}
return $this->configHash;
}
/**
* Load the module definition file.
*
* @return array
*/
private function getModuleDefinition(): array {
if ( $this->moduleDef !== null ) {
return $this->moduleDef;
}
$this->routeFileTimestamp = filemtime( $this->definitionFile );
$moduleDef = $this->loadJsonFile( $this->definitionFile );
if ( !$moduleDef ) {
throw new ModuleConfigurationException(
'Malformed module definition file: ' . $this->definitionFile
);
}
if ( !isset( $moduleDef['mwapi'] ) ) {
throw new ModuleConfigurationException(
'Missing mwapi version field in ' . $this->definitionFile
);
}
// Require OpenAPI version 3.1 or compatible.
if ( !version_compare( $moduleDef['mwapi'], '1.0.999', '<=' ) ||
!version_compare( $moduleDef['mwapi'], '1.0.0', '>=' )
) {
throw new ModuleConfigurationException(
"Unsupported openapi version {$moduleDef['mwapi']} in "
. $this->definitionFile
);
}
$this->moduleDef = $moduleDef;
return $this->moduleDef;
}
/**
* Get last modification times of the module definition file.
*/
private function getRouteFileTimestamp(): int {
if ( $this->routeFileTimestamp === null ) {
$this->routeFileTimestamp = filemtime( $this->definitionFile );
}
return $this->routeFileTimestamp;
}
/**
* @unstable for testing
*
* @return array[]
*/
public function getDefinedPaths(): array {
$paths = [];
$moduleDef = $this->getModuleDefinition();
foreach ( $moduleDef['paths'] as $path => $pSpec ) {
$paths[$path] = [];
foreach ( $pSpec as $method => $opSpec ) {
$paths[$path][] = strtoupper( $method );
}
}
return $paths;
}
protected function initRoutes(): void {
$moduleDef = $this->getModuleDefinition();
// The structure is similar to OpenAPI, see docs/rest/mwapi.1.0.json
foreach ( $moduleDef['paths'] as $path => $pathSpec ) {
foreach ( $pathSpec as $method => $opSpec ) {
$info = $this->makeRouteInfo( $path, $opSpec );
$this->addRoute( $method, $path, $info );
}
}
}
/**
* Generate a route info array to be stored in the matcher tree,
* in the form expected by MatcherBasedModule::addRoute()
* and ultimately Module::getHandlerForPath().
*/
private function makeRouteInfo( string $path, array $opSpec ): array {
static $objectSpecKeys = [
'class',
'factory',
'services',
'optional_services',
'args',
];
static $oasKeys = [
'parameters',
'responses',
'summary',
'description',
'tags',
'externalDocs',
];
if ( isset( $opSpec['redirect'] ) ) {
// Redirect shorthand
$opSpec['handler'] = [
'class' => RedirectHandler::class,
'redirect' => $opSpec['redirect'],
];
unset( $opSpec['redirect'] );
}
$handlerSpec = $opSpec['handler'] ?? null;
if ( !$handlerSpec ) {
throw new RouteDefinitionException( 'Missing handler spec' );
}
$info = [
'spec' => array_intersect_key( $handlerSpec, array_flip( $objectSpecKeys ) ),
'config' => array_diff_key( $handlerSpec, array_flip( $objectSpecKeys ) ),
'OAS' => array_intersect_key( $opSpec, array_flip( $oasKeys ) ),
'path' => $path,
];
return $info;
}
public function getOpenApiInfo() {
$def = $this->getModuleDefinition();
return $def['info'] ?? [];
}
}
|