Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.61% |
193 / 204 |
|
66.67% |
12 / 18 |
CRAP | |
0.00% |
0 / 1 |
| ModuleSpecHandler | |
94.61% |
193 / 204 |
|
66.67% |
12 / 18 |
60.56 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| run | |
86.84% |
33 / 38 |
|
0.00% |
0 / 1 |
9.18 | |||
| getInfoSpec | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
7 | |||
| getLicenseSpec | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| getContactSpec | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| getServerSpec | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
2 | |||
| getModuleRouteUrl | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| getLocalModuleSandboxUrl | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| getPathsSpec | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| getRouteSpec | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
3.01 | |||
| getOpenApiSecurityRequirements | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
| generateOperationId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| summaryToOperationId | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| pathToOperationId | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
5.01 | |||
| getComponentsSpec | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
| getResponseBodySchemaFileName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getParamSettings | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\Rest\Handler; |
| 4 | |
| 5 | use MediaWiki\Config\Config; |
| 6 | use MediaWiki\Config\ServiceOptions; |
| 7 | use MediaWiki\MainConfigNames; |
| 8 | use MediaWiki\Rest\Handler; |
| 9 | use MediaWiki\Rest\LocalizedHttpException; |
| 10 | use MediaWiki\Rest\Module\Module; |
| 11 | use MediaWiki\Rest\Module\ModuleMode; |
| 12 | use MediaWiki\Rest\RequestData; |
| 13 | use MediaWiki\Rest\Response; |
| 14 | use MediaWiki\Rest\ResponseFactory; |
| 15 | use MediaWiki\Rest\SimpleHandler; |
| 16 | use MediaWiki\Rest\Validator\Validator; |
| 17 | use MediaWiki\Session\SessionManagerInterface; |
| 18 | use Wikimedia\Message\MessageValue; |
| 19 | use Wikimedia\ParamValidator\ParamValidator; |
| 20 | |
| 21 | /** |
| 22 | * Core REST API endpoint that outputs an OpenAPI spec of a set of routes. |
| 23 | */ |
| 24 | class ModuleSpecHandler extends SimpleHandler { |
| 25 | |
| 26 | public const MODULE_SPEC_PATH = '/coredev/v0/specs/module/{module}'; |
| 27 | |
| 28 | /** |
| 29 | * @internal |
| 30 | */ |
| 31 | private const CONSTRUCTOR_OPTIONS = [ |
| 32 | MainConfigNames::RightsUrl, |
| 33 | MainConfigNames::RightsText, |
| 34 | MainConfigNames::EmergencyContact, |
| 35 | MainConfigNames::Sitename, |
| 36 | MainConfigNames::CanonicalServer, |
| 37 | MainConfigNames::RestExternalModules, |
| 38 | MainConfigNames::RestLocalModuleTestBaseUrl, |
| 39 | MainConfigNames::RestTermsOfServiceUrl, |
| 40 | ]; |
| 41 | |
| 42 | private readonly ServiceOptions $options; |
| 43 | |
| 44 | public function __construct( |
| 45 | Config $config, |
| 46 | private readonly SessionManagerInterface $sessionManager, |
| 47 | ) { |
| 48 | $options = new ServiceOptions( self::CONSTRUCTOR_OPTIONS, $config ); |
| 49 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
| 50 | $this->options = $options; |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * @param string $moduleName |
| 55 | * @param string $version |
| 56 | * |
| 57 | * @return array|Response OpenAPI operation object, or Response object if redirect is needed |
| 58 | */ |
| 59 | public function run( $moduleName, $version = '' ): array|Response { |
| 60 | // TODO: implement caching, get cache key from Router. |
| 61 | |
| 62 | if ( $version !== '' ) { |
| 63 | $moduleName .= '/' . $version; |
| 64 | } |
| 65 | |
| 66 | $mode = null; |
| 67 | if ( $moduleName === '-' ) { |
| 68 | // Hack that allows us to fetch a spec for the empty module prefix |
| 69 | $moduleName = ''; |
| 70 | $mode = ModuleMode::PUBLISHED; |
| 71 | } |
| 72 | |
| 73 | // Suppress OpenAPI spec for HIDDEN or DISABLED modules. This is not a security or |
| 74 | // protection mechanism. MediaWiki is open source, so callers can learn the details of |
| 75 | // its endpoints. This is just a way to hide the spec in cases where it should not be |
| 76 | // available. |
| 77 | $mode ??= $this->getRouter()->getModuleManager()->getModuleMode( $moduleName ); |
| 78 | if ( $mode === ModuleMode::HIDDEN || $mode === ModuleMode::DISABLED ) { |
| 79 | throw new LocalizedHttpException( |
| 80 | MessageValue::new( 'rest-unavailable-spec' )->params( $moduleName ), |
| 81 | 403 |
| 82 | ); |
| 83 | } |
| 84 | |
| 85 | // If this is an external module, redirect to its spec |
| 86 | $restExternalModules = $this->options->get( MainConfigNames::RestExternalModules ); |
| 87 | $em = $restExternalModules[$moduleName] ?? null; |
| 88 | if ( $em ) { |
| 89 | $response = $this->getResponseFactory()->createPermanentRedirect( $em['spec'] ); |
| 90 | return $response; |
| 91 | } |
| 92 | |
| 93 | $module = $this->getRouter()->getModule( $moduleName ); |
| 94 | if ( !$module ) { |
| 95 | throw new LocalizedHttpException( |
| 96 | MessageValue::new( 'rest-unknown-module' )->params( $moduleName ), |
| 97 | 404 |
| 98 | ); |
| 99 | } |
| 100 | |
| 101 | $spec = [ |
| 102 | 'openapi' => '3.0.0', |
| 103 | 'info' => $this->getInfoSpec( $module ), |
| 104 | 'servers' => $this->getServerSpec( $module ), |
| 105 | 'externalDocs' => $module->getOpenApiExternalDocs(), |
| 106 | 'tags' => $module->getOpenApiTags(), |
| 107 | 'paths' => $this->getPathsSpec( $module ), |
| 108 | 'components' => $this->getComponentsSpec(), |
| 109 | ]; |
| 110 | |
| 111 | unset( $spec['info']['deprecationSettings'] ); |
| 112 | |
| 113 | if ( !$spec['externalDocs'] ) { |
| 114 | unset( $spec['externalDocs'] ); |
| 115 | } |
| 116 | |
| 117 | if ( empty( $spec['tags'] ) ) { |
| 118 | unset( $spec['tags'] ); |
| 119 | } |
| 120 | |
| 121 | return $spec; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * @see https://spec.openapis.org/oas/v3.0.0#info-object |
| 126 | */ |
| 127 | private function getInfoSpec( Module $module ): array { |
| 128 | // Modules manage their own version see SpecBasedModule::getOpenApiInfo() |
| 129 | // and ExtraRoutesModule::getOpenApiInfo(). |
| 130 | // We add a blank string as the version fallback here so the required info.version field is |
| 131 | // always present in the spec, even when empty, as required by the OpenAPI specification. |
| 132 | // A blank version still raises an error in OpenAPI spec linters, such as the WMF spectral linter. |
| 133 | |
| 134 | $prefix = $module->getPathPrefix(); |
| 135 | |
| 136 | if ( $prefix === '' ) { |
| 137 | $title = $this->getJsonLocalizer()->getFormattedMessage( 'rest-default-module' ); |
| 138 | } else { |
| 139 | $moduleStr = $this->getJsonLocalizer()->getFormattedMessage( 'rest-module' ); |
| 140 | $title = "$prefix " . $moduleStr; |
| 141 | } |
| 142 | |
| 143 | $info = $module->getOpenApiInfo() + [ |
| 144 | 'title' => $title, |
| 145 | 'version' => '', |
| 146 | 'license' => $this->getLicenseSpec(), |
| 147 | 'contact' => $this->getContactSpec(), |
| 148 | ]; |
| 149 | |
| 150 | $sandboxUrl = $this->getLocalModuleSandboxUrl( $module ); |
| 151 | if ( $sandboxUrl !== null ) { |
| 152 | $host = parse_url( $sandboxUrl, PHP_URL_HOST ); |
| 153 | if ( isset( $info['description'] ) && $info['description'] !== '' ) { |
| 154 | $info['description'] = $this->getJsonLocalizer()->getFormattedMessage( |
| 155 | new MessageValue( |
| 156 | 'rest-sandbox-recommend-test-server-with-description', |
| 157 | [ $info['description'], $host ] |
| 158 | ) |
| 159 | ); |
| 160 | } else { |
| 161 | $info['description'] = $this->getJsonLocalizer()->getFormattedMessage( |
| 162 | new MessageValue( 'rest-sandbox-recommend-test-server', [ $host ] ) |
| 163 | ); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | $termsOfService = $this->options->get( MainConfigNames::RestTermsOfServiceUrl ); |
| 168 | if ( is_string( $termsOfService ) && $termsOfService !== '' ) { |
| 169 | $info['termsOfService'] = $termsOfService; |
| 170 | } |
| 171 | |
| 172 | return $info; |
| 173 | } |
| 174 | |
| 175 | private function getLicenseSpec(): array { |
| 176 | return [ |
| 177 | 'name' => $this->options->get( MainConfigNames::RightsText ), |
| 178 | 'url' => $this->options->get( MainConfigNames::RightsUrl ), |
| 179 | ]; |
| 180 | } |
| 181 | |
| 182 | private function getContactSpec(): array { |
| 183 | $contact = [ |
| 184 | 'name' => $this->options->get( MainConfigNames::Sitename ), |
| 185 | 'url' => $this->options->get( MainConfigNames::CanonicalServer ), |
| 186 | ]; |
| 187 | |
| 188 | $email = $this->options->get( MainConfigNames::EmergencyContact ); |
| 189 | // OpenAPI requires contact.email to be a valid email address. Keep the rest |
| 190 | // of the contact object intact and omit the field when the configured value |
| 191 | // does not satisfy that format. |
| 192 | if ( is_string( $email ) && filter_var( $email, FILTER_VALIDATE_EMAIL ) !== false ) { |
| 193 | $contact['email'] = $email; |
| 194 | } |
| 195 | |
| 196 | return $contact; |
| 197 | } |
| 198 | |
| 199 | private function getServerSpec( Module $module ): array { |
| 200 | $prodUrl = $this->getModuleRouteUrl( $module ); |
| 201 | $sandboxUrl = $this->getLocalModuleSandboxUrl( $module ); |
| 202 | |
| 203 | if ( $sandboxUrl !== null ) { |
| 204 | $localizer = $this->getJsonLocalizer(); |
| 205 | return [ |
| 206 | [ |
| 207 | 'url' => $prodUrl, |
| 208 | 'description' => $localizer->getFormattedMessage( 'rest-sandbox-server-production' ), |
| 209 | ], |
| 210 | [ |
| 211 | 'url' => $sandboxUrl, |
| 212 | 'description' => $localizer->getFormattedMessage( 'rest-sandbox-server-sandbox' ), |
| 213 | ] |
| 214 | ]; |
| 215 | } |
| 216 | |
| 217 | return [ |
| 218 | [ |
| 219 | 'url' => $prodUrl, |
| 220 | ] |
| 221 | ]; |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Get the absolute entry point route URL for the given module's path prefix. |
| 226 | * |
| 227 | * @param Module $module The REST module to resolve the prefix for |
| 228 | * @return string The absolute route URL (e.g., https://en.wikipedia.org/w/rest.php/specs/v0) |
| 229 | */ |
| 230 | private function getModuleRouteUrl( Module $module ): string { |
| 231 | $prefix = $module->getPathPrefix(); |
| 232 | if ( $prefix !== '' ) { |
| 233 | $prefix = "/$prefix"; |
| 234 | } |
| 235 | return $this->getRouter()->getRouteUrl( $prefix ); |
| 236 | } |
| 237 | |
| 238 | /** |
| 239 | * Get the absolute sandbox route URL for the given module, if configured via $wgRestLocalModuleTestBaseUrl. |
| 240 | * |
| 241 | * @param Module $module The REST module to resolve the sandbox URL for |
| 242 | * @return string|null The absolute sandbox route URL, or null if not configured |
| 243 | */ |
| 244 | private function getLocalModuleSandboxUrl( Module $module ): ?string { |
| 245 | $sandboxBaseUrl = $this->options->get( MainConfigNames::RestLocalModuleTestBaseUrl ); |
| 246 | if ( $sandboxBaseUrl === null || $sandboxBaseUrl === '' ) { |
| 247 | return null; |
| 248 | } |
| 249 | $prefix = $module->getPathPrefix(); |
| 250 | if ( $prefix !== '' ) { |
| 251 | return rtrim( $sandboxBaseUrl, '/' ) . '/' . $prefix; |
| 252 | } |
| 253 | return $sandboxBaseUrl; |
| 254 | } |
| 255 | |
| 256 | private function getPathsSpec( Module $module ): array { |
| 257 | $specs = []; |
| 258 | $usedOpIds = []; |
| 259 | |
| 260 | // XXX: We currently don't support meta-data on OpenAPI path objects |
| 261 | // (summary, description). |
| 262 | |
| 263 | foreach ( $module->getDefinedPaths() as $path => $methods ) { |
| 264 | foreach ( $methods as $mth ) { |
| 265 | $key = strtolower( $mth ); |
| 266 | $mth = strtoupper( $mth ); |
| 267 | $specs[ $path ][ $key ] = $this->getRouteSpec( $module, $path, $mth, $usedOpIds ); |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | return $specs; |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * Build the OpenAPI operation object for a single route. |
| 276 | * |
| 277 | * Operation IDs are arbitrary opaque strings required to be unique within |
| 278 | * this spec, but they carry no meaning outside it and need not be unique |
| 279 | * across different OpenAPI specs generated by other modules or wikis. |
| 280 | * |
| 281 | * @param Module $module |
| 282 | * @param string $path Route path, e.g. "/v1/page/{title}" |
| 283 | * @param string $method HTTP method (case-insensitive) |
| 284 | * @param array &$usedOpIds Operation IDs already assigned in this spec, |
| 285 | * updated in-place to include the ID assigned here |
| 286 | * @return array OpenAPI operation object |
| 287 | */ |
| 288 | private function getRouteSpec( Module $module, string $path, string $method, array &$usedOpIds ): array { |
| 289 | $request = new RequestData( [ 'method' => $method ] ); |
| 290 | $handler = $module->getHandlerForPath( $path, $request, false ); |
| 291 | |
| 292 | $operationSpec = $handler->getOpenApiSpec( $method ); |
| 293 | |
| 294 | $operationSpec['security'] = $this->getOpenApiSecurityRequirements( $handler ); |
| 295 | |
| 296 | // If the spec already contains an explicit operationId (e.g. set in the JSON |
| 297 | // definition file via $oasKeys), respect it. Otherwise auto-generate one. |
| 298 | if ( !isset( $operationSpec['operationId'] ) ) { |
| 299 | $baseId = self::generateOperationId( |
| 300 | $method, |
| 301 | $operationSpec['summary'] ?? null, |
| 302 | $path |
| 303 | ); |
| 304 | $operationId = $baseId; |
| 305 | $counter = 2; |
| 306 | while ( in_array( $operationId, $usedOpIds, true ) ) { |
| 307 | $operationId = $baseId . $counter; |
| 308 | $counter++; |
| 309 | } |
| 310 | $operationSpec['operationId'] = $operationId; |
| 311 | } |
| 312 | |
| 313 | $usedOpIds[] = $operationSpec['operationId']; |
| 314 | |
| 315 | return $operationSpec; |
| 316 | } |
| 317 | |
| 318 | /** |
| 319 | * Build the OpenAPI security requirements array for a route's handler. |
| 320 | * |
| 321 | * Each session provider contributes a single requirement object grouping all of |
| 322 | * its schemes together (logical AND); distinct providers are emitted as separate |
| 323 | * objects (logical OR). Routes that do not require write access additionally allow |
| 324 | * unauthenticated access, represented by a leading empty requirement object ({}). |
| 325 | * |
| 326 | * needsWriteAccess() is used as a heuristic for whether anonymous access is allowed, |
| 327 | * since it is the only signal Handler exposes today. It is not a guarantee: some |
| 328 | * handlers that don't need write access still reject anonymous requests internally |
| 329 | * (e.g. ReadingLists' ListsHandler-derived endpoints, which require a logged-in user |
| 330 | * even though they are read-only). Such routes will be spec'd as allowing anonymous |
| 331 | * access even though the handler will reject anonymous requests at runtime. |
| 332 | * |
| 333 | * Grouping relies on the "{providerBaseName}-{subKey}" naming convention produced by |
| 334 | * SessionManager::getAllOpenApiSecuritySchemes(), where the provider's sanitized class |
| 335 | * name forms the base and the per-scheme suffix (subKey) contains no hyphen. The last |
| 336 | * hyphen therefore separates the provider base name from the suffix. |
| 337 | * |
| 338 | * @see https://spec.openapis.org/oas/v3.0.0#security-requirement-object |
| 339 | * @param Handler $handler |
| 340 | * @return array<int, array|\stdClass> OpenAPI security requirement objects |
| 341 | */ |
| 342 | private function getOpenApiSecurityRequirements( Handler $handler ): array { |
| 343 | $requirements = []; |
| 344 | |
| 345 | // Read-only endpoints are assumed to permit anonymous access. This is only a |
| 346 | // heuristic; see the note above for known exceptions. |
| 347 | if ( !$handler->needsWriteAccess() ) { |
| 348 | $requirements[] = (object)[]; // Represents {} in JSON. |
| 349 | } |
| 350 | |
| 351 | // Group schemes by provider (AND within a provider, OR across providers). |
| 352 | $providerGroups = []; |
| 353 | foreach ( $this->sessionManager->getAllOpenApiSecuritySchemes() as $schemeName => $_ ) { |
| 354 | $lastDash = strrpos( $schemeName, '-' ); |
| 355 | $baseName = $lastDash !== false ? substr( $schemeName, 0, $lastDash ) : $schemeName; |
| 356 | $providerGroups[$baseName][$schemeName] = []; |
| 357 | } |
| 358 | |
| 359 | foreach ( $providerGroups as $groupSchemes ) { |
| 360 | $requirements[] = $groupSchemes; |
| 361 | } |
| 362 | |
| 363 | return $requirements; |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * Generate an operationId for an operation. |
| 368 | * |
| 369 | * Uses the summary when available (more readable), falls back to the path. |
| 370 | * |
| 371 | * @param string $method HTTP method (case-insensitive; will be normalized to lowercase) |
| 372 | * @param string|null $summary Localized summary, or null/empty if absent |
| 373 | * @param string $path Route path, e.g. "/v1/page/{title}" |
| 374 | * @return string camelCase operationId |
| 375 | */ |
| 376 | private static function generateOperationId( |
| 377 | string $method, |
| 378 | ?string $summary, |
| 379 | string $path |
| 380 | ): string { |
| 381 | if ( $summary !== null && trim( $summary ) !== '' ) { |
| 382 | return self::summaryToOperationId( $method, $summary ); |
| 383 | } |
| 384 | return self::pathToOperationId( $method, $path ); |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * Derive an operationId from the HTTP method and operation summary. |
| 389 | * |
| 390 | * Converts the summary to camelCase and prepends the HTTP method in lowercase. |
| 391 | * Example: method=GET, summary="Search pages" → "getSearchPages" |
| 392 | * |
| 393 | * @param string $method HTTP method (case-insensitive) |
| 394 | * @param string $summary The operation summary string |
| 395 | * @return string camelCase operationId |
| 396 | */ |
| 397 | private static function summaryToOperationId( string $method, string $summary ): string { |
| 398 | // Replace any non-alphanumeric character with a space, then split into words. |
| 399 | $clean = preg_replace( '/[^a-zA-Z0-9]/', ' ', $summary ); |
| 400 | $words = preg_split( '/\s+/', trim( $clean ), -1, PREG_SPLIT_NO_EMPTY ); |
| 401 | $id = strtolower( $method ); |
| 402 | foreach ( $words as $word ) { |
| 403 | $id .= ucfirst( strtolower( $word ) ); |
| 404 | } |
| 405 | return $id; |
| 406 | } |
| 407 | |
| 408 | /** |
| 409 | * Derive an operationId from the HTTP method and route path. |
| 410 | * Used as a fallback when no summary is available. |
| 411 | * |
| 412 | * Path parameters ({name}) become "ByName". Hyphens, underscores and other |
| 413 | * non-alphanumeric characters act as word separators. |
| 414 | * Example: method=GET, path="/v1/page/{title}/links" → "getV1PageByTitleLinks" |
| 415 | * |
| 416 | * @param string $method HTTP method (case-insensitive) |
| 417 | * @param string $path Route path, e.g. "/v1/page/{title}/links" |
| 418 | * @return string camelCase operationId |
| 419 | */ |
| 420 | private static function pathToOperationId( string $method, string $path ): string { |
| 421 | $segments = explode( '/', trim( $path, '/' ) ); |
| 422 | $id = strtolower( $method ); |
| 423 | foreach ( $segments as $segment ) { |
| 424 | if ( $segment === '' ) { |
| 425 | continue; |
| 426 | } |
| 427 | // Convert {paramName} to "ByParamname" |
| 428 | if ( preg_match( '/^\{(.+)\}$/', $segment, $matches ) ) { |
| 429 | $id .= 'By' . ucfirst( strtolower( $matches[1] ) ); |
| 430 | } else { |
| 431 | // Split on any non-alphanumeric char and ucfirst each word |
| 432 | $clean = preg_replace( '/[^a-zA-Z0-9]/', ' ', $segment ); |
| 433 | $words = preg_split( '/\s+/', trim( $clean ), -1, PREG_SPLIT_NO_EMPTY ); |
| 434 | foreach ( $words as $word ) { |
| 435 | $id .= ucfirst( strtolower( $word ) ); |
| 436 | } |
| 437 | } |
| 438 | } |
| 439 | return $id; |
| 440 | } |
| 441 | |
| 442 | private function getComponentsSpec(): array { |
| 443 | $components = []; |
| 444 | |
| 445 | // Resolve x-i18n-message references |
| 446 | $resolvedComponents = $this->getJsonLocalizer()->localizeJson( |
| 447 | ResponseFactory::getResponseComponents() |
| 448 | ); |
| 449 | |
| 450 | // XXX: also collect reusable components from handler specs (but how to avoid name collisions?). |
| 451 | $componentsSources = [ |
| 452 | [ 'schemas' => Validator::getParameterTypeSchemas() ], |
| 453 | $resolvedComponents |
| 454 | ]; |
| 455 | |
| 456 | // 2D merge |
| 457 | foreach ( $componentsSources as $cmps ) { |
| 458 | foreach ( $cmps as $name => $cmp ) { |
| 459 | $components[$name] = array_merge( $components[$name] ?? [], $cmp ); |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | // Security schemes are declared by the installed session providers. Resolve any |
| 464 | // localizable descriptions (MessageValue); plain-string descriptions, e.g. from |
| 465 | // third-party providers, are emitted as-is. The key is omitted when no provider |
| 466 | // declares schemes, to avoid an empty securitySchemes object. |
| 467 | $securitySchemes = $this->sessionManager->getAllOpenApiSecuritySchemes(); |
| 468 | foreach ( $securitySchemes as &$scheme ) { |
| 469 | if ( isset( $scheme['description'] ) && $scheme['description'] instanceof MessageValue ) { |
| 470 | $scheme['description'] = $this->getJsonLocalizer()->getFormattedMessage( $scheme['description'] ); |
| 471 | } |
| 472 | } |
| 473 | unset( $scheme ); |
| 474 | if ( $securitySchemes ) { |
| 475 | $components['securitySchemes'] = $securitySchemes; |
| 476 | } |
| 477 | |
| 478 | return $components; |
| 479 | } |
| 480 | |
| 481 | protected function getResponseBodySchemaFileName( string $method ): ?string { |
| 482 | return __DIR__ . '/Schema/ModuleSpec.json'; |
| 483 | } |
| 484 | |
| 485 | /** @inheritDoc */ |
| 486 | public function needsWriteAccess() { |
| 487 | return false; |
| 488 | } |
| 489 | |
| 490 | /** @inheritDoc */ |
| 491 | public function getParamSettings() { |
| 492 | return [ |
| 493 | 'module' => [ |
| 494 | self::PARAM_SOURCE => 'path', |
| 495 | ParamValidator::PARAM_TYPE => 'string', |
| 496 | ParamValidator::PARAM_REQUIRED => true, |
| 497 | Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-module-spec-module' ), |
| 498 | ], |
| 499 | 'version' => [ |
| 500 | self::PARAM_SOURCE => 'path', |
| 501 | ParamValidator::PARAM_TYPE => 'string', |
| 502 | ParamValidator::PARAM_DEFAULT => '', |
| 503 | Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-module-spec-version' ), |
| 504 | ], |
| 505 | ]; |
| 506 | } |
| 507 | |
| 508 | } |