Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.19% |
43 / 64 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
TemplateStylesContentHandler | |
67.19% |
43 / 64 |
|
83.33% |
5 / 6 |
29.45 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateSave | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContentClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fillParserOutput | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
processErrors | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
sanitize | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\TemplateStyles; |
4 | |
5 | /** |
6 | * @file |
7 | * @license GPL-2.0-or-later |
8 | */ |
9 | |
10 | use CSSJanus; |
11 | use MediaWiki\Content\CodeContentHandler; |
12 | use MediaWiki\Content\Content; |
13 | use MediaWiki\Content\Renderer\ContentParseParams; |
14 | use MediaWiki\Content\ValidationParams; |
15 | use MediaWiki\MediaWikiServices; |
16 | use MediaWiki\Message\Message; |
17 | use MediaWiki\Parser\ParserOutput; |
18 | use MediaWiki\Status\Status; |
19 | use StatusValue; |
20 | use Wikimedia\CSS\Parser\Parser as CSSParser; |
21 | use Wikimedia\CSS\Util as CSSUtil; |
22 | |
23 | /** |
24 | * Content handler for sanitized CSS |
25 | */ |
26 | class TemplateStylesContentHandler extends CodeContentHandler { |
27 | |
28 | /** |
29 | * @param string $modelId |
30 | */ |
31 | public function __construct( $modelId = 'sanitized-css' ) { |
32 | parent::__construct( $modelId, [ CONTENT_FORMAT_CSS ] ); |
33 | } |
34 | |
35 | /** |
36 | * @inheritDoc |
37 | */ |
38 | public function validateSave( |
39 | Content $content, |
40 | ValidationParams $validationParams |
41 | ) { |
42 | '@phan-var TemplateStylesContent $content'; |
43 | return $this->sanitize( $content, [ 'novalue' => true, 'severity' => 'fatal' ] ); |
44 | } |
45 | |
46 | /** |
47 | * @return string |
48 | */ |
49 | protected function getContentClass() { |
50 | return TemplateStylesContent::class; |
51 | } |
52 | |
53 | /** |
54 | * @inheritDoc |
55 | */ |
56 | protected function fillParserOutput( |
57 | Content $content, |
58 | ContentParseParams $cpoParams, |
59 | ParserOutput &$output |
60 | ) { |
61 | '@phan-var TemplateStylesContent $content'; |
62 | $services = MediaWikiServices::getInstance(); |
63 | $page = $cpoParams->getPage(); |
64 | $parserOptions = $cpoParams->getParserOptions(); |
65 | |
66 | // Inject our warnings into the resulting ParserOutput |
67 | parent::fillParserOutput( $content, $cpoParams, $output ); |
68 | |
69 | if ( $cpoParams->getGenerateHtml() ) { |
70 | $html = ""; |
71 | $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n"; |
72 | $html .= htmlspecialchars( $content->getNativeData(), ENT_NOQUOTES ); |
73 | $html .= "\n</pre>\n"; |
74 | } else { |
75 | $html = ''; |
76 | } |
77 | |
78 | $output->clearWrapperDivClass(); |
79 | $output->setRawText( $html ); |
80 | |
81 | $status = $this->sanitize( $content, [ 'novalue' => true, 'class' => $parserOptions->getWrapOutputClass() ] ); |
82 | if ( $status->getErrors() ) { |
83 | foreach ( $status->getErrors() as $error ) { |
84 | $output->addWarningMsg( $error['message'], $error['params'] ); |
85 | } |
86 | $services->getTrackingCategories()->addTrackingCategory( |
87 | $output, |
88 | 'templatestyles-stylesheet-error-category', |
89 | $page |
90 | ); |
91 | } |
92 | } |
93 | |
94 | /** |
95 | * Handle errors from the CSS parser and/or sanitizer |
96 | * @param StatusValue $status Object to add errors to |
97 | * @param array[] $errors Error array |
98 | * @param string $severity Whether to consider errors as 'warning' or 'fatal' |
99 | */ |
100 | protected static function processErrors( StatusValue $status, array $errors, $severity ) { |
101 | if ( $severity !== 'warning' && $severity !== 'fatal' ) { |
102 | // @codeCoverageIgnoreStart |
103 | throw new \InvalidArgumentException( 'Invalid $severity' ); |
104 | // @codeCoverageIgnoreEnd |
105 | } |
106 | foreach ( $errors as $error ) { |
107 | $error[0] = 'templatestyles-error-' . $error[0]; |
108 | call_user_func_array( [ $status, $severity ], $error ); |
109 | } |
110 | } |
111 | |
112 | /** |
113 | * Sanitize the content |
114 | * @param TemplateStylesContent $content |
115 | * @param array $options Options are: |
116 | * - class: (string) Class to prefix selectors with |
117 | * - extraWrapper: (string) Extra simple selector to prefix selectors with |
118 | * - flip: (bool) Have CSSJanus flip the stylesheet. |
119 | * - minify: (bool) Whether to minify. Default true. |
120 | * - novalue: (bool) Don't bother returning the actual stylesheet, just |
121 | * fill the Status with warnings. |
122 | * - severity: (string) Whether to consider errors as 'warning' or 'fatal' |
123 | * @return Status |
124 | */ |
125 | public function sanitize( TemplateStylesContent $content, array $options = [] ) { |
126 | $options += [ |
127 | 'class' => false, |
128 | 'extraWrapper' => null, |
129 | 'flip' => false, |
130 | 'minify' => true, |
131 | 'novalue' => false, |
132 | 'severity' => 'warning', |
133 | ]; |
134 | |
135 | $status = Status::newGood(); |
136 | |
137 | $style = $content->getText(); |
138 | $maxSize = Hooks::getConfig()->get( 'TemplateStylesMaxStylesheetSize' ); |
139 | if ( $maxSize !== null && strlen( $style ) > $maxSize ) { |
140 | $status->fatal( |
141 | // Status::getWikiText() chokes on the Message::sizeParam if we |
142 | // don't wrap it in a Message ourself. |
143 | wfMessage( 'templatestyles-size-exceeded', $maxSize, Message::sizeParam( $maxSize ) ) |
144 | ); |
145 | return $status; |
146 | } |
147 | |
148 | if ( $options['flip'] ) { |
149 | $style = CSSJanus::transform( $style, true, false ); |
150 | } |
151 | |
152 | // Parse it, and collect any errors |
153 | $cssParser = CSSParser::newFromString( $style ); |
154 | $stylesheet = $cssParser->parseStylesheet(); |
155 | self::processErrors( $status, $cssParser->getParseErrors(), $options['severity'] ); |
156 | |
157 | // Sanitize it, and collect any errors |
158 | $sanitizer = Hooks::getSanitizer( |
159 | $options['class'] ?: 'mw-parser-output', $options['extraWrapper'] |
160 | ); |
161 | // Just in case |
162 | $sanitizer->clearSanitizationErrors(); |
163 | $stylesheet = $sanitizer->sanitize( $stylesheet ); |
164 | self::processErrors( $status, $sanitizer->getSanitizationErrors(), $options['severity'] ); |
165 | $sanitizer->clearSanitizationErrors(); |
166 | |
167 | // Stringify it while minifying |
168 | $value = CSSUtil::stringify( $stylesheet, [ 'minify' => $options['minify'] ] ); |
169 | |
170 | // Sanity check, don't allow "</style" if one somehow sneaks through the sanitizer |
171 | if ( preg_match( '!</style!i', $value ) ) { |
172 | $value = ''; |
173 | $status->fatal( 'templatestyles-end-tag-injection' ); |
174 | } |
175 | |
176 | if ( !$options['novalue'] ) { |
177 | $status->value = $value; |
178 | |
179 | // Sanity check, don't allow raw U+007F if one somehow sneaks through the sanitizer |
180 | $status->value = strtr( $status->value, [ "\x7f" => '�' ] ); |
181 | } |
182 | |
183 | return $status; |
184 | } |
185 | } |