aboutsummaryrefslogtreecommitdiffstats
path: root/includes/Rest/Module/MatcherBasedModule.php
blob: c3c322449f6595d00ea409143a3dc2933a8c1f19 (plain) (blame)
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
<?php

namespace MediaWiki\Rest\Module;

use InvalidArgumentException;
use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;

/**
 * MatcherBasedModules respond to requests by matching the requested path
 * against a list of known routes to identify the appropriate handler.
 *
 * @see Matcher
 *
 * @since 1.43
 */
abstract class MatcherBasedModule extends Module {

	/** @var PathMatcher[] Path matchers by method */
	private ?array $matchers = [];

	private bool $matchersInitialized = false;

	public function getCacheData(): array {
		$cacheData = [];

		foreach ( $this->getMatchers() as $method => $matcher ) {
			$cacheData[$method] = $matcher->getCacheData();
		}

		$cacheData[self::CACHE_CONFIG_HASH_KEY] = $this->getConfigHash();
		return $cacheData;
	}

	public function initFromCacheData( array $cacheData ): bool {
		if ( $cacheData[self::CACHE_CONFIG_HASH_KEY] !== $this->getConfigHash() ) {
			return false;
		}

		unset( $cacheData[self::CACHE_CONFIG_HASH_KEY] );
		$this->matchers = [];

		foreach ( $cacheData as $method => $data ) {
			$this->matchers[$method] = PathMatcher::newFromCache( $data );
		}

		$this->matchersInitialized = true;
		return true;
	}

	/**
	 * Get a config version hash for cache invalidation
	 *
	 * @return string
	 */
	abstract protected function getConfigHash(): string;

	/**
	 * Get an array of PathMatcher objects indexed by HTTP method
	 *
	 * @return PathMatcher[]
	 */
	protected function getMatchers() {
		if ( !$this->matchersInitialized ) {
			$this->initRoutes();
			$this->matchersInitialized = true;
		}

		return $this->matchers;
	}

	/**
	 * Initialize matchers by calling addRoute() for each known route.
	 */
	abstract protected function initRoutes(): void;

	/**
	 * @param string|string[] $method The method(s) the route should be registered for
	 * @param string $path The path pattern for the route
	 * @param array $info Information to be associated with the route. Supported keys:
	 *        - "spec": an object spec for use with ObjectFactory for constructing a Handler object.
	 *        - "config": an array of configuration valies to be passed to Handler::initContext.
	 */
	protected function addRoute( $method, string $path, array $info ) {
		$methods = (array)$method;

		// Make sure the matched path is known.
		if ( !isset( $info['spec'] ) ) {
			throw new InvalidArgumentException( 'Missing key in $info: "spec"' );
		}

		$info['path'] = $path;

		foreach ( $methods as $method ) {
			$method = strtoupper( $method );

			if ( !isset( $this->matchers[$method] ) ) {
				$this->matchers[$method] = new PathMatcher;
			}

			$this->matchers[$method]->add( $path, $info );
		}
	}

	/**
	 * @inheritDoc
	 */
	public function findHandlerMatch(
		string $path,
		string $requestMethod
	): array {
		$requestMethod = strtoupper( $requestMethod );

		$matchers = $this->getMatchers();
		$matcher = $matchers[$requestMethod] ?? null;
		$match = $matcher ? $matcher->match( $path ) : null;

		if ( !$match ) {
			// Return allowed methods, to support CORS and 405 responses.
			return [
				'found' => false,
				'methods' => $this->getAllowedMethods( $path ),
			];
		} else {
			$info = $match['userData'];
			$info['found'] = true;
			$info['method'] = $requestMethod;
			$info['params'] = $match['params'] ?? [];

			return $info;
		}
	}

	/**
	 * Get the allowed methods for a path.
	 * Useful to check for 405 wrong method.
	 *
	 * @param string $relPath A concrete request path.
	 * @return string[]
	 */
	public function getAllowedMethods( string $relPath ): array {
		$allowed = [];
		foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
			if ( $allowedMatcher->match( $relPath ) ) {
				$allowed[] = $allowedMethod;
			}
		}

		return array_unique(
			in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed
		);
	}

}