33 private $routesFromFiles;
36 private $routeFileTimestamps;
42 private $privateBaseUrl;
57 private $responseFactory;
66 private $objectFactory;
69 private $restValidator;
75 private $errorReporter;
78 private $hookContainer;
87 public const CONSTRUCTOR_OPTIONS = [
116 ObjectFactory $objectFactory,
124 $this->routeFiles = $routeFiles;
125 $this->extraRoutes = $extraRoutes;
129 $this->cacheBag = $cacheBag;
130 $this->responseFactory = $responseFactory;
131 $this->basicAuth = $basicAuth;
132 $this->authority = $authority;
133 $this->objectFactory = $objectFactory;
134 $this->restValidator = $restValidator;
135 $this->errorReporter = $errorReporter;
136 $this->hookContainer = $hookContainer;
137 $this->session = $session;
145 private function fetchCacheData() {
146 $cacheData = $this->cacheBag->
get( $this->getCacheKey() );
147 if ( $cacheData && $cacheData[
'CONFIG-HASH'] === $this->getConfigHash() ) {
148 unset( $cacheData[
'CONFIG-HASH'] );
158 private function getCacheKey() {
159 return $this->cacheBag->makeKey( __CLASS__,
'1' );
167 private function getConfigHash() {
168 if ( $this->configHash ===
null ) {
169 $this->configHash = md5( json_encode( [
171 $this->getRouteFileTimestamps()
174 return $this->configHash;
182 private function getRoutesFromFiles() {
183 if ( $this->routesFromFiles ===
null ) {
184 $this->routeFileTimestamps = [];
185 foreach ( $this->routeFiles as $fileName ) {
186 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
187 $routes = json_decode( file_get_contents( $fileName ),
true );
188 if ( $this->routesFromFiles ===
null ) {
189 $this->routesFromFiles = $routes;
191 $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
195 return $this->routesFromFiles;
203 private function getRouteFileTimestamps() {
204 if ( $this->routeFileTimestamps ===
null ) {
205 $this->routeFileTimestamps = [];
206 foreach ( $this->routeFiles as $fileName ) {
207 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
210 return $this->routeFileTimestamps;
219 private function getAllRoutes() {
220 $iterator =
new AppendIterator;
221 $iterator->append(
new \ArrayIterator( $this->getRoutesFromFiles() ) );
222 $iterator->append(
new \ArrayIterator( $this->extraRoutes ) );
231 private function getMatchers() {
232 if ( $this->matchers ===
null ) {
233 $cacheData = $this->fetchCacheData();
236 foreach ( $cacheData as $method => $data ) {
240 foreach ( $this->getAllRoutes() as $spec ) {
241 $methods = $spec[
'method'] ?? [
'GET' ];
242 if ( !is_array( $methods ) ) {
243 $methods = [ $methods ];
245 foreach ( $methods as $method ) {
246 if ( !isset( $matchers[$method] ) ) {
247 $matchers[$method] =
new PathMatcher;
249 $matchers[$method]->
add( $spec[
'path'], $spec );
253 $cacheData = [
'CONFIG-HASH' => $this->getConfigHash() ];
254 foreach ( $matchers as $method => $matcher ) {
255 $cacheData[$method] = $matcher->getCacheData();
257 $this->cacheBag->set( $this->getCacheKey(), $cacheData );
259 $this->matchers = $matchers;
261 return $this->matchers;
271 private function getRelativePath(
$path ) {
272 if ( !str_starts_with(
$path, $this->rootPath ) ) {
275 return substr(
$path, strlen( $this->rootPath ) );
292 array $pathParams = [],
293 array $queryParams = []
296 $url = $this->baseUrl . $this->rootPath . $route;
321 array $pathParams = [],
322 array $queryParams = []
324 $route = $this->substPathParams( $route, $pathParams );
325 $url = $this->privateBaseUrl . $this->rootPath . $route;
336 foreach ( $pathParams as $param => $value ) {
340 $route = str_replace(
'{' . $param .
'}', rawurlencode( (
string)$value ), $route );
354 $relPath = $this->getRelativePath(
$path );
355 if ( $relPath ===
false ) {
356 return $this->responseFactory->createLocalizedHttpError( 404,
358 ->plaintextParams(
$path, $this->rootPath )
363 $matchers = $this->getMatchers();
364 $matcher = $matchers[$requestMethod] ??
null;
365 $match = $matcher ? $matcher->match( $relPath ) :
null;
369 if ( !$match && $requestMethod ===
'HEAD' && isset( $matchers[
'GET'] ) ) {
370 $match = $matchers[
'GET']->match( $relPath );
375 $allowed = $this->getAllowedMethods( $relPath );
379 if ( $this->cors && $requestMethod ===
'OPTIONS' ) {
380 return $this->cors->createPreflightResponse( $allowed );
384 $response = $this->responseFactory->createLocalizedHttpError( 405,
386 ->textParams( $requestMethod )
387 ->commaListParams( $allowed )
388 ->numParams( count( $allowed ) )
390 $response->setHeader(
'Allow', $allowed );
394 return $this->responseFactory->createLocalizedHttpError( 404,
396 ->plaintextParams( $relPath )
402 $request->
setPathParams( array_map(
'rawurldecode', $match[
'params'] ) );
403 $handler = $this->createHandler( $request, $match[
'userData'] );
406 return $this->executeHandler( $handler );
408 return $this->responseFactory->createFromException( $e );
409 }
catch ( Throwable $e ) {
410 $this->errorReporter->reportError( $e, $handler, $request );
411 return $this->responseFactory->createFromException( $e );
421 private function getAllowedMethods(
string $relPath ): array {
424 foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
425 if ( $allowedMatcher->match( $relPath ) ) {
426 $allowed[] = $allowedMethod;
431 in_array(
'GET', $allowed ) ? array_merge( [
'HEAD' ], $allowed ) : $allowed
441 private function createHandler( RequestInterface $request, array $spec ): Handler {
442 $objectFactorySpec = array_intersect_key(
449 'optional_services' =>
true
453 $handler = $this->objectFactory->createObject( $objectFactorySpec );
454 $handler->init( $this, $request, $spec, $this->authority, $this->responseFactory,
455 $this->hookContainer, $this->session
467 private function executeHandler( $handler ): ResponseInterface {
469 $authResult = $this->basicAuth->authorize( $handler->getRequest(), $handler );
471 return $this->responseFactory->createHttpError( 403, [
'error' => $authResult ] );
475 $handler->checkSession();
478 $handler->validate( $this->restValidator );
481 $earlyResponse = $handler->checkPreconditions();
482 if ( $earlyResponse ) {
483 return $earlyResponse;
487 $response = $handler->execute();
488 if ( !( $response instanceof ResponseInterface ) ) {
489 $response = $this->responseFactory->createFromReturnValue( $response );
493 $handler->applyConditionalResponseHeaders( $response );