Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
TemplateStylesMatcherFactory
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
4 / 4
12
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%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 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
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    /** @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}