Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
27 / 27 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
TemplateStylesMatcherFactory | |
100.00% |
27 / 27 |
|
100.00% |
4 / 4 |
12 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkUrl | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
urlstring | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
url | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\TemplateStyles; |
4 | |
5 | /** |
6 | * @file |
7 | * @license GPL-2.0-or-later |
8 | */ |
9 | |
10 | use Wikimedia\CSS\Grammar\TokenMatcher; |
11 | use Wikimedia\CSS\Grammar\UrlMatcher; |
12 | use Wikimedia\CSS\Objects\Token; |
13 | |
14 | /** |
15 | * Extend the standard factory for TemplateStyles-specific matchers |
16 | */ |
17 | class TemplateStylesMatcherFactory extends \Wikimedia\CSS\Grammar\MatcherFactory { |
18 | |
19 | /** @var array URL validation regexes */ |
20 | protected $allowedDomains; |
21 | |
22 | /** |
23 | * @param array $allowedDomains See $wgTemplateStylesAllowedUrls |
24 | */ |
25 | public function __construct( array $allowedDomains ) { |
26 | $this->allowedDomains = $allowedDomains; |
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 | if ( strpos( '"#%<>[\]^`{|}/?&=+;', $char ) === false ) { |
41 | # Unescape it |
42 | return $char; |
43 | } |
44 | return $m[0]; |
45 | }, $url ); |
46 | |
47 | // Don't allow unescaped \ or /../ in the non-query part of the URL |
48 | $tmp = preg_replace( '<[#?].*$>', '', $url ); |
49 | if ( str_contains( $tmp, '\\' ) || preg_match( '<(?:^|/|%2[fF])\.+(?:/|%2[fF]|$)>', $tmp ) ) { |
50 | return false; |
51 | } |
52 | |
53 | // Check if it is allowed |
54 | $regexes = $this->allowedDomains[$type] ?? []; |
55 | foreach ( $regexes as $regex ) { |
56 | if ( preg_match( $regex, $url ) ) { |
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 | } |