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