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 | } |