Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
25 / 25
TemplateStylesMatcherFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
12
100.00% covered (success)
100.00%
25 / 25
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 checkUrl
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
13 / 13
 urlstring
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 url
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
<?php
namespace MediaWiki\Extension\TemplateStyles;
/**
 * @file
 * @license GPL-2.0-or-later
 */
use Wikimedia\CSS\Grammar\TokenMatcher;
use Wikimedia\CSS\Grammar\UrlMatcher;
use Wikimedia\CSS\Objects\Token;
/**
 * Extend the standard factory for TemplateStyles-specific matchers
 */
class TemplateStylesMatcherFactory extends \Wikimedia\CSS\Grammar\MatcherFactory {
    /** @var array URL validation regexes */
    protected $allowedDomains;
    /**
     * @param array $allowedDomains See $wgTemplateStylesAllowedUrls
     */
    public function __construct( array $allowedDomains ) {
        $this->allowedDomains = $allowedDomains;
    }
    /**
     * Check a URL for safety
     * @param string $type
     * @param string $url
     * @return bool
     */
    protected function checkUrl( $type, $url ) {
        // Undo unnecessary percent encoding
        $url = preg_replace_callback( '/%[2-7][0-9A-Fa-f]/', static function ( $m ) {
            $char = urldecode( $m[0] );
            /** @phan-suppress-next-line PhanParamSuspiciousOrder */
            if ( strpos( '"#%<>[\]^`{|}/?&=+;', $char ) === false ) {
                # Unescape it
                return $char;
            }
            return $m[0];
        }, $url );
        // Don't allow unescaped \ or /../ in the non-query part of the URL
        $tmp = preg_replace( '<[#?].*$>', '', $url );
        if ( strpos( $tmp, '\\' ) !== false || preg_match( '<(?:^|/|%2[fF])\.+(?:/|%2[fF]|$)>', $tmp ) ) {
            return false;
        }
        // Check if it is allowed
        $regexes = $this->allowedDomains[$type] ?? [];
        foreach ( $regexes as $regex ) {
            if ( preg_match( $regex, $url ) ) {
                return true;
            }
        }
        return false;
    }
    /**
     * @inheritDoc
     */
    public function urlstring( $type ) {
        $key = __METHOD__ . ':' . $type;
        if ( !isset( $this->cache[$key] ) ) {
            $this->cache[$key] = new TokenMatcher( Token::T_STRING, function ( Token $t ) use ( $type ) {
                return $this->checkUrl( $type, $t->value() );
            } );
        }
        return $this->cache[$key];
    }
    /**
     * @inheritDoc
     */
    public function url( $type ) {
        $key = __METHOD__ . ':' . $type;
        if ( !isset( $this->cache[$key] ) ) {
            $this->cache[$key] = new UrlMatcher( function ( $url, $modifiers ) use ( $type ) {
                return !$modifiers && $this->checkUrl( $type, $url );
            } );
        }
        return $this->cache[$key];
    }
}