Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.87% covered (success)
97.87%
46 / 47
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MatcherBasedModule
97.87% covered (success)
97.87%
46 / 47
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
88.89% covered (warning)
88.89%
8 / 9
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 valies 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            $this->matchers[$method]->add( $path, $info );
101        }
102    }
103
104    /**
105     * @inheritDoc
106     */
107    public function findHandlerMatch(
108        string $path,
109        string $requestMethod
110    ): array {
111        $requestMethod = strtoupper( $requestMethod );
112
113        $matchers = $this->getMatchers();
114        $matcher = $matchers[$requestMethod] ?? null;
115        $match = $matcher ? $matcher->match( $path ) : null;
116
117        if ( !$match ) {
118            // Return allowed methods, to support CORS and 405 responses.
119            return [
120                'found' => false,
121                'methods' => $this->getAllowedMethods( $path ),
122            ];
123        } else {
124            $info = $match['userData'];
125            $info['found'] = true;
126            $info['method'] = $requestMethod;
127            $info['params'] = $match['params'] ?? [];
128
129            return $info;
130        }
131    }
132
133    /**
134     * Get the allowed methods for a path.
135     * Useful to check for 405 wrong method.
136     *
137     * @param string $relPath A concrete request path.
138     * @return string[]
139     */
140    public function getAllowedMethods( string $relPath ): array {
141        $allowed = [];
142        foreach ( $this->getMatchers() as $allowedMethod => $allowedMatcher ) {
143            if ( $allowedMatcher->match( $relPath ) ) {
144                $allowed[] = $allowedMethod;
145            }
146        }
147
148        return array_unique(
149            in_array( 'GET', $allowed ) ? array_merge( [ 'HEAD' ], $allowed ) : $allowed
150        );
151    }
152
153}