Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.92% covered (success)
97.92%
47 / 48
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MatcherBasedModule
97.92% covered (success)
97.92%
47 / 48
83.33% covered (warning)
83.33%
5 / 6
18
0.00% covered (danger)
0.00%
0 / 1
 getCacheData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 initFromCacheData
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getConfigHash
n/a
0 / 0
n/a
0 / 0
0
 getMatchers
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 initRoutes
n/a
0 / 0
n/a
0 / 0
0
 addRoute
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 findHandlerMatch
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getAllowedMethods
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace MediaWiki\Rest\Module;
4
5use InvalidArgumentException;
6use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
7
8/**
9 * MatcherBasedModules respond to requests by matching the requested path
10 * against a list of known routes to identify the appropriate handler.
11 *
12 * @see Matcher
13 *
14 * @since 1.43
15 */
16abstract class MatcherBasedModule extends Module {
17
18    /** @var PathMatcher[] Path matchers by method */
19    private ?array $matchers = [];
20
21    private bool $matchersInitialized = false;
22
23    public function getCacheData(): array {
24        $cacheData = [];
25
26        foreach ( $this->getMatchers() as $method => $matcher ) {
27            $cacheData[$method] = $matcher->getCacheData();
28        }
29
30        $cacheData[self::CACHE_CONFIG_HASH_KEY] = $this->getConfigHash();
31        return $cacheData;
32    }
33
34    public function initFromCacheData( array $cacheData ): bool {
35        if ( $cacheData[self::CACHE_CONFIG_HASH_KEY] !== $this->getConfigHash() ) {
36            return false;
37        }
38
39        unset( $cacheData[self::CACHE_CONFIG_HASH_KEY] );
40        $this->matchers = [];
41
42        foreach ( $cacheData as $method => $data ) {
43            $this->matchers[$method] = PathMatcher::newFromCache( $data );
44        }
45
46        $this->matchersInitialized = true;
47        return true;
48    }
49
50    /**
51     * Get a config version hash for cache invalidation
52     *
53     * @return string
54     */
55    abstract protected function getConfigHash(): string;
56
57    /**
58     * Get an array of PathMatcher objects indexed by HTTP method
59     *
60     * @return PathMatcher[]
61     */
62    protected function getMatchers() {
63        if ( !$this->matchersInitialized ) {
64            $this->initRoutes();
65            $this->matchersInitialized = true;
66        }
67
68        return $this->matchers;
69    }
70
71    /**
72     * Initialize matchers by calling addRoute() for each known route.
73     */
74    abstract protected function initRoutes(): void;
75
76    /**
77     * @param string|string[] $method The method(s) the route should be registered for
78     * @param string $path The path pattern for the route
79     * @param array $info Information to be associated with the route. Supported keys:
80     *   - "spec": an object spec for use with ObjectFactory for constructing a Handler object.
81     *   - "config": an array of configuration values to be passed to Handler::initContext.
82     */
83    protected function addRoute( $method, string $path, array $info ) {
84        $methods = (array)$method;
85
86        // Make sure the matched path is known.
87        if ( !isset( $info['spec'] ) ) {
88            throw new InvalidArgumentException( 'Missing key in $info: "spec"' );
89        }
90
91        $info['path'] = $path;
92
93        foreach ( $methods as $method ) {
94            $method = strtoupper( $method );
95
96            if ( !isset( $this->matchers[$method] ) ) {
97                $this->matchers[$method] = new PathMatcher;
98            }
99
100            $info['config']['method'] = $method;
101            $this->matchers[$method]->add( $path, $info );
102        }
103    }
104
105    /**
106     * @inheritDoc
107     */
108    public function findHandlerMatch(
109        string $path,
110        string $requestMethod
111    ): array {
112        $requestMethod = strtoupper( $requestMethod );
113
114        $matchers = $this->getMatchers();
115        $matcher = $matchers[$requestMethod] ?? null;
116        $match = $matcher ? $matcher->match( $path ) : null;
117
118        if ( !$match ) {
119            // Return allowed methods, to support CORS and 405 responses.
120            return [
121                'found' => false,
122                'methods' => $this->getAllowedMethods( $path ),
123            ];
124        } else {
125            $info = $match['userData'];
126            $info['found'] = true;
127            $info['method'] = $requestMethod;
128            $info['params'] = $match['params'] ?? [];
129
130            return $info;
131        }
132    }
133
134    /**
135     * Get the allowed methods for a path.
136     * Useful to check for 405 wrong method.
137     *
138     * @param string $relPath A concrete request path.
139     * @return string[]
140     */
141    public function getAllowedMethods( string $relPath ): array {
142        $allowed = [];
143        foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
144            if ( $allowedMatcher->match( $relPath ) ) {
145                $allowed[] = $allowedMethod;
146            }
147        }
148
149        return array_unique(
150            in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed
151        );
152    }
153
154}