These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Preprocessor using PHP's dom extension |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | * @ingroup Parser |
||
22 | */ |
||
23 | |||
24 | /** |
||
25 | * @ingroup Parser |
||
26 | */ |
||
27 | // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps |
||
28 | class Preprocessor_DOM extends Preprocessor { |
||
29 | // @codingStandardsIgnoreEnd |
||
30 | |||
31 | /** |
||
32 | * @var Parser |
||
33 | */ |
||
34 | public $parser; |
||
35 | |||
36 | public $memoryLimit; |
||
37 | |||
38 | const CACHE_PREFIX = 'preprocess-xml'; |
||
39 | |||
40 | public function __construct( $parser ) { |
||
41 | $this->parser = $parser; |
||
42 | $mem = ini_get( 'memory_limit' ); |
||
43 | $this->memoryLimit = false; |
||
44 | if ( strval( $mem ) !== '' && $mem != -1 ) { |
||
45 | if ( preg_match( '/^\d+$/', $mem ) ) { |
||
46 | $this->memoryLimit = $mem; |
||
47 | } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) { |
||
48 | $this->memoryLimit = $m[1] * 1048576; |
||
49 | } |
||
50 | } |
||
51 | } |
||
52 | |||
53 | /** |
||
54 | * @return PPFrame_DOM |
||
55 | */ |
||
56 | public function newFrame() { |
||
57 | return new PPFrame_DOM( $this ); |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | * @param array $args |
||
62 | * @return PPCustomFrame_DOM |
||
63 | */ |
||
64 | public function newCustomFrame( $args ) { |
||
65 | return new PPCustomFrame_DOM( $this, $args ); |
||
66 | } |
||
67 | |||
68 | /** |
||
69 | * @param array $values |
||
70 | * @return PPNode_DOM |
||
71 | * @throws MWException |
||
72 | */ |
||
73 | public function newPartNodeArray( $values ) { |
||
74 | // NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais) |
||
75 | $xml = "<list>"; |
||
76 | |||
77 | foreach ( $values as $k => $val ) { |
||
78 | if ( is_int( $k ) ) { |
||
79 | $xml .= "<part><name index=\"$k\"/><value>" |
||
80 | . htmlspecialchars( $val ) . "</value></part>"; |
||
81 | } else { |
||
82 | $xml .= "<part><name>" . htmlspecialchars( $k ) |
||
83 | . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>"; |
||
84 | } |
||
85 | } |
||
86 | |||
87 | $xml .= "</list>"; |
||
88 | |||
89 | $dom = new DOMDocument(); |
||
90 | MediaWiki\suppressWarnings(); |
||
91 | $result = $dom->loadXML( $xml ); |
||
92 | MediaWiki\restoreWarnings(); |
||
93 | View Code Duplication | if ( !$result ) { |
|
94 | // Try running the XML through UtfNormal to get rid of invalid characters |
||
95 | $xml = UtfNormal\Validator::cleanUp( $xml ); |
||
96 | // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 |
||
97 | // don't barf when the XML is >256 levels deep |
||
98 | $result = $dom->loadXML( $xml, 1 << 19 ); |
||
99 | } |
||
100 | |||
101 | if ( !$result ) { |
||
102 | throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' ); |
||
103 | } |
||
104 | |||
105 | $root = $dom->documentElement; |
||
106 | $node = new PPNode_DOM( $root->childNodes ); |
||
107 | return $node; |
||
108 | } |
||
109 | |||
110 | /** |
||
111 | * @throws MWException |
||
112 | * @return bool |
||
113 | */ |
||
114 | public function memCheck() { |
||
115 | if ( $this->memoryLimit === false ) { |
||
116 | return true; |
||
117 | } |
||
118 | $usage = memory_get_usage(); |
||
119 | if ( $usage > $this->memoryLimit * 0.9 ) { |
||
120 | $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 ); |
||
121 | throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" ); |
||
122 | } |
||
123 | return $usage <= $this->memoryLimit * 0.8; |
||
124 | } |
||
125 | |||
126 | /** |
||
127 | * Preprocess some wikitext and return the document tree. |
||
128 | * This is the ghost of Parser::replace_variables(). |
||
129 | * |
||
130 | * @param string $text The text to parse |
||
131 | * @param int $flags Bitwise combination of: |
||
132 | * Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" |
||
133 | * as if the text is being included. Default |
||
134 | * is to assume a direct page view. |
||
135 | * |
||
136 | * The generated DOM tree must depend only on the input text and the flags. |
||
137 | * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. |
||
138 | * |
||
139 | * Any flag added to the $flags parameter here, or any other parameter liable to cause a |
||
140 | * change in the DOM tree for a given text, must be passed through the section identifier |
||
141 | * in the section edit link and thus back to extractSections(). |
||
142 | * |
||
143 | * The output of this function is currently only cached in process memory, but a persistent |
||
144 | * cache may be implemented at a later date which takes further advantage of these strict |
||
145 | * dependency requirements. |
||
146 | * |
||
147 | * @throws MWException |
||
148 | * @return PPNode_DOM |
||
149 | */ |
||
150 | public function preprocessToObj( $text, $flags = 0 ) { |
||
151 | |||
152 | $xml = $this->cacheGetTree( $text, $flags ); |
||
153 | if ( $xml === false ) { |
||
154 | $xml = $this->preprocessToXml( $text, $flags ); |
||
155 | $this->cacheSetTree( $text, $flags, $xml ); |
||
156 | } |
||
157 | |||
158 | // Fail if the number of elements exceeds acceptable limits |
||
159 | // Do not attempt to generate the DOM |
||
160 | $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); |
||
161 | $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount(); |
||
162 | if ( $this->parser->mGeneratedPPNodeCount > $max ) { |
||
163 | // if ( $cacheable ) { ... } |
||
164 | throw new MWException( __METHOD__ . ': generated node count limit exceeded' ); |
||
165 | } |
||
166 | |||
167 | $dom = new DOMDocument; |
||
168 | MediaWiki\suppressWarnings(); |
||
169 | $result = $dom->loadXML( $xml ); |
||
170 | MediaWiki\restoreWarnings(); |
||
171 | View Code Duplication | if ( !$result ) { |
|
172 | // Try running the XML through UtfNormal to get rid of invalid characters |
||
173 | $xml = UtfNormal\Validator::cleanUp( $xml ); |
||
174 | // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 |
||
175 | // don't barf when the XML is >256 levels deep. |
||
176 | $result = $dom->loadXML( $xml, 1 << 19 ); |
||
177 | } |
||
178 | if ( $result ) { |
||
179 | $obj = new PPNode_DOM( $dom->documentElement ); |
||
180 | } |
||
181 | |||
182 | // if ( $cacheable ) { ... } |
||
183 | |||
184 | if ( !$result ) { |
||
185 | throw new MWException( __METHOD__ . ' generated invalid XML' ); |
||
186 | } |
||
187 | return $obj; |
||
0 ignored issues
–
show
|
|||
188 | } |
||
189 | |||
190 | /** |
||
191 | * @param string $text |
||
192 | * @param int $flags |
||
193 | * @return string |
||
194 | */ |
||
195 | public function preprocessToXml( $text, $flags = 0 ) { |
||
196 | $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; |
||
197 | |||
198 | $xmlishElements = $this->parser->getStripList(); |
||
199 | $xmlishAllowMissingEndTag = [ 'includeonly', 'noinclude', 'onlyinclude' ]; |
||
200 | $enableOnlyinclude = false; |
||
201 | View Code Duplication | if ( $forInclusion ) { |
|
202 | $ignoredTags = [ 'includeonly', '/includeonly' ]; |
||
203 | $ignoredElements = [ 'noinclude' ]; |
||
204 | $xmlishElements[] = 'noinclude'; |
||
205 | if ( strpos( $text, '<onlyinclude>' ) !== false |
||
206 | && strpos( $text, '</onlyinclude>' ) !== false |
||
207 | ) { |
||
208 | $enableOnlyinclude = true; |
||
209 | } |
||
210 | } else { |
||
211 | $ignoredTags = [ 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ]; |
||
212 | $ignoredElements = [ 'includeonly' ]; |
||
213 | $xmlishElements[] = 'includeonly'; |
||
214 | } |
||
215 | $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); |
||
216 | |||
217 | // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset |
||
218 | $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; |
||
219 | |||
220 | $stack = new PPDStack; |
||
221 | |||
222 | $searchBase = "[{<\n"; # } |
||
223 | // For fast reverse searches |
||
224 | $revText = strrev( $text ); |
||
225 | $lengthText = strlen( $text ); |
||
226 | |||
227 | // Input pointer, starts out pointing to a pseudo-newline before the start |
||
228 | $i = 0; |
||
229 | // Current accumulator |
||
230 | $accum =& $stack->getAccum(); |
||
231 | $accum = '<root>'; |
||
232 | // True to find equals signs in arguments |
||
233 | $findEquals = false; |
||
234 | // True to take notice of pipe characters |
||
235 | $findPipe = false; |
||
236 | $headingIndex = 1; |
||
237 | // True if $i is inside a possible heading |
||
238 | $inHeading = false; |
||
239 | // True if there are no more greater-than (>) signs right of $i |
||
240 | $noMoreGT = false; |
||
241 | // Map of tag name => true if there are no more closing tags of given type right of $i |
||
242 | $noMoreClosingTag = []; |
||
243 | // True to ignore all input up to the next <onlyinclude> |
||
244 | $findOnlyinclude = $enableOnlyinclude; |
||
245 | // Do a line-start run without outputting an LF character |
||
246 | $fakeLineStart = true; |
||
247 | |||
248 | while ( true ) { |
||
249 | // $this->memCheck(); |
||
250 | |||
251 | if ( $findOnlyinclude ) { |
||
252 | // Ignore all input up to the next <onlyinclude> |
||
253 | $startPos = strpos( $text, '<onlyinclude>', $i ); |
||
254 | if ( $startPos === false ) { |
||
255 | // Ignored section runs to the end |
||
256 | $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>'; |
||
257 | break; |
||
258 | } |
||
259 | $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end |
||
260 | $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>'; |
||
261 | $i = $tagEndPos; |
||
262 | $findOnlyinclude = false; |
||
263 | } |
||
264 | |||
265 | if ( $fakeLineStart ) { |
||
266 | $found = 'line-start'; |
||
267 | $curChar = ''; |
||
268 | View Code Duplication | } else { |
|
269 | # Find next opening brace, closing brace or pipe |
||
270 | $search = $searchBase; |
||
271 | if ( $stack->top === false ) { |
||
272 | $currentClosing = ''; |
||
273 | } else { |
||
274 | $currentClosing = $stack->top->close; |
||
275 | $search .= $currentClosing; |
||
276 | } |
||
277 | if ( $findPipe ) { |
||
278 | $search .= '|'; |
||
279 | } |
||
280 | if ( $findEquals ) { |
||
281 | // First equals will be for the template |
||
282 | $search .= '='; |
||
283 | } |
||
284 | $rule = null; |
||
285 | # Output literal section, advance input counter |
||
286 | $literalLength = strcspn( $text, $search, $i ); |
||
287 | if ( $literalLength > 0 ) { |
||
288 | $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) ); |
||
289 | $i += $literalLength; |
||
290 | } |
||
291 | if ( $i >= $lengthText ) { |
||
292 | if ( $currentClosing == "\n" ) { |
||
293 | // Do a past-the-end run to finish off the heading |
||
294 | $curChar = ''; |
||
295 | $found = 'line-end'; |
||
296 | } else { |
||
297 | # All done |
||
298 | break; |
||
299 | } |
||
300 | } else { |
||
301 | $curChar = $text[$i]; |
||
302 | if ( $curChar == '|' ) { |
||
303 | $found = 'pipe'; |
||
304 | } elseif ( $curChar == '=' ) { |
||
305 | $found = 'equals'; |
||
306 | } elseif ( $curChar == '<' ) { |
||
307 | $found = 'angle'; |
||
308 | } elseif ( $curChar == "\n" ) { |
||
309 | if ( $inHeading ) { |
||
310 | $found = 'line-end'; |
||
311 | } else { |
||
312 | $found = 'line-start'; |
||
313 | } |
||
314 | } elseif ( $curChar == $currentClosing ) { |
||
315 | $found = 'close'; |
||
316 | } elseif ( isset( $this->rules[$curChar] ) ) { |
||
317 | $found = 'open'; |
||
318 | $rule = $this->rules[$curChar]; |
||
319 | } else { |
||
320 | # Some versions of PHP have a strcspn which stops on null characters |
||
321 | # Ignore and continue |
||
322 | ++$i; |
||
323 | continue; |
||
324 | } |
||
325 | } |
||
326 | } |
||
327 | |||
328 | if ( $found == 'angle' ) { |
||
329 | $matches = false; |
||
330 | // Handle </onlyinclude> |
||
331 | View Code Duplication | if ( $enableOnlyinclude |
|
332 | && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' |
||
333 | ) { |
||
334 | $findOnlyinclude = true; |
||
335 | continue; |
||
336 | } |
||
337 | |||
338 | // Determine element name |
||
339 | View Code Duplication | if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { |
|
340 | // Element name missing or not listed |
||
341 | $accum .= '<'; |
||
342 | ++$i; |
||
343 | continue; |
||
344 | } |
||
345 | // Handle comments |
||
346 | if ( isset( $matches[2] ) && $matches[2] == '!--' ) { |
||
347 | |||
348 | // To avoid leaving blank lines, when a sequence of |
||
349 | // space-separated comments is both preceded and followed by |
||
350 | // a newline (ignoring spaces), then |
||
351 | // trim leading and trailing spaces and the trailing newline. |
||
352 | |||
353 | // Find the end |
||
354 | $endPos = strpos( $text, '-->', $i + 4 ); |
||
355 | if ( $endPos === false ) { |
||
356 | // Unclosed comment in input, runs to end |
||
357 | $inner = substr( $text, $i ); |
||
358 | $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; |
||
359 | $i = $lengthText; |
||
360 | } else { |
||
361 | // Search backwards for leading whitespace |
||
362 | $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0; |
||
363 | |||
364 | // Search forwards for trailing whitespace |
||
365 | // $wsEnd will be the position of the last space (or the '>' if there's none) |
||
366 | $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 ); |
||
367 | |||
368 | // Keep looking forward as long as we're finding more |
||
369 | // comments. |
||
370 | $comments = [ [ $wsStart, $wsEnd ] ]; |
||
371 | View Code Duplication | while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) { |
|
372 | $c = strpos( $text, '-->', $wsEnd + 4 ); |
||
373 | if ( $c === false ) { |
||
374 | break; |
||
375 | } |
||
376 | $c = $c + 2 + strspn( $text, " \t", $c + 3 ); |
||
377 | $comments[] = [ $wsEnd + 1, $c ]; |
||
378 | $wsEnd = $c; |
||
379 | } |
||
380 | |||
381 | // Eat the line if possible |
||
382 | // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at |
||
383 | // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but |
||
384 | // it's a possible beneficial b/c break. |
||
385 | if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" |
||
386 | && substr( $text, $wsEnd + 1, 1 ) == "\n" |
||
387 | ) { |
||
388 | // Remove leading whitespace from the end of the accumulator |
||
389 | // Sanity check first though |
||
390 | $wsLength = $i - $wsStart; |
||
391 | if ( $wsLength > 0 |
||
392 | && strspn( $accum, " \t", -$wsLength ) === $wsLength |
||
393 | ) { |
||
394 | $accum = substr( $accum, 0, -$wsLength ); |
||
395 | } |
||
396 | |||
397 | // Dump all but the last comment to the accumulator |
||
398 | foreach ( $comments as $j => $com ) { |
||
399 | $startPos = $com[0]; |
||
400 | $endPos = $com[1] + 1; |
||
401 | if ( $j == ( count( $comments ) - 1 ) ) { |
||
402 | break; |
||
403 | } |
||
404 | $inner = substr( $text, $startPos, $endPos - $startPos ); |
||
405 | $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; |
||
406 | } |
||
407 | |||
408 | // Do a line-start run next time to look for headings after the comment |
||
409 | $fakeLineStart = true; |
||
410 | } else { |
||
411 | // No line to eat, just take the comment itself |
||
412 | $startPos = $i; |
||
413 | $endPos += 2; |
||
414 | } |
||
415 | |||
416 | View Code Duplication | if ( $stack->top ) { |
|
417 | $part = $stack->top->getCurrentPart(); |
||
418 | if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) { |
||
419 | $part->visualEnd = $wsStart; |
||
420 | } |
||
421 | // Else comments abutting, no change in visual end |
||
422 | $part->commentEnd = $endPos; |
||
423 | } |
||
424 | $i = $endPos + 1; |
||
425 | $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); |
||
426 | $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; |
||
427 | } |
||
428 | continue; |
||
429 | } |
||
430 | $name = $matches[1]; |
||
431 | $lowerName = strtolower( $name ); |
||
432 | $attrStart = $i + strlen( $name ) + 1; |
||
433 | |||
434 | // Find end of tag |
||
435 | $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); |
||
436 | if ( $tagEndPos === false ) { |
||
437 | // Infinite backtrack |
||
438 | // Disable tag search to prevent worst-case O(N^2) performance |
||
439 | $noMoreGT = true; |
||
440 | $accum .= '<'; |
||
441 | ++$i; |
||
442 | continue; |
||
443 | } |
||
444 | |||
445 | // Handle ignored tags |
||
446 | if ( in_array( $lowerName, $ignoredTags ) ) { |
||
447 | $accum .= '<ignore>' |
||
448 | . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) |
||
449 | . '</ignore>'; |
||
450 | $i = $tagEndPos + 1; |
||
451 | continue; |
||
452 | } |
||
453 | |||
454 | $tagStartPos = $i; |
||
455 | if ( $text[$tagEndPos - 1] == '/' ) { |
||
456 | $attrEnd = $tagEndPos - 1; |
||
457 | $inner = null; |
||
458 | $i = $tagEndPos + 1; |
||
459 | $close = ''; |
||
460 | } else { |
||
461 | $attrEnd = $tagEndPos; |
||
462 | // Find closing tag |
||
463 | if ( |
||
464 | !isset( $noMoreClosingTag[$name] ) && |
||
465 | preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", |
||
466 | $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) |
||
467 | ) { |
||
468 | $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); |
||
469 | $i = $matches[0][1] + strlen( $matches[0][0] ); |
||
470 | $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>'; |
||
471 | View Code Duplication | } else { |
|
472 | // No end tag |
||
473 | if ( in_array( $name, $xmlishAllowMissingEndTag ) ) { |
||
474 | // Let it run out to the end of the text. |
||
475 | $inner = substr( $text, $tagEndPos + 1 ); |
||
476 | $i = $lengthText; |
||
477 | $close = ''; |
||
478 | } else { |
||
479 | // Don't match the tag, treat opening tag as literal and resume parsing. |
||
480 | $i = $tagEndPos + 1; |
||
481 | $accum .= htmlspecialchars( substr( $text, $tagStartPos, $tagEndPos + 1 - $tagStartPos ) ); |
||
482 | // Cache results, otherwise we have O(N^2) performance for input like <foo><foo><foo>... |
||
483 | $noMoreClosingTag[$name] = true; |
||
484 | continue; |
||
485 | } |
||
486 | } |
||
487 | } |
||
488 | // <includeonly> and <noinclude> just become <ignore> tags |
||
489 | if ( in_array( $lowerName, $ignoredElements ) ) { |
||
490 | $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) ) |
||
491 | . '</ignore>'; |
||
492 | continue; |
||
493 | } |
||
494 | |||
495 | $accum .= '<ext>'; |
||
496 | View Code Duplication | if ( $attrEnd <= $attrStart ) { |
|
497 | $attr = ''; |
||
498 | } else { |
||
499 | $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); |
||
500 | } |
||
501 | $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' . |
||
502 | // Note that the attr element contains the whitespace between name and attribute, |
||
503 | // this is necessary for precise reconstruction during pre-save transform. |
||
504 | '<attr>' . htmlspecialchars( $attr ) . '</attr>'; |
||
505 | if ( $inner !== null ) { |
||
506 | $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>'; |
||
507 | } |
||
508 | $accum .= $close . '</ext>'; |
||
509 | } elseif ( $found == 'line-start' ) { |
||
510 | // Is this the start of a heading? |
||
511 | // Line break belongs before the heading element in any case |
||
512 | if ( $fakeLineStart ) { |
||
513 | $fakeLineStart = false; |
||
514 | } else { |
||
515 | $accum .= $curChar; |
||
516 | $i++; |
||
517 | } |
||
518 | |||
519 | $count = strspn( $text, '=', $i, 6 ); |
||
520 | if ( $count == 1 && $findEquals ) { |
||
521 | // DWIM: This looks kind of like a name/value separator. |
||
522 | // Let's let the equals handler have it and break the |
||
523 | // potential heading. This is heuristic, but AFAICT the |
||
524 | // methods for completely correct disambiguation are very |
||
525 | // complex. |
||
526 | } elseif ( $count > 0 ) { |
||
527 | $piece = [ |
||
528 | 'open' => "\n", |
||
529 | 'close' => "\n", |
||
530 | 'parts' => [ new PPDPart( str_repeat( '=', $count ) ) ], |
||
531 | 'startPos' => $i, |
||
532 | 'count' => $count ]; |
||
533 | $stack->push( $piece ); |
||
534 | $accum =& $stack->getAccum(); |
||
535 | $flags = $stack->getFlags(); |
||
536 | extract( $flags ); |
||
537 | $i += $count; |
||
538 | } |
||
539 | } elseif ( $found == 'line-end' ) { |
||
540 | $piece = $stack->top; |
||
541 | // A heading must be open, otherwise \n wouldn't have been in the search list |
||
542 | assert( $piece->open === "\n" ); |
||
543 | $part = $piece->getCurrentPart(); |
||
544 | // Search back through the input to see if it has a proper close. |
||
545 | // Do this using the reversed string since the other solutions |
||
546 | // (end anchor, etc.) are inefficient. |
||
547 | $wsLength = strspn( $revText, " \t", $lengthText - $i ); |
||
548 | $searchStart = $i - $wsLength; |
||
549 | View Code Duplication | if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { |
|
550 | // Comment found at line end |
||
551 | // Search for equals signs before the comment |
||
552 | $searchStart = $part->visualEnd; |
||
553 | $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); |
||
554 | } |
||
555 | $count = $piece->count; |
||
556 | $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); |
||
557 | if ( $equalsLength > 0 ) { |
||
558 | View Code Duplication | if ( $searchStart - $equalsLength == $piece->startPos ) { |
|
559 | // This is just a single string of equals signs on its own line |
||
560 | // Replicate the doHeadings behavior /={count}(.+)={count}/ |
||
561 | // First find out how many equals signs there really are (don't stop at 6) |
||
562 | $count = $equalsLength; |
||
563 | if ( $count < 3 ) { |
||
564 | $count = 0; |
||
565 | } else { |
||
566 | $count = min( 6, intval( ( $count - 1 ) / 2 ) ); |
||
567 | } |
||
568 | } else { |
||
569 | $count = min( $equalsLength, $count ); |
||
570 | } |
||
571 | if ( $count > 0 ) { |
||
572 | // Normal match, output <h> |
||
573 | $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>"; |
||
574 | $headingIndex++; |
||
575 | } else { |
||
576 | // Single equals sign on its own line, count=0 |
||
577 | $element = $accum; |
||
578 | } |
||
579 | } else { |
||
580 | // No match, no <h>, just pass down the inner text |
||
581 | $element = $accum; |
||
582 | } |
||
583 | // Unwind the stack |
||
584 | $stack->pop(); |
||
585 | $accum =& $stack->getAccum(); |
||
586 | $flags = $stack->getFlags(); |
||
587 | extract( $flags ); |
||
588 | |||
589 | // Append the result to the enclosing accumulator |
||
590 | $accum .= $element; |
||
591 | // Note that we do NOT increment the input pointer. |
||
592 | // This is because the closing linebreak could be the opening linebreak of |
||
593 | // another heading. Infinite loops are avoided because the next iteration MUST |
||
594 | // hit the heading open case above, which unconditionally increments the |
||
595 | // input pointer. |
||
596 | View Code Duplication | } elseif ( $found == 'open' ) { |
|
597 | # count opening brace characters |
||
598 | $count = strspn( $text, $curChar, $i ); |
||
599 | |||
600 | # we need to add to stack only if opening brace count is enough for one of the rules |
||
601 | if ( $count >= $rule['min'] ) { |
||
602 | # Add it to the stack |
||
603 | $piece = [ |
||
604 | 'open' => $curChar, |
||
605 | 'close' => $rule['end'], |
||
606 | 'count' => $count, |
||
607 | 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ), |
||
608 | ]; |
||
609 | |||
610 | $stack->push( $piece ); |
||
611 | $accum =& $stack->getAccum(); |
||
612 | $flags = $stack->getFlags(); |
||
613 | extract( $flags ); |
||
614 | } else { |
||
615 | # Add literal brace(s) |
||
616 | $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); |
||
617 | } |
||
618 | $i += $count; |
||
619 | } elseif ( $found == 'close' ) { |
||
620 | $piece = $stack->top; |
||
621 | # lets check if there are enough characters for closing brace |
||
622 | $maxCount = $piece->count; |
||
623 | $count = strspn( $text, $curChar, $i, $maxCount ); |
||
624 | |||
625 | # check for maximum matching characters (if there are 5 closing |
||
626 | # characters, we will probably need only 3 - depending on the rules) |
||
627 | $rule = $this->rules[$piece->open]; |
||
628 | View Code Duplication | if ( $count > $rule['max'] ) { |
|
629 | # The specified maximum exists in the callback array, unless the caller |
||
630 | # has made an error |
||
631 | $matchingCount = $rule['max']; |
||
632 | } else { |
||
633 | # Count is less than the maximum |
||
634 | # Skip any gaps in the callback array to find the true largest match |
||
635 | # Need to use array_key_exists not isset because the callback can be null |
||
636 | $matchingCount = $count; |
||
637 | while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { |
||
638 | --$matchingCount; |
||
639 | } |
||
640 | } |
||
641 | |||
642 | if ( $matchingCount <= 0 ) { |
||
643 | # No matching element found in callback array |
||
644 | # Output a literal closing brace and continue |
||
645 | $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); |
||
646 | $i += $count; |
||
647 | continue; |
||
648 | } |
||
649 | $name = $rule['names'][$matchingCount]; |
||
650 | if ( $name === null ) { |
||
651 | // No element, just literal text |
||
652 | $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount ); |
||
653 | } else { |
||
654 | # Create XML element |
||
655 | # Note: $parts is already XML, does not need to be encoded further |
||
656 | $parts = $piece->parts; |
||
657 | $title = $parts[0]->out; |
||
658 | unset( $parts[0] ); |
||
659 | |||
660 | # The invocation is at the start of the line if lineStart is set in |
||
661 | # the stack, and all opening brackets are used up. |
||
662 | if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { |
||
663 | $attr = ' lineStart="1"'; |
||
664 | } else { |
||
665 | $attr = ''; |
||
666 | } |
||
667 | |||
668 | $element = "<$name$attr>"; |
||
669 | $element .= "<title>$title</title>"; |
||
670 | $argIndex = 1; |
||
671 | foreach ( $parts as $part ) { |
||
672 | if ( isset( $part->eqpos ) ) { |
||
673 | $argName = substr( $part->out, 0, $part->eqpos ); |
||
674 | $argValue = substr( $part->out, $part->eqpos + 1 ); |
||
675 | $element .= "<part><name>$argName</name>=<value>$argValue</value></part>"; |
||
676 | } else { |
||
677 | $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>"; |
||
678 | $argIndex++; |
||
679 | } |
||
680 | } |
||
681 | $element .= "</$name>"; |
||
682 | } |
||
683 | |||
684 | # Advance input pointer |
||
685 | $i += $matchingCount; |
||
686 | |||
687 | # Unwind the stack |
||
688 | $stack->pop(); |
||
689 | $accum =& $stack->getAccum(); |
||
690 | |||
691 | # Re-add the old stack element if it still has unmatched opening characters remaining |
||
692 | View Code Duplication | if ( $matchingCount < $piece->count ) { |
|
693 | $piece->parts = [ new PPDPart ]; |
||
694 | $piece->count -= $matchingCount; |
||
695 | # do we still qualify for any callback with remaining count? |
||
696 | $min = $this->rules[$piece->open]['min']; |
||
697 | if ( $piece->count >= $min ) { |
||
698 | $stack->push( $piece ); |
||
699 | $accum =& $stack->getAccum(); |
||
700 | } else { |
||
701 | $accum .= str_repeat( $piece->open, $piece->count ); |
||
702 | } |
||
703 | } |
||
704 | $flags = $stack->getFlags(); |
||
705 | extract( $flags ); |
||
706 | |||
707 | # Add XML element to the enclosing accumulator |
||
708 | $accum .= $element; |
||
709 | View Code Duplication | } elseif ( $found == 'pipe' ) { |
|
710 | $findEquals = true; // shortcut for getFlags() |
||
711 | $stack->addPart(); |
||
712 | $accum =& $stack->getAccum(); |
||
713 | ++$i; |
||
714 | } elseif ( $found == 'equals' ) { |
||
715 | $findEquals = false; // shortcut for getFlags() |
||
716 | $stack->getCurrentPart()->eqpos = strlen( $accum ); |
||
717 | $accum .= '='; |
||
718 | ++$i; |
||
719 | } |
||
720 | } |
||
721 | |||
722 | # Output any remaining unclosed brackets |
||
723 | foreach ( $stack->stack as $piece ) { |
||
724 | $stack->rootAccum .= $piece->breakSyntax(); |
||
725 | } |
||
726 | $stack->rootAccum .= '</root>'; |
||
727 | $xml = $stack->rootAccum; |
||
728 | |||
729 | return $xml; |
||
730 | } |
||
731 | } |
||
732 | |||
733 | /** |
||
734 | * Stack class to help Preprocessor::preprocessToObj() |
||
735 | * @ingroup Parser |
||
736 | */ |
||
737 | class PPDStack { |
||
738 | public $stack, $rootAccum; |
||
739 | |||
740 | /** |
||
741 | * @var PPDStack |
||
742 | */ |
||
743 | public $top; |
||
744 | public $out; |
||
745 | public $elementClass = 'PPDStackElement'; |
||
746 | |||
747 | public static $false = false; |
||
748 | |||
749 | public function __construct() { |
||
750 | $this->stack = []; |
||
751 | $this->top = false; |
||
752 | $this->rootAccum = ''; |
||
753 | $this->accum =& $this->rootAccum; |
||
754 | } |
||
755 | |||
756 | /** |
||
757 | * @return int |
||
758 | */ |
||
759 | public function count() { |
||
760 | return count( $this->stack ); |
||
761 | } |
||
762 | |||
763 | public function &getAccum() { |
||
764 | return $this->accum; |
||
765 | } |
||
766 | |||
767 | public function getCurrentPart() { |
||
768 | if ( $this->top === false ) { |
||
769 | return false; |
||
770 | } else { |
||
771 | return $this->top->getCurrentPart(); |
||
772 | } |
||
773 | } |
||
774 | |||
775 | public function push( $data ) { |
||
776 | if ( $data instanceof $this->elementClass ) { |
||
777 | $this->stack[] = $data; |
||
778 | } else { |
||
779 | $class = $this->elementClass; |
||
780 | $this->stack[] = new $class( $data ); |
||
781 | } |
||
782 | $this->top = $this->stack[count( $this->stack ) - 1]; |
||
783 | $this->accum =& $this->top->getAccum(); |
||
784 | } |
||
785 | |||
786 | public function pop() { |
||
787 | if ( !count( $this->stack ) ) { |
||
788 | throw new MWException( __METHOD__ . ': no elements remaining' ); |
||
789 | } |
||
790 | $temp = array_pop( $this->stack ); |
||
791 | |||
792 | if ( count( $this->stack ) ) { |
||
793 | $this->top = $this->stack[count( $this->stack ) - 1]; |
||
794 | $this->accum =& $this->top->getAccum(); |
||
795 | } else { |
||
796 | $this->top = self::$false; |
||
797 | $this->accum =& $this->rootAccum; |
||
798 | } |
||
799 | return $temp; |
||
800 | } |
||
801 | |||
802 | public function addPart( $s = '' ) { |
||
803 | $this->top->addPart( $s ); |
||
804 | $this->accum =& $this->top->getAccum(); |
||
805 | } |
||
806 | |||
807 | /** |
||
808 | * @return array |
||
809 | */ |
||
810 | public function getFlags() { |
||
811 | if ( !count( $this->stack ) ) { |
||
812 | return [ |
||
813 | 'findEquals' => false, |
||
814 | 'findPipe' => false, |
||
815 | 'inHeading' => false, |
||
816 | ]; |
||
817 | } else { |
||
818 | return $this->top->getFlags(); |
||
819 | } |
||
820 | } |
||
821 | } |
||
822 | |||
823 | /** |
||
824 | * @ingroup Parser |
||
825 | */ |
||
826 | class PPDStackElement { |
||
827 | /** |
||
828 | * @var string Opening character (\n for heading) |
||
829 | */ |
||
830 | public $open; |
||
831 | |||
832 | /** |
||
833 | * @var string Matching closing character |
||
834 | */ |
||
835 | public $close; |
||
836 | |||
837 | /** |
||
838 | * @var int Number of opening characters found (number of "=" for heading) |
||
839 | */ |
||
840 | public $count; |
||
841 | |||
842 | /** |
||
843 | * @var PPDPart[] Array of PPDPart objects describing pipe-separated parts. |
||
844 | */ |
||
845 | public $parts; |
||
846 | |||
847 | /** |
||
848 | * @var bool True if the open char appeared at the start of the input line. |
||
849 | * Not set for headings. |
||
850 | */ |
||
851 | public $lineStart; |
||
852 | |||
853 | public $partClass = 'PPDPart'; |
||
854 | |||
855 | public function __construct( $data = [] ) { |
||
856 | $class = $this->partClass; |
||
857 | $this->parts = [ new $class ]; |
||
858 | |||
859 | foreach ( $data as $name => $value ) { |
||
860 | $this->$name = $value; |
||
861 | } |
||
862 | } |
||
863 | |||
864 | public function &getAccum() { |
||
865 | return $this->parts[count( $this->parts ) - 1]->out; |
||
866 | } |
||
867 | |||
868 | public function addPart( $s = '' ) { |
||
869 | $class = $this->partClass; |
||
870 | $this->parts[] = new $class( $s ); |
||
871 | } |
||
872 | |||
873 | public function getCurrentPart() { |
||
874 | return $this->parts[count( $this->parts ) - 1]; |
||
875 | } |
||
876 | |||
877 | /** |
||
878 | * @return array |
||
879 | */ |
||
880 | public function getFlags() { |
||
881 | $partCount = count( $this->parts ); |
||
882 | $findPipe = $this->open != "\n" && $this->open != '['; |
||
883 | return [ |
||
884 | 'findPipe' => $findPipe, |
||
885 | 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ), |
||
886 | 'inHeading' => $this->open == "\n", |
||
887 | ]; |
||
888 | } |
||
889 | |||
890 | /** |
||
891 | * Get the output string that would result if the close is not found. |
||
892 | * |
||
893 | * @param bool|int $openingCount |
||
894 | * @return string |
||
895 | */ |
||
896 | public function breakSyntax( $openingCount = false ) { |
||
897 | if ( $this->open == "\n" ) { |
||
898 | $s = $this->parts[0]->out; |
||
899 | } else { |
||
900 | if ( $openingCount === false ) { |
||
901 | $openingCount = $this->count; |
||
902 | } |
||
903 | $s = str_repeat( $this->open, $openingCount ); |
||
904 | $first = true; |
||
905 | foreach ( $this->parts as $part ) { |
||
906 | if ( $first ) { |
||
907 | $first = false; |
||
908 | } else { |
||
909 | $s .= '|'; |
||
910 | } |
||
911 | $s .= $part->out; |
||
912 | } |
||
913 | } |
||
914 | return $s; |
||
915 | } |
||
916 | } |
||
917 | |||
918 | /** |
||
919 | * @ingroup Parser |
||
920 | */ |
||
921 | class PPDPart { |
||
922 | /** |
||
923 | * @var string Output accumulator string |
||
924 | */ |
||
925 | public $out; |
||
926 | |||
927 | // Optional member variables: |
||
928 | // eqpos Position of equals sign in output accumulator |
||
929 | // commentEnd Past-the-end input pointer for the last comment encountered |
||
930 | // visualEnd Past-the-end input pointer for the end of the accumulator minus comments |
||
931 | |||
932 | public function __construct( $out = '' ) { |
||
933 | $this->out = $out; |
||
934 | } |
||
935 | } |
||
936 | |||
937 | /** |
||
938 | * An expansion frame, used as a context to expand the result of preprocessToObj() |
||
939 | * @ingroup Parser |
||
940 | */ |
||
941 | // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps |
||
942 | class PPFrame_DOM implements PPFrame { |
||
943 | // @codingStandardsIgnoreEnd |
||
944 | |||
945 | /** |
||
946 | * @var Preprocessor |
||
947 | */ |
||
948 | public $preprocessor; |
||
949 | |||
950 | /** |
||
951 | * @var Parser |
||
952 | */ |
||
953 | public $parser; |
||
954 | |||
955 | /** |
||
956 | * @var Title |
||
957 | */ |
||
958 | public $title; |
||
959 | public $titleCache; |
||
960 | |||
961 | /** |
||
962 | * Hashtable listing templates which are disallowed for expansion in this frame, |
||
963 | * having been encountered previously in parent frames. |
||
964 | */ |
||
965 | public $loopCheckHash; |
||
966 | |||
967 | /** |
||
968 | * Recursion depth of this frame, top = 0 |
||
969 | * Note that this is NOT the same as expansion depth in expand() |
||
970 | */ |
||
971 | public $depth; |
||
972 | |||
973 | private $volatile = false; |
||
974 | private $ttl = null; |
||
975 | |||
976 | /** |
||
977 | * @var array |
||
978 | */ |
||
979 | protected $childExpansionCache; |
||
980 | |||
981 | /** |
||
982 | * Construct a new preprocessor frame. |
||
983 | * @param Preprocessor $preprocessor The parent preprocessor |
||
984 | */ |
||
985 | View Code Duplication | public function __construct( $preprocessor ) { |
|
986 | $this->preprocessor = $preprocessor; |
||
987 | $this->parser = $preprocessor->parser; |
||
988 | $this->title = $this->parser->mTitle; |
||
989 | $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ]; |
||
990 | $this->loopCheckHash = []; |
||
991 | $this->depth = 0; |
||
992 | $this->childExpansionCache = []; |
||
993 | } |
||
994 | |||
995 | /** |
||
996 | * Create a new child frame |
||
997 | * $args is optionally a multi-root PPNode or array containing the template arguments |
||
998 | * |
||
999 | * @param bool|array $args |
||
1000 | * @param Title|bool $title |
||
1001 | * @param int $indexOffset |
||
1002 | * @return PPTemplateFrame_DOM |
||
1003 | */ |
||
1004 | public function newChild( $args = false, $title = false, $indexOffset = 0 ) { |
||
1005 | $namedArgs = []; |
||
1006 | $numberedArgs = []; |
||
1007 | if ( $title === false ) { |
||
1008 | $title = $this->title; |
||
1009 | } |
||
1010 | if ( $args !== false ) { |
||
1011 | $xpath = false; |
||
1012 | if ( $args instanceof PPNode ) { |
||
1013 | $args = $args->node; |
||
1014 | } |
||
1015 | foreach ( $args as $arg ) { |
||
1016 | if ( $arg instanceof PPNode ) { |
||
1017 | $arg = $arg->node; |
||
1018 | } |
||
1019 | if ( !$xpath || $xpath->document !== $arg->ownerDocument ) { |
||
1020 | $xpath = new DOMXPath( $arg->ownerDocument ); |
||
1021 | } |
||
1022 | |||
1023 | $nameNodes = $xpath->query( 'name', $arg ); |
||
1024 | $value = $xpath->query( 'value', $arg ); |
||
1025 | if ( $nameNodes->item( 0 )->hasAttributes() ) { |
||
1026 | // Numbered parameter |
||
1027 | $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent; |
||
1028 | $index = $index - $indexOffset; |
||
1029 | if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) { |
||
1030 | $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning', |
||
1031 | wfEscapeWikiText( $this->title ), |
||
1032 | wfEscapeWikiText( $title ), |
||
1033 | wfEscapeWikiText( $index ) )->text() ); |
||
1034 | $this->parser->addTrackingCategory( 'duplicate-args-category' ); |
||
1035 | } |
||
1036 | $numberedArgs[$index] = $value->item( 0 ); |
||
1037 | unset( $namedArgs[$index] ); |
||
1038 | View Code Duplication | } else { |
|
1039 | // Named parameter |
||
1040 | $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) ); |
||
1041 | if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) { |
||
1042 | $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning', |
||
1043 | wfEscapeWikiText( $this->title ), |
||
1044 | wfEscapeWikiText( $title ), |
||
1045 | wfEscapeWikiText( $name ) )->text() ); |
||
1046 | $this->parser->addTrackingCategory( 'duplicate-args-category' ); |
||
1047 | } |
||
1048 | $namedArgs[$name] = $value->item( 0 ); |
||
1049 | unset( $numberedArgs[$name] ); |
||
1050 | } |
||
1051 | } |
||
1052 | } |
||
1053 | return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); |
||
1054 | } |
||
1055 | |||
1056 | /** |
||
1057 | * @throws MWException |
||
1058 | * @param string|int $key |
||
1059 | * @param string|PPNode_DOM|DOMDocument $root |
||
1060 | * @param int $flags |
||
1061 | * @return string |
||
1062 | */ |
||
1063 | public function cachedExpand( $key, $root, $flags = 0 ) { |
||
1064 | // we don't have a parent, so we don't have a cache |
||
1065 | return $this->expand( $root, $flags ); |
||
1066 | } |
||
1067 | |||
1068 | /** |
||
1069 | * @throws MWException |
||
1070 | * @param string|PPNode_DOM|DOMDocument $root |
||
1071 | * @param int $flags |
||
1072 | * @return string |
||
1073 | */ |
||
1074 | public function expand( $root, $flags = 0 ) { |
||
1075 | static $expansionDepth = 0; |
||
1076 | if ( is_string( $root ) ) { |
||
1077 | return $root; |
||
1078 | } |
||
1079 | |||
1080 | View Code Duplication | if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { |
|
1081 | $this->parser->limitationWarn( 'node-count-exceeded', |
||
1082 | $this->parser->mPPNodeCount, |
||
1083 | $this->parser->mOptions->getMaxPPNodeCount() |
||
1084 | ); |
||
1085 | return '<span class="error">Node-count limit exceeded</span>'; |
||
1086 | } |
||
1087 | |||
1088 | View Code Duplication | if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { |
|
1089 | $this->parser->limitationWarn( 'expansion-depth-exceeded', |
||
1090 | $expansionDepth, |
||
1091 | $this->parser->mOptions->getMaxPPExpandDepth() |
||
1092 | ); |
||
1093 | return '<span class="error">Expansion depth limit exceeded</span>'; |
||
1094 | } |
||
1095 | ++$expansionDepth; |
||
1096 | if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { |
||
1097 | $this->parser->mHighestExpansionDepth = $expansionDepth; |
||
1098 | } |
||
1099 | |||
1100 | if ( $root instanceof PPNode_DOM ) { |
||
1101 | $root = $root->node; |
||
1102 | } |
||
1103 | if ( $root instanceof DOMDocument ) { |
||
1104 | $root = $root->documentElement; |
||
1105 | } |
||
1106 | |||
1107 | $outStack = [ '', '' ]; |
||
1108 | $iteratorStack = [ false, $root ]; |
||
1109 | $indexStack = [ 0, 0 ]; |
||
1110 | |||
1111 | while ( count( $iteratorStack ) > 1 ) { |
||
1112 | $level = count( $outStack ) - 1; |
||
1113 | $iteratorNode =& $iteratorStack[$level]; |
||
1114 | $out =& $outStack[$level]; |
||
1115 | $index =& $indexStack[$level]; |
||
1116 | |||
1117 | if ( $iteratorNode instanceof PPNode_DOM ) { |
||
1118 | $iteratorNode = $iteratorNode->node; |
||
1119 | } |
||
1120 | |||
1121 | View Code Duplication | if ( is_array( $iteratorNode ) ) { |
|
1122 | if ( $index >= count( $iteratorNode ) ) { |
||
1123 | // All done with this iterator |
||
1124 | $iteratorStack[$level] = false; |
||
1125 | $contextNode = false; |
||
1126 | } else { |
||
1127 | $contextNode = $iteratorNode[$index]; |
||
1128 | $index++; |
||
1129 | } |
||
1130 | } elseif ( $iteratorNode instanceof DOMNodeList ) { |
||
1131 | if ( $index >= $iteratorNode->length ) { |
||
1132 | // All done with this iterator |
||
1133 | $iteratorStack[$level] = false; |
||
1134 | $contextNode = false; |
||
1135 | } else { |
||
1136 | $contextNode = $iteratorNode->item( $index ); |
||
1137 | $index++; |
||
1138 | } |
||
1139 | } else { |
||
1140 | // Copy to $contextNode and then delete from iterator stack, |
||
1141 | // because this is not an iterator but we do have to execute it once |
||
1142 | $contextNode = $iteratorStack[$level]; |
||
1143 | $iteratorStack[$level] = false; |
||
1144 | } |
||
1145 | |||
1146 | if ( $contextNode instanceof PPNode_DOM ) { |
||
1147 | $contextNode = $contextNode->node; |
||
1148 | } |
||
1149 | |||
1150 | $newIterator = false; |
||
1151 | |||
1152 | if ( $contextNode === false ) { |
||
1153 | // nothing to do |
||
1154 | } elseif ( is_string( $contextNode ) ) { |
||
1155 | $out .= $contextNode; |
||
1156 | } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) { |
||
1157 | $newIterator = $contextNode; |
||
1158 | } elseif ( $contextNode instanceof DOMNode ) { |
||
1159 | if ( $contextNode->nodeType == XML_TEXT_NODE ) { |
||
1160 | $out .= $contextNode->nodeValue; |
||
1161 | } elseif ( $contextNode->nodeName == 'template' ) { |
||
1162 | # Double-brace expansion |
||
1163 | $xpath = new DOMXPath( $contextNode->ownerDocument ); |
||
1164 | $titles = $xpath->query( 'title', $contextNode ); |
||
1165 | $title = $titles->item( 0 ); |
||
1166 | $parts = $xpath->query( 'part', $contextNode ); |
||
1167 | if ( $flags & PPFrame::NO_TEMPLATES ) { |
||
1168 | $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts ); |
||
1169 | View Code Duplication | } else { |
|
1170 | $lineStart = $contextNode->getAttribute( 'lineStart' ); |
||
1171 | $params = [ |
||
1172 | 'title' => new PPNode_DOM( $title ), |
||
1173 | 'parts' => new PPNode_DOM( $parts ), |
||
1174 | 'lineStart' => $lineStart ]; |
||
1175 | $ret = $this->parser->braceSubstitution( $params, $this ); |
||
1176 | if ( isset( $ret['object'] ) ) { |
||
1177 | $newIterator = $ret['object']; |
||
1178 | } else { |
||
1179 | $out .= $ret['text']; |
||
1180 | } |
||
1181 | } |
||
1182 | } elseif ( $contextNode->nodeName == 'tplarg' ) { |
||
1183 | # Triple-brace expansion |
||
1184 | $xpath = new DOMXPath( $contextNode->ownerDocument ); |
||
1185 | $titles = $xpath->query( 'title', $contextNode ); |
||
1186 | $title = $titles->item( 0 ); |
||
1187 | $parts = $xpath->query( 'part', $contextNode ); |
||
1188 | View Code Duplication | if ( $flags & PPFrame::NO_ARGS ) { |
|
1189 | $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts ); |
||
1190 | } else { |
||
1191 | $params = [ |
||
1192 | 'title' => new PPNode_DOM( $title ), |
||
1193 | 'parts' => new PPNode_DOM( $parts ) ]; |
||
1194 | $ret = $this->parser->argSubstitution( $params, $this ); |
||
1195 | if ( isset( $ret['object'] ) ) { |
||
1196 | $newIterator = $ret['object']; |
||
1197 | } else { |
||
1198 | $out .= $ret['text']; |
||
1199 | } |
||
1200 | } |
||
1201 | } elseif ( $contextNode->nodeName == 'comment' ) { |
||
1202 | # HTML-style comment |
||
1203 | # Remove it in HTML, pre+remove and STRIP_COMMENTS modes |
||
1204 | # Not in RECOVER_COMMENTS mode (msgnw) though. |
||
1205 | if ( ( $this->parser->ot['html'] |
||
1206 | || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) |
||
1207 | || ( $flags & PPFrame::STRIP_COMMENTS ) |
||
1208 | ) && !( $flags & PPFrame::RECOVER_COMMENTS ) |
||
1209 | ) { |
||
1210 | $out .= ''; |
||
1211 | } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { |
||
1212 | # Add a strip marker in PST mode so that pstPass2() can |
||
1213 | # run some old-fashioned regexes on the result. |
||
1214 | # Not in RECOVER_COMMENTS mode (extractSections) though. |
||
1215 | $out .= $this->parser->insertStripItem( $contextNode->textContent ); |
||
1216 | } else { |
||
1217 | # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove |
||
1218 | $out .= $contextNode->textContent; |
||
1219 | } |
||
1220 | } elseif ( $contextNode->nodeName == 'ignore' ) { |
||
1221 | # Output suppression used by <includeonly> etc. |
||
1222 | # OT_WIKI will only respect <ignore> in substed templates. |
||
1223 | # The other output types respect it unless NO_IGNORE is set. |
||
1224 | # extractSections() sets NO_IGNORE and so never respects it. |
||
1225 | View Code Duplication | if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) |
|
1226 | || ( $flags & PPFrame::NO_IGNORE ) |
||
1227 | ) { |
||
1228 | $out .= $contextNode->textContent; |
||
1229 | } else { |
||
1230 | $out .= ''; |
||
1231 | } |
||
1232 | } elseif ( $contextNode->nodeName == 'ext' ) { |
||
1233 | # Extension tag |
||
1234 | $xpath = new DOMXPath( $contextNode->ownerDocument ); |
||
1235 | $names = $xpath->query( 'name', $contextNode ); |
||
1236 | $attrs = $xpath->query( 'attr', $contextNode ); |
||
1237 | $inners = $xpath->query( 'inner', $contextNode ); |
||
1238 | $closes = $xpath->query( 'close', $contextNode ); |
||
1239 | if ( $flags & PPFrame::NO_TAGS ) { |
||
1240 | $s = '<' . $this->expand( $names->item( 0 ), $flags ); |
||
1241 | if ( $attrs->length > 0 ) { |
||
1242 | $s .= $this->expand( $attrs->item( 0 ), $flags ); |
||
1243 | } |
||
1244 | if ( $inners->length > 0 ) { |
||
1245 | $s .= '>' . $this->expand( $inners->item( 0 ), $flags ); |
||
1246 | if ( $closes->length > 0 ) { |
||
1247 | $s .= $this->expand( $closes->item( 0 ), $flags ); |
||
1248 | } |
||
1249 | } else { |
||
1250 | $s .= '/>'; |
||
1251 | } |
||
1252 | $out .= $s; |
||
1253 | } else { |
||
1254 | $params = [ |
||
1255 | 'name' => new PPNode_DOM( $names->item( 0 ) ), |
||
1256 | 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, |
||
1257 | 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, |
||
1258 | 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, |
||
1259 | ]; |
||
1260 | $out .= $this->parser->extensionSubstitution( $params, $this ); |
||
1261 | } |
||
1262 | } elseif ( $contextNode->nodeName == 'h' ) { |
||
1263 | # Heading |
||
1264 | $s = $this->expand( $contextNode->childNodes, $flags ); |
||
1265 | |||
1266 | # Insert a heading marker only for <h> children of <root> |
||
1267 | # This is to stop extractSections from going over multiple tree levels |
||
1268 | if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) { |
||
1269 | # Insert heading index marker |
||
1270 | $headingIndex = $contextNode->getAttribute( 'i' ); |
||
1271 | $titleText = $this->title->getPrefixedDBkey(); |
||
1272 | $this->parser->mHeadings[] = [ $titleText, $headingIndex ]; |
||
1273 | $serial = count( $this->parser->mHeadings ) - 1; |
||
1274 | $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX; |
||
1275 | $count = $contextNode->getAttribute( 'level' ); |
||
1276 | $s = substr( $s, 0, $count ) . $marker . substr( $s, $count ); |
||
1277 | $this->parser->mStripState->addGeneral( $marker, '' ); |
||
1278 | } |
||
1279 | $out .= $s; |
||
1280 | } else { |
||
1281 | # Generic recursive expansion |
||
1282 | $newIterator = $contextNode->childNodes; |
||
1283 | } |
||
1284 | } else { |
||
1285 | throw new MWException( __METHOD__ . ': Invalid parameter type' ); |
||
1286 | } |
||
1287 | |||
1288 | if ( $newIterator !== false ) { |
||
1289 | if ( $newIterator instanceof PPNode_DOM ) { |
||
1290 | $newIterator = $newIterator->node; |
||
1291 | } |
||
1292 | $outStack[] = ''; |
||
1293 | $iteratorStack[] = $newIterator; |
||
1294 | $indexStack[] = 0; |
||
1295 | View Code Duplication | } elseif ( $iteratorStack[$level] === false ) { |
|
1296 | // Return accumulated value to parent |
||
1297 | // With tail recursion |
||
1298 | while ( $iteratorStack[$level] === false && $level > 0 ) { |
||
1299 | $outStack[$level - 1] .= $out; |
||
1300 | array_pop( $outStack ); |
||
1301 | array_pop( $iteratorStack ); |
||
1302 | array_pop( $indexStack ); |
||
1303 | $level--; |
||
1304 | } |
||
1305 | } |
||
1306 | } |
||
1307 | --$expansionDepth; |
||
1308 | return $outStack[0]; |
||
1309 | } |
||
1310 | |||
1311 | /** |
||
1312 | * @param string $sep |
||
1313 | * @param int $flags |
||
1314 | * @param string|PPNode_DOM|DOMDocument $args,... |
||
1315 | * @return string |
||
1316 | */ |
||
1317 | View Code Duplication | public function implodeWithFlags( $sep, $flags /*, ... */ ) { |
|
1318 | $args = array_slice( func_get_args(), 2 ); |
||
1319 | |||
1320 | $first = true; |
||
1321 | $s = ''; |
||
1322 | foreach ( $args as $root ) { |
||
1323 | if ( $root instanceof PPNode_DOM ) { |
||
1324 | $root = $root->node; |
||
1325 | } |
||
1326 | if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { |
||
1327 | $root = [ $root ]; |
||
1328 | } |
||
1329 | foreach ( $root as $node ) { |
||
1330 | if ( $first ) { |
||
1331 | $first = false; |
||
1332 | } else { |
||
1333 | $s .= $sep; |
||
1334 | } |
||
1335 | $s .= $this->expand( $node, $flags ); |
||
1336 | } |
||
1337 | } |
||
1338 | return $s; |
||
1339 | } |
||
1340 | |||
1341 | /** |
||
1342 | * Implode with no flags specified |
||
1343 | * This previously called implodeWithFlags but has now been inlined to reduce stack depth |
||
1344 | * |
||
1345 | * @param string $sep |
||
1346 | * @param string|PPNode_DOM|DOMDocument $args,... |
||
1347 | * @return string |
||
1348 | */ |
||
1349 | View Code Duplication | public function implode( $sep /*, ... */ ) { |
|
1350 | $args = array_slice( func_get_args(), 1 ); |
||
1351 | |||
1352 | $first = true; |
||
1353 | $s = ''; |
||
1354 | foreach ( $args as $root ) { |
||
1355 | if ( $root instanceof PPNode_DOM ) { |
||
1356 | $root = $root->node; |
||
1357 | } |
||
1358 | if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { |
||
1359 | $root = [ $root ]; |
||
1360 | } |
||
1361 | foreach ( $root as $node ) { |
||
1362 | if ( $first ) { |
||
1363 | $first = false; |
||
1364 | } else { |
||
1365 | $s .= $sep; |
||
1366 | } |
||
1367 | $s .= $this->expand( $node ); |
||
1368 | } |
||
1369 | } |
||
1370 | return $s; |
||
1371 | } |
||
1372 | |||
1373 | /** |
||
1374 | * Makes an object that, when expand()ed, will be the same as one obtained |
||
1375 | * with implode() |
||
1376 | * |
||
1377 | * @param string $sep |
||
1378 | * @param string|PPNode_DOM|DOMDocument $args,... |
||
1379 | * @return array |
||
1380 | */ |
||
1381 | View Code Duplication | public function virtualImplode( $sep /*, ... */ ) { |
|
1382 | $args = array_slice( func_get_args(), 1 ); |
||
1383 | $out = []; |
||
1384 | $first = true; |
||
1385 | |||
1386 | foreach ( $args as $root ) { |
||
1387 | if ( $root instanceof PPNode_DOM ) { |
||
1388 | $root = $root->node; |
||
1389 | } |
||
1390 | if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { |
||
1391 | $root = [ $root ]; |
||
1392 | } |
||
1393 | foreach ( $root as $node ) { |
||
1394 | if ( $first ) { |
||
1395 | $first = false; |
||
1396 | } else { |
||
1397 | $out[] = $sep; |
||
1398 | } |
||
1399 | $out[] = $node; |
||
1400 | } |
||
1401 | } |
||
1402 | return $out; |
||
1403 | } |
||
1404 | |||
1405 | /** |
||
1406 | * Virtual implode with brackets |
||
1407 | * @param string $start |
||
1408 | * @param string $sep |
||
1409 | * @param string $end |
||
1410 | * @param string|PPNode_DOM|DOMDocument $args,... |
||
1411 | * @return array |
||
1412 | */ |
||
1413 | View Code Duplication | public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { |
|
1414 | $args = array_slice( func_get_args(), 3 ); |
||
1415 | $out = [ $start ]; |
||
1416 | $first = true; |
||
1417 | |||
1418 | foreach ( $args as $root ) { |
||
1419 | if ( $root instanceof PPNode_DOM ) { |
||
1420 | $root = $root->node; |
||
1421 | } |
||
1422 | if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { |
||
1423 | $root = [ $root ]; |
||
1424 | } |
||
1425 | foreach ( $root as $node ) { |
||
1426 | if ( $first ) { |
||
1427 | $first = false; |
||
1428 | } else { |
||
1429 | $out[] = $sep; |
||
1430 | } |
||
1431 | $out[] = $node; |
||
1432 | } |
||
1433 | } |
||
1434 | $out[] = $end; |
||
1435 | return $out; |
||
1436 | } |
||
1437 | |||
1438 | public function __toString() { |
||
1439 | return 'frame{}'; |
||
1440 | } |
||
1441 | |||
1442 | View Code Duplication | public function getPDBK( $level = false ) { |
|
1443 | if ( $level === false ) { |
||
1444 | return $this->title->getPrefixedDBkey(); |
||
1445 | } else { |
||
1446 | return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; |
||
1447 | } |
||
1448 | } |
||
1449 | |||
1450 | /** |
||
1451 | * @return array |
||
1452 | */ |
||
1453 | public function getArguments() { |
||
1454 | return []; |
||
1455 | } |
||
1456 | |||
1457 | /** |
||
1458 | * @return array |
||
1459 | */ |
||
1460 | public function getNumberedArguments() { |
||
1461 | return []; |
||
1462 | } |
||
1463 | |||
1464 | /** |
||
1465 | * @return array |
||
1466 | */ |
||
1467 | public function getNamedArguments() { |
||
1468 | return []; |
||
1469 | } |
||
1470 | |||
1471 | /** |
||
1472 | * Returns true if there are no arguments in this frame |
||
1473 | * |
||
1474 | * @return bool |
||
1475 | */ |
||
1476 | public function isEmpty() { |
||
1477 | return true; |
||
1478 | } |
||
1479 | |||
1480 | /** |
||
1481 | * @param int|string $name |
||
1482 | * @return bool Always false in this implementation. |
||
1483 | */ |
||
1484 | public function getArgument( $name ) { |
||
1485 | return false; |
||
1486 | } |
||
1487 | |||
1488 | /** |
||
1489 | * Returns true if the infinite loop check is OK, false if a loop is detected |
||
1490 | * |
||
1491 | * @param Title $title |
||
1492 | * @return bool |
||
1493 | */ |
||
1494 | public function loopCheck( $title ) { |
||
1495 | return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); |
||
1496 | } |
||
1497 | |||
1498 | /** |
||
1499 | * Return true if the frame is a template frame |
||
1500 | * |
||
1501 | * @return bool |
||
1502 | */ |
||
1503 | public function isTemplate() { |
||
1504 | return false; |
||
1505 | } |
||
1506 | |||
1507 | /** |
||
1508 | * Get a title of frame |
||
1509 | * |
||
1510 | * @return Title |
||
1511 | */ |
||
1512 | public function getTitle() { |
||
1513 | return $this->title; |
||
1514 | } |
||
1515 | |||
1516 | /** |
||
1517 | * Set the volatile flag |
||
1518 | * |
||
1519 | * @param bool $flag |
||
1520 | */ |
||
1521 | public function setVolatile( $flag = true ) { |
||
1522 | $this->volatile = $flag; |
||
1523 | } |
||
1524 | |||
1525 | /** |
||
1526 | * Get the volatile flag |
||
1527 | * |
||
1528 | * @return bool |
||
1529 | */ |
||
1530 | public function isVolatile() { |
||
1531 | return $this->volatile; |
||
1532 | } |
||
1533 | |||
1534 | /** |
||
1535 | * Set the TTL |
||
1536 | * |
||
1537 | * @param int $ttl |
||
1538 | */ |
||
1539 | public function setTTL( $ttl ) { |
||
1540 | View Code Duplication | if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) { |
|
1541 | $this->ttl = $ttl; |
||
1542 | } |
||
1543 | } |
||
1544 | |||
1545 | /** |
||
1546 | * Get the TTL |
||
1547 | * |
||
1548 | * @return int|null |
||
1549 | */ |
||
1550 | public function getTTL() { |
||
1551 | return $this->ttl; |
||
1552 | } |
||
1553 | } |
||
1554 | |||
1555 | /** |
||
1556 | * Expansion frame with template arguments |
||
1557 | * @ingroup Parser |
||
1558 | */ |
||
1559 | // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps |
||
1560 | View Code Duplication | class PPTemplateFrame_DOM extends PPFrame_DOM { |
|
1561 | // @codingStandardsIgnoreEnd |
||
1562 | |||
1563 | public $numberedArgs, $namedArgs; |
||
1564 | |||
1565 | /** |
||
1566 | * @var PPFrame_DOM |
||
1567 | */ |
||
1568 | public $parent; |
||
1569 | public $numberedExpansionCache, $namedExpansionCache; |
||
1570 | |||
1571 | /** |
||
1572 | * @param Preprocessor $preprocessor |
||
1573 | * @param bool|PPFrame_DOM $parent |
||
1574 | * @param array $numberedArgs |
||
1575 | * @param array $namedArgs |
||
1576 | * @param bool|Title $title |
||
1577 | */ |
||
1578 | public function __construct( $preprocessor, $parent = false, $numberedArgs = [], |
||
1579 | $namedArgs = [], $title = false |
||
1580 | ) { |
||
1581 | parent::__construct( $preprocessor ); |
||
1582 | |||
1583 | $this->parent = $parent; |
||
1584 | $this->numberedArgs = $numberedArgs; |
||
1585 | $this->namedArgs = $namedArgs; |
||
1586 | $this->title = $title; |
||
1587 | $pdbk = $title ? $title->getPrefixedDBkey() : false; |
||
1588 | $this->titleCache = $parent->titleCache; |
||
1589 | $this->titleCache[] = $pdbk; |
||
1590 | $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; |
||
1591 | if ( $pdbk !== false ) { |
||
1592 | $this->loopCheckHash[$pdbk] = true; |
||
1593 | } |
||
1594 | $this->depth = $parent->depth + 1; |
||
1595 | $this->numberedExpansionCache = $this->namedExpansionCache = []; |
||
1596 | } |
||
1597 | |||
1598 | public function __toString() { |
||
1599 | $s = 'tplframe{'; |
||
1600 | $first = true; |
||
1601 | $args = $this->numberedArgs + $this->namedArgs; |
||
1602 | foreach ( $args as $name => $value ) { |
||
1603 | if ( $first ) { |
||
1604 | $first = false; |
||
1605 | } else { |
||
1606 | $s .= ', '; |
||
1607 | } |
||
1608 | $s .= "\"$name\":\"" . |
||
1609 | str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"'; |
||
1610 | } |
||
1611 | $s .= '}'; |
||
1612 | return $s; |
||
1613 | } |
||
1614 | |||
1615 | /** |
||
1616 | * @throws MWException |
||
1617 | * @param string|int $key |
||
1618 | * @param string|PPNode_DOM|DOMDocument $root |
||
1619 | * @param int $flags |
||
1620 | * @return string |
||
1621 | */ |
||
1622 | public function cachedExpand( $key, $root, $flags = 0 ) { |
||
1623 | if ( isset( $this->parent->childExpansionCache[$key] ) ) { |
||
1624 | return $this->parent->childExpansionCache[$key]; |
||
1625 | } |
||
1626 | $retval = $this->expand( $root, $flags ); |
||
1627 | if ( !$this->isVolatile() ) { |
||
1628 | $this->parent->childExpansionCache[$key] = $retval; |
||
1629 | } |
||
1630 | return $retval; |
||
1631 | } |
||
1632 | |||
1633 | /** |
||
1634 | * Returns true if there are no arguments in this frame |
||
1635 | * |
||
1636 | * @return bool |
||
1637 | */ |
||
1638 | public function isEmpty() { |
||
1639 | return !count( $this->numberedArgs ) && !count( $this->namedArgs ); |
||
1640 | } |
||
1641 | |||
1642 | public function getArguments() { |
||
1643 | $arguments = []; |
||
1644 | foreach ( array_merge( |
||
1645 | array_keys( $this->numberedArgs ), |
||
1646 | array_keys( $this->namedArgs ) ) as $key ) { |
||
1647 | $arguments[$key] = $this->getArgument( $key ); |
||
1648 | } |
||
1649 | return $arguments; |
||
1650 | } |
||
1651 | |||
1652 | public function getNumberedArguments() { |
||
1653 | $arguments = []; |
||
1654 | foreach ( array_keys( $this->numberedArgs ) as $key ) { |
||
1655 | $arguments[$key] = $this->getArgument( $key ); |
||
1656 | } |
||
1657 | return $arguments; |
||
1658 | } |
||
1659 | |||
1660 | public function getNamedArguments() { |
||
1661 | $arguments = []; |
||
1662 | foreach ( array_keys( $this->namedArgs ) as $key ) { |
||
1663 | $arguments[$key] = $this->getArgument( $key ); |
||
1664 | } |
||
1665 | return $arguments; |
||
1666 | } |
||
1667 | |||
1668 | /** |
||
1669 | * @param int $index |
||
1670 | * @return string|bool |
||
1671 | */ |
||
1672 | public function getNumberedArgument( $index ) { |
||
1673 | if ( !isset( $this->numberedArgs[$index] ) ) { |
||
1674 | return false; |
||
1675 | } |
||
1676 | if ( !isset( $this->numberedExpansionCache[$index] ) ) { |
||
1677 | # No trimming for unnamed arguments |
||
1678 | $this->numberedExpansionCache[$index] = $this->parent->expand( |
||
1679 | $this->numberedArgs[$index], |
||
1680 | PPFrame::STRIP_COMMENTS |
||
1681 | ); |
||
1682 | } |
||
1683 | return $this->numberedExpansionCache[$index]; |
||
1684 | } |
||
1685 | |||
1686 | /** |
||
1687 | * @param string $name |
||
1688 | * @return string|bool |
||
1689 | */ |
||
1690 | public function getNamedArgument( $name ) { |
||
1691 | if ( !isset( $this->namedArgs[$name] ) ) { |
||
1692 | return false; |
||
1693 | } |
||
1694 | if ( !isset( $this->namedExpansionCache[$name] ) ) { |
||
1695 | # Trim named arguments post-expand, for backwards compatibility |
||
1696 | $this->namedExpansionCache[$name] = trim( |
||
1697 | $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) ); |
||
1698 | } |
||
1699 | return $this->namedExpansionCache[$name]; |
||
1700 | } |
||
1701 | |||
1702 | /** |
||
1703 | * @param int|string $name |
||
1704 | * @return string|bool |
||
1705 | */ |
||
1706 | public function getArgument( $name ) { |
||
1707 | $text = $this->getNumberedArgument( $name ); |
||
1708 | if ( $text === false ) { |
||
1709 | $text = $this->getNamedArgument( $name ); |
||
1710 | } |
||
1711 | return $text; |
||
1712 | } |
||
1713 | |||
1714 | /** |
||
1715 | * Return true if the frame is a template frame |
||
1716 | * |
||
1717 | * @return bool |
||
1718 | */ |
||
1719 | public function isTemplate() { |
||
1720 | return true; |
||
1721 | } |
||
1722 | |||
1723 | public function setVolatile( $flag = true ) { |
||
1724 | parent::setVolatile( $flag ); |
||
1725 | $this->parent->setVolatile( $flag ); |
||
1726 | } |
||
1727 | |||
1728 | public function setTTL( $ttl ) { |
||
1729 | parent::setTTL( $ttl ); |
||
1730 | $this->parent->setTTL( $ttl ); |
||
1731 | } |
||
1732 | } |
||
1733 | |||
1734 | /** |
||
1735 | * Expansion frame with custom arguments |
||
1736 | * @ingroup Parser |
||
1737 | */ |
||
1738 | // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps |
||
1739 | View Code Duplication | class PPCustomFrame_DOM extends PPFrame_DOM { |
|
1740 | // @codingStandardsIgnoreEnd |
||
1741 | |||
1742 | public $args; |
||
1743 | |||
1744 | public function __construct( $preprocessor, $args ) { |
||
1745 | parent::__construct( $preprocessor ); |
||
1746 | $this->args = $args; |
||
1747 | } |
||
1748 | |||
1749 | public function __toString() { |
||
1750 | $s = 'cstmframe{'; |
||
1751 | $first = true; |
||
1752 | foreach ( $this->args as $name => $value ) { |
||
1753 | if ( $first ) { |
||
1754 | $first = false; |
||
1755 | } else { |
||
1756 | $s .= ', '; |
||
1757 | } |
||
1758 | $s .= "\"$name\":\"" . |
||
1759 | str_replace( '"', '\\"', $value->__toString() ) . '"'; |
||
1760 | } |
||
1761 | $s .= '}'; |
||
1762 | return $s; |
||
1763 | } |
||
1764 | |||
1765 | /** |
||
1766 | * @return bool |
||
1767 | */ |
||
1768 | public function isEmpty() { |
||
1769 | return !count( $this->args ); |
||
1770 | } |
||
1771 | |||
1772 | /** |
||
1773 | * @param int|string $index |
||
1774 | * @return string|bool |
||
1775 | */ |
||
1776 | public function getArgument( $index ) { |
||
1777 | if ( !isset( $this->args[$index] ) ) { |
||
1778 | return false; |
||
1779 | } |
||
1780 | return $this->args[$index]; |
||
1781 | } |
||
1782 | |||
1783 | public function getArguments() { |
||
1784 | return $this->args; |
||
1785 | } |
||
1786 | } |
||
1787 | |||
1788 | /** |
||
1789 | * @ingroup Parser |
||
1790 | */ |
||
1791 | // @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps |
||
1792 | class PPNode_DOM implements PPNode { |
||
1793 | // @codingStandardsIgnoreEnd |
||
1794 | |||
1795 | /** |
||
1796 | * @var DOMElement |
||
1797 | */ |
||
1798 | public $node; |
||
1799 | public $xpath; |
||
1800 | |||
1801 | public function __construct( $node, $xpath = false ) { |
||
1802 | $this->node = $node; |
||
1803 | } |
||
1804 | |||
1805 | /** |
||
1806 | * @return DOMXPath |
||
1807 | */ |
||
1808 | public function getXPath() { |
||
1809 | if ( $this->xpath === null ) { |
||
1810 | $this->xpath = new DOMXPath( $this->node->ownerDocument ); |
||
1811 | } |
||
1812 | return $this->xpath; |
||
1813 | } |
||
1814 | |||
1815 | public function __toString() { |
||
1816 | if ( $this->node instanceof DOMNodeList ) { |
||
1817 | $s = ''; |
||
1818 | foreach ( $this->node as $node ) { |
||
1819 | $s .= $node->ownerDocument->saveXML( $node ); |
||
1820 | } |
||
1821 | } else { |
||
1822 | $s = $this->node->ownerDocument->saveXML( $this->node ); |
||
1823 | } |
||
1824 | return $s; |
||
1825 | } |
||
1826 | |||
1827 | /** |
||
1828 | * @return bool|PPNode_DOM |
||
1829 | */ |
||
1830 | public function getChildren() { |
||
1831 | return $this->node->childNodes ? new self( $this->node->childNodes ) : false; |
||
1832 | } |
||
1833 | |||
1834 | /** |
||
1835 | * @return bool|PPNode_DOM |
||
1836 | */ |
||
1837 | public function getFirstChild() { |
||
1838 | return $this->node->firstChild ? new self( $this->node->firstChild ) : false; |
||
1839 | } |
||
1840 | |||
1841 | /** |
||
1842 | * @return bool|PPNode_DOM |
||
1843 | */ |
||
1844 | public function getNextSibling() { |
||
1845 | return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false; |
||
1846 | } |
||
1847 | |||
1848 | /** |
||
1849 | * @param string $type |
||
1850 | * |
||
1851 | * @return bool|PPNode_DOM |
||
1852 | */ |
||
1853 | public function getChildrenOfType( $type ) { |
||
1854 | return new self( $this->getXPath()->query( $type, $this->node ) ); |
||
1855 | } |
||
1856 | |||
1857 | /** |
||
1858 | * @return int |
||
1859 | */ |
||
1860 | public function getLength() { |
||
1861 | if ( $this->node instanceof DOMNodeList ) { |
||
1862 | return $this->node->length; |
||
1863 | } else { |
||
1864 | return false; |
||
1865 | } |
||
1866 | } |
||
1867 | |||
1868 | /** |
||
1869 | * @param int $i |
||
1870 | * @return bool|PPNode_DOM |
||
1871 | */ |
||
1872 | public function item( $i ) { |
||
1873 | $item = $this->node->item( $i ); |
||
1874 | return $item ? new self( $item ) : false; |
||
1875 | } |
||
1876 | |||
1877 | /** |
||
1878 | * @return string |
||
1879 | */ |
||
1880 | public function getName() { |
||
1881 | if ( $this->node instanceof DOMNodeList ) { |
||
1882 | return '#nodelist'; |
||
1883 | } else { |
||
1884 | return $this->node->nodeName; |
||
1885 | } |
||
1886 | } |
||
1887 | |||
1888 | /** |
||
1889 | * Split a "<part>" node into an associative array containing: |
||
1890 | * - name PPNode name |
||
1891 | * - index String index |
||
1892 | * - value PPNode value |
||
1893 | * |
||
1894 | * @throws MWException |
||
1895 | * @return array |
||
1896 | */ |
||
1897 | public function splitArg() { |
||
1898 | $xpath = $this->getXPath(); |
||
1899 | $names = $xpath->query( 'name', $this->node ); |
||
1900 | $values = $xpath->query( 'value', $this->node ); |
||
1901 | if ( !$names->length || !$values->length ) { |
||
1902 | throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); |
||
1903 | } |
||
1904 | $name = $names->item( 0 ); |
||
1905 | $index = $name->getAttribute( 'index' ); |
||
1906 | return [ |
||
1907 | 'name' => new self( $name ), |
||
1908 | 'index' => $index, |
||
1909 | 'value' => new self( $values->item( 0 ) ) ]; |
||
1910 | } |
||
1911 | |||
1912 | /** |
||
1913 | * Split an "<ext>" node into an associative array containing name, attr, inner and close |
||
1914 | * All values in the resulting array are PPNodes. Inner and close are optional. |
||
1915 | * |
||
1916 | * @throws MWException |
||
1917 | * @return array |
||
1918 | */ |
||
1919 | public function splitExt() { |
||
1920 | $xpath = $this->getXPath(); |
||
1921 | $names = $xpath->query( 'name', $this->node ); |
||
1922 | $attrs = $xpath->query( 'attr', $this->node ); |
||
1923 | $inners = $xpath->query( 'inner', $this->node ); |
||
1924 | $closes = $xpath->query( 'close', $this->node ); |
||
1925 | if ( !$names->length || !$attrs->length ) { |
||
1926 | throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); |
||
1927 | } |
||
1928 | $parts = [ |
||
1929 | 'name' => new self( $names->item( 0 ) ), |
||
1930 | 'attr' => new self( $attrs->item( 0 ) ) ]; |
||
1931 | if ( $inners->length ) { |
||
1932 | $parts['inner'] = new self( $inners->item( 0 ) ); |
||
1933 | } |
||
1934 | if ( $closes->length ) { |
||
1935 | $parts['close'] = new self( $closes->item( 0 ) ); |
||
1936 | } |
||
1937 | return $parts; |
||
1938 | } |
||
1939 | |||
1940 | /** |
||
1941 | * Split a "<h>" node |
||
1942 | * @throws MWException |
||
1943 | * @return array |
||
1944 | */ |
||
1945 | public function splitHeading() { |
||
1946 | if ( $this->getName() !== 'h' ) { |
||
1947 | throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); |
||
1948 | } |
||
1949 | return [ |
||
1950 | 'i' => $this->node->getAttribute( 'i' ), |
||
1951 | 'level' => $this->node->getAttribute( 'level' ), |
||
1952 | 'contents' => $this->getChildren() |
||
1953 | ]; |
||
1954 | } |
||
1955 | } |
||
1956 |
If you define a variable conditionally, it can happen that it is not defined for all execution paths.
Let’s take a look at an example:
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.
Available Fixes
Check for existence of the variable explicitly:
Define a default value for the variable:
Add a value for the missing path: