SECURITY: blacklist CSS var()
[lhc/web/wiklou.git] / tests / parser / TestFileReader.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Testing
20 */
21
22 class TestFileReader {
23 private $file;
24 private $fh;
25 private $section = null;
26 /** String|null: current test section being analyzed */
27 private $sectionData = [];
28 private $sectionLineNum = [];
29 private $lineNum = 0;
30 private $runDisabled;
31 private $regex;
32
33 private $articles = [];
34 private $requirements = [];
35 private $tests = [];
36
37 public static function read( $file, array $options = [] ) {
38 $reader = new self( $file, $options );
39 $reader->execute();
40
41 $requirements = [];
42 foreach ( $reader->requirements as $type => $reqsOfType ) {
43 foreach ( $reqsOfType as $name => $unused ) {
44 $requirements[] = [
45 'type' => $type,
46 'name' => $name
47 ];
48 }
49 }
50
51 return [
52 'requirements' => $requirements,
53 'tests' => $reader->tests,
54 'articles' => $reader->articles
55 ];
56 }
57
58 private function __construct( $file, $options ) {
59 $this->file = $file;
60 $this->fh = fopen( $this->file, "rt" );
61
62 if ( !$this->fh ) {
63 throw new MWException( "Couldn't open file '$file'\n" );
64 }
65
66 $options = $options + [
67 'runDisabled' => false,
68 'regex' => '//',
69 ];
70 $this->runDisabled = $options['runDisabled'];
71 $this->regex = $options['regex'];
72 }
73
74 private function addCurrentTest() {
75 // "input" and "result" are old section names allowed
76 // for backwards-compatibility.
77 $input = $this->checkSection( [ 'wikitext', 'input' ], false );
78 $nonTidySection = $this->checkSection(
79 [ 'html/php', 'html/*', 'html', 'result' ], false );
80 // Some tests have "with tidy" and "without tidy" variants
81 $tidySection = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
82
83 // Remove trailing newline
84 $data = array_map( 'ParserTestRunner::chomp', $this->sectionData );
85
86 // Apply defaults
87 $data += [
88 'options' => '',
89 'config' => ''
90 ];
91
92 if ( $input === false ) {
93 throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
94 "lacks input section" );
95 }
96
97 if ( preg_match( '/\\bdisabled\\b/i', $data['options'] ) && !$this->runDisabled ) {
98 // Disabled
99 return;
100 }
101
102 if ( $tidySection === false && $nonTidySection === false ) {
103 if ( isset( $data['html/parsoid'] ) || isset( $data['wikitext/edited'] ) ) {
104 // Parsoid only
105 return;
106 } else {
107 throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
108 "lacks result section" );
109 }
110 }
111
112 if ( !preg_match( $this->regex, $data['test'] ) ) {
113 // Filtered test
114 return;
115 }
116
117 $commonInfo = [
118 'test' => $data['test'],
119 'desc' => $data['test'],
120 'input' => $data[$input],
121 'options' => $data['options'],
122 'config' => $data['config'],
123 'line' => $this->sectionLineNum['test'],
124 'file' => $this->file
125 ];
126
127 if ( $nonTidySection !== false ) {
128 // Add non-tidy test
129 $this->tests[] = [
130 'result' => $data[$nonTidySection],
131 'resultSection' => $nonTidySection
132 ] + $commonInfo;
133
134 if ( $tidySection !== false ) {
135 // Add tidy subtest
136 $this->tests[] = [
137 'desc' => $data['test'] . ' (with tidy)',
138 'result' => $data[$tidySection],
139 'resultSection' => $tidySection,
140 'options' => $data['options'] . ' tidy',
141 'isSubtest' => true,
142 ] + $commonInfo;
143 }
144 } elseif ( $tidySection !== false ) {
145 // No need to override desc when there is no subtest
146 $this->tests[] = [
147 'result' => $data[$tidySection],
148 'resultSection' => $tidySection,
149 'options' => $data['options'] . ' tidy'
150 ] + $commonInfo;
151 } else {
152 throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
153 "lacks result section" );
154 }
155 }
156
157 private function execute() {
158 while ( ( $line = fgets( $this->fh ) ) !== false ) {
159 $this->lineNum++;
160 $matches = [];
161
162 if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
163 $this->section = strtolower( $matches[1] );
164
165 if ( $this->section == 'endarticle' ) {
166 $this->checkSection( 'text' );
167 $this->checkSection( 'article' );
168
169 $this->addArticle(
170 ParserTestRunner::chomp( $this->sectionData['article'] ),
171 $this->sectionData['text'], $this->lineNum );
172
173 $this->clearSection();
174
175 continue;
176 }
177
178 if ( $this->section == 'endhooks' ) {
179 $this->checkSection( 'hooks' );
180
181 foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
182 $line = trim( $line );
183
184 if ( $line ) {
185 $this->addRequirement( 'hook', $line );
186 }
187 }
188
189 $this->clearSection();
190
191 continue;
192 }
193
194 if ( $this->section == 'endfunctionhooks' ) {
195 $this->checkSection( 'functionhooks' );
196
197 foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
198 $line = trim( $line );
199
200 if ( $line ) {
201 $this->addRequirement( 'functionHook', $line );
202 }
203 }
204
205 $this->clearSection();
206
207 continue;
208 }
209
210 if ( $this->section == 'endtransparenthooks' ) {
211 $this->checkSection( 'transparenthooks' );
212
213 foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
214 $line = trim( $line );
215
216 if ( $line ) {
217 $this->addRequirement( 'transparentHook', $line );
218 }
219 }
220
221 $this->clearSection();
222
223 continue;
224 }
225
226 if ( $this->section == 'end' ) {
227 $this->checkSection( 'test' );
228 $this->addCurrentTest();
229 $this->clearSection();
230 continue;
231 }
232
233 if ( isset( $this->sectionData[$this->section] ) ) {
234 throw new MWException( "duplicate section '$this->section' "
235 . "at line {$this->lineNum} of $this->file\n" );
236 }
237
238 $this->sectionLineNum[$this->section] = $this->lineNum;
239 $this->sectionData[$this->section] = '';
240
241 continue;
242 }
243
244 if ( $this->section ) {
245 $this->sectionData[$this->section] .= $line;
246 }
247 }
248 }
249
250 /**
251 * Clear section name and its data
252 */
253 private function clearSection() {
254 $this->sectionLineNum = [];
255 $this->sectionData = [];
256 $this->section = null;
257 }
258
259 /**
260 * Verify the current section data has some value for the given token
261 * name(s) (first parameter).
262 * Throw an exception if it is not set, referencing current section
263 * and adding the current file name and line number
264 *
265 * @param string|array $tokens Expected token(s) that should have been
266 * mentioned before closing this section
267 * @param bool $fatal True iff an exception should be thrown if
268 * the section is not found.
269 * @return bool|string
270 * @throws MWException
271 */
272 private function checkSection( $tokens, $fatal = true ) {
273 if ( is_null( $this->section ) ) {
274 throw new MWException( __METHOD__ . " can not verify a null section!\n" );
275 }
276 if ( !is_array( $tokens ) ) {
277 $tokens = [ $tokens ];
278 }
279 if ( count( $tokens ) == 0 ) {
280 throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
281 }
282
283 $data = $this->sectionData;
284 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
285 return isset( $data[$token] );
286 } );
287
288 if ( count( $tokens ) == 0 ) {
289 if ( !$fatal ) {
290 return false;
291 }
292 throw new MWException( sprintf(
293 "'%s' without '%s' at line %s of %s\n",
294 $this->section,
295 implode( ',', $tokens ),
296 $this->lineNum,
297 $this->file
298 ) );
299 }
300 if ( count( $tokens ) > 1 ) {
301 throw new MWException( sprintf(
302 "'%s' with unexpected tokens '%s' at line %s of %s\n",
303 $this->section,
304 implode( ',', $tokens ),
305 $this->lineNum,
306 $this->file
307 ) );
308 }
309
310 return array_values( $tokens )[0];
311 }
312
313 private function addArticle( $name, $text, $line ) {
314 $this->articles[] = [
315 'name' => $name,
316 'text' => $text,
317 'line' => $line,
318 'file' => $this->file
319 ];
320 }
321
322 private function addRequirement( $type, $name ) {
323 $this->requirements[$type][$name] = true;
324 }
325 }