35 private $routesFromFiles;
38 private $routeFileTimestamps;
44 private $privateBaseUrl;
59 private $responseFactory;
68 private $objectFactory;
71 private $restValidator;
77 private $errorReporter;
80 private $hookContainer;
92 public const CONSTRUCTOR_OPTIONS = [
121 ObjectFactory $objectFactory,
129 $this->routeFiles = $routeFiles;
130 $this->extraRoutes = $extraRoutes;
134 $this->cacheBag = $cacheBag;
135 $this->responseFactory = $responseFactory;
136 $this->basicAuth = $basicAuth;
137 $this->authority = $authority;
138 $this->objectFactory = $objectFactory;
139 $this->restValidator = $restValidator;
140 $this->errorReporter = $errorReporter;
141 $this->hookContainer = $hookContainer;
142 $this->session = $session;
152 private function fetchCacheData() {
153 $cacheData = $this->cacheBag->get( $this->getCacheKey() );
154 if ( $cacheData && $cacheData[
'CONFIG-HASH'] === $this->getConfigHash() ) {
155 unset( $cacheData[
'CONFIG-HASH'] );
165 private function getCacheKey() {
166 return $this->cacheBag->makeKey( __CLASS__,
'1' );
174 private function getConfigHash() {
175 if ( $this->configHash ===
null ) {
176 $this->configHash = md5( json_encode( [
178 $this->getRouteFileTimestamps()
181 return $this->configHash;
189 private function getRoutesFromFiles() {
190 if ( $this->routesFromFiles ===
null ) {
191 $this->routeFileTimestamps = [];
192 foreach ( $this->routeFiles as $fileName ) {
193 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
194 $routes = json_decode( file_get_contents( $fileName ),
true );
195 if ( $this->routesFromFiles ===
null ) {
196 $this->routesFromFiles = $routes;
198 $this->routesFromFiles = array_merge( $this->routesFromFiles, $routes );
202 return $this->routesFromFiles;
210 private function getRouteFileTimestamps() {
211 if ( $this->routeFileTimestamps ===
null ) {
212 $this->routeFileTimestamps = [];
213 foreach ( $this->routeFiles as $fileName ) {
214 $this->routeFileTimestamps[$fileName] = filemtime( $fileName );
217 return $this->routeFileTimestamps;
226 private function getAllRoutes() {
227 $iterator =
new AppendIterator;
228 $iterator->append(
new \ArrayIterator( $this->getRoutesFromFiles() ) );
229 $iterator->append(
new \ArrayIterator( $this->extraRoutes ) );
238 private function getMatchers() {
239 if ( $this->matchers ===
null ) {
240 $cacheData = $this->fetchCacheData();
243 foreach ( $cacheData as $method => $data ) {
247 foreach ( $this->getAllRoutes() as $spec ) {
248 $methods = $spec[
'method'] ?? [
'GET' ];
249 if ( !is_array( $methods ) ) {
250 $methods = [ $methods ];
252 foreach ( $methods as $method ) {
253 if ( !isset( $matchers[$method] ) ) {
254 $matchers[$method] =
new PathMatcher;
256 $matchers[$method]->
add( $spec[
'path'], $spec );
260 $cacheData = [
'CONFIG-HASH' => $this->getConfigHash() ];
261 foreach ( $matchers as $method => $matcher ) {
262 $cacheData[$method] = $matcher->getCacheData();
264 $this->cacheBag->set( $this->getCacheKey(), $cacheData );
266 $this->matchers = $matchers;
268 return $this->matchers;
278 private function getRelativePath(
$path ) {
279 if ( !str_starts_with(
$path, $this->rootPath ) ) {
282 return substr(
$path, strlen( $this->rootPath ) );
299 array $pathParams = [],
300 array $queryParams = []
303 $url = $this->baseUrl . $this->rootPath . $route;
328 array $pathParams = [],
329 array $queryParams = []
331 $route = $this->substPathParams( $route, $pathParams );
332 $url = $this->privateBaseUrl . $this->rootPath . $route;
343 foreach ( $pathParams as $param => $value ) {
347 $route = str_replace(
'{' . $param .
'}', rawurlencode( (
string)$value ), $route );
361 $relPath = $this->getRelativePath(
$path );
362 if ( $relPath ===
false ) {
363 return $this->responseFactory->createLocalizedHttpError( 404,
365 ->plaintextParams(
$path, $this->rootPath )
370 $matchers = $this->getMatchers();
371 $matcher = $matchers[$requestMethod] ??
null;
372 $match = $matcher ? $matcher->match( $relPath ) :
null;
376 if ( !$match && $requestMethod ===
'HEAD' && isset( $matchers[
'GET'] ) ) {
377 $match = $matchers[
'GET']->match( $relPath );
382 $allowed = $this->getAllowedMethods( $relPath );
386 if ( $this->cors && $requestMethod ===
'OPTIONS' ) {
387 return $this->cors->createPreflightResponse( $allowed );
391 $response = $this->responseFactory->createLocalizedHttpError( 405,
393 ->textParams( $requestMethod )
394 ->commaListParams( $allowed )
395 ->numParams( count( $allowed ) )
397 $response->setHeader(
'Allow', $allowed );
401 return $this->responseFactory->createLocalizedHttpError( 404,
403 ->plaintextParams( $relPath )
411 $request->
setPathParams( array_map(
'rawurldecode', $match[
'params'] ) );
412 $handler = $this->createHandler( $request, $match[
'userData'] );
415 $pathForMetrics = $handler->getPath();
416 $pathForMetrics = strtr( $pathForMetrics,
'{}:',
'-' );
417 $pathForMetrics = strtr( $pathForMetrics,
'/.',
'_' );
419 $statTime = microtime(
true );
421 $response = $this->executeHandler( $handler );
423 $response = $this->responseFactory->createFromException( $e );
424 }
catch ( Throwable $e ) {
425 $this->errorReporter->reportError( $e, $handler, $request );
426 $response = $this->responseFactory->createFromException( $e );
430 if ( $response->getStatusCode() >= 400 ) {
432 $statusCode = $response->getStatusCode();
433 $this->stats->increment(
"rest_api_errors.$pathForMetrics.$requestMethod.$statusCode" );
436 $microtime = ( microtime(
true ) - $statTime ) * 1000;
437 $this->stats->timing(
"rest_api_latency.$pathForMetrics.$requestMethod", $microtime );
449 private function getAllowedMethods(
string $relPath ): array {
452 foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
453 if ( $allowedMatcher->match( $relPath ) ) {
454 $allowed[] = $allowedMethod;
459 in_array(
'GET', $allowed ) ? array_merge( [
'HEAD' ], $allowed ) : $allowed
469 private function createHandler( RequestInterface $request, array $spec ): Handler {
470 $objectFactorySpec = array_intersect_key(
477 'optional_services' =>
true
481 $handler = $this->objectFactory->createObject( $objectFactorySpec );
482 $handler->init( $this, $request, $spec, $this->authority, $this->responseFactory,
483 $this->hookContainer, $this->session
495 private function executeHandler( $handler ): ResponseInterface {
497 $authResult = $this->basicAuth->authorize( $handler->
getRequest(), $handler );
499 return $this->responseFactory->createHttpError( 403, [
'error' => $authResult ] );
503 $handler->checkSession();
506 $handler->validate( $this->restValidator );
509 $earlyResponse = $handler->checkPreconditions();
510 if ( $earlyResponse ) {
511 return $earlyResponse;
515 $response = $handler->execute();
516 if ( !( $response instanceof ResponseInterface ) ) {
517 $response = $this->responseFactory->createFromReturnValue( $response );
521 $handler->applyConditionalResponseHeaders( $response );
523 $handler->applyCacheControl( $response );
543 public function setStats( StatsdDataFactoryInterface $stats ): self {
544 $this->stats = $stats;