Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/parser/Preprocessor_DOM.php (28 issues)

Upgrade to new PHP Analysis Engine

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
The variable $obj does not seem to be defined for all execution paths leading up to this point.

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:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

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

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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;
0 ignored issues
show
The property close does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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 .= '&lt;';
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 .= '&lt;';
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 ) {
0 ignored issues
show
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
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" );
0 ignored issues
show
The property open does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
The property count does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
556
				$equalsLength = strspn( $revText, '=', $lengthText - $searchStart );
557
				if ( $equalsLength > 0 ) {
558 View Code Duplication
					if ( $searchStart - $equalsLength == $piece->startPos ) {
0 ignored issues
show
The property startPos does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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 );
0 ignored issues
show
The method breakSyntax() does not seem to exist on object<PPDStack>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
653
				} else {
654
					# Create XML element
655
					# Note: $parts is already XML, does not need to be encoded further
656
					$parts = $piece->parts;
0 ignored issues
show
The property parts does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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 ) ) {
0 ignored issues
show
The property lineStart does not seem to exist in PPDStack.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type object<PPDStack> of property $top.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
752
		$this->rootAccum = '';
753
		$this->accum =& $this->rootAccum;
0 ignored issues
show
The property accum does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like self::$false of type boolean is incompatible with the declared type object<PPDStack> of property $top.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 ];
0 ignored issues
show
Documentation Bug introduced by
It seems like array(new $class()) of type array<integer,object,{"0":"object"}> is incompatible with the declared type array<integer,object<PPDPart>> of property $parts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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;
0 ignored issues
show
The property parser does not seem to exist in Preprocessor.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Accessing node on the interface PPNode suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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 ) {
0 ignored issues
show
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
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,...
0 ignored issues
show
There is no parameter named $args,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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,...
0 ignored issues
show
There is no parameter named $args,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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,...
0 ignored issues
show
There is no parameter named $args,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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,...
0 ignored issues
show
There is no parameter named $args,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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 {
0 ignored issues
show
This class seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1561
	// @codingStandardsIgnoreEnd
1562
1563
	public $numberedArgs, $namedArgs;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
1564
1565
	/**
1566
	 * @var PPFrame_DOM
1567
	 */
1568
	public $parent;
1569
	public $numberedExpansionCache, $namedExpansionCache;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parent can also be of type boolean. However, the property $parent is declared as type object<PPFrame_DOM>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1584
		$this->numberedArgs = $numberedArgs;
1585
		$this->namedArgs = $namedArgs;
1586
		$this->title = $title;
0 ignored issues
show
Documentation Bug introduced by
It seems like $title can also be of type boolean. However, the property $title is declared as type object<Title>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1587
		$pdbk = $title ? $title->getPrefixedDBkey() : false;
0 ignored issues
show
It seems like $title is not always an object, but can also be of type boolean. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
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 {
0 ignored issues
show
This class seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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