Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
78.38% |
58 / 74 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
ExtraRoutesModule | |
78.38% |
58 / 74 |
|
44.44% |
4 / 9 |
22.65 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getConfigHash | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getRoutesFromFiles | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
getRouteFileTimestamps | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
4.94 | |||
getDefinedPaths | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getAllRoutes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
initRoutes | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
makeRouteInfo | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
2 | |||
getOpenApiInfo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Module; |
4 | |
5 | use AppendIterator; |
6 | use ArrayIterator; |
7 | use Iterator; |
8 | use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface; |
9 | use MediaWiki\Rest\Handler\RedirectHandler; |
10 | use MediaWiki\Rest\PathTemplateMatcher\ModuleConfigurationException; |
11 | use MediaWiki\Rest\Reporter\ErrorReporter; |
12 | use MediaWiki\Rest\ResponseFactory; |
13 | use MediaWiki\Rest\RouteDefinitionException; |
14 | use MediaWiki\Rest\Router; |
15 | use MediaWiki\Rest\Validator\Validator; |
16 | use Wikimedia\ObjectFactory\ObjectFactory; |
17 | |
18 | /** |
19 | * A Module that is based on flat route definitions in the form originally |
20 | * introduced in MW 1.35. This module acts as a "catch all" since it doesn't |
21 | * use a module prefix. So it handles all routes that do not explicitly belong |
22 | * to a module. |
23 | * |
24 | * This module responds to requests by matching the requested path against a |
25 | * list of known routes to identify the appropriate handler. |
26 | * The routes are loaded from the route definition files or in extension.json |
27 | * files using the RestRoutes key. |
28 | * |
29 | * Flat files just contain a list (a JSON array) or route definitions (see below). |
30 | * Annotated route definition files contain a map (a JSON object) with the |
31 | * following fields: |
32 | * - "module": the module name (string). The router uses this name to find the |
33 | * correct module for handling a request by matching it against the prefix |
34 | * of the request path. The module name must be unique. |
35 | * - "routes": a list (JSON array) or route definitions (see below). |
36 | * |
37 | * Each route definition maps a path pattern to a handler class. It is given as |
38 | * a map (JSON object) with the following fields: |
39 | * - "path": the path pattern (string) relative to the module prefix. Required. |
40 | * The path may contain placeholders for path parameters. |
41 | * - "method": the HTTP method(s) or "verbs" supported by the route. If not given, |
42 | * it is assumed that the route supports the "GET" method. The "OPTIONS" method |
43 | * for CORS is supported implicitly. |
44 | * - "class" or "factory": The handler class (string) or factory function |
45 | * (callable) of an "object spec" for use with ObjectFactory::createObject. |
46 | * See there for the usage of additional fields like "services". If a shorthand |
47 | * is used (see below), no object spec is needed. |
48 | * |
49 | * The following fields are supported as a shorthand notation: |
50 | * - "redirect": the route represents a redirect and will be handled by |
51 | * the RedirectHandler class. The redirect is specified as a JSON object |
52 | * that specifies the target "path", and optionally the redirect "code". |
53 | * |
54 | * More shorthands may be added in the future. |
55 | * |
56 | * Route definitions can contain additional fields to configure the handler. |
57 | * The handler can access the route definition by calling getConfig(). |
58 | * |
59 | * @internal |
60 | * @since 1.43 |
61 | */ |
62 | class ExtraRoutesModule extends MatcherBasedModule { |
63 | |
64 | /** @var string[] */ |
65 | private array $routeFiles; |
66 | |
67 | /** |
68 | * @var array<int,array> A list of route definitions |
69 | */ |
70 | private array $extraRoutes; |
71 | |
72 | /** |
73 | * @var array<int,array>|null A list of route definitions loaded from |
74 | * the files specified by $routeFiles |
75 | */ |
76 | private ?array $routesFromFiles = null; |
77 | |
78 | /** @var int[]|null */ |
79 | private ?array $routeFileTimestamps = null; |
80 | |
81 | /** @var string|null */ |
82 | private ?string $configHash = null; |
83 | |
84 | /** |
85 | * @param string[] $routeFiles List of names of JSON files containing routes |
86 | * See the documentation of this class for a description of the file |
87 | * format. |
88 | * @param array<int,array> $extraRoutes Extension route array. The content of |
89 | * this array must be a list of route definitions. See the documentation |
90 | * of this class for a description of the expected structure. |
91 | */ |
92 | public function __construct( |
93 | array $routeFiles, |
94 | array $extraRoutes, |
95 | Router $router, |
96 | ResponseFactory $responseFactory, |
97 | BasicAuthorizerInterface $basicAuth, |
98 | ObjectFactory $objectFactory, |
99 | Validator $restValidator, |
100 | ErrorReporter $errorReporter |
101 | ) { |
102 | parent::__construct( |
103 | $router, |
104 | '', |
105 | $responseFactory, |
106 | $basicAuth, |
107 | $objectFactory, |
108 | $restValidator, |
109 | $errorReporter |
110 | ); |
111 | $this->routeFiles = $routeFiles; |
112 | $this->extraRoutes = $extraRoutes; |
113 | } |
114 | |
115 | /** |
116 | * Get a config version hash for cache invalidation |
117 | * |
118 | * @return string |
119 | */ |
120 | protected function getConfigHash(): string { |
121 | if ( $this->configHash === null ) { |
122 | $this->configHash = md5( json_encode( [ |
123 | 'class' => __CLASS__, |
124 | 'version' => 1, |
125 | 'extraRoutes' => $this->extraRoutes, |
126 | 'fileTimestamps' => $this->getRouteFileTimestamps() |
127 | ] ) ); |
128 | } |
129 | return $this->configHash; |
130 | } |
131 | |
132 | /** |
133 | * Load the defined JSON files and return the merged routes. |
134 | * |
135 | * @return array<int,array> A list of route definitions. See this class's |
136 | * documentation for a description of the format of route definitions. |
137 | * @throws ModuleConfigurationException If a route file cannot be loaded or processed. |
138 | */ |
139 | private function getRoutesFromFiles(): array { |
140 | if ( $this->routesFromFiles !== null ) { |
141 | return $this->routesFromFiles; |
142 | } |
143 | |
144 | $this->routesFromFiles = []; |
145 | $this->routeFileTimestamps = []; |
146 | foreach ( $this->routeFiles as $fileName ) { |
147 | $this->routeFileTimestamps[$fileName] = filemtime( $fileName ); |
148 | |
149 | $routes = $this->loadJsonFile( $fileName ); |
150 | |
151 | $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes ); |
152 | } |
153 | |
154 | return $this->routesFromFiles; |
155 | } |
156 | |
157 | /** |
158 | * Get an array of last modification times of the defined route files. |
159 | * |
160 | * @return int[] Last modification times |
161 | */ |
162 | private function getRouteFileTimestamps(): array { |
163 | if ( $this->routeFileTimestamps === null ) { |
164 | $this->routeFileTimestamps = []; |
165 | foreach ( $this->routeFiles as $fileName ) { |
166 | $this->routeFileTimestamps[$fileName] = filemtime( $fileName ); |
167 | } |
168 | } |
169 | return $this->routeFileTimestamps; |
170 | } |
171 | |
172 | /** |
173 | * @return array[] |
174 | */ |
175 | public function getDefinedPaths(): array { |
176 | $paths = []; |
177 | foreach ( $this->getAllRoutes() as $spec ) { |
178 | $key = $spec['path']; |
179 | |
180 | $methods = isset( $spec['method'] ) ? (array)$spec['method'] : [ 'GET' ]; |
181 | |
182 | $paths[$key] = array_merge( $paths[$key] ?? [], $methods ); |
183 | } |
184 | |
185 | return $paths; |
186 | } |
187 | |
188 | /** |
189 | * @return Iterator<array> |
190 | */ |
191 | private function getAllRoutes() { |
192 | $iterator = new AppendIterator; |
193 | $iterator->append( new ArrayIterator( $this->getRoutesFromFiles() ) ); |
194 | $iterator->append( new ArrayIterator( $this->extraRoutes ) ); |
195 | return $iterator; |
196 | } |
197 | |
198 | protected function initRoutes(): void { |
199 | $routeDefs = $this->getAllRoutes(); |
200 | |
201 | foreach ( $routeDefs as $route ) { |
202 | if ( !isset( $route['path'] ) ) { |
203 | throw new RouteDefinitionException( 'Missing path' ); |
204 | } |
205 | |
206 | $path = $route['path']; |
207 | $method = $route['method'] ?? 'GET'; |
208 | $info = $this->makeRouteInfo( $route ); |
209 | |
210 | $this->addRoute( $method, $path, $info ); |
211 | } |
212 | } |
213 | |
214 | /** |
215 | * Generate a route info array to be stored in the matcher tree, |
216 | * in the form expected by MatcherBasedModule::addRoute() |
217 | * and ultimately Module::getHandlerForPath(). |
218 | */ |
219 | private function makeRouteInfo( array $route ): array { |
220 | static $objectSpecKeys = [ |
221 | 'class', |
222 | 'factory', |
223 | 'services', |
224 | 'optional_services', |
225 | 'args', |
226 | ]; |
227 | |
228 | if ( isset( $route['redirect'] ) ) { |
229 | // Redirect shorthand |
230 | $info = [ |
231 | 'spec' => [ 'class' => RedirectHandler::class ], |
232 | 'config' => $route, |
233 | ]; |
234 | } else { |
235 | // Object spec at the top level |
236 | $info = [ |
237 | 'spec' => array_intersect_key( $route, array_flip( $objectSpecKeys ) ), |
238 | 'config' => array_diff_key( $route, array_flip( $objectSpecKeys ) ), |
239 | ]; |
240 | } |
241 | |
242 | $info['path'] = $route['path']; |
243 | return $info; |
244 | } |
245 | |
246 | public function getOpenApiInfo() { |
247 | // Note that mwapi-1.0 is based on OAS 3.0, so it doesn't support the |
248 | // "summary" property introduced in 3.1. |
249 | return [ |
250 | 'title' => 'Extra Routes', |
251 | 'description' => 'REST endpoints not associated with a module', |
252 | 'version' => 'undefined', |
253 | ]; |
254 | } |
255 | |
256 | } |