Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
96 / 96 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
| FontFaceAtRuleSanitizer | |
100.00% |
96 / 96 |
|
100.00% |
4 / 4 |
10 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
1 | |||
| fontMatchData | |
100.00% |
61 / 61 |
|
100.00% |
1 / 1 |
2 | |||
| handlesRule | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| doSanitize | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @file |
| 4 | * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0 |
| 5 | */ |
| 6 | |
| 7 | namespace Wikimedia\CSS\Sanitizer; |
| 8 | |
| 9 | use Wikimedia\CSS\Grammar\Alternative; |
| 10 | use Wikimedia\CSS\Grammar\FunctionMatcher; |
| 11 | use Wikimedia\CSS\Grammar\Juxtaposition; |
| 12 | use Wikimedia\CSS\Grammar\KeywordMatcher; |
| 13 | use Wikimedia\CSS\Grammar\MatcherFactory; |
| 14 | use Wikimedia\CSS\Grammar\Quantifier; |
| 15 | use Wikimedia\CSS\Grammar\TokenMatcher; |
| 16 | use Wikimedia\CSS\Grammar\UnorderedGroup; |
| 17 | use Wikimedia\CSS\Grammar\UrangeMatcher; |
| 18 | use Wikimedia\CSS\Objects\AtRule; |
| 19 | use Wikimedia\CSS\Objects\CSSObject; |
| 20 | use Wikimedia\CSS\Objects\Rule; |
| 21 | use Wikimedia\CSS\Objects\Token; |
| 22 | use Wikimedia\CSS\Util; |
| 23 | |
| 24 | /** |
| 25 | * Sanitizes a CSS \@font-face rule |
| 26 | * @see https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-resources |
| 27 | */ |
| 28 | class FontFaceAtRuleSanitizer extends RuleSanitizer { |
| 29 | |
| 30 | /** @var PropertySanitizer */ |
| 31 | protected $propertySanitizer; |
| 32 | |
| 33 | /** |
| 34 | * @param MatcherFactory $matcherFactory |
| 35 | */ |
| 36 | public function __construct( MatcherFactory $matcherFactory ) { |
| 37 | $matchData = self::fontMatchData( $matcherFactory ); |
| 38 | |
| 39 | $this->propertySanitizer = new PropertySanitizer(); |
| 40 | $this->propertySanitizer->setKnownProperties( [ |
| 41 | 'font-family' => $matchData['familyName'], |
| 42 | 'src' => Quantifier::hash( new Alternative( [ |
| 43 | new Juxtaposition( [ |
| 44 | $matcherFactory->url( 'font' ), |
| 45 | Quantifier::optional( |
| 46 | new FunctionMatcher( 'format', Quantifier::hash( $matcherFactory->string() ) ) |
| 47 | ), |
| 48 | ] ), |
| 49 | new FunctionMatcher( 'local', $matchData['familyName'] ), |
| 50 | ] ) ), |
| 51 | 'font-style' => $matchData['font-style'], |
| 52 | 'font-weight' => new Alternative( [ |
| 53 | new KeywordMatcher( [ 'normal', 'bold' ] ), $matchData['numWeight'] |
| 54 | ] ), |
| 55 | 'font-stretch' => $matchData['font-stretch'], |
| 56 | 'unicode-range' => Quantifier::hash( new UrangeMatcher() ), |
| 57 | 'font-feature-settings' => $matchData['font-feature-settings'], |
| 58 | ] ); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Get some shared data for font declaration matchers |
| 63 | * @param MatcherFactory $matcherFactory |
| 64 | * @return array |
| 65 | */ |
| 66 | public static function fontMatchData( MatcherFactory $matcherFactory ) { |
| 67 | $ret = [ |
| 68 | 'familyName' => new Alternative( [ |
| 69 | $matcherFactory->string(), |
| 70 | Quantifier::plus( $matcherFactory->ident() ), |
| 71 | ] ), |
| 72 | 'numWeight' => new TokenMatcher( Token::T_NUMBER, static function ( Token $t ) { |
| 73 | return $t->typeFlag() === 'integer' && preg_match( '/^[1-9]00$/', $t->representation() ); |
| 74 | } ), |
| 75 | 'font-style' => new KeywordMatcher( [ 'normal', 'italic', 'oblique' ] ), |
| 76 | 'font-stretch' => new KeywordMatcher( [ |
| 77 | 'normal', 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', |
| 78 | 'expanded', 'extra-expanded', 'ultra-expanded' |
| 79 | ] ), |
| 80 | 'font-feature-settings' => new Alternative( [ |
| 81 | new KeywordMatcher( 'normal' ), |
| 82 | Quantifier::hash( new Juxtaposition( [ |
| 83 | new TokenMatcher( Token::T_STRING, static function ( Token $t ) { |
| 84 | return preg_match( '/^[\x20-\x7e]{4}$/', $t->value() ); |
| 85 | } ), |
| 86 | Quantifier::optional( new Alternative( [ |
| 87 | $matcherFactory->integer(), |
| 88 | new KeywordMatcher( [ 'on', 'off' ] ), |
| 89 | ] ) ) |
| 90 | ] ) ) |
| 91 | ] ), |
| 92 | 'ligatures' => [ |
| 93 | new KeywordMatcher( [ 'common-ligatures', 'no-common-ligatures' ] ), |
| 94 | new KeywordMatcher( [ 'discretionary-ligatures', 'no-discretionary-ligatures' ] ), |
| 95 | new KeywordMatcher( [ 'historical-ligatures', 'no-historical-ligatures' ] ), |
| 96 | new KeywordMatcher( [ 'contextual', 'no-contextual' ] ) |
| 97 | ], |
| 98 | 'capsKeywords' => [ |
| 99 | 'small-caps', 'all-small-caps', 'petite-caps', 'all-petite-caps', 'unicase', 'titling-caps' |
| 100 | ], |
| 101 | 'numeric' => [ |
| 102 | new KeywordMatcher( [ 'lining-nums', 'oldstyle-nums' ] ), |
| 103 | new KeywordMatcher( [ 'proportional-nums', 'tabular-nums' ] ), |
| 104 | new KeywordMatcher( [ 'diagonal-fractions', 'stacked-fractions' ] ), |
| 105 | new KeywordMatcher( 'ordinal' ), |
| 106 | new KeywordMatcher( 'slashed-zero' ), |
| 107 | ], |
| 108 | 'eastAsian' => [ |
| 109 | new KeywordMatcher( [ 'jis78', 'jis83', 'jis90', 'jis04', 'simplified', 'traditional' ] ), |
| 110 | new KeywordMatcher( [ 'full-width', 'proportional-width' ] ), |
| 111 | new KeywordMatcher( 'ruby' ), |
| 112 | ], |
| 113 | 'positionKeywords' => [ |
| 114 | 'sub', 'super', |
| 115 | ], |
| 116 | ]; |
| 117 | $ret['font-variant'] = new Alternative( [ |
| 118 | new KeywordMatcher( [ 'normal', 'none' ] ), |
| 119 | UnorderedGroup::someOf( array_merge( |
| 120 | $ret['ligatures'], |
| 121 | [ new KeywordMatcher( $ret['capsKeywords'] ) ], |
| 122 | $ret['numeric'], |
| 123 | $ret['eastAsian'], |
| 124 | [ new KeywordMatcher( $ret['positionKeywords'] ) ] |
| 125 | ) ) |
| 126 | ] ); |
| 127 | return $ret; |
| 128 | } |
| 129 | |
| 130 | /** @inheritDoc */ |
| 131 | public function handlesRule( Rule $rule ) { |
| 132 | return $rule instanceof AtRule && !strcasecmp( $rule->getName(), 'font-face' ); |
| 133 | } |
| 134 | |
| 135 | /** @inheritDoc */ |
| 136 | protected function doSanitize( CSSObject $object ) { |
| 137 | if ( !$object instanceof AtRule || !$this->handlesRule( $object ) ) { |
| 138 | $this->sanitizationError( 'expected-at-rule', $object, [ 'font-face' ] ); |
| 139 | return null; |
| 140 | } |
| 141 | |
| 142 | if ( $object->getBlock() === null ) { |
| 143 | $this->sanitizationError( 'at-rule-block-required', $object, [ 'font-face' ] ); |
| 144 | return null; |
| 145 | } |
| 146 | |
| 147 | // No non-whitespace prelude allowed |
| 148 | if ( Util::findFirstNonWhitespace( $object->getPrelude() ) ) { |
| 149 | $this->sanitizationError( 'invalid-font-face-at-rule', $object ); |
| 150 | return null; |
| 151 | } |
| 152 | |
| 153 | $ret = clone $object; |
| 154 | $this->fixPreludeWhitespace( $ret, false ); |
| 155 | $this->sanitizeDeclarationBlock( $ret->getBlock(), $this->propertySanitizer ); |
| 156 | |
| 157 | return $ret; |
| 158 | } |
| 159 | } |