Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
TemplateStylesMatcherFactory
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
6 / 6
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkUrl
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
 urlstring
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 url
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 clearFileNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\TemplateStyles;
4
5/**
6 * @file
7 * @license GPL-2.0-or-later
8 */
9
10use Wikimedia\CSS\Grammar\TokenMatcher;
11use Wikimedia\CSS\Grammar\UrlMatcher;
12use Wikimedia\CSS\Objects\Token;
13
14/**
15 * Extend the standard factory for TemplateStyles-specific matchers
16 */
17class TemplateStylesMatcherFactory extends \Wikimedia\CSS\Grammar\MatcherFactory {
18
19    private array $fileNames = [];
20
21    /**
22     * @param array<string,string[]> $allowedDomains See $wgTemplateStylesAllowedUrls
23     */
24    public function __construct(
25        private readonly array $allowedDomains,
26    ) {
27    }
28
29    /**
30     * Check a URL for safety
31     * @param string $type
32     * @param string $url
33     * @return bool
34     */
35    protected function checkUrl( $type, $url ) {
36        // Undo unnecessary percent encoding
37        $url = preg_replace_callback( '/%[2-7][0-9A-Fa-f]/', static function ( $m ) {
38            $char = urldecode( $m[0] );
39            /** @phan-suppress-next-line PhanParamSuspiciousOrder */
40            return str_contains( '"#%<>[\]^`{|}/?&=+;', $char ) ? $m[0] : $char;
41        }, $url );
42
43        // Don't allow unescaped \ or /../ in the non-query part of the URL
44        $tmp = preg_replace( '<[#?].*$>', '', $url );
45        if ( str_contains( $tmp, '\\' ) || preg_match( '<(?:^|/|%2[fF])\.+(?:/|%2[fF]|$)>', $tmp ) ) {
46            return false;
47        }
48
49        // Check if it is allowed
50        $regexes = $this->allowedDomains[$type] ?? [];
51        foreach ( $regexes as $regex ) {
52            $m = [];
53            if ( preg_match( $regex, $url, $m ) ) {
54                if ( isset( $m['filename'] ) && $m['filename'] !== '' ) {
55                    $this->fileNames[] = rawurldecode( $m['filename'] );
56                }
57                return true;
58            }
59        }
60
61        return false;
62    }
63
64    /**
65     * @inheritDoc
66     */
67    public function urlstring( $type ) {
68        $key = __METHOD__ . ':' . $type;
69        if ( !isset( $this->cache[$key] ) ) {
70            $this->cache[$key] = new TokenMatcher( Token::T_STRING, function ( Token $t ) use ( $type ) {
71                return $this->checkUrl( $type, $t->value() );
72            } );
73        }
74        return $this->cache[$key];
75    }
76
77    /**
78     * @inheritDoc
79     */
80    public function url( $type ) {
81        $key = __METHOD__ . ':' . $type;
82        if ( !isset( $this->cache[$key] ) ) {
83            $this->cache[$key] = new UrlMatcher( function ( $url, $modifiers ) use ( $type ) {
84                return !$modifiers && $this->checkUrl( $type, $url );
85            } );
86        }
87        return $this->cache[$key];
88    }
89
90    /**
91     * Clear list of captured file names from urls
92     */
93    public function clearFileNames() {
94        $this->fileNames = [];
95    }
96
97    /**
98     * Get a list of filenames captured from used URLs
99     *
100     * This corresponds to filename named group in the URL regex
101     * @return array
102     */
103    public function getFileNames() {
104        return $this->fileNames;
105    }
106}