Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
25 / 25 |
TemplateStylesMatcherFactory | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
12 | |
100.00% |
25 / 25 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
checkUrl | |
100.00% |
1 / 1 |
6 | |
100.00% |
13 / 13 |
|||
urlstring | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
url | |
100.00% |
1 / 1 |
3 | |
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]; | |
} | |
} |