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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
|
<?php
namespace MediaWiki\Rest\Module;
use AppendIterator;
use ArrayIterator;
use Iterator;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
use MediaWiki\Rest\Handler\RedirectHandler;
use MediaWiki\Rest\JsonLocalizer;
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 flat route definitions in the form originally
* introduced in MW 1.35. This module acts as a "catch all" since it doesn't
* use a module prefix. So it handles all routes that do not explicitly belong
* to a module.
*
* This module responds to requests by matching the requested path against a
* list of known routes to identify the appropriate handler.
* The routes are loaded from the route definition files or in extension.json
* files using the RestRoutes key.
*
* Flat files just contain a list (a JSON array) or route definitions (see below).
* Annotated route definition files contain a map (a JSON object) with the
* following fields:
* - "module": the module name (string). The router uses this name to find the
* correct module for handling a request by matching it against the prefix
* of the request path. The module name must be unique.
* - "routes": a list (JSON array) or route definitions (see below).
*
* Each route definition maps a path pattern to a handler class. It is given as
* a map (JSON object) with the following fields:
* - "path": the path pattern (string) relative to the module prefix. Required.
* The path may contain placeholders for path parameters.
* - "method": the HTTP method(s) or "verbs" supported by the route. If not given,
* it is assumed that the route supports the "GET" method. The "OPTIONS" method
* for CORS is supported implicitly.
* - "class" or "factory": The handler class (string) or factory function
* (callable) of an "object spec" for use with ObjectFactory::createObject.
* See there for the usage of additional fields like "services". If a shorthand
* is used (see below), no object spec is needed.
*
* 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 optionally the redirect "code".
*
* 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 ExtraRoutesModule extends MatcherBasedModule {
/** @var string[] */
private array $routeFiles;
/**
* @var array<int,array> A list of route definitions
*/
private array $extraRoutes;
/**
* @var array<int,array>|null A list of route definitions loaded from
* the files specified by $routeFiles
*/
private ?array $routesFromFiles = null;
/** @var int[]|null */
private ?array $routeFileTimestamps = null;
private ?string $configHash = null;
/**
* @param string[] $routeFiles List of names of JSON files containing routes
* See the documentation of this class for a description of the file
* format.
* @param array<int,array> $extraRoutes Extension route array. The content of
* this array must be a list of route definitions. See the documentation
* of this class for a description of the expected structure.
*/
public function __construct(
array $routeFiles,
array $extraRoutes,
Router $router,
ResponseFactory $responseFactory,
BasicAuthorizerInterface $basicAuth,
ObjectFactory $objectFactory,
Validator $restValidator,
ErrorReporter $errorReporter,
HookContainer $hookContainer
) {
parent::__construct(
$router,
'',
$responseFactory,
$basicAuth,
$objectFactory,
$restValidator,
$errorReporter,
$hookContainer
);
$this->routeFiles = $routeFiles;
$this->extraRoutes = $extraRoutes;
}
/**
* Get a config version hash for cache invalidation
*/
protected function getConfigHash(): string {
if ( $this->configHash === null ) {
$this->configHash = md5( json_encode( [
'class' => __CLASS__,
'version' => 1,
'extraRoutes' => $this->extraRoutes,
'fileTimestamps' => $this->getRouteFileTimestamps()
] ) );
}
return $this->configHash;
}
/**
* Load the defined JSON files and return the merged routes.
*
* @return array<int,array> A list of route definitions. See this class's
* documentation for a description of the format of route definitions.
* @throws ModuleConfigurationException If a route file cannot be loaded or processed.
*/
private function getRoutesFromFiles(): array {
if ( $this->routesFromFiles !== null ) {
return $this->routesFromFiles;
}
$this->routesFromFiles = [];
$this->routeFileTimestamps = [];
foreach ( $this->routeFiles as $fileName ) {
$this->routeFileTimestamps[$fileName] = filemtime( $fileName );
$routes = $this->loadJsonFile( $fileName );
$this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
}
return $this->routesFromFiles;
}
/**
* Get an array of last modification times of the defined route files.
*
* @return int[] Last modification times
*/
private function getRouteFileTimestamps(): array {
if ( $this->routeFileTimestamps === null ) {
$this->routeFileTimestamps = [];
foreach ( $this->routeFiles as $fileName ) {
$this->routeFileTimestamps[$fileName] = filemtime( $fileName );
}
}
return $this->routeFileTimestamps;
}
/**
* @return array[]
*/
public function getDefinedPaths(): array {
$paths = [];
foreach ( $this->getAllRoutes() as $spec ) {
$key = $spec['path'];
$methods = isset( $spec['method'] ) ? (array)$spec['method'] : [ 'GET' ];
$paths[$key] = array_merge( $paths[$key] ?? [], $methods );
}
return $paths;
}
/**
* @return Iterator<array>
*/
private function getAllRoutes() {
$iterator = new AppendIterator;
$iterator->append( new ArrayIterator( $this->getRoutesFromFiles() ) );
$iterator->append( new ArrayIterator( $this->extraRoutes ) );
return $iterator;
}
protected function initRoutes(): void {
$routeDefs = $this->getAllRoutes();
foreach ( $routeDefs as $route ) {
if ( !isset( $route['path'] ) ) {
throw new RouteDefinitionException( 'Missing path' );
}
$path = $route['path'];
$method = $route['method'] ?? 'GET';
$info = $this->makeRouteInfo( $route );
$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( array $route ): array {
static $objectSpecKeys = [
'class',
'factory',
'services',
'optional_services',
'args',
];
if ( isset( $route['redirect'] ) ) {
// Redirect shorthand
$info = [
'spec' => [ 'class' => RedirectHandler::class ],
'config' => $route,
];
} else {
// Object spec at the top level
$info = [
'spec' => array_intersect_key( $route, array_flip( $objectSpecKeys ) ),
'config' => array_diff_key( $route, array_flip( $objectSpecKeys ) ),
];
}
$info['path'] = $route['path'];
return $info;
}
public function getOpenApiInfo() {
// Note that mwapi-1.0 is based on OAS 3.0, so it doesn't support the
// "summary" property introduced in 3.1.
$localizer = new JsonLocalizer( $this->responseFactory );
return [
'title' => $localizer->getFormattedMessage( 'rest-module-extra-routes-title' ),
'description' => $localizer->getFormattedMessage( 'rest-module-extra-routes-desc' ),
'version' => 'undefined',
];
}
}
|