Parser::braceSubstitution()   F
last analyzed

Complexity

Conditions 66
Paths > 20000

Size

Total Lines 309
Code Lines 179

Duplication

Lines 7
Ratio 2.27 %

Importance

Changes 0
Metric Value
cc 66
eloc 179
c 0
b 0
f 0
nc 197184600
nop 2
dl 7
loc 309
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHP parser that converts wiki markup to HTML.
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
use MediaWiki\Linker\LinkRenderer;
24
use MediaWiki\MediaWikiServices;
25
use Wikimedia\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
26
27
/**
28
 * @defgroup Parser Parser
29
 */
30
31
/**
32
 * PHP Parser - Processes wiki markup (which uses a more user-friendly
33
 * syntax, such as "[[link]]" for making links), and provides a one-way
34
 * transformation of that wiki markup it into (X)HTML output / markup
35
 * (which in turn the browser understands, and can display).
36
 *
37
 * There are seven main entry points into the Parser class:
38
 *
39
 * - Parser::parse()
40
 *     produces HTML output
41
 * - Parser::preSaveTransform()
42
 *     produces altered wiki markup
43
 * - Parser::preprocess()
44
 *     removes HTML comments and expands templates
45
 * - Parser::cleanSig() and Parser::cleanSigInSig()
46
 *     cleans a signature before saving it to preferences
47
 * - Parser::getSection()
48
 *     return the content of a section from an article for section editing
49
 * - Parser::replaceSection()
50
 *     replaces a section by number inside an article
51
 * - Parser::getPreloadText()
52
 *     removes <noinclude> sections and <includeonly> tags
53
 *
54
 * Globals used:
55
 *    object: $wgContLang
56
 *
57
 * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
58
 *
59
 * @par Settings:
60
 * $wgNamespacesWithSubpages
61
 *
62
 * @par Settings only within ParserOptions:
63
 * $wgAllowExternalImages
64
 * $wgAllowSpecialInclusion
65
 * $wgInterwikiMagic
66
 * $wgMaxArticleSize
67
 *
68
 * @ingroup Parser
69
 */
70
class Parser {
71
	/**
72
	 * Update this version number when the ParserOutput format
73
	 * changes in an incompatible way, so the parser cache
74
	 * can automatically discard old data.
75
	 */
76
	const VERSION = '1.6.4';
77
78
	/**
79
	 * Update this version number when the output of serialiseHalfParsedText()
80
	 * changes in an incompatible way
81
	 */
82
	const HALF_PARSED_VERSION = 2;
83
84
	# Flags for Parser::setFunctionHook
85
	const SFH_NO_HASH = 1;
86
	const SFH_OBJECT_ARGS = 2;
87
88
	# Constants needed for external link processing
89
	# Everything except bracket, space, or control characters
90
	# \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
91
	# as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
92
	const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
93
	# Simplified expression to match an IPv4 or IPv6 address, or
94
	# at least one character of a host name (embeds EXT_LINK_URL_CLASS)
95
	const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
96
	# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
97
	// @codingStandardsIgnoreStart Generic.Files.LineLength
98
	const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
99
		\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
100
	// @codingStandardsIgnoreEnd
101
102
	# Regular expression for a non-newline space
103
	const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
104
105
	# Flags for preprocessToDom
106
	const PTD_FOR_INCLUSION = 1;
107
108
	# Allowed values for $this->mOutputType
109
	# Parameter to startExternalParse().
110
	const OT_HTML = 1; # like parse()
111
	const OT_WIKI = 2; # like preSaveTransform()
112
	const OT_PREPROCESS = 3; # like preprocess()
113
	const OT_MSG = 3;
114
	const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
115
116
	/**
117
	 * @var string Prefix and suffix for temporary replacement strings
118
	 * for the multipass parser.
119
	 *
120
	 * \x7f should never appear in input as it's disallowed in XML.
121
	 * Using it at the front also gives us a little extra robustness
122
	 * since it shouldn't match when butted up against identifier-like
123
	 * string constructs.
124
	 *
125
	 * Must not consist of all title characters, or else it will change
126
	 * the behavior of <nowiki> in a link.
127
	 *
128
	 * Must have a character that needs escaping in attributes, otherwise
129
	 * someone could put a strip marker in an attribute, to get around
130
	 * escaping quote marks, and break out of the attribute. Thus we add
131
	 * `'".
132
	 */
133
	const MARKER_SUFFIX = "-QINU`\"'\x7f";
134
	const MARKER_PREFIX = "\x7f'\"`UNIQ-";
135
136
	# Markers used for wrapping the table of contents
137
	const TOC_START = '<mw:toc>';
138
	const TOC_END = '</mw:toc>';
139
140
	# Persistent:
141
	public $mTagHooks = [];
142
	public $mTransparentTagHooks = [];
143
	public $mFunctionHooks = [];
144
	public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
145
	public $mFunctionTagHooks = [];
146
	public $mStripList = [];
147
	public $mDefaultStripList = [];
148
	public $mVarCache = [];
149
	public $mImageParams = [];
150
	public $mImageParamsMagicArray = [];
151
	public $mMarkerIndex = 0;
152
	public $mFirstCall = true;
153
154
	# Initialised by initialiseVariables()
155
156
	/**
157
	 * @var MagicWordArray
158
	 */
159
	public $mVariables;
160
161
	/**
162
	 * @var MagicWordArray
163
	 */
164
	public $mSubstWords;
165
	# Initialised in constructor
166
	public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
0 ignored issues
show
Coding Style introduced by
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...
167
168
	# Initialized in getPreprocessor()
169
	/** @var Preprocessor */
170
	public $mPreprocessor;
171
172
	# Cleared with clearState():
173
	/**
174
	 * @var ParserOutput
175
	 */
176
	public $mOutput;
177
	public $mAutonumber;
178
179
	/**
180
	 * @var StripState
181
	 */
182
	public $mStripState;
183
184
	public $mIncludeCount;
185
	/**
186
	 * @var LinkHolderArray
187
	 */
188
	public $mLinkHolders;
189
190
	public $mLinkID;
191
	public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
0 ignored issues
show
Coding Style introduced by
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...
192
	public $mDefaultSort;
193
	public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
0 ignored issues
show
Coding Style introduced by
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...
194
	public $mExpensiveFunctionCount; # number of expensive parser function calls
195
	public $mShowToc, $mForceTocPosition;
0 ignored issues
show
Coding Style introduced by
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...
196
197
	/**
198
	 * @var User
199
	 */
200
	public $mUser; # User object; only used when doing pre-save transform
201
202
	# Temporary
203
	# These are variables reset at least once per parse regardless of $clearState
204
205
	/**
206
	 * @var ParserOptions
207
	 */
208
	public $mOptions;
209
210
	/**
211
	 * @var Title
212
	 */
213
	public $mTitle;        # Title context, used for self-link rendering and similar things
214
	public $mOutputType;   # Output type, one of the OT_xxx constants
215
	public $ot;            # Shortcut alias, see setOutputType()
216
	public $mRevisionObject; # The revision object of the specified revision ID
217
	public $mRevisionId;   # ID to display in {{REVISIONID}} tags
218
	public $mRevisionTimestamp; # The timestamp of the specified revision ID
219
	public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
220
	public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
221
	public $mRevIdForTs;   # The revision ID which was used to fetch the timestamp
222
	public $mInputSize = false; # For {{PAGESIZE}} on current page.
223
224
	/**
225
	 * @var string Deprecated accessor for the strip marker prefix.
226
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
227
	 **/
228
	public $mUniqPrefix = Parser::MARKER_PREFIX;
229
230
	/**
231
	 * @var array Array with the language name of each language link (i.e. the
232
	 * interwiki prefix) in the key, value arbitrary. Used to avoid sending
233
	 * duplicate language links to the ParserOutput.
234
	 */
235
	public $mLangLinkLanguages;
236
237
	/**
238
	 * @var MapCacheLRU|null
239
	 * @since 1.24
240
	 *
241
	 * A cache of the current revisions of titles. Keys are $title->getPrefixedDbKey()
242
	 */
243
	public $currentRevisionCache;
244
245
	/**
246
	 * @var bool Recursive call protection.
247
	 * This variable should be treated as if it were private.
248
	 */
249
	public $mInParse = false;
250
251
	/** @var SectionProfiler */
252
	protected $mProfiler;
253
254
	/**
255
	 * @var LinkRenderer
256
	 */
257
	protected $mLinkRenderer;
258
259
	/**
260
	 * @param array $conf
261
	 */
262
	public function __construct( $conf = [] ) {
263
		$this->mConf = $conf;
264
		$this->mUrlProtocols = wfUrlProtocols();
265
		$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
266
			self::EXT_LINK_ADDR .
267
			self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
268
		if ( isset( $conf['preprocessorClass'] ) ) {
269
			$this->mPreprocessorClass = $conf['preprocessorClass'];
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
270
		} elseif ( defined( 'HPHP_VERSION' ) ) {
271
			# Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
272
			$this->mPreprocessorClass = 'Preprocessor_Hash';
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
273
		} elseif ( extension_loaded( 'domxml' ) ) {
274
			# PECL extension that conflicts with the core DOM extension (bug 13770)
275
			wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
276
			$this->mPreprocessorClass = 'Preprocessor_Hash';
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
277
		} elseif ( extension_loaded( 'dom' ) ) {
278
			$this->mPreprocessorClass = 'Preprocessor_DOM';
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
279
		} else {
280
			$this->mPreprocessorClass = 'Preprocessor_Hash';
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
281
		}
282
		wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
283
	}
284
285
	/**
286
	 * Reduce memory usage to reduce the impact of circular references
287
	 */
288
	public function __destruct() {
289
		if ( isset( $this->mLinkHolders ) ) {
290
			unset( $this->mLinkHolders );
291
		}
292
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Parser> is not traversable.
Loading history...
293
			unset( $this->$name );
294
		}
295
	}
296
297
	/**
298
	 * Allow extensions to clean up when the parser is cloned
299
	 */
300
	public function __clone() {
301
		$this->mInParse = false;
302
303
		// Bug 56226: When you create a reference "to" an object field, that
304
		// makes the object field itself be a reference too (until the other
305
		// reference goes out of scope). When cloning, any field that's a
306
		// reference is copied as a reference in the new object. Both of these
307
		// are defined PHP5 behaviors, as inconvenient as it is for us when old
308
		// hooks from PHP4 days are passing fields by reference.
309
		foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
310
			// Make a non-reference copy of the field, then rebind the field to
311
			// reference the new copy.
312
			$tmp = $this->$k;
313
			$this->$k =& $tmp;
314
			unset( $tmp );
315
		}
316
317
		Hooks::run( 'ParserCloned', [ $this ] );
318
	}
319
320
	/**
321
	 * Do various kinds of initialisation on the first call of the parser
322
	 */
323
	public function firstCallInit() {
324
		if ( !$this->mFirstCall ) {
325
			return;
326
		}
327
		$this->mFirstCall = false;
328
329
		CoreParserFunctions::register( $this );
330
		CoreTagHooks::register( $this );
331
		$this->initialiseVariables();
332
333
		Hooks::run( 'ParserFirstCallInit', [ &$this ] );
334
	}
335
336
	/**
337
	 * Clear Parser state
338
	 *
339
	 * @private
340
	 */
341
	public function clearState() {
342
		if ( $this->mFirstCall ) {
343
			$this->firstCallInit();
344
		}
345
		$this->mOutput = new ParserOutput;
346
		$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
347
		$this->mAutonumber = 0;
348
		$this->mIncludeCount = [];
349
		$this->mLinkHolders = new LinkHolderArray( $this );
350
		$this->mLinkID = 0;
351
		$this->mRevisionObject = $this->mRevisionTimestamp =
352
			$this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
353
		$this->mVarCache = [];
354
		$this->mUser = null;
355
		$this->mLangLinkLanguages = [];
356
		$this->currentRevisionCache = null;
357
358
		$this->mStripState = new StripState;
359
360
		# Clear these on every parse, bug 4549
361
		$this->mTplRedirCache = $this->mTplDomCache = [];
362
363
		$this->mShowToc = true;
364
		$this->mForceTocPosition = false;
365
		$this->mIncludeSizes = [
366
			'post-expand' => 0,
367
			'arg' => 0,
368
		];
369
		$this->mPPNodeCount = 0;
370
		$this->mGeneratedPPNodeCount = 0;
371
		$this->mHighestExpansionDepth = 0;
372
		$this->mDefaultSort = false;
373
		$this->mHeadings = [];
374
		$this->mDoubleUnderscores = [];
375
		$this->mExpensiveFunctionCount = 0;
376
377
		# Fix cloning
378
		if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
0 ignored issues
show
Bug introduced by
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...
379
			$this->mPreprocessor = null;
380
		}
381
382
		$this->mProfiler = new SectionProfiler();
383
384
		Hooks::run( 'ParserClearState', [ &$this ] );
385
	}
386
387
	/**
388
	 * Convert wikitext to HTML
389
	 * Do not call this function recursively.
390
	 *
391
	 * @param string $text Text we want to parse
392
	 * @param Title $title
393
	 * @param ParserOptions $options
394
	 * @param bool $linestart
395
	 * @param bool $clearState
396
	 * @param int $revid Number to pass in {{REVISIONID}}
397
	 * @return ParserOutput A ParserOutput
398
	 */
399
	public function parse(
400
		$text, Title $title, ParserOptions $options,
401
		$linestart = true, $clearState = true, $revid = null
402
	) {
403
		/**
404
		 * First pass--just handle <nowiki> sections, pass the rest off
405
		 * to internalParse() which does all the real work.
406
		 */
407
408
		global $wgShowHostnames;
409
410
		if ( $clearState ) {
411
			// We use U+007F DELETE to construct strip markers, so we have to make
412
			// sure that this character does not occur in the input text.
413
			$text = strtr( $text, "\x7f", "?" );
414
			$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
415
		}
416
417
		$this->startParse( $title, $options, self::OT_HTML, $clearState );
418
419
		$this->currentRevisionCache = null;
420
		$this->mInputSize = strlen( $text );
0 ignored issues
show
Documentation Bug introduced by
The property $mInputSize was declared of type boolean, but strlen($text) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
421
		if ( $this->mOptions->getEnableLimitReport() ) {
422
			$this->mOutput->resetParseStartTime();
423
		}
424
425
		$oldRevisionId = $this->mRevisionId;
426
		$oldRevisionObject = $this->mRevisionObject;
427
		$oldRevisionTimestamp = $this->mRevisionTimestamp;
428
		$oldRevisionUser = $this->mRevisionUser;
429
		$oldRevisionSize = $this->mRevisionSize;
430
		if ( $revid !== null ) {
431
			$this->mRevisionId = $revid;
432
			$this->mRevisionObject = null;
433
			$this->mRevisionTimestamp = null;
434
			$this->mRevisionUser = null;
435
			$this->mRevisionSize = null;
436
		}
437
438
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
439
		# No more strip!
440
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
441
		$text = $this->internalParse( $text );
442
		Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] );
443
444
		$text = $this->internalParseHalfParsed( $text, true, $linestart );
445
446
		/**
447
		 * A converted title will be provided in the output object if title and
448
		 * content conversion are enabled, the article text does not contain
449
		 * a conversion-suppressing double-underscore tag, and no
450
		 * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
451
		 * automatic link conversion.
452
		 */
453
		if ( !( $options->getDisableTitleConversion()
454
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] )
455
			|| isset( $this->mDoubleUnderscores['notitleconvert'] )
456
			|| $this->mOutput->getDisplayTitle() !== false )
457
		) {
458
			$convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
459
			if ( $convruletitle ) {
460
				$this->mOutput->setTitleText( $convruletitle );
461
			} else {
462
				$titleText = $this->getConverterLanguage()->convertTitle( $title );
463
				$this->mOutput->setTitleText( $titleText );
464
			}
465
		}
466
467
		# Done parsing! Compute runtime adaptive expiry if set
468
		$this->mOutput->finalizeAdaptiveCacheExpiry();
469
470
		# Warn if too many heavyweight parser functions were used
471
		if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
472
			$this->limitationWarn( 'expensive-parserfunction',
473
				$this->mExpensiveFunctionCount,
474
				$this->mOptions->getExpensiveParserFunctionLimit()
475
			);
476
		}
477
478
		# Information on include size limits, for the benefit of users who try to skirt them
479
		if ( $this->mOptions->getEnableLimitReport() ) {
480
			$max = $this->mOptions->getMaxIncludeSize();
481
482
			$cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
483
			if ( $cpuTime !== null ) {
484
				$this->mOutput->setLimitReportData( 'limitreport-cputime',
485
					sprintf( "%.3f", $cpuTime )
486
				);
487
			}
488
489
			$wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
490
			$this->mOutput->setLimitReportData( 'limitreport-walltime',
491
				sprintf( "%.3f", $wallTime )
492
			);
493
494
			$this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
495
				[ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
496
			);
497
			$this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
498
				[ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
499
			);
500
			$this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
501
				[ $this->mIncludeSizes['post-expand'], $max ]
502
			);
503
			$this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
504
				[ $this->mIncludeSizes['arg'], $max ]
505
			);
506
			$this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
507
				[ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
508
			);
509
			$this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
510
				[ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
511
			);
512
			Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
513
514
			$limitReport = "NewPP limit report\n";
515
			if ( $wgShowHostnames ) {
516
				$limitReport .= 'Parsed by ' . wfHostname() . "\n";
517
			}
518
			$limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
519
			$limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
520
			$limitReport .= 'Dynamic content: ' .
521
				( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
522
				"\n";
523
524
			foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
525
				if ( Hooks::run( 'ParserLimitReportFormat',
526
					[ $key, &$value, &$limitReport, false, false ]
527
				) ) {
528
					$keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
529
					$valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
530
						->inLanguage( 'en' )->useDatabase( false );
531
					if ( !$valueMsg->exists() ) {
532
						$valueMsg = new RawMessage( '$1' );
533
					}
534
					if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
535
						$valueMsg->params( $value );
536
						$limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
537
					}
538
				}
539
			}
540
			// Since we're not really outputting HTML, decode the entities and
541
			// then re-encode the things that need hiding inside HTML comments.
542
			$limitReport = htmlspecialchars_decode( $limitReport );
543
			Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ] );
544
545
			// Sanitize for comment. Note '‐' in the replacement is U+2010,
546
			// which looks much like the problematic '-'.
547
			$limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
548
			$text .= "\n<!-- \n$limitReport-->\n";
549
550
			// Add on template profiling data
551
			$dataByFunc = $this->mProfiler->getFunctionStats();
552
			uasort( $dataByFunc, function ( $a, $b ) {
553
				return $a['real'] < $b['real']; // descending order
554
			} );
555
			$profileReport = "Transclusion expansion time report (%,ms,calls,template)\n";
556
			foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
557
				$profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n",
558
					$item['%real'], $item['real'], $item['calls'],
559
					htmlspecialchars( $item['name'] ) );
560
			}
561
			$text .= "\n<!-- \n$profileReport-->\n";
562
563
			if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
564
				wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
565
					$this->mTitle->getPrefixedDBkey() );
566
			}
567
		}
568
		$this->mOutput->setText( $text );
569
570
		$this->mRevisionId = $oldRevisionId;
571
		$this->mRevisionObject = $oldRevisionObject;
572
		$this->mRevisionTimestamp = $oldRevisionTimestamp;
573
		$this->mRevisionUser = $oldRevisionUser;
574
		$this->mRevisionSize = $oldRevisionSize;
575
		$this->mInputSize = false;
576
		$this->currentRevisionCache = null;
577
578
		return $this->mOutput;
579
	}
580
581
	/**
582
	 * Half-parse wikitext to half-parsed HTML. This recursive parser entry point
583
	 * can be called from an extension tag hook.
584
	 *
585
	 * The output of this function IS NOT SAFE PARSED HTML; it is "half-parsed"
586
	 * instead, which means that lists and links have not been fully parsed yet,
587
	 * and strip markers are still present.
588
	 *
589
	 * Use recursiveTagParseFully() to fully parse wikitext to output-safe HTML.
590
	 *
591
	 * Use this function if you're a parser tag hook and you want to parse
592
	 * wikitext before or after applying additional transformations, and you
593
	 * intend to *return the result as hook output*, which will cause it to go
594
	 * through the rest of parsing process automatically.
595
	 *
596
	 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
597
	 * $text are not expanded
598
	 *
599
	 * @param string $text Text extension wants to have parsed
600
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
601
	 * @return string UNSAFE half-parsed HTML
602
	 */
603
	public function recursiveTagParse( $text, $frame = false ) {
604
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
605
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
606
		$text = $this->internalParse( $text, false, $frame );
607
		return $text;
608
	}
609
610
	/**
611
	 * Fully parse wikitext to fully parsed HTML. This recursive parser entry
612
	 * point can be called from an extension tag hook.
613
	 *
614
	 * The output of this function is fully-parsed HTML that is safe for output.
615
	 * If you're a parser tag hook, you might want to use recursiveTagParse()
616
	 * instead.
617
	 *
618
	 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
619
	 * $text are not expanded
620
	 *
621
	 * @since 1.25
622
	 *
623
	 * @param string $text Text extension wants to have parsed
624
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
625
	 * @return string Fully parsed HTML
626
	 */
627
	public function recursiveTagParseFully( $text, $frame = false ) {
628
		$text = $this->recursiveTagParse( $text, $frame );
629
		$text = $this->internalParseHalfParsed( $text, false );
630
		return $text;
631
	}
632
633
	/**
634
	 * Expand templates and variables in the text, producing valid, static wikitext.
635
	 * Also removes comments.
636
	 * Do not call this function recursively.
637
	 * @param string $text
638
	 * @param Title $title
639
	 * @param ParserOptions $options
640
	 * @param int|null $revid
641
	 * @param bool|PPFrame $frame
642
	 * @return mixed|string
643
	 */
644
	public function preprocess( $text, Title $title = null,
645
		ParserOptions $options, $revid = null, $frame = false
646
	) {
647
		$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
648
		$this->startParse( $title, $options, self::OT_PREPROCESS, true );
649
		if ( $revid !== null ) {
650
			$this->mRevisionId = $revid;
651
		}
652
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
653
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
654
		$text = $this->replaceVariables( $text, $frame );
655
		$text = $this->mStripState->unstripBoth( $text );
656
		return $text;
657
	}
658
659
	/**
660
	 * Recursive parser entry point that can be called from an extension tag
661
	 * hook.
662
	 *
663
	 * @param string $text Text to be expanded
664
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
665
	 * @return string
666
	 * @since 1.19
667
	 */
668
	public function recursivePreprocess( $text, $frame = false ) {
669
		$text = $this->replaceVariables( $text, $frame );
670
		$text = $this->mStripState->unstripBoth( $text );
671
		return $text;
672
	}
673
674
	/**
675
	 * Process the wikitext for the "?preload=" feature. (bug 5210)
676
	 *
677
	 * "<noinclude>", "<includeonly>" etc. are parsed as for template
678
	 * transclusion, comments, templates, arguments, tags hooks and parser
679
	 * functions are untouched.
680
	 *
681
	 * @param string $text
682
	 * @param Title $title
683
	 * @param ParserOptions $options
684
	 * @param array $params
685
	 * @return string
686
	 */
687
	public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
688
		$msg = new RawMessage( $text );
689
		$text = $msg->params( $params )->plain();
690
691
		# Parser (re)initialisation
692
		$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
693
		$this->startParse( $title, $options, self::OT_PLAIN, true );
694
695
		$flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
696
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
697
		$text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
698
		$text = $this->mStripState->unstripBoth( $text );
699
		return $text;
700
	}
701
702
	/**
703
	 * Get a random string
704
	 *
705
	 * @return string
706
	 * @deprecated since 1.26; use wfRandomString() instead.
707
	 */
708
	public static function getRandomString() {
709
		wfDeprecated( __METHOD__, '1.26' );
710
		return wfRandomString( 16 );
711
	}
712
713
	/**
714
	 * Set the current user.
715
	 * Should only be used when doing pre-save transform.
716
	 *
717
	 * @param User|null $user User object or null (to reset)
718
	 */
719
	public function setUser( $user ) {
720
		$this->mUser = $user;
721
	}
722
723
	/**
724
	 * Accessor for mUniqPrefix.
725
	 *
726
	 * @return string
727
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
728
	 */
729
	public function uniqPrefix() {
730
		wfDeprecated( __METHOD__, '1.26' );
731
		return self::MARKER_PREFIX;
732
	}
733
734
	/**
735
	 * Set the context title
736
	 *
737
	 * @param Title $t
738
	 */
739
	public function setTitle( $t ) {
740
		if ( !$t ) {
741
			$t = Title::newFromText( 'NO TITLE' );
742
		}
743
744
		if ( $t->hasFragment() ) {
745
			# Strip the fragment to avoid various odd effects
746
			$this->mTitle = $t->createFragmentTarget( '' );
0 ignored issues
show
Bug introduced by
It seems like $t is not always an object, but can also be of type null. 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...
747
		} else {
748
			$this->mTitle = $t;
749
		}
750
	}
751
752
	/**
753
	 * Accessor for the Title object
754
	 *
755
	 * @return Title
756
	 */
757
	public function getTitle() {
758
		return $this->mTitle;
759
	}
760
761
	/**
762
	 * Accessor/mutator for the Title object
763
	 *
764
	 * @param Title $x Title object or null to just get the current one
765
	 * @return Title
766
	 */
767
	public function Title( $x = null ) {
768
		return wfSetVar( $this->mTitle, $x );
769
	}
770
771
	/**
772
	 * Set the output type
773
	 *
774
	 * @param int $ot New value
775
	 */
776
	public function setOutputType( $ot ) {
777
		$this->mOutputType = $ot;
778
		# Shortcut alias
779
		$this->ot = [
780
			'html' => $ot == self::OT_HTML,
781
			'wiki' => $ot == self::OT_WIKI,
782
			'pre' => $ot == self::OT_PREPROCESS,
783
			'plain' => $ot == self::OT_PLAIN,
784
		];
785
	}
786
787
	/**
788
	 * Accessor/mutator for the output type
789
	 *
790
	 * @param int|null $x New value or null to just get the current one
791
	 * @return int
792
	 */
793
	public function OutputType( $x = null ) {
794
		return wfSetVar( $this->mOutputType, $x );
795
	}
796
797
	/**
798
	 * Get the ParserOutput object
799
	 *
800
	 * @return ParserOutput
801
	 */
802
	public function getOutput() {
803
		return $this->mOutput;
804
	}
805
806
	/**
807
	 * Get the ParserOptions object
808
	 *
809
	 * @return ParserOptions
810
	 */
811
	public function getOptions() {
812
		return $this->mOptions;
813
	}
814
815
	/**
816
	 * Accessor/mutator for the ParserOptions object
817
	 *
818
	 * @param ParserOptions $x New value or null to just get the current one
819
	 * @return ParserOptions Current ParserOptions object
820
	 */
821
	public function Options( $x = null ) {
822
		return wfSetVar( $this->mOptions, $x );
823
	}
824
825
	/**
826
	 * @return int
827
	 */
828
	public function nextLinkID() {
829
		return $this->mLinkID++;
830
	}
831
832
	/**
833
	 * @param int $id
834
	 */
835
	public function setLinkID( $id ) {
836
		$this->mLinkID = $id;
837
	}
838
839
	/**
840
	 * Get a language object for use in parser functions such as {{FORMATNUM:}}
841
	 * @return Language
842
	 */
843
	public function getFunctionLang() {
844
		return $this->getTargetLanguage();
845
	}
846
847
	/**
848
	 * Get the target language for the content being parsed. This is usually the
849
	 * language that the content is in.
850
	 *
851
	 * @since 1.19
852
	 *
853
	 * @throws MWException
854
	 * @return Language
855
	 */
856
	public function getTargetLanguage() {
857
		$target = $this->mOptions->getTargetLanguage();
858
859
		if ( $target !== null ) {
860
			return $target;
861
		} elseif ( $this->mOptions->getInterfaceMessage() ) {
862
			return $this->mOptions->getUserLangObj();
863
		} elseif ( is_null( $this->mTitle ) ) {
864
			throw new MWException( __METHOD__ . ': $this->mTitle is null' );
865
		}
866
867
		return $this->mTitle->getPageLanguage();
868
	}
869
870
	/**
871
	 * Get the language object for language conversion
872
	 * @return Language|null
873
	 */
874
	public function getConverterLanguage() {
875
		return $this->getTargetLanguage();
876
	}
877
878
	/**
879
	 * Get a User object either from $this->mUser, if set, or from the
880
	 * ParserOptions object otherwise
881
	 *
882
	 * @return User
883
	 */
884
	public function getUser() {
885
		if ( !is_null( $this->mUser ) ) {
886
			return $this->mUser;
887
		}
888
		return $this->mOptions->getUser();
889
	}
890
891
	/**
892
	 * Get a preprocessor object
893
	 *
894
	 * @return Preprocessor
895
	 */
896
	public function getPreprocessor() {
897
		if ( !isset( $this->mPreprocessor ) ) {
898
			$class = $this->mPreprocessorClass;
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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...
899
			$this->mPreprocessor = new $class( $this );
900
		}
901
		return $this->mPreprocessor;
902
	}
903
904
	/**
905
	 * Get a LinkRenderer instance to make links with
906
	 *
907
	 * @since 1.28
908
	 * @return LinkRenderer
909
	 */
910
	public function getLinkRenderer() {
911
		if ( !$this->mLinkRenderer ) {
912
			$this->mLinkRenderer = MediaWikiServices::getInstance()
913
				->getLinkRendererFactory()->create();
914
			$this->mLinkRenderer->setStubThreshold(
915
				$this->getOptions()->getStubThreshold()
916
			);
917
		}
918
919
		return $this->mLinkRenderer;
920
	}
921
922
	/**
923
	 * Replaces all occurrences of HTML-style comments and the given tags
924
	 * in the text with a random marker and returns the next text. The output
925
	 * parameter $matches will be an associative array filled with data in
926
	 * the form:
927
	 *
928
	 * @code
929
	 *   'UNIQ-xxxxx' => [
930
	 *     'element',
931
	 *     'tag content',
932
	 *     [ 'param' => 'x' ],
933
	 *     '<element param="x">tag content</element>' ]
934
	 * @endcode
935
	 *
936
	 * @param array $elements List of element names. Comments are always extracted.
937
	 * @param string $text Source text string.
938
	 * @param array $matches Out parameter, Array: extracted tags
939
	 * @param string|null $uniq_prefix
940
	 * @return string Stripped text
941
	 * @since 1.26 The uniq_prefix argument is deprecated.
942
	 */
943
	public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
944
		if ( $uniq_prefix !== null ) {
945
			wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
946
		}
947
		static $n = 1;
948
		$stripped = '';
949
		$matches = [];
950
951
		$taglist = implode( '|', $elements );
952
		$start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
953
954
		while ( $text != '' ) {
955
			$p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
956
			$stripped .= $p[0];
957
			if ( count( $p ) < 5 ) {
958
				break;
959
			}
960
			if ( count( $p ) > 5 ) {
961
				# comment
962
				$element = $p[4];
963
				$attributes = '';
964
				$close = '';
965
				$inside = $p[5];
966
			} else {
967
				# tag
968
				$element = $p[1];
969
				$attributes = $p[2];
970
				$close = $p[3];
971
				$inside = $p[4];
972
			}
973
974
			$marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
975
			$stripped .= $marker;
976
977
			if ( $close === '/>' ) {
978
				# Empty element tag, <tag />
979
				$content = null;
980
				$text = $inside;
981
				$tail = null;
982
			} else {
983
				if ( $element === '!--' ) {
984
					$end = '/(-->)/';
985
				} else {
986
					$end = "/(<\\/$element\\s*>)/i";
987
				}
988
				$q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
989
				$content = $q[0];
990
				if ( count( $q ) < 3 ) {
991
					# No end tag -- let it run out to the end of the text.
992
					$tail = '';
993
					$text = '';
994
				} else {
995
					$tail = $q[1];
996
					$text = $q[2];
997
				}
998
			}
999
1000
			$matches[$marker] = [ $element,
1001
				$content,
1002
				Sanitizer::decodeTagAttributes( $attributes ),
1003
				"<$element$attributes$close$content$tail" ];
1004
		}
1005
		return $stripped;
1006
	}
1007
1008
	/**
1009
	 * Get a list of strippable XML-like elements
1010
	 *
1011
	 * @return array
1012
	 */
1013
	public function getStripList() {
1014
		return $this->mStripList;
1015
	}
1016
1017
	/**
1018
	 * Add an item to the strip state
1019
	 * Returns the unique tag which must be inserted into the stripped text
1020
	 * The tag will be replaced with the original text in unstrip()
1021
	 *
1022
	 * @param string $text
1023
	 *
1024
	 * @return string
1025
	 */
1026
	public function insertStripItem( $text ) {
1027
		$marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1028
		$this->mMarkerIndex++;
1029
		$this->mStripState->addGeneral( $marker, $text );
1030
		return $marker;
1031
	}
1032
1033
	/**
1034
	 * parse the wiki syntax used to render tables
1035
	 *
1036
	 * @private
1037
	 * @param string $text
1038
	 * @return string
1039
	 */
1040
	public function doTableStuff( $text ) {
1041
1042
		$lines = StringUtils::explode( "\n", $text );
1043
		$out = '';
1044
		$td_history = []; # Is currently a td tag open?
1045
		$last_tag_history = []; # Save history of last lag activated (td, th or caption)
1046
		$tr_history = []; # Is currently a tr tag open?
1047
		$tr_attributes = []; # history of tr attributes
1048
		$has_opened_tr = []; # Did this table open a <tr> element?
1049
		$indent_level = 0; # indent level of the table
1050
1051
		foreach ( $lines as $outLine ) {
1052
			$line = trim( $outLine );
1053
1054
			if ( $line === '' ) { # empty line, go to next line
1055
				$out .= $outLine . "\n";
1056
				continue;
1057
			}
1058
1059
			$first_character = $line[0];
1060
			$first_two = substr( $line, 0, 2 );
1061
			$matches = [];
1062
1063
			if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1064
				# First check if we are starting a new table
1065
				$indent_level = strlen( $matches[1] );
1066
1067
				$attributes = $this->mStripState->unstripBoth( $matches[2] );
1068
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1069
1070
				$outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1071
				array_push( $td_history, false );
1072
				array_push( $last_tag_history, '' );
1073
				array_push( $tr_history, false );
1074
				array_push( $tr_attributes, '' );
1075
				array_push( $has_opened_tr, false );
1076
			} elseif ( count( $td_history ) == 0 ) {
1077
				# Don't do any of the following
1078
				$out .= $outLine . "\n";
1079
				continue;
1080
			} elseif ( $first_two === '|}' ) {
1081
				# We are ending a table
1082
				$line = '</table>' . substr( $line, 2 );
1083
				$last_tag = array_pop( $last_tag_history );
1084
1085
				if ( !array_pop( $has_opened_tr ) ) {
1086
					$line = "<tr><td></td></tr>{$line}";
1087
				}
1088
1089
				if ( array_pop( $tr_history ) ) {
1090
					$line = "</tr>{$line}";
1091
				}
1092
1093
				if ( array_pop( $td_history ) ) {
1094
					$line = "</{$last_tag}>{$line}";
1095
				}
1096
				array_pop( $tr_attributes );
1097
				$outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1098
			} elseif ( $first_two === '|-' ) {
1099
				# Now we have a table row
1100
				$line = preg_replace( '#^\|-+#', '', $line );
1101
1102
				# Whats after the tag is now only attributes
1103
				$attributes = $this->mStripState->unstripBoth( $line );
1104
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1105
				array_pop( $tr_attributes );
1106
				array_push( $tr_attributes, $attributes );
1107
1108
				$line = '';
1109
				$last_tag = array_pop( $last_tag_history );
1110
				array_pop( $has_opened_tr );
1111
				array_push( $has_opened_tr, true );
1112
1113
				if ( array_pop( $tr_history ) ) {
1114
					$line = '</tr>';
1115
				}
1116
1117
				if ( array_pop( $td_history ) ) {
1118
					$line = "</{$last_tag}>{$line}";
1119
				}
1120
1121
				$outLine = $line;
1122
				array_push( $tr_history, false );
1123
				array_push( $td_history, false );
1124
				array_push( $last_tag_history, '' );
1125
			} elseif ( $first_character === '|'
1126
				|| $first_character === '!'
1127
				|| $first_two === '|+'
1128
			) {
1129
				# This might be cell elements, td, th or captions
1130
				if ( $first_two === '|+' ) {
1131
					$first_character = '+';
1132
					$line = substr( $line, 2 );
1133
				} else {
1134
					$line = substr( $line, 1 );
1135
				}
1136
1137
				// Implies both are valid for table headings.
1138
				if ( $first_character === '!' ) {
1139
					$line = StringUtils::replaceMarkup( '!!', '||', $line );
1140
				}
1141
1142
				# Split up multiple cells on the same line.
1143
				# FIXME : This can result in improper nesting of tags processed
1144
				# by earlier parser steps.
1145
				$cells = explode( '||', $line );
1146
1147
				$outLine = '';
1148
1149
				# Loop through each table cell
1150
				foreach ( $cells as $cell ) {
1151
					$previous = '';
1152
					if ( $first_character !== '+' ) {
1153
						$tr_after = array_pop( $tr_attributes );
1154
						if ( !array_pop( $tr_history ) ) {
1155
							$previous = "<tr{$tr_after}>\n";
1156
						}
1157
						array_push( $tr_history, true );
1158
						array_push( $tr_attributes, '' );
1159
						array_pop( $has_opened_tr );
1160
						array_push( $has_opened_tr, true );
1161
					}
1162
1163
					$last_tag = array_pop( $last_tag_history );
1164
1165
					if ( array_pop( $td_history ) ) {
1166
						$previous = "</{$last_tag}>\n{$previous}";
1167
					}
1168
1169
					if ( $first_character === '|' ) {
1170
						$last_tag = 'td';
1171
					} elseif ( $first_character === '!' ) {
1172
						$last_tag = 'th';
1173
					} elseif ( $first_character === '+' ) {
1174
						$last_tag = 'caption';
1175
					} else {
1176
						$last_tag = '';
1177
					}
1178
1179
					array_push( $last_tag_history, $last_tag );
1180
1181
					# A cell could contain both parameters and data
1182
					$cell_data = explode( '|', $cell, 2 );
1183
1184
					# Bug 553: Note that a '|' inside an invalid link should not
1185
					# be mistaken as delimiting cell parameters
1186
					if ( strpos( $cell_data[0], '[[' ) !== false ) {
1187
						$cell = "{$previous}<{$last_tag}>{$cell}";
1188
					} elseif ( count( $cell_data ) == 1 ) {
1189
						$cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1190
					} else {
1191
						$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1192
						$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1193
						$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1194
					}
1195
1196
					$outLine .= $cell;
1197
					array_push( $td_history, true );
1198
				}
1199
			}
1200
			$out .= $outLine . "\n";
1201
		}
1202
1203
		# Closing open td, tr && table
1204
		while ( count( $td_history ) > 0 ) {
1205
			if ( array_pop( $td_history ) ) {
1206
				$out .= "</td>\n";
1207
			}
1208
			if ( array_pop( $tr_history ) ) {
1209
				$out .= "</tr>\n";
1210
			}
1211
			if ( !array_pop( $has_opened_tr ) ) {
1212
				$out .= "<tr><td></td></tr>\n";
1213
			}
1214
1215
			$out .= "</table>\n";
1216
		}
1217
1218
		# Remove trailing line-ending (b/c)
1219 View Code Duplication
		if ( substr( $out, -1 ) === "\n" ) {
1220
			$out = substr( $out, 0, -1 );
1221
		}
1222
1223
		# special case: don't return empty table
1224
		if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1225
			$out = '';
1226
		}
1227
1228
		return $out;
1229
	}
1230
1231
	/**
1232
	 * Helper function for parse() that transforms wiki markup into half-parsed
1233
	 * HTML. Only called for $mOutputType == self::OT_HTML.
1234
	 *
1235
	 * @private
1236
	 *
1237
	 * @param string $text The text to parse
1238
	 * @param bool $isMain Whether this is being called from the main parse() function
1239
	 * @param PPFrame|bool $frame A pre-processor frame
1240
	 *
1241
	 * @return string
1242
	 */
1243
	public function internalParse( $text, $isMain = true, $frame = false ) {
1244
1245
		$origText = $text;
1246
1247
		# Hook to suspend the parser in this state
1248
		if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) {
1249
			return $text;
1250
		}
1251
1252
		# if $frame is provided, then use $frame for replacing any variables
1253
		if ( $frame ) {
1254
			# use frame depth to infer how include/noinclude tags should be handled
1255
			# depth=0 means this is the top-level document; otherwise it's an included document
1256
			if ( !$frame->depth ) {
1257
				$flag = 0;
1258
			} else {
1259
				$flag = Parser::PTD_FOR_INCLUSION;
1260
			}
1261
			$dom = $this->preprocessToDom( $text, $flag );
1262
			$text = $frame->expand( $dom );
0 ignored issues
show
Bug introduced by
It seems like $frame 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...
1263
		} else {
1264
			# if $frame is not provided, then use old-style replaceVariables
1265
			$text = $this->replaceVariables( $text );
1266
		}
1267
1268
		Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] );
1269
		$text = Sanitizer::removeHTMLtags(
1270
			$text,
1271
			[ &$this, 'attributeStripCallback' ],
1272
			false,
1273
			array_keys( $this->mTransparentTagHooks ),
1274
			[],
1275
			[ &$this, 'addTrackingCategory' ]
1276
		);
1277
		Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
1278
1279
		# Tables need to come after variable replacement for things to work
1280
		# properly; putting them before other transformations should keep
1281
		# exciting things like link expansions from showing up in surprising
1282
		# places.
1283
		$text = $this->doTableStuff( $text );
1284
1285
		$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1286
1287
		$text = $this->doDoubleUnderscore( $text );
1288
1289
		$text = $this->doHeadings( $text );
1290
		$text = $this->replaceInternalLinks( $text );
1291
		$text = $this->doAllQuotes( $text );
1292
		$text = $this->replaceExternalLinks( $text );
1293
1294
		# replaceInternalLinks may sometimes leave behind
1295
		# absolute URLs, which have to be masked to hide them from replaceExternalLinks
1296
		$text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1297
1298
		$text = $this->doMagicLinks( $text );
1299
		$text = $this->formatHeadings( $text, $origText, $isMain );
1300
1301
		return $text;
1302
	}
1303
1304
	/**
1305
	 * Helper function for parse() that transforms half-parsed HTML into fully
1306
	 * parsed HTML.
1307
	 *
1308
	 * @param string $text
1309
	 * @param bool $isMain
1310
	 * @param bool $linestart
1311
	 * @return string
1312
	 */
1313
	private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1314
		$text = $this->mStripState->unstripGeneral( $text );
1315
1316
		if ( $isMain ) {
1317
			Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
1318
		}
1319
1320
		# Clean up special characters, only run once, next-to-last before doBlockLevels
1321
		$fixtags = [
1322
			# french spaces, last one Guillemet-left
1323
			# only if there is something before the space
1324
			'/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1325
			# french spaces, Guillemet-right
1326
			'/(\\302\\253) /' => '\\1&#160;',
1327
			'/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1328
		];
1329
		$text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1330
1331
		$text = $this->doBlockLevels( $text, $linestart );
1332
1333
		$this->replaceLinkHolders( $text );
1334
1335
		/**
1336
		 * The input doesn't get language converted if
1337
		 * a) It's disabled
1338
		 * b) Content isn't converted
1339
		 * c) It's a conversion table
1340
		 * d) it is an interface message (which is in the user language)
1341
		 */
1342
		if ( !( $this->mOptions->getDisableContentConversion()
1343
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1344
		) {
1345
			if ( !$this->mOptions->getInterfaceMessage() ) {
1346
				# The position of the convert() call should not be changed. it
1347
				# assumes that the links are all replaced and the only thing left
1348
				# is the <nowiki> mark.
1349
				$text = $this->getConverterLanguage()->convert( $text );
1350
			}
1351
		}
1352
1353
		$text = $this->mStripState->unstripNoWiki( $text );
1354
1355
		if ( $isMain ) {
1356
			Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
1357
		}
1358
1359
		$text = $this->replaceTransparentTags( $text );
1360
		$text = $this->mStripState->unstripGeneral( $text );
1361
1362
		$text = Sanitizer::normalizeCharReferences( $text );
1363
1364
		if ( MWTidy::isEnabled() ) {
1365
			if ( $this->mOptions->getTidy() ) {
1366
				$text = MWTidy::tidy( $text );
1367
			}
1368
		} else {
1369
			# attempt to sanitize at least some nesting problems
1370
			# (bug #2702 and quite a few others)
1371
			$tidyregs = [
1372
				# ''Something [http://www.cool.com cool''] -->
1373
				# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1374
				'/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1375
				'\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1376
				# fix up an anchor inside another anchor, only
1377
				# at least for a single single nested link (bug 3695)
1378
				'/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1379
				'\\1\\2</a>\\3</a>\\1\\4</a>',
1380
				# fix div inside inline elements- doBlockLevels won't wrap a line which
1381
				# contains a div, so fix it up here; replace
1382
				# div with escaped text
1383
				'/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1384
				'\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1385
				# remove empty italic or bold tag pairs, some
1386
				# introduced by rules above
1387
				'/<([bi])><\/\\1>/' => '',
1388
			];
1389
1390
			$text = preg_replace(
1391
				array_keys( $tidyregs ),
1392
				array_values( $tidyregs ),
1393
				$text );
1394
		}
1395
1396
		if ( $isMain ) {
1397
			Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
1398
		}
1399
1400
		return $text;
1401
	}
1402
1403
	/**
1404
	 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1405
	 * magic external links.
1406
	 *
1407
	 * DML
1408
	 * @private
1409
	 *
1410
	 * @param string $text
1411
	 *
1412
	 * @return string
1413
	 */
1414
	public function doMagicLinks( $text ) {
1415
		$prots = wfUrlProtocolsWithoutProtRel();
1416
		$urlChar = self::EXT_LINK_URL_CLASS;
1417
		$addr = self::EXT_LINK_ADDR;
1418
		$space = self::SPACE_NOT_NL; #  non-newline space
1419
		$spdash = "(?:-|$space)"; # a dash or a non-newline space
1420
		$spaces = "$space++"; # possessive match of 1 or more spaces
1421
		$text = preg_replace_callback(
1422
			'!(?:                            # Start cases
1423
				(<a[ \t\r\n>].*?</a>) |      # m[1]: Skip link text
1424
				(<.*?>) |                    # m[2]: Skip stuff inside
1425
				                             #       HTML elements' . "
1426
				(\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1427
				                             # m[4]: Post-protocol path
1428
				\b(?:RFC|PMID) $spaces       # m[5]: RFC or PMID, capture number
1429
					([0-9]+)\b |
1430
				\bISBN $spaces (             # m[6]: ISBN, capture number
1431
					(?: 97[89] $spdash? )?   #  optional 13-digit ISBN prefix
1432
					(?: [0-9]  $spdash? ){9} #  9 digits with opt. delimiters
1433
					[0-9Xx]                  #  check digit
1434
				)\b
1435
			)!xu", [ &$this, 'magicLinkCallback' ], $text );
1436
		return $text;
1437
	}
1438
1439
	/**
1440
	 * @throws MWException
1441
	 * @param array $m
1442
	 * @return HTML|string
1443
	 */
1444
	public function magicLinkCallback( $m ) {
1445
		if ( isset( $m[1] ) && $m[1] !== '' ) {
1446
			# Skip anchor
1447
			return $m[0];
1448
		} elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1449
			# Skip HTML element
1450
			return $m[0];
1451
		} elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1452
			# Free external link
1453
			return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1454
		} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1455
			# RFC or PMID
1456
			if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1457
				if ( !$this->mOptions->getMagicRFCLinks() ) {
1458
					return $m[0];
1459
				}
1460
				$keyword = 'RFC';
1461
				$urlmsg = 'rfcurl';
1462
				$cssClass = 'mw-magiclink-rfc';
1463
				$trackingCat = 'magiclink-tracking-rfc';
1464
				$id = $m[5];
1465
			} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1466
				if ( !$this->mOptions->getMagicPMIDLinks() ) {
1467
					return $m[0];
1468
				}
1469
				$keyword = 'PMID';
1470
				$urlmsg = 'pubmedurl';
1471
				$cssClass = 'mw-magiclink-pmid';
1472
				$trackingCat = 'magiclink-tracking-pmid';
1473
				$id = $m[5];
1474
			} else {
1475
				throw new MWException( __METHOD__ . ': unrecognised match type "' .
1476
					substr( $m[0], 0, 20 ) . '"' );
1477
			}
1478
			$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1479
			$this->addTrackingCategory( $trackingCat );
1480
			return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1481
		} elseif ( isset( $m[6] ) && $m[6] !== ''
1482
			&& $this->mOptions->getMagicISBNLinks()
1483
		) {
1484
			# ISBN
1485
			$isbn = $m[6];
1486
			$space = self::SPACE_NOT_NL; #  non-newline space
1487
			$isbn = preg_replace( "/$space/", ' ', $isbn );
1488
			$num = strtr( $isbn, [
1489
				'-' => '',
1490
				' ' => '',
1491
				'x' => 'X',
1492
			] );
1493
			$this->addTrackingCategory( 'magiclink-tracking-isbn' );
1494
			return $this->getLinkRenderer()->makeKnownLink(
1495
				SpecialPage::getTitleFor( 'Booksources', $num ),
1496
				"ISBN $isbn",
1497
				[
1498
					'class' => 'internal mw-magiclink-isbn',
1499
					'title' => false // suppress title attribute
1500
				]
1501
			);
1502
		} else {
1503
			return $m[0];
1504
		}
1505
	}
1506
1507
	/**
1508
	 * Make a free external link, given a user-supplied URL
1509
	 *
1510
	 * @param string $url
1511
	 * @param int $numPostProto
1512
	 *   The number of characters after the protocol.
1513
	 * @return string HTML
1514
	 * @private
1515
	 */
1516
	public function makeFreeExternalLink( $url, $numPostProto ) {
1517
		$trail = '';
1518
1519
		# The characters '<' and '>' (which were escaped by
1520
		# removeHTMLtags()) should not be included in
1521
		# URLs, per RFC 2396.
1522
		# Make &nbsp; terminate a URL as well (bug T84937)
1523
		$m2 = [];
1524 View Code Duplication
		if ( preg_match(
1525
			'/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1526
			$url,
1527
			$m2,
1528
			PREG_OFFSET_CAPTURE
1529
		) ) {
1530
			$trail = substr( $url, $m2[0][1] ) . $trail;
1531
			$url = substr( $url, 0, $m2[0][1] );
1532
		}
1533
1534
		# Move trailing punctuation to $trail
1535
		$sep = ',;\.:!?';
1536
		# If there is no left bracket, then consider right brackets fair game too
1537
		if ( strpos( $url, '(' ) === false ) {
1538
			$sep .= ')';
1539
		}
1540
1541
		$urlRev = strrev( $url );
1542
		$numSepChars = strspn( $urlRev, $sep );
1543
		# Don't break a trailing HTML entity by moving the ; into $trail
1544
		# This is in hot code, so use substr_compare to avoid having to
1545
		# create a new string object for the comparison
1546
		if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1547
			# more optimization: instead of running preg_match with a $
1548
			# anchor, which can be slow, do the match on the reversed
1549
			# string starting at the desired offset.
1550
			# un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1551
			if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1552
				$numSepChars--;
1553
			}
1554
		}
1555
		if ( $numSepChars ) {
1556
			$trail = substr( $url, -$numSepChars ) . $trail;
1557
			$url = substr( $url, 0, -$numSepChars );
1558
		}
1559
1560
		# Verify that we still have a real URL after trail removal, and
1561
		# not just lone protocol
1562
		if ( strlen( $trail ) >= $numPostProto ) {
1563
			return $url . $trail;
1564
		}
1565
1566
		$url = Sanitizer::cleanUrl( $url );
1567
1568
		# Is this an external image?
1569
		$text = $this->maybeMakeExternalImage( $url );
1570
		if ( $text === false ) {
1571
			# Not an image, make a link
1572
			$text = Linker::makeExternalLink( $url,
1573
				$this->getConverterLanguage()->markNoConversion( $url, true ),
1574
				true, 'free',
1575
				$this->getExternalLinkAttribs( $url ), $this->mTitle );
1576
			# Register it in the output object...
1577
			# Replace unnecessary URL escape codes with their equivalent characters
1578
			$pasteurized = self::normalizeLinkUrl( $url );
1579
			$this->mOutput->addExternalLink( $pasteurized );
1580
		}
1581
		return $text . $trail;
1582
	}
1583
1584
	/**
1585
	 * Parse headers and return html
1586
	 *
1587
	 * @private
1588
	 *
1589
	 * @param string $text
1590
	 *
1591
	 * @return string
1592
	 */
1593
	public function doHeadings( $text ) {
1594
		for ( $i = 6; $i >= 1; --$i ) {
1595
			$h = str_repeat( '=', $i );
1596
			$text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1597
		}
1598
		return $text;
1599
	}
1600
1601
	/**
1602
	 * Replace single quotes with HTML markup
1603
	 * @private
1604
	 *
1605
	 * @param string $text
1606
	 *
1607
	 * @return string The altered text
1608
	 */
1609
	public function doAllQuotes( $text ) {
1610
		$outtext = '';
1611
		$lines = StringUtils::explode( "\n", $text );
1612
		foreach ( $lines as $line ) {
1613
			$outtext .= $this->doQuotes( $line ) . "\n";
1614
		}
1615
		$outtext = substr( $outtext, 0, -1 );
1616
		return $outtext;
1617
	}
1618
1619
	/**
1620
	 * Helper function for doAllQuotes()
1621
	 *
1622
	 * @param string $text
1623
	 *
1624
	 * @return string
1625
	 */
1626
	public function doQuotes( $text ) {
1627
		$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1628
		$countarr = count( $arr );
1629
		if ( $countarr == 1 ) {
1630
			return $text;
1631
		}
1632
1633
		// First, do some preliminary work. This may shift some apostrophes from
1634
		// being mark-up to being text. It also counts the number of occurrences
1635
		// of bold and italics mark-ups.
1636
		$numbold = 0;
1637
		$numitalics = 0;
1638
		for ( $i = 1; $i < $countarr; $i += 2 ) {
1639
			$thislen = strlen( $arr[$i] );
1640
			// If there are ever four apostrophes, assume the first is supposed to
1641
			// be text, and the remaining three constitute mark-up for bold text.
1642
			// (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1643
			if ( $thislen == 4 ) {
1644
				$arr[$i - 1] .= "'";
1645
				$arr[$i] = "'''";
1646
				$thislen = 3;
1647
			} elseif ( $thislen > 5 ) {
1648
				// If there are more than 5 apostrophes in a row, assume they're all
1649
				// text except for the last 5.
1650
				// (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1651
				$arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1652
				$arr[$i] = "'''''";
1653
				$thislen = 5;
1654
			}
1655
			// Count the number of occurrences of bold and italics mark-ups.
1656
			if ( $thislen == 2 ) {
1657
				$numitalics++;
1658
			} elseif ( $thislen == 3 ) {
1659
				$numbold++;
1660
			} elseif ( $thislen == 5 ) {
1661
				$numitalics++;
1662
				$numbold++;
1663
			}
1664
		}
1665
1666
		// If there is an odd number of both bold and italics, it is likely
1667
		// that one of the bold ones was meant to be an apostrophe followed
1668
		// by italics. Which one we cannot know for certain, but it is more
1669
		// likely to be one that has a single-letter word before it.
1670
		if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1671
			$firstsingleletterword = -1;
1672
			$firstmultiletterword = -1;
1673
			$firstspace = -1;
1674
			for ( $i = 1; $i < $countarr; $i += 2 ) {
1675
				if ( strlen( $arr[$i] ) == 3 ) {
1676
					$x1 = substr( $arr[$i - 1], -1 );
1677
					$x2 = substr( $arr[$i - 1], -2, 1 );
1678
					if ( $x1 === ' ' ) {
1679
						if ( $firstspace == -1 ) {
1680
							$firstspace = $i;
1681
						}
1682
					} elseif ( $x2 === ' ' ) {
1683
						$firstsingleletterword = $i;
1684
						// if $firstsingleletterword is set, we don't
1685
						// look at the other options, so we can bail early.
1686
						break;
1687
					} else {
1688
						if ( $firstmultiletterword == -1 ) {
1689
							$firstmultiletterword = $i;
1690
						}
1691
					}
1692
				}
1693
			}
1694
1695
			// If there is a single-letter word, use it!
1696
			if ( $firstsingleletterword > -1 ) {
1697
				$arr[$firstsingleletterword] = "''";
1698
				$arr[$firstsingleletterword - 1] .= "'";
1699
			} elseif ( $firstmultiletterword > -1 ) {
1700
				// If not, but there's a multi-letter word, use that one.
1701
				$arr[$firstmultiletterword] = "''";
1702
				$arr[$firstmultiletterword - 1] .= "'";
1703
			} elseif ( $firstspace > -1 ) {
1704
				// ... otherwise use the first one that has neither.
1705
				// (notice that it is possible for all three to be -1 if, for example,
1706
				// there is only one pentuple-apostrophe in the line)
1707
				$arr[$firstspace] = "''";
1708
				$arr[$firstspace - 1] .= "'";
1709
			}
1710
		}
1711
1712
		// Now let's actually convert our apostrophic mush to HTML!
1713
		$output = '';
1714
		$buffer = '';
1715
		$state = '';
1716
		$i = 0;
1717
		foreach ( $arr as $r ) {
1718
			if ( ( $i % 2 ) == 0 ) {
1719
				if ( $state === 'both' ) {
1720
					$buffer .= $r;
1721
				} else {
1722
					$output .= $r;
1723
				}
1724
			} else {
1725
				$thislen = strlen( $r );
1726
				if ( $thislen == 2 ) {
1727 View Code Duplication
					if ( $state === 'i' ) {
1728
						$output .= '</i>';
1729
						$state = '';
1730
					} elseif ( $state === 'bi' ) {
1731
						$output .= '</i>';
1732
						$state = 'b';
1733
					} elseif ( $state === 'ib' ) {
1734
						$output .= '</b></i><b>';
1735
						$state = 'b';
1736
					} elseif ( $state === 'both' ) {
1737
						$output .= '<b><i>' . $buffer . '</i>';
1738
						$state = 'b';
1739
					} else { // $state can be 'b' or ''
1740
						$output .= '<i>';
1741
						$state .= 'i';
1742
					}
1743 View Code Duplication
				} elseif ( $thislen == 3 ) {
1744
					if ( $state === 'b' ) {
1745
						$output .= '</b>';
1746
						$state = '';
1747
					} elseif ( $state === 'bi' ) {
1748
						$output .= '</i></b><i>';
1749
						$state = 'i';
1750
					} elseif ( $state === 'ib' ) {
1751
						$output .= '</b>';
1752
						$state = 'i';
1753
					} elseif ( $state === 'both' ) {
1754
						$output .= '<i><b>' . $buffer . '</b>';
1755
						$state = 'i';
1756
					} else { // $state can be 'i' or ''
1757
						$output .= '<b>';
1758
						$state .= 'b';
1759
					}
1760
				} elseif ( $thislen == 5 ) {
1761
					if ( $state === 'b' ) {
1762
						$output .= '</b><i>';
1763
						$state = 'i';
1764
					} elseif ( $state === 'i' ) {
1765
						$output .= '</i><b>';
1766
						$state = 'b';
1767
					} elseif ( $state === 'bi' ) {
1768
						$output .= '</i></b>';
1769
						$state = '';
1770
					} elseif ( $state === 'ib' ) {
1771
						$output .= '</b></i>';
1772
						$state = '';
1773
					} elseif ( $state === 'both' ) {
1774
						$output .= '<i><b>' . $buffer . '</b></i>';
1775
						$state = '';
1776
					} else { // ($state == '')
1777
						$buffer = '';
1778
						$state = 'both';
1779
					}
1780
				}
1781
			}
1782
			$i++;
1783
		}
1784
		// Now close all remaining tags.  Notice that the order is important.
1785
		if ( $state === 'b' || $state === 'ib' ) {
1786
			$output .= '</b>';
1787
		}
1788
		if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1789
			$output .= '</i>';
1790
		}
1791
		if ( $state === 'bi' ) {
1792
			$output .= '</b>';
1793
		}
1794
		// There might be lonely ''''', so make sure we have a buffer
1795
		if ( $state === 'both' && $buffer ) {
1796
			$output .= '<b><i>' . $buffer . '</i></b>';
1797
		}
1798
		return $output;
1799
	}
1800
1801
	/**
1802
	 * Replace external links (REL)
1803
	 *
1804
	 * Note: this is all very hackish and the order of execution matters a lot.
1805
	 * Make sure to run tests/parser/parserTests.php if you change this code.
1806
	 *
1807
	 * @private
1808
	 *
1809
	 * @param string $text
1810
	 *
1811
	 * @throws MWException
1812
	 * @return string
1813
	 */
1814
	public function replaceExternalLinks( $text ) {
1815
1816
		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1817
		if ( $bits === false ) {
1818
			throw new MWException( "PCRE needs to be compiled with "
1819
				. "--enable-unicode-properties in order for MediaWiki to function" );
1820
		}
1821
		$s = array_shift( $bits );
1822
1823
		$i = 0;
1824
		while ( $i < count( $bits ) ) {
1825
			$url = $bits[$i++];
1826
			$i++; // protocol
1827
			$text = $bits[$i++];
1828
			$trail = $bits[$i++];
1829
1830
			# The characters '<' and '>' (which were escaped by
1831
			# removeHTMLtags()) should not be included in
1832
			# URLs, per RFC 2396.
1833
			$m2 = [];
1834 View Code Duplication
			if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1835
				$text = substr( $url, $m2[0][1] ) . ' ' . $text;
1836
				$url = substr( $url, 0, $m2[0][1] );
1837
			}
1838
1839
			# If the link text is an image URL, replace it with an <img> tag
1840
			# This happened by accident in the original parser, but some people used it extensively
1841
			$img = $this->maybeMakeExternalImage( $text );
1842
			if ( $img !== false ) {
1843
				$text = $img;
1844
			}
1845
1846
			$dtrail = '';
1847
1848
			# Set linktype for CSS - if URL==text, link is essentially free
1849
			$linktype = ( $text === $url ) ? 'free' : 'text';
1850
1851
			# No link text, e.g. [http://domain.tld/some.link]
1852
			if ( $text == '' ) {
1853
				# Autonumber
1854
				$langObj = $this->getTargetLanguage();
1855
				$text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1856
				$linktype = 'autonumber';
1857
			} else {
1858
				# Have link text, e.g. [http://domain.tld/some.link text]s
1859
				# Check for trail
1860
				list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1861
			}
1862
1863
			$text = $this->getConverterLanguage()->markNoConversion( $text );
1864
1865
			$url = Sanitizer::cleanUrl( $url );
1866
1867
			# Use the encoded URL
1868
			# This means that users can paste URLs directly into the text
1869
			# Funny characters like ö aren't valid in URLs anyway
1870
			# This was changed in August 2004
1871
			$s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1872
				$this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1873
1874
			# Register link in the output object.
1875
			# Replace unnecessary URL escape codes with the referenced character
1876
			# This prevents spammers from hiding links from the filters
1877
			$pasteurized = self::normalizeLinkUrl( $url );
1878
			$this->mOutput->addExternalLink( $pasteurized );
1879
		}
1880
1881
		return $s;
1882
	}
1883
1884
	/**
1885
	 * Get the rel attribute for a particular external link.
1886
	 *
1887
	 * @since 1.21
1888
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1889
	 *   nofollow if appropriate
1890
	 * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups
1891
	 * @return string|null Rel attribute for $url
1892
	 */
1893
	public static function getExternalLinkRel( $url = false, $title = null ) {
1894
		global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1895
		$ns = $title ? $title->getNamespace() : false;
1896
		if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1897
			&& !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
0 ignored issues
show
Bug introduced by
It seems like $url defined by parameter $url on line 1893 can also be of type boolean; however, wfMatchesDomainList() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1898
		) {
1899
			return 'nofollow';
1900
		}
1901
		return null;
1902
	}
1903
1904
	/**
1905
	 * Get an associative array of additional HTML attributes appropriate for a
1906
	 * particular external link.  This currently may include rel => nofollow
1907
	 * (depending on configuration, namespace, and the URL's domain) and/or a
1908
	 * target attribute (depending on configuration).
1909
	 *
1910
	 * @param string $url URL to extract the domain from for rel =>
1911
	 *   nofollow if appropriate
1912
	 * @return array Associative array of HTML attributes
1913
	 */
1914
	public function getExternalLinkAttribs( $url ) {
1915
		$attribs = [];
1916
		$rel = self::getExternalLinkRel( $url, $this->mTitle );
1917
1918
		$target = $this->mOptions->getExternalLinkTarget();
1919
		if ( $target ) {
1920
			$attribs['target'] = $target;
1921
			if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1922
				// T133507. New windows can navigate parent cross-origin.
1923
				// Including noreferrer due to lacking browser
1924
				// support of noopener. Eventually noreferrer should be removed.
1925
				if ( $rel !== '' ) {
1926
					$rel .= ' ';
1927
				}
1928
				$rel .= 'noreferrer noopener';
1929
			}
1930
		}
1931
		$attribs['rel'] = $rel;
1932
		return $attribs;
1933
	}
1934
1935
	/**
1936
	 * Replace unusual escape codes in a URL with their equivalent characters
1937
	 *
1938
	 * @deprecated since 1.24, use normalizeLinkUrl
1939
	 * @param string $url
1940
	 * @return string
1941
	 */
1942
	public static function replaceUnusualEscapes( $url ) {
1943
		wfDeprecated( __METHOD__, '1.24' );
1944
		return self::normalizeLinkUrl( $url );
1945
	}
1946
1947
	/**
1948
	 * Replace unusual escape codes in a URL with their equivalent characters
1949
	 *
1950
	 * This generally follows the syntax defined in RFC 3986, with special
1951
	 * consideration for HTTP query strings.
1952
	 *
1953
	 * @param string $url
1954
	 * @return string
1955
	 */
1956
	public static function normalizeLinkUrl( $url ) {
1957
		# First, make sure unsafe characters are encoded
1958
		$url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1959
			function ( $m ) {
1960
				return rawurlencode( $m[0] );
1961
			},
1962
			$url
1963
		);
1964
1965
		$ret = '';
1966
		$end = strlen( $url );
1967
1968
		# Fragment part - 'fragment'
1969
		$start = strpos( $url, '#' );
1970 View Code Duplication
		if ( $start !== false && $start < $end ) {
1971
			$ret = self::normalizeUrlComponent(
1972
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1973
			$end = $start;
1974
		}
1975
1976
		# Query part - 'query' minus &=+;
1977
		$start = strpos( $url, '?' );
1978 View Code Duplication
		if ( $start !== false && $start < $end ) {
1979
			$ret = self::normalizeUrlComponent(
1980
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1981
			$end = $start;
1982
		}
1983
1984
		# Scheme and path part - 'pchar'
1985
		# (we assume no userinfo or encoded colons in the host)
1986
		$ret = self::normalizeUrlComponent(
1987
			substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1988
1989
		return $ret;
1990
	}
1991
1992
	private static function normalizeUrlComponent( $component, $unsafe ) {
1993
		$callback = function ( $matches ) use ( $unsafe ) {
1994
			$char = urldecode( $matches[0] );
1995
			$ord = ord( $char );
1996
			if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1997
				# Unescape it
1998
				return $char;
1999
			} else {
2000
				# Leave it escaped, but use uppercase for a-f
2001
				return strtoupper( $matches[0] );
2002
			}
2003
		};
2004
		return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2005
	}
2006
2007
	/**
2008
	 * make an image if it's allowed, either through the global
2009
	 * option, through the exception, or through the on-wiki whitelist
2010
	 *
2011
	 * @param string $url
2012
	 *
2013
	 * @return string
2014
	 */
2015
	private function maybeMakeExternalImage( $url ) {
2016
		$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2017
		$imagesexception = !empty( $imagesfrom );
2018
		$text = false;
2019
		# $imagesfrom could be either a single string or an array of strings, parse out the latter
2020
		if ( $imagesexception && is_array( $imagesfrom ) ) {
2021
			$imagematch = false;
2022
			foreach ( $imagesfrom as $match ) {
2023
				if ( strpos( $url, $match ) === 0 ) {
2024
					$imagematch = true;
2025
					break;
2026
				}
2027
			}
2028
		} elseif ( $imagesexception ) {
2029
			$imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2030
		} else {
2031
			$imagematch = false;
2032
		}
2033
2034
		if ( $this->mOptions->getAllowExternalImages()
2035
			|| ( $imagesexception && $imagematch )
2036
		) {
2037
			if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2038
				# Image found
2039
				$text = Linker::makeExternalImage( $url );
2040
			}
2041
		}
2042
		if ( !$text && $this->mOptions->getEnableImageWhitelist()
0 ignored issues
show
Bug Best Practice introduced by
The expression $text of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2043
			&& preg_match( self::EXT_IMAGE_REGEX, $url )
2044
		) {
2045
			$whitelist = explode(
2046
				"\n",
2047
				wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2048
			);
2049
2050
			foreach ( $whitelist as $entry ) {
2051
				# Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2052
				if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2053
					continue;
2054
				}
2055
				if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2056
					# Image matches a whitelist entry
2057
					$text = Linker::makeExternalImage( $url );
2058
					break;
2059
				}
2060
			}
2061
		}
2062
		return $text;
2063
	}
2064
2065
	/**
2066
	 * Process [[ ]] wikilinks
2067
	 *
2068
	 * @param string $s
2069
	 *
2070
	 * @return string Processed text
2071
	 *
2072
	 * @private
2073
	 */
2074
	public function replaceInternalLinks( $s ) {
2075
		$this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2076
		return $s;
2077
	}
2078
2079
	/**
2080
	 * Process [[ ]] wikilinks (RIL)
2081
	 * @param string $s
2082
	 * @throws MWException
2083
	 * @return LinkHolderArray
2084
	 *
2085
	 * @private
2086
	 */
2087
	public function replaceInternalLinks2( &$s ) {
2088
		global $wgExtraInterlanguageLinkPrefixes;
2089
2090
		static $tc = false, $e1, $e1_img;
2091
		# the % is needed to support urlencoded titles as well
2092
		if ( !$tc ) {
2093
			$tc = Title::legalChars() . '#%';
2094
			# Match a link having the form [[namespace:link|alternate]]trail
2095
			$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2096
			# Match cases where there is no "]]", which might still be images
2097
			$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2098
		}
2099
2100
		$holders = new LinkHolderArray( $this );
2101
2102
		# split the entire text string on occurrences of [[
2103
		$a = StringUtils::explode( '[[', ' ' . $s );
2104
		# get the first element (all text up to first [[), and remove the space we added
2105
		$s = $a->current();
2106
		$a->next();
2107
		$line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2108
		$s = substr( $s, 1 );
2109
2110
		$useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2111
		$e2 = null;
2112
		if ( $useLinkPrefixExtension ) {
2113
			# Match the end of a line for a word that's not followed by whitespace,
2114
			# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2115
			global $wgContLang;
2116
			$charset = $wgContLang->linkPrefixCharset();
2117
			$e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2118
		}
2119
2120
		if ( is_null( $this->mTitle ) ) {
2121
			throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2122
		}
2123
		$nottalk = !$this->mTitle->isTalkPage();
2124
2125 View Code Duplication
		if ( $useLinkPrefixExtension ) {
2126
			$m = [];
2127
			if ( preg_match( $e2, $s, $m ) ) {
2128
				$first_prefix = $m[2];
2129
			} else {
2130
				$first_prefix = false;
2131
			}
2132
		} else {
2133
			$prefix = '';
2134
		}
2135
2136
		$useSubpages = $this->areSubpagesAllowed();
2137
2138
		// @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2139
		# Loop for each link
2140
		for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2141
			// @codingStandardsIgnoreEnd
2142
2143
			# Check for excessive memory usage
2144
			if ( $holders->isBig() ) {
2145
				# Too big
2146
				# Do the existence check, replace the link holders and clear the array
2147
				$holders->replace( $s );
2148
				$holders->clear();
2149
			}
2150
2151
			if ( $useLinkPrefixExtension ) {
2152 View Code Duplication
				if ( preg_match( $e2, $s, $m ) ) {
2153
					$prefix = $m[2];
2154
					$s = $m[1];
2155
				} else {
2156
					$prefix = '';
2157
				}
2158
				# first link
2159
				if ( $first_prefix ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $first_prefix of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2160
					$prefix = $first_prefix;
0 ignored issues
show
Bug introduced by
The variable $first_prefix 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...
2161
					$first_prefix = false;
2162
				}
2163
			}
2164
2165
			$might_be_img = false;
2166
2167
			if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2168
				$text = $m[2];
2169
				# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2170
				# [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2171
				# the real problem is with the $e1 regex
2172
				# See bug 1300.
2173
				# Still some problems for cases where the ] is meant to be outside punctuation,
2174
				# and no image is in sight. See bug 2095.
2175
				if ( $text !== ''
2176
					&& substr( $m[3], 0, 1 ) === ']'
2177
					&& strpos( $text, '[' ) !== false
2178
				) {
2179
					$text .= ']'; # so that replaceExternalLinks($text) works later
2180
					$m[3] = substr( $m[3], 1 );
2181
				}
2182
				# fix up urlencoded title texts
2183 View Code Duplication
				if ( strpos( $m[1], '%' ) !== false ) {
2184
					# Should anchors '#' also be rejected?
2185
					$m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2186
				}
2187
				$trail = $m[3];
2188
			} elseif ( preg_match( $e1_img, $line, $m ) ) {
2189
				# Invalid, but might be an image with a link in its caption
2190
				$might_be_img = true;
2191
				$text = $m[2];
2192 View Code Duplication
				if ( strpos( $m[1], '%' ) !== false ) {
2193
					$m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2194
				}
2195
				$trail = "";
2196
			} else { # Invalid form; output directly
2197
				$s .= $prefix . '[[' . $line;
0 ignored issues
show
Bug introduced by
The variable $prefix 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...
2198
				continue;
2199
			}
2200
2201
			$origLink = $m[1];
2202
2203
			# Don't allow internal links to pages containing
2204
			# PROTO: where PROTO is a valid URL protocol; these
2205
			# should be external links.
2206
			if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2207
				$s .= $prefix . '[[' . $line;
2208
				continue;
2209
			}
2210
2211
			# Make subpage if necessary
2212
			if ( $useSubpages ) {
2213
				$link = $this->maybeDoSubpageLink( $origLink, $text );
2214
			} else {
2215
				$link = $origLink;
2216
			}
2217
2218
			$noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2219
			if ( !$noforce ) {
2220
				# Strip off leading ':'
2221
				$link = substr( $link, 1 );
2222
			}
2223
2224
			$unstrip = $this->mStripState->unstripNoWiki( $link );
2225
			$nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2226
			if ( $nt === null ) {
2227
				$s .= $prefix . '[[' . $line;
2228
				continue;
2229
			}
2230
2231
			$ns = $nt->getNamespace();
2232
			$iw = $nt->getInterwiki();
2233
2234
			if ( $might_be_img ) { # if this is actually an invalid link
2235
				if ( $ns == NS_FILE && $noforce ) { # but might be an image
2236
					$found = false;
2237
					while ( true ) {
2238
						# look at the next 'line' to see if we can close it there
2239
						$a->next();
2240
						$next_line = $a->current();
2241
						if ( $next_line === false || $next_line === null ) {
2242
							break;
2243
						}
2244
						$m = explode( ']]', $next_line, 3 );
2245
						if ( count( $m ) == 3 ) {
2246
							# the first ]] closes the inner link, the second the image
2247
							$found = true;
2248
							$text .= "[[{$m[0]}]]{$m[1]}";
2249
							$trail = $m[2];
2250
							break;
2251
						} elseif ( count( $m ) == 2 ) {
2252
							# if there's exactly one ]] that's fine, we'll keep looking
2253
							$text .= "[[{$m[0]}]]{$m[1]}";
2254
						} else {
2255
							# if $next_line is invalid too, we need look no further
2256
							$text .= '[[' . $next_line;
2257
							break;
2258
						}
2259
					}
2260
					if ( !$found ) {
2261
						# we couldn't find the end of this imageLink, so output it raw
2262
						# but don't ignore what might be perfectly normal links in the text we've examined
2263
						$holders->merge( $this->replaceInternalLinks2( $text ) );
2264
						$s .= "{$prefix}[[$link|$text";
2265
						# note: no $trail, because without an end, there *is* no trail
2266
						continue;
2267
					}
2268
				} else { # it's not an image, so output it raw
2269
					$s .= "{$prefix}[[$link|$text";
2270
					# note: no $trail, because without an end, there *is* no trail
2271
					continue;
2272
				}
2273
			}
2274
2275
			$wasblank = ( $text == '' );
2276
			if ( $wasblank ) {
2277
				$text = $link;
2278
			} else {
2279
				# Bug 4598 madness. Handle the quotes only if they come from the alternate part
2280
				# [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2281
				# [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2282
				#    -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2283
				$text = $this->doQuotes( $text );
2284
			}
2285
2286
			# Link not escaped by : , create the various objects
2287
			if ( $noforce && !$nt->wasLocalInterwiki() ) {
2288
				# Interwikis
2289
				if (
2290
					$iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2291
						Language::fetchLanguageName( $iw, null, 'mw' ) ||
2292
						in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2293
					)
2294
				) {
2295
					# Bug 24502: filter duplicates
2296
					if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2297
						$this->mLangLinkLanguages[$iw] = true;
2298
						$this->mOutput->addLanguageLink( $nt->getFullText() );
2299
					}
2300
2301
					$s = rtrim( $s . $prefix );
2302
					$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2303
					continue;
2304
				}
2305
2306
				if ( $ns == NS_FILE ) {
2307
					if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2308
						if ( $wasblank ) {
2309
							# if no parameters were passed, $text
2310
							# becomes something like "File:Foo.png",
2311
							# which we don't want to pass on to the
2312
							# image generator
2313
							$text = '';
2314
						} else {
2315
							# recursively parse links inside the image caption
2316
							# actually, this will parse them in any other parameters, too,
2317
							# but it might be hard to fix that, and it doesn't matter ATM
2318
							$text = $this->replaceExternalLinks( $text );
2319
							$holders->merge( $this->replaceInternalLinks2( $text ) );
2320
						}
2321
						# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2322
						$s .= $prefix . $this->armorLinks(
2323
							$this->makeImage( $nt, $text, $holders ) ) . $trail;
2324
						continue;
2325
					}
2326
				} elseif ( $ns == NS_CATEGORY ) {
2327
					$s = rtrim( $s . "\n" ); # bug 87
2328
2329
					if ( $wasblank ) {
2330
						$sortkey = $this->getDefaultSort();
2331
					} else {
2332
						$sortkey = $text;
2333
					}
2334
					$sortkey = Sanitizer::decodeCharReferences( $sortkey );
2335
					$sortkey = str_replace( "\n", '', $sortkey );
2336
					$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2337
					$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2338
2339
					/**
2340
					 * Strip the whitespace Category links produce, see bug 87
2341
					 */
2342
					$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2343
2344
					continue;
2345
				}
2346
			}
2347
2348
			# Self-link checking. For some languages, variants of the title are checked in
2349
			# LinkHolderArray::doVariants() to allow batching the existence checks necessary
2350
			# for linking to a different variant.
2351
			if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2352
				$s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2353
				continue;
2354
			}
2355
2356
			# NS_MEDIA is a pseudo-namespace for linking directly to a file
2357
			# @todo FIXME: Should do batch file existence checks, see comment below
2358
			if ( $ns == NS_MEDIA ) {
2359
				# Give extensions a chance to select the file revision for us
2360
				$options = [];
2361
				$descQuery = false;
2362
				Hooks::run( 'BeforeParserFetchFileAndTitle',
2363
					[ $this, $nt, &$options, &$descQuery ] );
2364
				# Fetch and register the file (file title may be different via hooks)
2365
				list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2366
				# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2367
				$s .= $prefix . $this->armorLinks(
2368
					Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2369
				continue;
2370
			}
2371
2372
			# Some titles, such as valid special pages or files in foreign repos, should
2373
			# be shown as bluelinks even though they're not included in the page table
2374
			# @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2375
			# batch file existence checks for NS_FILE and NS_MEDIA
2376
			if ( $iw == '' && $nt->isAlwaysKnown() ) {
2377
				$this->mOutput->addLink( $nt );
2378
				$s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2379
			} else {
2380
				# Links will be added to the output link list after checking
2381
				$s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2382
			}
2383
		}
2384
		return $holders;
2385
	}
2386
2387
	/**
2388
	 * Render a forced-blue link inline; protect against double expansion of
2389
	 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2390
	 * Since this little disaster has to split off the trail text to avoid
2391
	 * breaking URLs in the following text without breaking trails on the
2392
	 * wiki links, it's been made into a horrible function.
2393
	 *
2394
	 * @param Title $nt
2395
	 * @param string $text
2396
	 * @param string $trail
2397
	 * @param string $prefix
2398
	 * @return string HTML-wikitext mix oh yuck
2399
	 */
2400
	protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2401
		list( $inside, $trail ) = Linker::splitTrail( $trail );
2402
2403
		if ( $text == '' ) {
2404
			$text = htmlspecialchars( $nt->getPrefixedText() );
2405
		}
2406
2407
		$link = $this->getLinkRenderer()->makeKnownLink(
2408
			$nt, new HtmlArmor( "$prefix$text$inside" )
2409
		);
2410
2411
		return $this->armorLinks( $link ) . $trail;
2412
	}
2413
2414
	/**
2415
	 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2416
	 * going to go through further parsing steps before inline URL expansion.
2417
	 *
2418
	 * Not needed quite as much as it used to be since free links are a bit
2419
	 * more sensible these days. But bracketed links are still an issue.
2420
	 *
2421
	 * @param string $text More-or-less HTML
2422
	 * @return string Less-or-more HTML with NOPARSE bits
2423
	 */
2424
	public function armorLinks( $text ) {
2425
		return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2426
			self::MARKER_PREFIX . "NOPARSE$1", $text );
2427
	}
2428
2429
	/**
2430
	 * Return true if subpage links should be expanded on this page.
2431
	 * @return bool
2432
	 */
2433
	public function areSubpagesAllowed() {
2434
		# Some namespaces don't allow subpages
2435
		return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2436
	}
2437
2438
	/**
2439
	 * Handle link to subpage if necessary
2440
	 *
2441
	 * @param string $target The source of the link
2442
	 * @param string &$text The link text, modified as necessary
2443
	 * @return string The full name of the link
2444
	 * @private
2445
	 */
2446
	public function maybeDoSubpageLink( $target, &$text ) {
2447
		return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2448
	}
2449
2450
	/**
2451
	 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
2452
	 *
2453
	 * @param string $text
2454
	 * @param bool $linestart Whether or not this is at the start of a line.
2455
	 * @private
2456
	 * @return string The lists rendered as HTML
2457
	 */
2458
	public function doBlockLevels( $text, $linestart ) {
2459
		return BlockLevelPass::doBlockLevels( $text, $linestart );
2460
	}
2461
2462
	/**
2463
	 * Return value of a magic variable (like PAGENAME)
2464
	 *
2465
	 * @private
2466
	 *
2467
	 * @param int $index
2468
	 * @param bool|PPFrame $frame
2469
	 *
2470
	 * @throws MWException
2471
	 * @return string
2472
	 */
2473
	public function getVariableValue( $index, $frame = false ) {
2474
		global $wgContLang, $wgSitename, $wgServer, $wgServerName;
2475
		global $wgArticlePath, $wgScriptPath, $wgStylePath;
2476
2477
		if ( is_null( $this->mTitle ) ) {
2478
			// If no title set, bad things are going to happen
2479
			// later. Title should always be set since this
2480
			// should only be called in the middle of a parse
2481
			// operation (but the unit-tests do funky stuff)
2482
			throw new MWException( __METHOD__ . ' Should only be '
2483
				. ' called while parsing (no title set)' );
2484
		}
2485
2486
		/**
2487
		 * Some of these require message or data lookups and can be
2488
		 * expensive to check many times.
2489
		 */
2490
		if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
2491
			if ( isset( $this->mVarCache[$index] ) ) {
2492
				return $this->mVarCache[$index];
2493
			}
2494
		}
2495
2496
		$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2497
		Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
2498
2499
		$pageLang = $this->getFunctionLang();
2500
2501
		switch ( $index ) {
2502
			case '!':
2503
				$value = '|';
2504
				break;
2505
			case 'currentmonth':
2506
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2507
				break;
2508
			case 'currentmonth1':
2509
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2510
				break;
2511
			case 'currentmonthname':
2512
				$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2513
				break;
2514
			case 'currentmonthnamegen':
2515
				$value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2516
				break;
2517
			case 'currentmonthabbrev':
2518
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2519
				break;
2520
			case 'currentday':
2521
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2522
				break;
2523
			case 'currentday2':
2524
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2525
				break;
2526
			case 'localmonth':
2527
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2528
				break;
2529
			case 'localmonth1':
2530
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2531
				break;
2532
			case 'localmonthname':
2533
				$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2534
				break;
2535
			case 'localmonthnamegen':
2536
				$value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2537
				break;
2538
			case 'localmonthabbrev':
2539
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2540
				break;
2541
			case 'localday':
2542
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2543
				break;
2544
			case 'localday2':
2545
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2546
				break;
2547
			case 'pagename':
2548
				$value = wfEscapeWikiText( $this->mTitle->getText() );
2549
				break;
2550
			case 'pagenamee':
2551
				$value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2552
				break;
2553
			case 'fullpagename':
2554
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2555
				break;
2556
			case 'fullpagenamee':
2557
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2558
				break;
2559
			case 'subpagename':
2560
				$value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2561
				break;
2562
			case 'subpagenamee':
2563
				$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2564
				break;
2565
			case 'rootpagename':
2566
				$value = wfEscapeWikiText( $this->mTitle->getRootText() );
2567
				break;
2568 View Code Duplication
			case 'rootpagenamee':
2569
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2570
					' ',
2571
					'_',
2572
					$this->mTitle->getRootText()
2573
				) ) );
2574
				break;
2575
			case 'basepagename':
2576
				$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2577
				break;
2578 View Code Duplication
			case 'basepagenamee':
2579
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2580
					' ',
2581
					'_',
2582
					$this->mTitle->getBaseText()
2583
				) ) );
2584
				break;
2585 View Code Duplication
			case 'talkpagename':
2586
				if ( $this->mTitle->canTalk() ) {
2587
					$talkPage = $this->mTitle->getTalkPage();
2588
					$value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2589
				} else {
2590
					$value = '';
2591
				}
2592
				break;
2593 View Code Duplication
			case 'talkpagenamee':
2594
				if ( $this->mTitle->canTalk() ) {
2595
					$talkPage = $this->mTitle->getTalkPage();
2596
					$value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2597
				} else {
2598
					$value = '';
2599
				}
2600
				break;
2601
			case 'subjectpagename':
2602
				$subjPage = $this->mTitle->getSubjectPage();
2603
				$value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2604
				break;
2605
			case 'subjectpagenamee':
2606
				$subjPage = $this->mTitle->getSubjectPage();
2607
				$value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2608
				break;
2609
			case 'pageid': // requested in bug 23427
2610
				$pageid = $this->getTitle()->getArticleID();
2611
				if ( $pageid == 0 ) {
2612
					# 0 means the page doesn't exist in the database,
2613
					# which means the user is previewing a new page.
2614
					# The vary-revision flag must be set, because the magic word
2615
					# will have a different value once the page is saved.
2616
					$this->mOutput->setFlag( 'vary-revision' );
2617
					wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2618
				}
2619
				$value = $pageid ? $pageid : null;
2620
				break;
2621
			case 'revisionid':
2622
				# Let the edit saving system know we should parse the page
2623
				# *after* a revision ID has been assigned.
2624
				$this->mOutput->setFlag( 'vary-revision-id' );
2625
				wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2626
				$value = $this->mRevisionId;
2627
				if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2628
					$value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
2629
					$this->mOutput->setSpeculativeRevIdUsed( $value );
2630
				}
2631
				break;
2632 View Code Duplication
			case 'revisionday':
2633
				# Let the edit saving system know we should parse the page
2634
				# *after* a revision ID has been assigned. This is for null edits.
2635
				$this->mOutput->setFlag( 'vary-revision' );
2636
				wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2637
				$value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2638
				break;
2639 View Code Duplication
			case 'revisionday2':
2640
				# Let the edit saving system know we should parse the page
2641
				# *after* a revision ID has been assigned. This is for null edits.
2642
				$this->mOutput->setFlag( 'vary-revision' );
2643
				wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2644
				$value = substr( $this->getRevisionTimestamp(), 6, 2 );
2645
				break;
2646 View Code Duplication
			case 'revisionmonth':
2647
				# Let the edit saving system know we should parse the page
2648
				# *after* a revision ID has been assigned. This is for null edits.
2649
				$this->mOutput->setFlag( 'vary-revision' );
2650
				wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2651
				$value = substr( $this->getRevisionTimestamp(), 4, 2 );
2652
				break;
2653 View Code Duplication
			case 'revisionmonth1':
2654
				# Let the edit saving system know we should parse the page
2655
				# *after* a revision ID has been assigned. This is for null edits.
2656
				$this->mOutput->setFlag( 'vary-revision' );
2657
				wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2658
				$value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2659
				break;
2660 View Code Duplication
			case 'revisionyear':
2661
				# Let the edit saving system know we should parse the page
2662
				# *after* a revision ID has been assigned. This is for null edits.
2663
				$this->mOutput->setFlag( 'vary-revision' );
2664
				wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2665
				$value = substr( $this->getRevisionTimestamp(), 0, 4 );
2666
				break;
2667
			case 'revisiontimestamp':
2668
				# Let the edit saving system know we should parse the page
2669
				# *after* a revision ID has been assigned. This is for null edits.
2670
				$this->mOutput->setFlag( 'vary-revision' );
2671
				wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2672
				$value = $this->getRevisionTimestamp();
2673
				break;
2674
			case 'revisionuser':
2675
				# Let the edit saving system know we should parse the page
2676
				# *after* a revision ID has been assigned for null edits.
2677
				$this->mOutput->setFlag( 'vary-user' );
2678
				wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2679
				$value = $this->getRevisionUser();
2680
				break;
2681
			case 'revisionsize':
2682
				$value = $this->getRevisionSize();
2683
				break;
2684
			case 'namespace':
2685
				$value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2686
				break;
2687
			case 'namespacee':
2688
				$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2689
				break;
2690
			case 'namespacenumber':
2691
				$value = $this->mTitle->getNamespace();
2692
				break;
2693
			case 'talkspace':
2694
				$value = $this->mTitle->canTalk()
2695
					? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2696
					: '';
2697
				break;
2698
			case 'talkspacee':
2699
				$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2700
				break;
2701
			case 'subjectspace':
2702
				$value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2703
				break;
2704
			case 'subjectspacee':
2705
				$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2706
				break;
2707
			case 'currentdayname':
2708
				$value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2709
				break;
2710
			case 'currentyear':
2711
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2712
				break;
2713
			case 'currenttime':
2714
				$value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
0 ignored issues
show
Security Bug introduced by
It seems like wfTimestamp(TS_MW, $ts) targeting wfTimestamp() can also be of type false; however, Language::time() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
2715
				break;
2716
			case 'currenthour':
2717
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2718
				break;
2719
			case 'currentweek':
2720
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2721
				# int to remove the padding
2722
				$value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2723
				break;
2724
			case 'currentdow':
2725
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2726
				break;
2727
			case 'localdayname':
2728
				$value = $pageLang->getWeekdayName(
2729
					(int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2730
				);
2731
				break;
2732
			case 'localyear':
2733
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2734
				break;
2735
			case 'localtime':
2736
				$value = $pageLang->time(
2737
					MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2738
					false,
2739
					false
2740
				);
2741
				break;
2742
			case 'localhour':
2743
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2744
				break;
2745
			case 'localweek':
2746
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2747
				# int to remove the padding
2748
				$value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2749
				break;
2750
			case 'localdow':
2751
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2752
				break;
2753
			case 'numberofarticles':
2754
				$value = $pageLang->formatNum( SiteStats::articles() );
2755
				break;
2756
			case 'numberoffiles':
2757
				$value = $pageLang->formatNum( SiteStats::images() );
2758
				break;
2759
			case 'numberofusers':
2760
				$value = $pageLang->formatNum( SiteStats::users() );
2761
				break;
2762
			case 'numberofactiveusers':
2763
				$value = $pageLang->formatNum( SiteStats::activeUsers() );
2764
				break;
2765
			case 'numberofpages':
2766
				$value = $pageLang->formatNum( SiteStats::pages() );
2767
				break;
2768
			case 'numberofadmins':
2769
				$value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2770
				break;
2771
			case 'numberofedits':
2772
				$value = $pageLang->formatNum( SiteStats::edits() );
2773
				break;
2774
			case 'currenttimestamp':
2775
				$value = wfTimestamp( TS_MW, $ts );
2776
				break;
2777
			case 'localtimestamp':
2778
				$value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2779
				break;
2780
			case 'currentversion':
2781
				$value = SpecialVersion::getVersion();
2782
				break;
2783
			case 'articlepath':
2784
				return $wgArticlePath;
2785
			case 'sitename':
2786
				return $wgSitename;
2787
			case 'server':
2788
				return $wgServer;
2789
			case 'servername':
2790
				return $wgServerName;
2791
			case 'scriptpath':
2792
				return $wgScriptPath;
2793
			case 'stylepath':
2794
				return $wgStylePath;
2795
			case 'directionmark':
2796
				return $pageLang->getDirMark();
2797
			case 'contentlanguage':
2798
				global $wgLanguageCode;
2799
				return $wgLanguageCode;
2800
			case 'cascadingsources':
2801
				$value = CoreParserFunctions::cascadingsources( $this );
2802
				break;
2803
			default:
2804
				$ret = null;
2805
				Hooks::run(
2806
					'ParserGetVariableValueSwitch',
2807
					[ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
2808
				);
2809
2810
				return $ret;
2811
		}
2812
2813
		if ( $index ) {
2814
			$this->mVarCache[$index] = $value;
2815
		}
2816
2817
		return $value;
2818
	}
2819
2820
	/**
2821
	 * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
2822
	 *
2823
	 * @private
2824
	 */
2825
	public function initialiseVariables() {
2826
		$variableIDs = MagicWord::getVariableIDs();
2827
		$substIDs = MagicWord::getSubstIDs();
2828
2829
		$this->mVariables = new MagicWordArray( $variableIDs );
2830
		$this->mSubstWords = new MagicWordArray( $substIDs );
2831
	}
2832
2833
	/**
2834
	 * Preprocess some wikitext and return the document tree.
2835
	 * This is the ghost of replace_variables().
2836
	 *
2837
	 * @param string $text The text to parse
2838
	 * @param int $flags Bitwise combination of:
2839
	 *   - self::PTD_FOR_INCLUSION: Handle "<noinclude>" and "<includeonly>" as if the text is being
2840
	 *     included. Default is to assume a direct page view.
2841
	 *
2842
	 * The generated DOM tree must depend only on the input text and the flags.
2843
	 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
2844
	 *
2845
	 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
2846
	 * change in the DOM tree for a given text, must be passed through the section identifier
2847
	 * in the section edit link and thus back to extractSections().
2848
	 *
2849
	 * The output of this function is currently only cached in process memory, but a persistent
2850
	 * cache may be implemented at a later date which takes further advantage of these strict
2851
	 * dependency requirements.
2852
	 *
2853
	 * @return PPNode
2854
	 */
2855
	public function preprocessToDom( $text, $flags = 0 ) {
2856
		$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2857
		return $dom;
2858
	}
2859
2860
	/**
2861
	 * Return a three-element array: leading whitespace, string contents, trailing whitespace
2862
	 *
2863
	 * @param string $s
2864
	 *
2865
	 * @return array
2866
	 */
2867
	public static function splitWhitespace( $s ) {
2868
		$ltrimmed = ltrim( $s );
2869
		$w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2870
		$trimmed = rtrim( $ltrimmed );
2871
		$diff = strlen( $ltrimmed ) - strlen( $trimmed );
2872
		if ( $diff > 0 ) {
2873
			$w2 = substr( $ltrimmed, -$diff );
2874
		} else {
2875
			$w2 = '';
2876
		}
2877
		return [ $w1, $trimmed, $w2 ];
2878
	}
2879
2880
	/**
2881
	 * Replace magic variables, templates, and template arguments
2882
	 * with the appropriate text. Templates are substituted recursively,
2883
	 * taking care to avoid infinite loops.
2884
	 *
2885
	 * Note that the substitution depends on value of $mOutputType:
2886
	 *  self::OT_WIKI: only {{subst:}} templates
2887
	 *  self::OT_PREPROCESS: templates but not extension tags
2888
	 *  self::OT_HTML: all templates and extension tags
2889
	 *
2890
	 * @param string $text The text to transform
2891
	 * @param bool|PPFrame $frame Object describing the arguments passed to the
2892
	 *   template. Arguments may also be provided as an associative array, as
2893
	 *   was the usual case before MW1.12. Providing arguments this way may be
2894
	 *   useful for extensions wishing to perform variable replacement
2895
	 *   explicitly.
2896
	 * @param bool $argsOnly Only do argument (triple-brace) expansion, not
2897
	 *   double-brace expansion.
2898
	 * @return string
2899
	 */
2900
	public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2901
		# Is there any text? Also, Prevent too big inclusions!
2902
		$textSize = strlen( $text );
2903
		if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2904
			return $text;
2905
		}
2906
2907
		if ( $frame === false ) {
2908
			$frame = $this->getPreprocessor()->newFrame();
2909
		} elseif ( !( $frame instanceof PPFrame ) ) {
2910
			wfDebug( __METHOD__ . " called using plain parameters instead of "
2911
				. "a PPFrame instance. Creating custom frame.\n" );
2912
			$frame = $this->getPreprocessor()->newCustomFrame( $frame );
2913
		}
2914
2915
		$dom = $this->preprocessToDom( $text );
2916
		$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2917
		$text = $frame->expand( $dom, $flags );
2918
2919
		return $text;
2920
	}
2921
2922
	/**
2923
	 * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
2924
	 *
2925
	 * @param array $args
2926
	 *
2927
	 * @return array
2928
	 */
2929
	public static function createAssocArgs( $args ) {
2930
		$assocArgs = [];
2931
		$index = 1;
2932
		foreach ( $args as $arg ) {
2933
			$eqpos = strpos( $arg, '=' );
2934
			if ( $eqpos === false ) {
2935
				$assocArgs[$index++] = $arg;
2936
			} else {
2937
				$name = trim( substr( $arg, 0, $eqpos ) );
2938
				$value = trim( substr( $arg, $eqpos + 1 ) );
2939
				if ( $value === false ) {
2940
					$value = '';
2941
				}
2942
				if ( $name !== false ) {
2943
					$assocArgs[$name] = $value;
2944
				}
2945
			}
2946
		}
2947
2948
		return $assocArgs;
2949
	}
2950
2951
	/**
2952
	 * Warn the user when a parser limitation is reached
2953
	 * Will warn at most once the user per limitation type
2954
	 *
2955
	 * The results are shown during preview and run through the Parser (See EditPage.php)
2956
	 *
2957
	 * @param string $limitationType Should be one of:
2958
	 *   'expensive-parserfunction' (corresponding messages:
2959
	 *       'expensive-parserfunction-warning',
2960
	 *       'expensive-parserfunction-category')
2961
	 *   'post-expand-template-argument' (corresponding messages:
2962
	 *       'post-expand-template-argument-warning',
2963
	 *       'post-expand-template-argument-category')
2964
	 *   'post-expand-template-inclusion' (corresponding messages:
2965
	 *       'post-expand-template-inclusion-warning',
2966
	 *       'post-expand-template-inclusion-category')
2967
	 *   'node-count-exceeded' (corresponding messages:
2968
	 *       'node-count-exceeded-warning',
2969
	 *       'node-count-exceeded-category')
2970
	 *   'expansion-depth-exceeded' (corresponding messages:
2971
	 *       'expansion-depth-exceeded-warning',
2972
	 *       'expansion-depth-exceeded-category')
2973
	 * @param string|int|null $current Current value
2974
	 * @param string|int|null $max Maximum allowed, when an explicit limit has been
2975
	 *	 exceeded, provide the values (optional)
2976
	 */
2977
	public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2978
		# does no harm if $current and $max are present but are unnecessary for the message
2979
		# Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2980
		# only during preview, and that would split the parser cache unnecessarily.
2981
		$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2982
			->text();
2983
		$this->mOutput->addWarning( $warning );
2984
		$this->addTrackingCategory( "$limitationType-category" );
2985
	}
2986
2987
	/**
2988
	 * Return the text of a template, after recursively
2989
	 * replacing any variables or templates within the template.
2990
	 *
2991
	 * @param array $piece The parts of the template
2992
	 *   $piece['title']: the title, i.e. the part before the |
2993
	 *   $piece['parts']: the parameter array
2994
	 *   $piece['lineStart']: whether the brace was at the start of a line
2995
	 * @param PPFrame $frame The current frame, contains template arguments
2996
	 * @throws Exception
2997
	 * @return string The text of the template
2998
	 */
2999
	public function braceSubstitution( $piece, $frame ) {
3000
3001
		// Flags
3002
3003
		// $text has been filled
3004
		$found = false;
3005
		// wiki markup in $text should be escaped
3006
		$nowiki = false;
3007
		// $text is HTML, armour it against wikitext transformation
3008
		$isHTML = false;
3009
		// Force interwiki transclusion to be done in raw mode not rendered
3010
		$forceRawInterwiki = false;
3011
		// $text is a DOM node needing expansion in a child frame
3012
		$isChildObj = false;
3013
		// $text is a DOM node needing expansion in the current frame
3014
		$isLocalObj = false;
3015
3016
		# Title object, where $text came from
3017
		$title = false;
3018
3019
		# $part1 is the bit before the first |, and must contain only title characters.
3020
		# Various prefixes will be stripped from it later.
3021
		$titleWithSpaces = $frame->expand( $piece['title'] );
3022
		$part1 = trim( $titleWithSpaces );
3023
		$titleText = false;
3024
3025
		# Original title text preserved for various purposes
3026
		$originalTitle = $part1;
3027
3028
		# $args is a list of argument nodes, starting from index 0, not including $part1
3029
		# @todo FIXME: If piece['parts'] is null then the call to getLength()
3030
		# below won't work b/c this $args isn't an object
3031
		$args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3032
3033
		$profileSection = null; // profile templates
3034
3035
		# SUBST
3036
		if ( !$found ) {
3037
			$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3038
3039
			# Possibilities for substMatch: "subst", "safesubst" or FALSE
3040
			# Decide whether to expand template or keep wikitext as-is.
3041
			if ( $this->ot['wiki'] ) {
3042
				if ( $substMatch === false ) {
3043
					$literal = true;  # literal when in PST with no prefix
3044
				} else {
3045
					$literal = false; # expand when in PST with subst: or safesubst:
3046
				}
3047
			} else {
3048
				if ( $substMatch == 'subst' ) {
3049
					$literal = true;  # literal when not in PST with plain subst:
3050
				} else {
3051
					$literal = false; # expand when not in PST with safesubst: or no prefix
3052
				}
3053
			}
3054
			if ( $literal ) {
3055
				$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3056
				$isLocalObj = true;
3057
				$found = true;
3058
			}
3059
		}
3060
3061
		# Variables
3062
		if ( !$found && $args->getLength() == 0 ) {
3063
			$id = $this->mVariables->matchStartToEnd( $part1 );
3064
			if ( $id !== false ) {
3065
				$text = $this->getVariableValue( $id, $frame );
3066
				if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3067
					$this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3068
				}
3069
				$found = true;
3070
			}
3071
		}
3072
3073
		# MSG, MSGNW and RAW
3074
		if ( !$found ) {
3075
			# Check for MSGNW:
3076
			$mwMsgnw = MagicWord::get( 'msgnw' );
3077
			if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3078
				$nowiki = true;
3079
			} else {
3080
				# Remove obsolete MSG:
3081
				$mwMsg = MagicWord::get( 'msg' );
3082
				$mwMsg->matchStartAndRemove( $part1 );
3083
			}
3084
3085
			# Check for RAW:
3086
			$mwRaw = MagicWord::get( 'raw' );
3087
			if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3088
				$forceRawInterwiki = true;
3089
			}
3090
		}
3091
3092
		# Parser functions
3093
		if ( !$found ) {
3094
			$colonPos = strpos( $part1, ':' );
3095
			if ( $colonPos !== false ) {
3096
				$func = substr( $part1, 0, $colonPos );
3097
				$funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3098
				$argsLength = $args->getLength();
3099
				for ( $i = 0; $i < $argsLength; $i++ ) {
3100
					$funcArgs[] = $args->item( $i );
3101
				}
3102
				try {
3103
					$result = $this->callParserFunction( $frame, $func, $funcArgs );
3104
				} catch ( Exception $ex ) {
3105
					throw $ex;
3106
				}
3107
3108
				# The interface for parser functions allows for extracting
3109
				# flags into the local scope. Extract any forwarded flags
3110
				# here.
3111
				extract( $result );
3112
			}
3113
		}
3114
3115
		# Finish mangling title and then check for loops.
3116
		# Set $title to a Title object and $titleText to the PDBK
3117
		if ( !$found ) {
3118
			$ns = NS_TEMPLATE;
3119
			# Split the title into page and subpage
3120
			$subpage = '';
3121
			$relative = $this->maybeDoSubpageLink( $part1, $subpage );
3122
			if ( $part1 !== $relative ) {
3123
				$part1 = $relative;
3124
				$ns = $this->mTitle->getNamespace();
3125
			}
3126
			$title = Title::newFromText( $part1, $ns );
3127
			if ( $title ) {
3128
				$titleText = $title->getPrefixedText();
3129
				# Check for language variants if the template is not found
3130
				if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3131
					$this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3132
				}
3133
				# Do recursion depth check
3134
				$limit = $this->mOptions->getMaxTemplateDepth();
3135 View Code Duplication
				if ( $frame->depth >= $limit ) {
0 ignored issues
show
Bug introduced by
Accessing depth on the interface PPFrame 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...
3136
					$found = true;
3137
					$text = '<span class="error">'
3138
						. wfMessage( 'parser-template-recursion-depth-warning' )
3139
							->numParams( $limit )->inContentLanguage()->text()
3140
						. '</span>';
3141
				}
3142
			}
3143
		}
3144
3145
		# Load from database
3146
		if ( !$found && $title ) {
3147
			$profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3148
			if ( !$title->isExternal() ) {
3149
				if ( $title->isSpecialPage()
3150
					&& $this->mOptions->getAllowSpecialInclusion()
3151
					&& $this->ot['html']
3152
				) {
3153
					$specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3154
					// Pass the template arguments as URL parameters.
3155
					// "uselang" will have no effect since the Language object
3156
					// is forced to the one defined in ParserOptions.
3157
					$pageArgs = [];
3158
					$argsLength = $args->getLength();
3159
					for ( $i = 0; $i < $argsLength; $i++ ) {
3160
						$bits = $args->item( $i )->splitArg();
3161
						if ( strval( $bits['index'] ) === '' ) {
3162
							$name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3163
							$value = trim( $frame->expand( $bits['value'] ) );
3164
							$pageArgs[$name] = $value;
3165
						}
3166
					}
3167
3168
					// Create a new context to execute the special page
3169
					$context = new RequestContext;
3170
					$context->setTitle( $title );
3171
					$context->setRequest( new FauxRequest( $pageArgs ) );
3172
					if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3173
						$context->setUser( $this->getUser() );
3174
					} else {
3175
						// If this page is cached, then we better not be per user.
3176
						$context->setUser( User::newFromName( '127.0.0.1', false ) );
0 ignored issues
show
Security Bug introduced by
It seems like \User::newFromName('127.0.0.1', false) targeting User::newFromName() can also be of type false; however, RequestContext::setUser() does only seem to accept object<User>, did you maybe forget to handle an error condition?
Loading history...
3177
					}
3178
					$context->setLanguage( $this->mOptions->getUserLangObj() );
3179
					$ret = SpecialPageFactory::capturePath(
3180
						$title, $context, $this->getLinkRenderer() );
3181
					if ( $ret ) {
3182
						$text = $context->getOutput()->getHTML();
3183
						$this->mOutput->addOutputPageMetadata( $context->getOutput() );
3184
						$found = true;
3185
						$isHTML = true;
3186
						if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3187
							$this->mOutput->updateRuntimeAdaptiveExpiry(
3188
								$specialPage->maxIncludeCacheTime()
3189
							);
3190
						}
3191
					}
3192
				} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3193
					$found = false; # access denied
3194
					wfDebug( __METHOD__ . ": template inclusion denied for " .
3195
						$title->getPrefixedDBkey() . "\n" );
3196
				} else {
3197
					list( $text, $title ) = $this->getTemplateDom( $title );
3198
					if ( $text !== false ) {
3199
						$found = true;
3200
						$isChildObj = true;
3201
					}
3202
				}
3203
3204
				# If the title is valid but undisplayable, make a link to it
3205
				if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3206
					$text = "[[:$titleText]]";
3207
					$found = true;
3208
				}
3209
			} elseif ( $title->isTrans() ) {
3210
				# Interwiki transclusion
3211
				if ( $this->ot['html'] && !$forceRawInterwiki ) {
3212
					$text = $this->interwikiTransclude( $title, 'render' );
3213
					$isHTML = true;
3214
				} else {
3215
					$text = $this->interwikiTransclude( $title, 'raw' );
3216
					# Preprocess it like a template
3217
					$text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3218
					$isChildObj = true;
3219
				}
3220
				$found = true;
3221
			}
3222
3223
			# Do infinite loop check
3224
			# This has to be done after redirect resolution to avoid infinite loops via redirects
3225
			if ( !$frame->loopCheck( $title ) ) {
3226
				$found = true;
3227
				$text = '<span class="error">'
3228
					. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3229
					. '</span>';
3230
				wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3231
			}
3232
		}
3233
3234
		# If we haven't found text to substitute by now, we're done
3235
		# Recover the source wikitext and return it
3236
		if ( !$found ) {
3237
			$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3238
			if ( $profileSection ) {
3239
				$this->mProfiler->scopedProfileOut( $profileSection );
3240
			}
3241
			return [ 'object' => $text ];
3242
		}
3243
3244
		# Expand DOM-style return values in a child frame
3245
		if ( $isChildObj ) {
3246
			# Clean up argument array
3247
			$newFrame = $frame->newChild( $args, $title );
3248
3249
			if ( $nowiki ) {
3250
				$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3251
			} elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3252
				# Expansion is eligible for the empty-frame cache
3253
				$text = $newFrame->cachedExpand( $titleText, $text );
3254
			} else {
3255
				# Uncached expansion
3256
				$text = $newFrame->expand( $text );
3257
			}
3258
		}
3259
		if ( $isLocalObj && $nowiki ) {
3260
			$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3261
			$isLocalObj = false;
3262
		}
3263
3264
		if ( $profileSection ) {
3265
			$this->mProfiler->scopedProfileOut( $profileSection );
3266
		}
3267
3268
		# Replace raw HTML by a placeholder
3269
		if ( $isHTML ) {
3270
			$text = $this->insertStripItem( $text );
3271
		} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3272
			# Escape nowiki-style return values
3273
			$text = wfEscapeWikiText( $text );
3274
		} elseif ( is_string( $text )
3275
			&& !$piece['lineStart']
3276
			&& preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3277
		) {
3278
			# Bug 529: if the template begins with a table or block-level
3279
			# element, it should be treated as beginning a new line.
3280
			# This behavior is somewhat controversial.
3281
			$text = "\n" . $text;
3282
		}
3283
3284
		if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3285
			# Error, oversize inclusion
3286
			if ( $titleText !== false ) {
3287
				# Make a working, properly escaped link if possible (bug 23588)
3288
				$text = "[[:$titleText]]";
3289
			} else {
3290
				# This will probably not be a working link, but at least it may
3291
				# provide some hint of where the problem is
3292
				preg_replace( '/^:/', '', $originalTitle );
3293
				$text = "[[:$originalTitle]]";
3294
			}
3295
			$text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3296
				. 'post-expand include size too large -->' );
3297
			$this->limitationWarn( 'post-expand-template-inclusion' );
3298
		}
3299
3300
		if ( $isLocalObj ) {
3301
			$ret = [ 'object' => $text ];
3302
		} else {
3303
			$ret = [ 'text' => $text ];
3304
		}
3305
3306
		return $ret;
3307
	}
3308
3309
	/**
3310
	 * Call a parser function and return an array with text and flags.
3311
	 *
3312
	 * The returned array will always contain a boolean 'found', indicating
3313
	 * whether the parser function was found or not. It may also contain the
3314
	 * following:
3315
	 *  text: string|object, resulting wikitext or PP DOM object
3316
	 *  isHTML: bool, $text is HTML, armour it against wikitext transformation
3317
	 *  isChildObj: bool, $text is a DOM node needing expansion in a child frame
3318
	 *  isLocalObj: bool, $text is a DOM node needing expansion in the current frame
3319
	 *  nowiki: bool, wiki markup in $text should be escaped
3320
	 *
3321
	 * @since 1.21
3322
	 * @param PPFrame $frame The current frame, contains template arguments
3323
	 * @param string $function Function name
3324
	 * @param array $args Arguments to the function
3325
	 * @throws MWException
3326
	 * @return array
3327
	 */
3328
	public function callParserFunction( $frame, $function, array $args = [] ) {
3329
		global $wgContLang;
3330
3331
		# Case sensitive functions
3332
		if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3333
			$function = $this->mFunctionSynonyms[1][$function];
3334
		} else {
3335
			# Case insensitive functions
3336
			$function = $wgContLang->lc( $function );
3337
			if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3338
				$function = $this->mFunctionSynonyms[0][$function];
3339
			} else {
3340
				return [ 'found' => false ];
3341
			}
3342
		}
3343
3344
		list( $callback, $flags ) = $this->mFunctionHooks[$function];
3345
3346
		# Workaround for PHP bug 35229 and similar
3347
		if ( !is_callable( $callback ) ) {
3348
			throw new MWException( "Tag hook for $function is not callable\n" );
3349
		}
3350
3351
		$allArgs = [ &$this ];
3352
		if ( $flags & self::SFH_OBJECT_ARGS ) {
3353
			# Convert arguments to PPNodes and collect for appending to $allArgs
3354
			$funcArgs = [];
3355
			foreach ( $args as $k => $v ) {
3356
				if ( $v instanceof PPNode || $k === 0 ) {
3357
					$funcArgs[] = $v;
3358
				} else {
3359
					$funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3360
				}
3361
			}
3362
3363
			# Add a frame parameter, and pass the arguments as an array
3364
			$allArgs[] = $frame;
3365
			$allArgs[] = $funcArgs;
3366
		} else {
3367
			# Convert arguments to plain text and append to $allArgs
3368
			foreach ( $args as $k => $v ) {
3369
				if ( $v instanceof PPNode ) {
3370
					$allArgs[] = trim( $frame->expand( $v ) );
3371
				} elseif ( is_int( $k ) && $k >= 0 ) {
3372
					$allArgs[] = trim( $v );
3373
				} else {
3374
					$allArgs[] = trim( "$k=$v" );
3375
				}
3376
			}
3377
		}
3378
3379
		$result = call_user_func_array( $callback, $allArgs );
3380
3381
		# The interface for function hooks allows them to return a wikitext
3382
		# string or an array containing the string and any flags. This mungs
3383
		# things around to match what this method should return.
3384
		if ( !is_array( $result ) ) {
3385
			$result =[
3386
				'found' => true,
3387
				'text' => $result,
3388
			];
3389
		} else {
3390
			if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3391
				$result['text'] = $result[0];
3392
			}
3393
			unset( $result[0] );
3394
			$result += [
3395
				'found' => true,
3396
			];
3397
		}
3398
3399
		$noparse = true;
3400
		$preprocessFlags = 0;
3401
		if ( isset( $result['noparse'] ) ) {
3402
			$noparse = $result['noparse'];
3403
		}
3404
		if ( isset( $result['preprocessFlags'] ) ) {
3405
			$preprocessFlags = $result['preprocessFlags'];
3406
		}
3407
3408
		if ( !$noparse ) {
3409
			$result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3410
			$result['isChildObj'] = true;
3411
		}
3412
3413
		return $result;
3414
	}
3415
3416
	/**
3417
	 * Get the semi-parsed DOM representation of a template with a given title,
3418
	 * and its redirect destination title. Cached.
3419
	 *
3420
	 * @param Title $title
3421
	 *
3422
	 * @return array
3423
	 */
3424
	public function getTemplateDom( $title ) {
3425
		$cacheTitle = $title;
3426
		$titleText = $title->getPrefixedDBkey();
3427
3428
		if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3429
			list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3430
			$title = Title::makeTitle( $ns, $dbk );
3431
			$titleText = $title->getPrefixedDBkey();
3432
		}
3433
		if ( isset( $this->mTplDomCache[$titleText] ) ) {
3434
			return [ $this->mTplDomCache[$titleText], $title ];
3435
		}
3436
3437
		# Cache miss, go to the database
3438
		list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3439
3440
		if ( $text === false ) {
3441
			$this->mTplDomCache[$titleText] = false;
3442
			return [ false, $title ];
3443
		}
3444
3445
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3446
		$this->mTplDomCache[$titleText] = $dom;
3447
3448
		if ( !$title->equals( $cacheTitle ) ) {
3449
			$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3450
				[ $title->getNamespace(), $cdb = $title->getDBkey() ];
3451
		}
3452
3453
		return [ $dom, $title ];
3454
	}
3455
3456
	/**
3457
	 * Fetch the current revision of a given title. Note that the revision
3458
	 * (and even the title) may not exist in the database, so everything
3459
	 * contributing to the output of the parser should use this method
3460
	 * where possible, rather than getting the revisions themselves. This
3461
	 * method also caches its results, so using it benefits performance.
3462
	 *
3463
	 * @since 1.24
3464
	 * @param Title $title
3465
	 * @return Revision
3466
	 */
3467
	public function fetchCurrentRevisionOfTitle( $title ) {
3468
		$cacheKey = $title->getPrefixedDBkey();
3469
		if ( !$this->currentRevisionCache ) {
3470
			$this->currentRevisionCache = new MapCacheLRU( 100 );
3471
		}
3472
		if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3473
			$this->currentRevisionCache->set( $cacheKey,
3474
				// Defaults to Parser::statelessFetchRevision()
3475
				call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3476
			);
3477
		}
3478
		return $this->currentRevisionCache->get( $cacheKey );
3479
	}
3480
3481
	/**
3482
	 * Wrapper around Revision::newFromTitle to allow passing additional parameters
3483
	 * without passing them on to it.
3484
	 *
3485
	 * @since 1.24
3486
	 * @param Title $title
3487
	 * @param Parser|bool $parser
3488
	 * @return Revision|bool False if missing
3489
	 */
3490
	public static function statelessFetchRevision( Title $title, $parser = false ) {
3491
		$pageId = $title->getArticleID();
3492
		$revId = $title->getLatestRevID();
3493
3494
		$rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
0 ignored issues
show
Bug introduced by
It seems like wfGetDB(DB_REPLICA) can be null; however, newKnownCurrent() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
3495
		if ( $rev ) {
3496
			$rev->setTitle( $title );
3497
		}
3498
3499
		return $rev;
3500
	}
3501
3502
	/**
3503
	 * Fetch the unparsed text of a template and register a reference to it.
3504
	 * @param Title $title
3505
	 * @return array ( string or false, Title )
3506
	 */
3507
	public function fetchTemplateAndTitle( $title ) {
3508
		// Defaults to Parser::statelessFetchTemplate()
3509
		$templateCb = $this->mOptions->getTemplateCallback();
3510
		$stuff = call_user_func( $templateCb, $title, $this );
3511
		// We use U+007F DELETE to distinguish strip markers from regular text.
3512
		$text = $stuff['text'];
3513
		if ( is_string( $stuff['text'] ) ) {
3514
			$text = strtr( $text, "\x7f", "?" );
3515
		}
3516
		$finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3517
		if ( isset( $stuff['deps'] ) ) {
3518
			foreach ( $stuff['deps'] as $dep ) {
3519
				$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3520
				if ( $dep['title']->equals( $this->getTitle() ) ) {
3521
					// If we transclude ourselves, the final result
3522
					// will change based on the new version of the page
3523
					$this->mOutput->setFlag( 'vary-revision' );
3524
				}
3525
			}
3526
		}
3527
		return [ $text, $finalTitle ];
3528
	}
3529
3530
	/**
3531
	 * Fetch the unparsed text of a template and register a reference to it.
3532
	 * @param Title $title
3533
	 * @return string|bool
3534
	 */
3535
	public function fetchTemplate( $title ) {
3536
		return $this->fetchTemplateAndTitle( $title )[0];
3537
	}
3538
3539
	/**
3540
	 * Static function to get a template
3541
	 * Can be overridden via ParserOptions::setTemplateCallback().
3542
	 *
3543
	 * @param Title $title
3544
	 * @param bool|Parser $parser
3545
	 *
3546
	 * @return array
3547
	 */
3548
	public static function statelessFetchTemplate( $title, $parser = false ) {
3549
		$text = $skip = false;
3550
		$finalTitle = $title;
3551
		$deps = [];
3552
3553
		# Loop to fetch the article, with up to 1 redirect
3554
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3555
		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3556
			// @codingStandardsIgnoreEnd
3557
			# Give extensions a chance to select the revision instead
3558
			$id = false; # Assume current
3559
			Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3560
				[ $parser, $title, &$skip, &$id ] );
3561
3562
			if ( $skip ) {
3563
				$text = false;
3564
				$deps[] = [
3565
					'title' => $title,
3566
					'page_id' => $title->getArticleID(),
3567
					'rev_id' => null
3568
				];
3569
				break;
3570
			}
3571
			# Get the revision
3572
			if ( $id ) {
3573
				$rev = Revision::newFromId( $id );
3574
			} elseif ( $parser ) {
3575
				$rev = $parser->fetchCurrentRevisionOfTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like $parser 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...
3576
			} else {
3577
				$rev = Revision::newFromTitle( $title );
3578
			}
3579
			$rev_id = $rev ? $rev->getId() : 0;
3580
			# If there is no current revision, there is no page
3581
			if ( $id === false && !$rev ) {
3582
				$linkCache = LinkCache::singleton();
3583
				$linkCache->addBadLinkObj( $title );
3584
			}
3585
3586
			$deps[] = [
3587
				'title' => $title,
3588
				'page_id' => $title->getArticleID(),
3589
				'rev_id' => $rev_id ];
3590
			if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3591
				# We fetched a rev from a different title; register it too...
3592
				$deps[] = [
3593
					'title' => $rev->getTitle(),
3594
					'page_id' => $rev->getPage(),
3595
					'rev_id' => $rev_id ];
3596
			}
3597
3598
			if ( $rev ) {
3599
				$content = $rev->getContent();
3600
				$text = $content ? $content->getWikitextForTransclusion() : null;
3601
3602
				if ( $text === false || $text === null ) {
3603
					$text = false;
3604
					break;
3605
				}
3606
			} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3607
				global $wgContLang;
3608
				$message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3609
				if ( !$message->exists() ) {
3610
					$text = false;
3611
					break;
3612
				}
3613
				$content = $message->content();
3614
				$text = $message->plain();
3615
			} else {
3616
				break;
3617
			}
3618
			if ( !$content ) {
3619
				break;
3620
			}
3621
			# Redirect?
3622
			$finalTitle = $title;
3623
			$title = $content->getRedirectTarget();
3624
		}
3625
		return [
3626
			'text' => $text,
3627
			'finalTitle' => $finalTitle,
3628
			'deps' => $deps ];
3629
	}
3630
3631
	/**
3632
	 * Fetch a file and its title and register a reference to it.
3633
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3634
	 * @param Title $title
3635
	 * @param array $options Array of options to RepoGroup::findFile
3636
	 * @return File|bool
3637
	 */
3638
	public function fetchFile( $title, $options = [] ) {
3639
		return $this->fetchFileAndTitle( $title, $options )[0];
3640
	}
3641
3642
	/**
3643
	 * Fetch a file and its title and register a reference to it.
3644
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3645
	 * @param Title $title
3646
	 * @param array $options Array of options to RepoGroup::findFile
3647
	 * @return array ( File or false, Title of file )
3648
	 */
3649
	public function fetchFileAndTitle( $title, $options = [] ) {
3650
		$file = $this->fetchFileNoRegister( $title, $options );
3651
3652
		$time = $file ? $file->getTimestamp() : false;
3653
		$sha1 = $file ? $file->getSha1() : false;
3654
		# Register the file as a dependency...
3655
		$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3656
		if ( $file && !$title->equals( $file->getTitle() ) ) {
3657
			# Update fetched file title
3658
			$title = $file->getTitle();
3659
			$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3660
		}
3661
		return [ $file, $title ];
3662
	}
3663
3664
	/**
3665
	 * Helper function for fetchFileAndTitle.
3666
	 *
3667
	 * Also useful if you need to fetch a file but not use it yet,
3668
	 * for example to get the file's handler.
3669
	 *
3670
	 * @param Title $title
3671
	 * @param array $options Array of options to RepoGroup::findFile
3672
	 * @return File|bool
3673
	 */
3674
	protected function fetchFileNoRegister( $title, $options = [] ) {
3675
		if ( isset( $options['broken'] ) ) {
3676
			$file = false; // broken thumbnail forced by hook
3677
		} elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3678
			$file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3679
		} else { // get by (name,timestamp)
3680
			$file = wfFindFile( $title, $options );
3681
		}
3682
		return $file;
3683
	}
3684
3685
	/**
3686
	 * Transclude an interwiki link.
3687
	 *
3688
	 * @param Title $title
3689
	 * @param string $action
3690
	 *
3691
	 * @return string
3692
	 */
3693
	public function interwikiTransclude( $title, $action ) {
3694
		global $wgEnableScaryTranscluding;
3695
3696
		if ( !$wgEnableScaryTranscluding ) {
3697
			return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3698
		}
3699
3700
		$url = $title->getFullURL( [ 'action' => $action ] );
3701
3702
		if ( strlen( $url ) > 255 ) {
3703
			return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3704
		}
3705
		return $this->fetchScaryTemplateMaybeFromCache( $url );
3706
	}
3707
3708
	/**
3709
	 * @param string $url
3710
	 * @return mixed|string
3711
	 */
3712
	public function fetchScaryTemplateMaybeFromCache( $url ) {
3713
		global $wgTranscludeCacheExpiry;
3714
		$dbr = wfGetDB( DB_REPLICA );
3715
		$tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3716
		$obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3717
				[ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
3718
		if ( $obj ) {
3719
			return $obj->tc_contents;
3720
		}
3721
3722
		$req = MWHttpRequest::factory( $url, [], __METHOD__ );
3723
		$status = $req->execute(); // Status object
3724
		if ( $status->isOK() ) {
3725
			$text = $req->getContent();
3726
		} elseif ( $req->getStatus() != 200 ) {
3727
			// Though we failed to fetch the content, this status is useless.
3728
			return wfMessage( 'scarytranscludefailed-httpstatus' )
3729
				->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3730
		} else {
3731
			return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3732
		}
3733
3734
		$dbw = wfGetDB( DB_MASTER );
3735
		$dbw->replace( 'transcache', [ 'tc_url' ], [
3736
			'tc_url' => $url,
3737
			'tc_time' => $dbw->timestamp( time() ),
3738
			'tc_contents' => $text
3739
		] );
3740
		return $text;
3741
	}
3742
3743
	/**
3744
	 * Triple brace replacement -- used for template arguments
3745
	 * @private
3746
	 *
3747
	 * @param array $piece
3748
	 * @param PPFrame $frame
3749
	 *
3750
	 * @return array
3751
	 */
3752
	public function argSubstitution( $piece, $frame ) {
3753
3754
		$error = false;
3755
		$parts = $piece['parts'];
3756
		$nameWithSpaces = $frame->expand( $piece['title'] );
3757
		$argName = trim( $nameWithSpaces );
3758
		$object = false;
3759
		$text = $frame->getArgument( $argName );
3760
		if ( $text === false && $parts->getLength() > 0
3761
			&& ( $this->ot['html']
3762
				|| $this->ot['pre']
3763
				|| ( $this->ot['wiki'] && $frame->isTemplate() )
3764
			)
3765
		) {
3766
			# No match in frame, use the supplied default
3767
			$object = $parts->item( 0 )->getChildren();
3768
		}
3769
		if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3770
			$error = '<!-- WARNING: argument omitted, expansion size too large -->';
3771
			$this->limitationWarn( 'post-expand-template-argument' );
3772
		}
3773
3774
		if ( $text === false && $object === false ) {
3775
			# No match anywhere
3776
			$object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3777
		}
3778
		if ( $error !== false ) {
3779
			$text .= $error;
3780
		}
3781
		if ( $object !== false ) {
3782
			$ret = [ 'object' => $object ];
3783
		} else {
3784
			$ret = [ 'text' => $text ];
3785
		}
3786
3787
		return $ret;
3788
	}
3789
3790
	/**
3791
	 * Return the text to be used for a given extension tag.
3792
	 * This is the ghost of strip().
3793
	 *
3794
	 * @param array $params Associative array of parameters:
3795
	 *     name       PPNode for the tag name
3796
	 *     attr       PPNode for unparsed text where tag attributes are thought to be
3797
	 *     attributes Optional associative array of parsed attributes
3798
	 *     inner      Contents of extension element
3799
	 *     noClose    Original text did not have a close tag
3800
	 * @param PPFrame $frame
3801
	 *
3802
	 * @throws MWException
3803
	 * @return string
3804
	 */
3805
	public function extensionSubstitution( $params, $frame ) {
3806
		static $errorStr = '<span class="error">';
3807
		static $errorLen = 20;
3808
3809
		$name = $frame->expand( $params['name'] );
3810
		if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3811
			// Probably expansion depth or node count exceeded. Just punt the
3812
			// error up.
3813
			return $name;
3814
		}
3815
3816
		$attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3817
		if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3818
			// See above
3819
			return $attrText;
3820
		}
3821
3822
		// We can't safely check if the expansion for $content resulted in an
3823
		// error, because the content could happen to be the error string
3824
		// (T149622).
3825
		$content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3826
3827
		$marker = self::MARKER_PREFIX . "-$name-"
3828
			. sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3829
3830
		$isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3831
			( $this->ot['html'] || $this->ot['pre'] );
3832
		if ( $isFunctionTag ) {
3833
			$markerType = 'none';
3834
		} else {
3835
			$markerType = 'general';
3836
		}
3837
		if ( $this->ot['html'] || $isFunctionTag ) {
3838
			$name = strtolower( $name );
3839
			$attributes = Sanitizer::decodeTagAttributes( $attrText );
3840
			if ( isset( $params['attributes'] ) ) {
3841
				$attributes = $attributes + $params['attributes'];
3842
			}
3843
3844
			if ( isset( $this->mTagHooks[$name] ) ) {
3845
				# Workaround for PHP bug 35229 and similar
3846
				if ( !is_callable( $this->mTagHooks[$name] ) ) {
3847
					throw new MWException( "Tag hook for $name is not callable\n" );
3848
				}
3849
				$output = call_user_func_array( $this->mTagHooks[$name],
3850
					[ $content, $attributes, $this, $frame ] );
3851
			} elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3852
				list( $callback, ) = $this->mFunctionTagHooks[$name];
3853
				if ( !is_callable( $callback ) ) {
3854
					throw new MWException( "Tag hook for $name is not callable\n" );
3855
				}
3856
3857
				$output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] );
3858
			} else {
3859
				$output = '<span class="error">Invalid tag extension name: ' .
3860
					htmlspecialchars( $name ) . '</span>';
3861
			}
3862
3863
			if ( is_array( $output ) ) {
3864
				# Extract flags to local scope (to override $markerType)
3865
				$flags = $output;
3866
				$output = $flags[0];
3867
				unset( $flags[0] );
3868
				extract( $flags );
3869
			}
3870
		} else {
3871
			if ( is_null( $attrText ) ) {
3872
				$attrText = '';
3873
			}
3874
			if ( isset( $params['attributes'] ) ) {
3875
				foreach ( $params['attributes'] as $attrName => $attrValue ) {
3876
					$attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3877
						htmlspecialchars( $attrValue ) . '"';
3878
				}
3879
			}
3880
			if ( $content === null ) {
3881
				$output = "<$name$attrText/>";
3882
			} else {
3883
				$close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3884
				if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3885
					// See above
3886
					return $close;
3887
				}
3888
				$output = "<$name$attrText>$content$close";
3889
			}
3890
		}
3891
3892
		if ( $markerType === 'none' ) {
3893
			return $output;
3894
		} elseif ( $markerType === 'nowiki' ) {
3895
			$this->mStripState->addNoWiki( $marker, $output );
3896
		} elseif ( $markerType === 'general' ) {
3897
			$this->mStripState->addGeneral( $marker, $output );
3898
		} else {
3899
			throw new MWException( __METHOD__ . ': invalid marker type' );
3900
		}
3901
		return $marker;
3902
	}
3903
3904
	/**
3905
	 * Increment an include size counter
3906
	 *
3907
	 * @param string $type The type of expansion
3908
	 * @param int $size The size of the text
3909
	 * @return bool False if this inclusion would take it over the maximum, true otherwise
3910
	 */
3911
	public function incrementIncludeSize( $type, $size ) {
3912
		if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3913
			return false;
3914
		} else {
3915
			$this->mIncludeSizes[$type] += $size;
3916
			return true;
3917
		}
3918
	}
3919
3920
	/**
3921
	 * Increment the expensive function count
3922
	 *
3923
	 * @return bool False if the limit has been exceeded
3924
	 */
3925
	public function incrementExpensiveFunctionCount() {
3926
		$this->mExpensiveFunctionCount++;
3927
		return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3928
	}
3929
3930
	/**
3931
	 * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
3932
	 * Fills $this->mDoubleUnderscores, returns the modified text
3933
	 *
3934
	 * @param string $text
3935
	 *
3936
	 * @return string
3937
	 */
3938
	public function doDoubleUnderscore( $text ) {
3939
3940
		# The position of __TOC__ needs to be recorded
3941
		$mw = MagicWord::get( 'toc' );
3942
		if ( $mw->match( $text ) ) {
3943
			$this->mShowToc = true;
3944
			$this->mForceTocPosition = true;
3945
3946
			# Set a placeholder. At the end we'll fill it in with the TOC.
3947
			$text = $mw->replace( '<!--MWTOC-->', $text, 1 );
3948
3949
			# Only keep the first one.
3950
			$text = $mw->replace( '', $text );
3951
		}
3952
3953
		# Now match and remove the rest of them
3954
		$mwa = MagicWord::getDoubleUnderscoreArray();
3955
		$this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3956
3957
		if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3958
			$this->mOutput->mNoGallery = true;
3959
		}
3960
		if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3961
			$this->mShowToc = false;
3962
		}
3963
		if ( isset( $this->mDoubleUnderscores['hiddencat'] )
3964
			&& $this->mTitle->getNamespace() == NS_CATEGORY
3965
		) {
3966
			$this->addTrackingCategory( 'hidden-category-category' );
3967
		}
3968
		# (bug 8068) Allow control over whether robots index a page.
3969
		# @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here!  This
3970
		# is not desirable, the last one on the page should win.
3971 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
3972
			$this->mOutput->setIndexPolicy( 'noindex' );
3973
			$this->addTrackingCategory( 'noindex-category' );
3974
		}
3975 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
3976
			$this->mOutput->setIndexPolicy( 'index' );
3977
			$this->addTrackingCategory( 'index-category' );
3978
		}
3979
3980
		# Cache all double underscores in the database
3981
		foreach ( $this->mDoubleUnderscores as $key => $val ) {
3982
			$this->mOutput->setProperty( $key, '' );
3983
		}
3984
3985
		return $text;
3986
	}
3987
3988
	/**
3989
	 * @see ParserOutput::addTrackingCategory()
3990
	 * @param string $msg Message key
3991
	 * @return bool Whether the addition was successful
3992
	 */
3993
	public function addTrackingCategory( $msg ) {
3994
		return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
3995
	}
3996
3997
	/**
3998
	 * This function accomplishes several tasks:
3999
	 * 1) Auto-number headings if that option is enabled
4000
	 * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
4001
	 * 3) Add a Table of contents on the top for users who have enabled the option
4002
	 * 4) Auto-anchor headings
4003
	 *
4004
	 * It loops through all headlines, collects the necessary data, then splits up the
4005
	 * string and re-inserts the newly formatted headlines.
4006
	 *
4007
	 * @param string $text
4008
	 * @param string $origText Original, untouched wikitext
4009
	 * @param bool $isMain
4010
	 * @return mixed|string
4011
	 * @private
4012
	 */
4013
	public function formatHeadings( $text, $origText, $isMain = true ) {
4014
		global $wgMaxTocLevel, $wgExperimentalHtmlIds;
4015
4016
		# Inhibit editsection links if requested in the page
4017
		if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4018
			$maybeShowEditLink = $showEditLink = false;
4019
		} else {
4020
			$maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4021
			$showEditLink = $this->mOptions->getEditSection();
4022
		}
4023
		if ( $showEditLink ) {
4024
			$this->mOutput->setEditSectionTokens( true );
4025
		}
4026
4027
		# Get all headlines for numbering them and adding funky stuff like [edit]
4028
		# links - this is for later, but we need the number of headlines right now
4029
		$matches = [];
4030
		$numMatches = preg_match_all(
4031
			'/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4032
			$text,
4033
			$matches
4034
		);
4035
4036
		# if there are fewer than 4 headlines in the article, do not show TOC
4037
		# unless it's been explicitly enabled.
4038
		$enoughToc = $this->mShowToc &&
4039
			( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4040
4041
		# Allow user to stipulate that a page should have a "new section"
4042
		# link added via __NEWSECTIONLINK__
4043
		if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4044
			$this->mOutput->setNewSection( true );
4045
		}
4046
4047
		# Allow user to remove the "new section"
4048
		# link via __NONEWSECTIONLINK__
4049
		if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4050
			$this->mOutput->hideNewSection( true );
4051
		}
4052
4053
		# if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4054
		# override above conditions and always show TOC above first header
4055
		if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4056
			$this->mShowToc = true;
4057
			$enoughToc = true;
4058
		}
4059
4060
		# headline counter
4061
		$headlineCount = 0;
4062
		$numVisible = 0;
4063
4064
		# Ugh .. the TOC should have neat indentation levels which can be
4065
		# passed to the skin functions. These are determined here
4066
		$toc = '';
4067
		$full = '';
4068
		$head = [];
4069
		$sublevelCount = [];
4070
		$levelCount = [];
4071
		$level = 0;
4072
		$prevlevel = 0;
4073
		$toclevel = 0;
4074
		$prevtoclevel = 0;
4075
		$markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4076
		$baseTitleText = $this->mTitle->getPrefixedDBkey();
4077
		$oldType = $this->mOutputType;
4078
		$this->setOutputType( self::OT_WIKI );
4079
		$frame = $this->getPreprocessor()->newFrame();
4080
		$root = $this->preprocessToDom( $origText );
4081
		$node = $root->getFirstChild();
4082
		$byteOffset = 0;
4083
		$tocraw = [];
4084
		$refers = [];
4085
4086
		$headlines = $numMatches !== false ? $matches[3] : [];
4087
4088
		foreach ( $headlines as $headline ) {
4089
			$isTemplate = false;
4090
			$titleText = false;
4091
			$sectionIndex = false;
4092
			$numbering = '';
4093
			$markerMatches = [];
4094
			if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4095
				$serial = $markerMatches[1];
4096
				list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4097
				$isTemplate = ( $titleText != $baseTitleText );
4098
				$headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4099
			}
4100
4101
			if ( $toclevel ) {
4102
				$prevlevel = $level;
4103
			}
4104
			$level = $matches[1][$headlineCount];
4105
4106
			if ( $level > $prevlevel ) {
4107
				# Increase TOC level
4108
				$toclevel++;
4109
				$sublevelCount[$toclevel] = 0;
4110
				if ( $toclevel < $wgMaxTocLevel ) {
4111
					$prevtoclevel = $toclevel;
4112
					$toc .= Linker::tocIndent();
4113
					$numVisible++;
4114
				}
4115
			} elseif ( $level < $prevlevel && $toclevel > 1 ) {
4116
				# Decrease TOC level, find level to jump to
4117
4118
				for ( $i = $toclevel; $i > 0; $i-- ) {
4119
					if ( $levelCount[$i] == $level ) {
4120
						# Found last matching level
4121
						$toclevel = $i;
4122
						break;
4123
					} elseif ( $levelCount[$i] < $level ) {
4124
						# Found first matching level below current level
4125
						$toclevel = $i + 1;
4126
						break;
4127
					}
4128
				}
4129
				if ( $i == 0 ) {
4130
					$toclevel = 1;
4131
				}
4132
				if ( $toclevel < $wgMaxTocLevel ) {
4133
					if ( $prevtoclevel < $wgMaxTocLevel ) {
4134
						# Unindent only if the previous toc level was shown :p
4135
						$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4136
						$prevtoclevel = $toclevel;
4137
					} else {
4138
						$toc .= Linker::tocLineEnd();
4139
					}
4140
				}
4141
			} else {
4142
				# No change in level, end TOC line
4143
				if ( $toclevel < $wgMaxTocLevel ) {
4144
					$toc .= Linker::tocLineEnd();
4145
				}
4146
			}
4147
4148
			$levelCount[$toclevel] = $level;
4149
4150
			# count number of headlines for each level
4151
			$sublevelCount[$toclevel]++;
4152
			$dot = 0;
4153
			for ( $i = 1; $i <= $toclevel; $i++ ) {
4154
				if ( !empty( $sublevelCount[$i] ) ) {
4155
					if ( $dot ) {
4156
						$numbering .= '.';
4157
					}
4158
					$numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4159
					$dot = 1;
4160
				}
4161
			}
4162
4163
			# The safe header is a version of the header text safe to use for links
4164
4165
			# Remove link placeholders by the link text.
4166
			#     <!--LINK number-->
4167
			# turns into
4168
			#     link text with suffix
4169
			# Do this before unstrip since link text can contain strip markers
4170
			$safeHeadline = $this->replaceLinkHoldersText( $headline );
4171
4172
			# Avoid insertion of weird stuff like <math> by expanding the relevant sections
4173
			$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4174
4175
			# Strip out HTML (first regex removes any tag not allowed)
4176
			# Allowed tags are:
4177
			# * <sup> and <sub> (bug 8393)
4178
			# * <i> (bug 26375)
4179
			# * <b> (r105284)
4180
			# * <bdi> (bug 72884)
4181
			# * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4182
			# * <s> and <strike> (T35715)
4183
			# We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4184
			# to allow setting directionality in toc items.
4185
			$tocline = preg_replace(
4186
				[
4187
					'#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4188
					'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4189
				],
4190
				[ '', '<$1>' ],
4191
				$safeHeadline
4192
			);
4193
4194
			# Strip '<span></span>', which is the result from the above if
4195
			# <span id="foo"></span> is used to produce an additional anchor
4196
			# for a section.
4197
			$tocline = str_replace( '<span></span>', '', $tocline );
4198
4199
			$tocline = trim( $tocline );
4200
4201
			# For the anchor, strip out HTML-y stuff period
4202
			$safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4203
			$safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4204
4205
			# Save headline for section edit hint before it's escaped
4206
			$headlineHint = $safeHeadline;
4207
4208
			if ( $wgExperimentalHtmlIds ) {
4209
				# For reverse compatibility, provide an id that's
4210
				# HTML4-compatible, like we used to.
4211
				# It may be worth noting, academically, that it's possible for
4212
				# the legacy anchor to conflict with a non-legacy headline
4213
				# anchor on the page.  In this case likely the "correct" thing
4214
				# would be to either drop the legacy anchors or make sure
4215
				# they're numbered first.  However, this would require people
4216
				# to type in section names like "abc_.D7.93.D7.90.D7.A4"
4217
				# manually, so let's not bother worrying about it.
4218
				$legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4219
					[ 'noninitial', 'legacy' ] );
4220
				$safeHeadline = Sanitizer::escapeId( $safeHeadline );
4221
4222
				if ( $legacyHeadline == $safeHeadline ) {
4223
					# No reason to have both (in fact, we can't)
4224
					$legacyHeadline = false;
4225
				}
4226
			} else {
4227
				$legacyHeadline = false;
4228
				$safeHeadline = Sanitizer::escapeId( $safeHeadline,
4229
					'noninitial' );
4230
			}
4231
4232
			# HTML names must be case-insensitively unique (bug 10721).
4233
			# This does not apply to Unicode characters per
4234
			# https://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4235
			# @todo FIXME: We may be changing them depending on the current locale.
4236
			$arrayKey = strtolower( $safeHeadline );
4237
			if ( $legacyHeadline === false ) {
4238
				$legacyArrayKey = false;
4239
			} else {
4240
				$legacyArrayKey = strtolower( $legacyHeadline );
4241
			}
4242
4243
			# Create the anchor for linking from the TOC to the section
4244
			$anchor = $safeHeadline;
4245
			$legacyAnchor = $legacyHeadline;
4246 View Code Duplication
			if ( isset( $refers[$arrayKey] ) ) {
4247
				// @codingStandardsIgnoreStart
4248
				for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4249
				// @codingStandardsIgnoreEnd
4250
				$anchor .= "_$i";
4251
				$refers["${arrayKey}_$i"] = true;
4252
			} else {
4253
				$refers[$arrayKey] = true;
4254
			}
4255 View Code Duplication
			if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4256
				// @codingStandardsIgnoreStart
4257
				for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4258
				// @codingStandardsIgnoreEnd
4259
				$legacyAnchor .= "_$i";
4260
				$refers["${legacyArrayKey}_$i"] = true;
4261
			} else {
4262
				$refers[$legacyArrayKey] = true;
4263
			}
4264
4265
			# Don't number the heading if it is the only one (looks silly)
4266
			if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4267
				# the two are different if the line contains a link
4268
				$headline = Html::element(
4269
					'span',
4270
					[ 'class' => 'mw-headline-number' ],
4271
					$numbering
4272
				) . ' ' . $headline;
4273
			}
4274
4275
			if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4276
				$toc .= Linker::tocLine( $anchor, $tocline,
4277
					$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4278
			}
4279
4280
			# Add the section to the section tree
4281
			# Find the DOM node for this header
4282
			$noOffset = ( $isTemplate || $sectionIndex === false );
4283
			while ( $node && !$noOffset ) {
4284
				if ( $node->getName() === 'h' ) {
4285
					$bits = $node->splitHeading();
4286
					if ( $bits['i'] == $sectionIndex ) {
4287
						break;
4288
					}
4289
				}
4290
				$byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4291
					$frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4292
				$node = $node->getNextSibling();
4293
			}
4294
			$tocraw[] = [
4295
				'toclevel' => $toclevel,
4296
				'level' => $level,
4297
				'line' => $tocline,
4298
				'number' => $numbering,
4299
				'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4300
				'fromtitle' => $titleText,
4301
				'byteoffset' => ( $noOffset ? null : $byteOffset ),
4302
				'anchor' => $anchor,
4303
			];
4304
4305
			# give headline the correct <h#> tag
4306
			if ( $maybeShowEditLink && $sectionIndex !== false ) {
4307
				// Output edit section links as markers with styles that can be customized by skins
4308
				if ( $isTemplate ) {
4309
					# Put a T flag in the section identifier, to indicate to extractSections()
4310
					# that sections inside <includeonly> should be counted.
4311
					$editsectionPage = $titleText;
4312
					$editsectionSection = "T-$sectionIndex";
4313
					$editsectionContent = null;
4314
				} else {
4315
					$editsectionPage = $this->mTitle->getPrefixedText();
4316
					$editsectionSection = $sectionIndex;
4317
					$editsectionContent = $headlineHint;
4318
				}
4319
				// We use a bit of pesudo-xml for editsection markers. The
4320
				// language converter is run later on. Using a UNIQ style marker
4321
				// leads to the converter screwing up the tokens when it
4322
				// converts stuff. And trying to insert strip tags fails too. At
4323
				// this point all real inputted tags have already been escaped,
4324
				// so we don't have to worry about a user trying to input one of
4325
				// these markers directly. We use a page and section attribute
4326
				// to stop the language converter from converting these
4327
				// important bits of data, but put the headline hint inside a
4328
				// content block because the language converter is supposed to
4329
				// be able to convert that piece of data.
4330
				// Gets replaced with html in ParserOutput::getText
4331
				$editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4332
				$editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4333
				if ( $editsectionContent !== null ) {
4334
					$editlink .= '>' . $editsectionContent . '</mw:editsection>';
4335
				} else {
4336
					$editlink .= '/>';
4337
				}
4338
			} else {
4339
				$editlink = '';
4340
			}
4341
			$head[$headlineCount] = Linker::makeHeadline( $level,
4342
				$matches['attrib'][$headlineCount], $anchor, $headline,
4343
				$editlink, $legacyAnchor );
4344
4345
			$headlineCount++;
4346
		}
4347
4348
		$this->setOutputType( $oldType );
4349
4350
		# Never ever show TOC if no headers
4351
		if ( $numVisible < 1 ) {
4352
			$enoughToc = false;
4353
		}
4354
4355
		if ( $enoughToc ) {
4356
			if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4357
				$toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4358
			}
4359
			$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4360
			$this->mOutput->setTOCHTML( $toc );
4361
			$toc = self::TOC_START . $toc . self::TOC_END;
4362
			$this->mOutput->addModules( 'mediawiki.toc' );
4363
		}
4364
4365
		if ( $isMain ) {
4366
			$this->mOutput->setSections( $tocraw );
4367
		}
4368
4369
		# split up and insert constructed headlines
4370
		$blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4371
		$i = 0;
4372
4373
		// build an array of document sections
4374
		$sections = [];
4375
		foreach ( $blocks as $block ) {
4376
			// $head is zero-based, sections aren't.
4377
			if ( empty( $head[$i - 1] ) ) {
4378
				$sections[$i] = $block;
4379
			} else {
4380
				$sections[$i] = $head[$i - 1] . $block;
4381
			}
4382
4383
			/**
4384
			 * Send a hook, one per section.
4385
			 * The idea here is to be able to make section-level DIVs, but to do so in a
4386
			 * lower-impact, more correct way than r50769
4387
			 *
4388
			 * $this : caller
4389
			 * $section : the section number
4390
			 * &$sectionContent : ref to the content of the section
4391
			 * $showEditLinks : boolean describing whether this section has an edit link
4392
			 */
4393
			Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4394
4395
			$i++;
4396
		}
4397
4398
		if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4399
			// append the TOC at the beginning
4400
			// Top anchor now in skin
4401
			$sections[0] = $sections[0] . $toc . "\n";
4402
		}
4403
4404
		$full .= implode( '', $sections );
4405
4406
		if ( $this->mForceTocPosition ) {
4407
			return str_replace( '<!--MWTOC-->', $toc, $full );
4408
		} else {
4409
			return $full;
4410
		}
4411
	}
4412
4413
	/**
4414
	 * Transform wiki markup when saving a page by doing "\r\n" -> "\n"
4415
	 * conversion, substituting signatures, {{subst:}} templates, etc.
4416
	 *
4417
	 * @param string $text The text to transform
4418
	 * @param Title $title The Title object for the current article
4419
	 * @param User $user The User object describing the current user
4420
	 * @param ParserOptions $options Parsing options
4421
	 * @param bool $clearState Whether to clear the parser state first
4422
	 * @return string The altered wiki markup
4423
	 */
4424
	public function preSaveTransform( $text, Title $title, User $user,
4425
		ParserOptions $options, $clearState = true
4426
	) {
4427
		if ( $clearState ) {
4428
			$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
4429
		}
4430
		$this->startParse( $title, $options, self::OT_WIKI, $clearState );
4431
		$this->setUser( $user );
4432
4433
		// We still normalize line endings for backwards-compatibility
4434
		// with other code that just calls PST, but this should already
4435
		// be handled in TextContent subclasses
4436
		$text = TextContent::normalizeLineEndings( $text );
4437
4438
		if ( $options->getPreSaveTransform() ) {
4439
			$text = $this->pstPass2( $text, $user );
4440
		}
4441
		$text = $this->mStripState->unstripBoth( $text );
4442
4443
		$this->setUser( null ); # Reset
4444
4445
		return $text;
4446
	}
4447
4448
	/**
4449
	 * Pre-save transform helper function
4450
	 *
4451
	 * @param string $text
4452
	 * @param User $user
4453
	 *
4454
	 * @return string
4455
	 */
4456
	private function pstPass2( $text, $user ) {
4457
		global $wgContLang;
4458
4459
		# Note: This is the timestamp saved as hardcoded wikitext to
4460
		# the database, we use $wgContLang here in order to give
4461
		# everyone the same signature and use the default one rather
4462
		# than the one selected in each user's preferences.
4463
		# (see also bug 12815)
4464
		$ts = $this->mOptions->getTimestamp();
4465
		$timestamp = MWTimestamp::getLocalInstance( $ts );
4466
		$ts = $timestamp->format( 'YmdHis' );
4467
		$tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4468
4469
		$d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4470
4471
		# Variable replacement
4472
		# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4473
		$text = $this->replaceVariables( $text );
4474
4475
		# This works almost by chance, as the replaceVariables are done before the getUserSig(),
4476
		# which may corrupt this parser instance via its wfMessage()->text() call-
4477
4478
		# Signatures
4479
		$sigText = $this->getUserSig( $user );
4480
		$text = strtr( $text, [
4481
			'~~~~~' => $d,
4482
			'~~~~' => "$sigText $d",
4483
			'~~~' => $sigText
4484
		] );
4485
4486
		# Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4487
		$tc = '[' . Title::legalChars() . ']';
4488
		$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4489
4490
		// [[ns:page (context)|]]
4491
		$p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4492
		// [[ns:page(context)|]] (double-width brackets, added in r40257)
4493
		$p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4494
		// [[ns:page (context), context|]] (using either single or double-width comma)
4495
		$p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4496
		// [[|page]] (reverse pipe trick: add context from page title)
4497
		$p2 = "/\[\[\\|($tc+)]]/";
4498
4499
		# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4500
		$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4501
		$text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4502
		$text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4503
4504
		$t = $this->mTitle->getText();
4505
		$m = [];
4506
		if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4507
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4508
		} elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4509
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4510
		} else {
4511
			# if there's no context, don't bother duplicating the title
4512
			$text = preg_replace( $p2, '[[\\1]]', $text );
4513
		}
4514
4515
		return $text;
4516
	}
4517
4518
	/**
4519
	 * Fetch the user's signature text, if any, and normalize to
4520
	 * validated, ready-to-insert wikitext.
4521
	 * If you have pre-fetched the nickname or the fancySig option, you can
4522
	 * specify them here to save a database query.
4523
	 * Do not reuse this parser instance after calling getUserSig(),
4524
	 * as it may have changed if it's the $wgParser.
4525
	 *
4526
	 * @param User $user
4527
	 * @param string|bool $nickname Nickname to use or false to use user's default nickname
4528
	 * @param bool|null $fancySig whether the nicknname is the complete signature
4529
	 *    or null to use default value
4530
	 * @return string
4531
	 */
4532
	public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4533
		global $wgMaxSigChars;
4534
4535
		$username = $user->getName();
4536
4537
		# If not given, retrieve from the user object.
4538
		if ( $nickname === false ) {
4539
			$nickname = $user->getOption( 'nickname' );
4540
		}
4541
4542
		if ( is_null( $fancySig ) ) {
4543
			$fancySig = $user->getBoolOption( 'fancysig' );
4544
		}
4545
4546
		$nickname = $nickname == null ? $username : $nickname;
4547
4548
		if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4549
			$nickname = $username;
4550
			wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4551
		} elseif ( $fancySig !== false ) {
4552
			# Sig. might contain markup; validate this
4553
			if ( $this->validateSig( $nickname ) !== false ) {
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4546 can also be of type boolean; however, Parser::validateSig() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4554
				# Validated; clean up (if needed) and return it
4555
				return $this->cleanSig( $nickname, true );
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4546 can also be of type boolean; however, Parser::cleanSig() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4556
			} else {
4557
				# Failed to validate; fall back to the default
4558
				$nickname = $username;
4559
				wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4560
			}
4561
		}
4562
4563
		# Make sure nickname doesnt get a sig in a sig
4564
		$nickname = self::cleanSigInSig( $nickname );
0 ignored issues
show
Bug introduced by
It seems like $nickname can also be of type boolean or null; however, Parser::cleanSigInSig() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
4565
4566
		# If we're still here, make it a link to the user page
4567
		$userText = wfEscapeWikiText( $username );
4568
		$nickText = wfEscapeWikiText( $nickname );
4569
		$msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4570
4571
		return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4572
			->title( $this->getTitle() )->text();
4573
	}
4574
4575
	/**
4576
	 * Check that the user's signature contains no bad XML
4577
	 *
4578
	 * @param string $text
4579
	 * @return string|bool An expanded string, or false if invalid.
4580
	 */
4581
	public function validateSig( $text ) {
4582
		return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4583
	}
4584
4585
	/**
4586
	 * Clean up signature text
4587
	 *
4588
	 * 1) Strip 3, 4 or 5 tildes out of signatures @see cleanSigInSig
4589
	 * 2) Substitute all transclusions
4590
	 *
4591
	 * @param string $text
4592
	 * @param bool $parsing Whether we're cleaning (preferences save) or parsing
4593
	 * @return string Signature text
4594
	 */
4595
	public function cleanSig( $text, $parsing = false ) {
4596
		if ( !$parsing ) {
4597
			global $wgTitle;
4598
			$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
4599
			$this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4600
		}
4601
4602
		# Option to disable this feature
4603
		if ( !$this->mOptions->getCleanSignatures() ) {
4604
			return $text;
4605
		}
4606
4607
		# @todo FIXME: Regex doesn't respect extension tags or nowiki
4608
		#  => Move this logic to braceSubstitution()
4609
		$substWord = MagicWord::get( 'subst' );
4610
		$substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4611
		$substText = '{{' . $substWord->getSynonym( 0 );
4612
4613
		$text = preg_replace( $substRegex, $substText, $text );
4614
		$text = self::cleanSigInSig( $text );
4615
		$dom = $this->preprocessToDom( $text );
4616
		$frame = $this->getPreprocessor()->newFrame();
4617
		$text = $frame->expand( $dom );
4618
4619
		if ( !$parsing ) {
4620
			$text = $this->mStripState->unstripBoth( $text );
4621
		}
4622
4623
		return $text;
4624
	}
4625
4626
	/**
4627
	 * Strip 3, 4 or 5 tildes out of signatures.
4628
	 *
4629
	 * @param string $text
4630
	 * @return string Signature text with /~{3,5}/ removed
4631
	 */
4632
	public static function cleanSigInSig( $text ) {
4633
		$text = preg_replace( '/~{3,5}/', '', $text );
4634
		return $text;
4635
	}
4636
4637
	/**
4638
	 * Set up some variables which are usually set up in parse()
4639
	 * so that an external function can call some class members with confidence
4640
	 *
4641
	 * @param Title|null $title
4642
	 * @param ParserOptions $options
4643
	 * @param int $outputType
4644
	 * @param bool $clearState
4645
	 */
4646
	public function startExternalParse( Title $title = null, ParserOptions $options,
4647
		$outputType, $clearState = true
4648
	) {
4649
		$this->startParse( $title, $options, $outputType, $clearState );
4650
	}
4651
4652
	/**
4653
	 * @param Title|null $title
4654
	 * @param ParserOptions $options
4655
	 * @param int $outputType
4656
	 * @param bool $clearState
4657
	 */
4658
	private function startParse( Title $title = null, ParserOptions $options,
4659
		$outputType, $clearState = true
4660
	) {
4661
		$this->setTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 4658 can be null; however, Parser::setTitle() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
4662
		$this->mOptions = $options;
4663
		$this->setOutputType( $outputType );
4664
		if ( $clearState ) {
4665
			$this->clearState();
4666
		}
4667
	}
4668
4669
	/**
4670
	 * Wrapper for preprocess()
4671
	 *
4672
	 * @param string $text The text to preprocess
4673
	 * @param ParserOptions $options Options
4674
	 * @param Title|null $title Title object or null to use $wgTitle
4675
	 * @return string
4676
	 */
4677
	public function transformMsg( $text, $options, $title = null ) {
4678
		static $executing = false;
4679
4680
		# Guard against infinite recursion
4681
		if ( $executing ) {
4682
			return $text;
4683
		}
4684
		$executing = true;
4685
4686
		if ( !$title ) {
4687
			global $wgTitle;
4688
			$title = $wgTitle;
4689
		}
4690
4691
		$text = $this->preprocess( $text, $title, $options );
4692
4693
		$executing = false;
4694
		return $text;
4695
	}
4696
4697
	/**
4698
	 * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
4699
	 * The callback should have the following form:
4700
	 *    function myParserHook( $text, $params, $parser, $frame ) { ... }
4701
	 *
4702
	 * Transform and return $text. Use $parser for any required context, e.g. use
4703
	 * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
4704
	 *
4705
	 * Hooks may return extended information by returning an array, of which the
4706
	 * first numbered element (index 0) must be the return string, and all other
4707
	 * entries are extracted into local variables within an internal function
4708
	 * in the Parser class.
4709
	 *
4710
	 * This interface (introduced r61913) appears to be undocumented, but
4711
	 * 'markerType' is used by some core tag hooks to override which strip
4712
	 * array their results are placed in. **Use great caution if attempting
4713
	 * this interface, as it is not documented and injudicious use could smash
4714
	 * private variables.**
4715
	 *
4716
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4717
	 * @param callable $callback The callback function (and object) to use for the tag
4718
	 * @throws MWException
4719
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4720
	 */
4721 View Code Duplication
	public function setHook( $tag, $callback ) {
4722
		$tag = strtolower( $tag );
4723
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4724
			throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4725
		}
4726
		$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4727
		$this->mTagHooks[$tag] = $callback;
4728
		if ( !in_array( $tag, $this->mStripList ) ) {
4729
			$this->mStripList[] = $tag;
4730
		}
4731
4732
		return $oldVal;
4733
	}
4734
4735
	/**
4736
	 * As setHook(), but letting the contents be parsed.
4737
	 *
4738
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
4739
	 * operate late in the transformation sequence, on HTML instead of wikitext.
4740
	 *
4741
	 * This is probably obsoleted by things dealing with parser frames?
4742
	 * The only extension currently using it is geoserver.
4743
	 *
4744
	 * @since 1.10
4745
	 * @todo better document or deprecate this
4746
	 *
4747
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4748
	 * @param callable $callback The callback function (and object) to use for the tag
4749
	 * @throws MWException
4750
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4751
	 */
4752
	public function setTransparentTagHook( $tag, $callback ) {
4753
		$tag = strtolower( $tag );
4754
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4755
			throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4756
		}
4757
		$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4758
		$this->mTransparentTagHooks[$tag] = $callback;
4759
4760
		return $oldVal;
4761
	}
4762
4763
	/**
4764
	 * Remove all tag hooks
4765
	 */
4766
	public function clearTagHooks() {
4767
		$this->mTagHooks = [];
4768
		$this->mFunctionTagHooks = [];
4769
		$this->mStripList = $this->mDefaultStripList;
4770
	}
4771
4772
	/**
4773
	 * Create a function, e.g. {{sum:1|2|3}}
4774
	 * The callback function should have the form:
4775
	 *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
4776
	 *
4777
	 * Or with Parser::SFH_OBJECT_ARGS:
4778
	 *    function myParserFunction( $parser, $frame, $args ) { ... }
4779
	 *
4780
	 * The callback may either return the text result of the function, or an array with the text
4781
	 * in element 0, and a number of flags in the other elements. The names of the flags are
4782
	 * specified in the keys. Valid flags are:
4783
	 *   found                     The text returned is valid, stop processing the template. This
4784
	 *                             is on by default.
4785
	 *   nowiki                    Wiki markup in the return value should be escaped
4786
	 *   isHTML                    The returned text is HTML, armour it against wikitext transformation
4787
	 *
4788
	 * @param string $id The magic word ID
4789
	 * @param callable $callback The callback function (and object) to use
4790
	 * @param int $flags A combination of the following flags:
4791
	 *     Parser::SFH_NO_HASH      No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
4792
	 *
4793
	 *     Parser::SFH_OBJECT_ARGS  Pass the template arguments as PPNode objects instead of text.
4794
	 *     This allows for conditional expansion of the parse tree, allowing you to eliminate dead
4795
	 *     branches and thus speed up parsing. It is also possible to analyse the parse tree of
4796
	 *     the arguments, and to control the way they are expanded.
4797
	 *
4798
	 *     The $frame parameter is a PPFrame. This can be used to produce expanded text from the
4799
	 *     arguments, for instance:
4800
	 *         $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
4801
	 *
4802
	 *     For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
4803
	 *     future versions. Please call $frame->expand() on it anyway so that your code keeps
4804
	 *     working if/when this is changed.
4805
	 *
4806
	 *     If you want whitespace to be trimmed from $args, you need to do it yourself, post-
4807
	 *     expansion.
4808
	 *
4809
	 *     Please read the documentation in includes/parser/Preprocessor.php for more information
4810
	 *     about the methods available in PPFrame and PPNode.
4811
	 *
4812
	 * @throws MWException
4813
	 * @return string|callable The old callback function for this name, if any
4814
	 */
4815
	public function setFunctionHook( $id, $callback, $flags = 0 ) {
4816
		global $wgContLang;
4817
4818
		$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4819
		$this->mFunctionHooks[$id] = [ $callback, $flags ];
4820
4821
		# Add to function cache
4822
		$mw = MagicWord::get( $id );
4823
		if ( !$mw ) {
4824
			throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4825
		}
4826
4827
		$synonyms = $mw->getSynonyms();
4828
		$sensitive = intval( $mw->isCaseSensitive() );
4829
4830
		foreach ( $synonyms as $syn ) {
4831
			# Case
4832
			if ( !$sensitive ) {
4833
				$syn = $wgContLang->lc( $syn );
4834
			}
4835
			# Add leading hash
4836
			if ( !( $flags & self::SFH_NO_HASH ) ) {
4837
				$syn = '#' . $syn;
4838
			}
4839
			# Remove trailing colon
4840
			if ( substr( $syn, -1, 1 ) === ':' ) {
4841
				$syn = substr( $syn, 0, -1 );
4842
			}
4843
			$this->mFunctionSynonyms[$sensitive][$syn] = $id;
4844
		}
4845
		return $oldVal;
4846
	}
4847
4848
	/**
4849
	 * Get all registered function hook identifiers
4850
	 *
4851
	 * @return array
4852
	 */
4853
	public function getFunctionHooks() {
4854
		return array_keys( $this->mFunctionHooks );
4855
	}
4856
4857
	/**
4858
	 * Create a tag function, e.g. "<test>some stuff</test>".
4859
	 * Unlike tag hooks, tag functions are parsed at preprocessor level.
4860
	 * Unlike parser functions, their content is not preprocessed.
4861
	 * @param string $tag
4862
	 * @param callable $callback
4863
	 * @param int $flags
4864
	 * @throws MWException
4865
	 * @return null
4866
	 */
4867 View Code Duplication
	public function setFunctionTagHook( $tag, $callback, $flags ) {
4868
		$tag = strtolower( $tag );
4869
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4870
			throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4871
		}
4872
		$old = isset( $this->mFunctionTagHooks[$tag] ) ?
4873
			$this->mFunctionTagHooks[$tag] : null;
4874
		$this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4875
4876
		if ( !in_array( $tag, $this->mStripList ) ) {
4877
			$this->mStripList[] = $tag;
4878
		}
4879
4880
		return $old;
4881
	}
4882
4883
	/**
4884
	 * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
4885
	 * Placeholders created in Linker::link()
4886
	 *
4887
	 * @param string $text
4888
	 * @param int $options
4889
	 */
4890
	public function replaceLinkHolders( &$text, $options = 0 ) {
4891
		$this->mLinkHolders->replace( $text );
4892
	}
4893
4894
	/**
4895
	 * Replace "<!--LINK-->" link placeholders with plain text of links
4896
	 * (not HTML-formatted).
4897
	 *
4898
	 * @param string $text
4899
	 * @return string
4900
	 */
4901
	public function replaceLinkHoldersText( $text ) {
4902
		return $this->mLinkHolders->replaceText( $text );
4903
	}
4904
4905
	/**
4906
	 * Renders an image gallery from a text with one line per image.
4907
	 * text labels may be given by using |-style alternative text. E.g.
4908
	 *   Image:one.jpg|The number "1"
4909
	 *   Image:tree.jpg|A tree
4910
	 * given as text will return the HTML of a gallery with two images,
4911
	 * labeled 'The number "1"' and
4912
	 * 'A tree'.
4913
	 *
4914
	 * @param string $text
4915
	 * @param array $params
4916
	 * @return string HTML
4917
	 */
4918
	public function renderImageGallery( $text, $params ) {
4919
4920
		$mode = false;
4921
		if ( isset( $params['mode'] ) ) {
4922
			$mode = $params['mode'];
4923
		}
4924
4925
		try {
4926
			$ig = ImageGalleryBase::factory( $mode );
4927
		} catch ( Exception $e ) {
4928
			// If invalid type set, fallback to default.
4929
			$ig = ImageGalleryBase::factory( false );
4930
		}
4931
4932
		$ig->setContextTitle( $this->mTitle );
4933
		$ig->setShowBytes( false );
4934
		$ig->setShowFilename( false );
4935
		$ig->setParser( $this );
4936
		$ig->setHideBadImages();
4937
		$ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
4938
4939
		if ( isset( $params['showfilename'] ) ) {
4940
			$ig->setShowFilename( true );
4941
		} else {
4942
			$ig->setShowFilename( false );
4943
		}
4944
		if ( isset( $params['caption'] ) ) {
4945
			$caption = $params['caption'];
4946
			$caption = htmlspecialchars( $caption );
4947
			$caption = $this->replaceInternalLinks( $caption );
4948
			$ig->setCaptionHtml( $caption );
4949
		}
4950
		if ( isset( $params['perrow'] ) ) {
4951
			$ig->setPerRow( $params['perrow'] );
4952
		}
4953
		if ( isset( $params['widths'] ) ) {
4954
			$ig->setWidths( $params['widths'] );
4955
		}
4956
		if ( isset( $params['heights'] ) ) {
4957
			$ig->setHeights( $params['heights'] );
4958
		}
4959
		$ig->setAdditionalOptions( $params );
4960
4961
		Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] );
4962
4963
		$lines = StringUtils::explode( "\n", $text );
4964
		foreach ( $lines as $line ) {
4965
			# match lines like these:
4966
			# Image:someimage.jpg|This is some image
4967
			$matches = [];
4968
			preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4969
			# Skip empty lines
4970
			if ( count( $matches ) == 0 ) {
4971
				continue;
4972
			}
4973
4974
			if ( strpos( $matches[0], '%' ) !== false ) {
4975
				$matches[1] = rawurldecode( $matches[1] );
4976
			}
4977
			$title = Title::newFromText( $matches[1], NS_FILE );
4978
			if ( is_null( $title ) ) {
4979
				# Bogus title. Ignore these so we don't bomb out later.
4980
				continue;
4981
			}
4982
4983
			# We need to get what handler the file uses, to figure out parameters.
4984
			# Note, a hook can overide the file name, and chose an entirely different
4985
			# file (which potentially could be of a different type and have different handler).
4986
			$options = [];
4987
			$descQuery = false;
4988
			Hooks::run( 'BeforeParserFetchFileAndTitle',
4989
				[ $this, $title, &$options, &$descQuery ] );
4990
			# Don't register it now, as ImageGallery does that later.
4991
			$file = $this->fetchFileNoRegister( $title, $options );
4992
			$handler = $file ? $file->getHandler() : false;
4993
4994
			$paramMap = [
4995
				'img_alt' => 'gallery-internal-alt',
4996
				'img_link' => 'gallery-internal-link',
4997
			];
4998
			if ( $handler ) {
4999
				$paramMap = $paramMap + $handler->getParamMap();
5000
				// We don't want people to specify per-image widths.
5001
				// Additionally the width parameter would need special casing anyhow.
5002
				unset( $paramMap['img_width'] );
5003
			}
5004
5005
			$mwArray = new MagicWordArray( array_keys( $paramMap ) );
5006
5007
			$label = '';
5008
			$alt = '';
5009
			$link = '';
5010
			$handlerOptions = [];
5011
			if ( isset( $matches[3] ) ) {
5012
				// look for an |alt= definition while trying not to break existing
5013
				// captions with multiple pipes (|) in it, until a more sensible grammar
5014
				// is defined for images in galleries
5015
5016
				// FIXME: Doing recursiveTagParse at this stage, and the trim before
5017
				// splitting on '|' is a bit odd, and different from makeImage.
5018
				$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5019
				$parameterMatches = StringUtils::explode( '|', $matches[3] );
5020
5021
				foreach ( $parameterMatches as $parameterMatch ) {
5022
					list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5023
					if ( $magicName ) {
5024
						$paramName = $paramMap[$magicName];
5025
5026
						switch ( $paramName ) {
5027
						case 'gallery-internal-alt':
5028
							$alt = $this->stripAltText( $match, false );
5029
							break;
5030
						case 'gallery-internal-link':
5031
							$linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5032
							$chars = self::EXT_LINK_URL_CLASS;
5033
							$addr = self::EXT_LINK_ADDR;
5034
							$prots = $this->mUrlProtocols;
5035
							// check to see if link matches an absolute url, if not then it must be a wiki link.
5036
							if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
5037
								$link = $linkValue;
5038
							} else {
5039
								$localLinkTitle = Title::newFromText( $linkValue );
5040
								if ( $localLinkTitle !== null ) {
5041
									$link = $localLinkTitle->getLinkURL();
5042
								}
5043
							}
5044
							break;
5045
						default:
5046
							// Must be a handler specific parameter.
5047
							if ( $handler->validateParam( $paramName, $match ) ) {
5048
								$handlerOptions[$paramName] = $match;
5049
							} else {
5050
								// Guess not, consider it as caption.
5051
								wfDebug( "$parameterMatch failed parameter validation\n" );
5052
								$label = '|' . $parameterMatch;
5053
							}
5054
						}
5055
5056
					} else {
5057
						// Last pipe wins.
5058
						$label = '|' . $parameterMatch;
5059
					}
5060
				}
5061
				// Remove the pipe.
5062
				$label = substr( $label, 1 );
5063
			}
5064
5065
			$ig->add( $title, $label, $alt, $link, $handlerOptions );
5066
		}
5067
		$html = $ig->toHTML();
5068
		Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5069
		return $html;
5070
	}
5071
5072
	/**
5073
	 * @param MediaHandler $handler
5074
	 * @return array
5075
	 */
5076
	public function getImageParams( $handler ) {
5077
		if ( $handler ) {
5078
			$handlerClass = get_class( $handler );
5079
		} else {
5080
			$handlerClass = '';
5081
		}
5082
		if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5083
			# Initialise static lists
5084
			static $internalParamNames = [
5085
				'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5086
				'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5087
					'bottom', 'text-bottom' ],
5088
				'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5089
					'upright', 'border', 'link', 'alt', 'class' ],
5090
			];
5091
			static $internalParamMap;
5092
			if ( !$internalParamMap ) {
5093
				$internalParamMap = [];
5094
				foreach ( $internalParamNames as $type => $names ) {
5095
					foreach ( $names as $name ) {
5096
						$magicName = str_replace( '-', '_', "img_$name" );
5097
						$internalParamMap[$magicName] = [ $type, $name ];
5098
					}
5099
				}
5100
			}
5101
5102
			# Add handler params
5103
			$paramMap = $internalParamMap;
5104
			if ( $handler ) {
5105
				$handlerParamMap = $handler->getParamMap();
5106
				foreach ( $handlerParamMap as $magic => $paramName ) {
5107
					$paramMap[$magic] = [ 'handler', $paramName ];
5108
				}
5109
			}
5110
			$this->mImageParams[$handlerClass] = $paramMap;
5111
			$this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5112
		}
5113
		return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5114
	}
5115
5116
	/**
5117
	 * Parse image options text and use it to make an image
5118
	 *
5119
	 * @param Title $title
5120
	 * @param string $options
5121
	 * @param LinkHolderArray|bool $holders
5122
	 * @return string HTML
5123
	 */
5124
	public function makeImage( $title, $options, $holders = false ) {
5125
		# Check if the options text is of the form "options|alt text"
5126
		# Options are:
5127
		#  * thumbnail  make a thumbnail with enlarge-icon and caption, alignment depends on lang
5128
		#  * left       no resizing, just left align. label is used for alt= only
5129
		#  * right      same, but right aligned
5130
		#  * none       same, but not aligned
5131
		#  * ___px      scale to ___ pixels width, no aligning. e.g. use in taxobox
5132
		#  * center     center the image
5133
		#  * frame      Keep original image size, no magnify-button.
5134
		#  * framed     Same as "frame"
5135
		#  * frameless  like 'thumb' but without a frame. Keeps user preferences for width
5136
		#  * upright    reduce width for upright images, rounded to full __0 px
5137
		#  * border     draw a 1px border around the image
5138
		#  * alt        Text for HTML alt attribute (defaults to empty)
5139
		#  * class      Set a class for img node
5140
		#  * link       Set the target of the image link. Can be external, interwiki, or local
5141
		# vertical-align values (no % or length right now):
5142
		#  * baseline
5143
		#  * sub
5144
		#  * super
5145
		#  * top
5146
		#  * text-top
5147
		#  * middle
5148
		#  * bottom
5149
		#  * text-bottom
5150
5151
		$parts = StringUtils::explode( "|", $options );
5152
5153
		# Give extensions a chance to select the file revision for us
5154
		$options = [];
5155
		$descQuery = false;
5156
		Hooks::run( 'BeforeParserFetchFileAndTitle',
5157
			[ $this, $title, &$options, &$descQuery ] );
5158
		# Fetch and register the file (file title may be different via hooks)
5159
		list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5160
5161
		# Get parameter map
5162
		$handler = $file ? $file->getHandler() : false;
5163
5164
		list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5165
5166
		if ( !$file ) {
5167
			$this->addTrackingCategory( 'broken-file-category' );
5168
		}
5169
5170
		# Process the input parameters
5171
		$caption = '';
5172
		$params = [ 'frame' => [], 'handler' => [],
5173
			'horizAlign' => [], 'vertAlign' => [] ];
5174
		$seenformat = false;
5175
		foreach ( $parts as $part ) {
5176
			$part = trim( $part );
5177
			list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5178
			$validated = false;
5179
			if ( isset( $paramMap[$magicName] ) ) {
5180
				list( $type, $paramName ) = $paramMap[$magicName];
5181
5182
				# Special case; width and height come in one variable together
5183
				if ( $type === 'handler' && $paramName === 'width' ) {
5184
					$parsedWidthParam = $this->parseWidthParam( $value );
5185 View Code Duplication
					if ( isset( $parsedWidthParam['width'] ) ) {
5186
						$width = $parsedWidthParam['width'];
5187
						if ( $handler->validateParam( 'width', $width ) ) {
5188
							$params[$type]['width'] = $width;
5189
							$validated = true;
5190
						}
5191
					}
5192 View Code Duplication
					if ( isset( $parsedWidthParam['height'] ) ) {
5193
						$height = $parsedWidthParam['height'];
5194
						if ( $handler->validateParam( 'height', $height ) ) {
5195
							$params[$type]['height'] = $height;
5196
							$validated = true;
5197
						}
5198
					}
5199
					# else no validation -- bug 13436
5200
				} else {
5201
					if ( $type === 'handler' ) {
5202
						# Validate handler parameter
5203
						$validated = $handler->validateParam( $paramName, $value );
5204
					} else {
5205
						# Validate internal parameters
5206
						switch ( $paramName ) {
5207
						case 'manualthumb':
5208
						case 'alt':
5209
						case 'class':
5210
							# @todo FIXME: Possibly check validity here for
5211
							# manualthumb? downstream behavior seems odd with
5212
							# missing manual thumbs.
5213
							$validated = true;
5214
							$value = $this->stripAltText( $value, $holders );
5215
							break;
5216
						case 'link':
5217
							$chars = self::EXT_LINK_URL_CLASS;
5218
							$addr = self::EXT_LINK_ADDR;
5219
							$prots = $this->mUrlProtocols;
5220
							if ( $value === '' ) {
5221
								$paramName = 'no-link';
5222
								$value = true;
5223
								$validated = true;
5224
							} elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5225
								if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5226
									$paramName = 'link-url';
5227
									$this->mOutput->addExternalLink( $value );
5228
									if ( $this->mOptions->getExternalLinkTarget() ) {
5229
										$params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5230
									}
5231
									$validated = true;
5232
								}
5233
							} else {
5234
								$linkTitle = Title::newFromText( $value );
5235
								if ( $linkTitle ) {
5236
									$paramName = 'link-title';
5237
									$value = $linkTitle;
5238
									$this->mOutput->addLink( $linkTitle );
5239
									$validated = true;
5240
								}
5241
							}
5242
							break;
5243
						case 'frameless':
5244
						case 'framed':
5245
						case 'thumbnail':
5246
							// use first appearing option, discard others.
5247
							$validated = ! $seenformat;
5248
							$seenformat = true;
5249
							break;
5250
						default:
5251
							# Most other things appear to be empty or numeric...
5252
							$validated = ( $value === false || is_numeric( trim( $value ) ) );
5253
						}
5254
					}
5255
5256
					if ( $validated ) {
5257
						$params[$type][$paramName] = $value;
5258
					}
5259
				}
5260
			}
5261
			if ( !$validated ) {
5262
				$caption = $part;
5263
			}
5264
		}
5265
5266
		# Process alignment parameters
5267
		if ( $params['horizAlign'] ) {
5268
			$params['frame']['align'] = key( $params['horizAlign'] );
5269
		}
5270
		if ( $params['vertAlign'] ) {
5271
			$params['frame']['valign'] = key( $params['vertAlign'] );
5272
		}
5273
5274
		$params['frame']['caption'] = $caption;
5275
5276
		# Will the image be presented in a frame, with the caption below?
5277
		$imageIsFramed = isset( $params['frame']['frame'] )
5278
			|| isset( $params['frame']['framed'] )
5279
			|| isset( $params['frame']['thumbnail'] )
5280
			|| isset( $params['frame']['manualthumb'] );
5281
5282
		# In the old days, [[Image:Foo|text...]] would set alt text.  Later it
5283
		# came to also set the caption, ordinary text after the image -- which
5284
		# makes no sense, because that just repeats the text multiple times in
5285
		# screen readers.  It *also* came to set the title attribute.
5286
		# Now that we have an alt attribute, we should not set the alt text to
5287
		# equal the caption: that's worse than useless, it just repeats the
5288
		# text.  This is the framed/thumbnail case.  If there's no caption, we
5289
		# use the unnamed parameter for alt text as well, just for the time be-
5290
		# ing, if the unnamed param is set and the alt param is not.
5291
		# For the future, we need to figure out if we want to tweak this more,
5292
		# e.g., introducing a title= parameter for the title; ignoring the un-
5293
		# named parameter entirely for images without a caption; adding an ex-
5294
		# plicit caption= parameter and preserving the old magic unnamed para-
5295
		# meter for BC; ...
5296
		if ( $imageIsFramed ) { # Framed image
5297
			if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5298
				# No caption or alt text, add the filename as the alt text so
5299
				# that screen readers at least get some description of the image
5300
				$params['frame']['alt'] = $title->getText();
5301
			}
5302
			# Do not set $params['frame']['title'] because tooltips don't make sense
5303
			# for framed images
5304
		} else { # Inline image
5305
			if ( !isset( $params['frame']['alt'] ) ) {
5306
				# No alt text, use the "caption" for the alt text
5307
				if ( $caption !== '' ) {
5308
					$params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5309
				} else {
5310
					# No caption, fall back to using the filename for the
5311
					# alt text
5312
					$params['frame']['alt'] = $title->getText();
5313
				}
5314
			}
5315
			# Use the "caption" for the tooltip text
5316
			$params['frame']['title'] = $this->stripAltText( $caption, $holders );
5317
		}
5318
5319
		Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5320
5321
		# Linker does the rest
5322
		$time = isset( $options['time'] ) ? $options['time'] : false;
5323
		$ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5324
			$time, $descQuery, $this->mOptions->getThumbSize() );
5325
5326
		# Give the handler a chance to modify the parser object
5327
		if ( $handler ) {
5328
			$handler->parserTransformHook( $this, $file );
5329
		}
5330
5331
		return $ret;
5332
	}
5333
5334
	/**
5335
	 * @param string $caption
5336
	 * @param LinkHolderArray|bool $holders
5337
	 * @return mixed|string
5338
	 */
5339
	protected function stripAltText( $caption, $holders ) {
5340
		# Strip bad stuff out of the title (tooltip).  We can't just use
5341
		# replaceLinkHoldersText() here, because if this function is called
5342
		# from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5343
		if ( $holders ) {
5344
			$tooltip = $holders->replaceText( $caption );
0 ignored issues
show
Bug introduced by
It seems like $holders 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...
5345
		} else {
5346
			$tooltip = $this->replaceLinkHoldersText( $caption );
5347
		}
5348
5349
		# make sure there are no placeholders in thumbnail attributes
5350
		# that are later expanded to html- so expand them now and
5351
		# remove the tags
5352
		$tooltip = $this->mStripState->unstripBoth( $tooltip );
5353
		$tooltip = Sanitizer::stripAllTags( $tooltip );
5354
5355
		return $tooltip;
5356
	}
5357
5358
	/**
5359
	 * Set a flag in the output object indicating that the content is dynamic and
5360
	 * shouldn't be cached.
5361
	 * @deprecated since 1.28; use getOutput()->updateCacheExpiry()
5362
	 */
5363
	public function disableCache() {
5364
		wfDebug( "Parser output marked as uncacheable.\n" );
5365
		if ( !$this->mOutput ) {
5366
			throw new MWException( __METHOD__ .
5367
				" can only be called when actually parsing something" );
5368
		}
5369
		$this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5370
	}
5371
5372
	/**
5373
	 * Callback from the Sanitizer for expanding items found in HTML attribute
5374
	 * values, so they can be safely tested and escaped.
5375
	 *
5376
	 * @param string $text
5377
	 * @param bool|PPFrame $frame
5378
	 * @return string
5379
	 */
5380
	public function attributeStripCallback( &$text, $frame = false ) {
5381
		$text = $this->replaceVariables( $text, $frame );
5382
		$text = $this->mStripState->unstripBoth( $text );
5383
		return $text;
5384
	}
5385
5386
	/**
5387
	 * Accessor
5388
	 *
5389
	 * @return array
5390
	 */
5391
	public function getTags() {
5392
		return array_merge(
5393
			array_keys( $this->mTransparentTagHooks ),
5394
			array_keys( $this->mTagHooks ),
5395
			array_keys( $this->mFunctionTagHooks )
5396
		);
5397
	}
5398
5399
	/**
5400
	 * Replace transparent tags in $text with the values given by the callbacks.
5401
	 *
5402
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
5403
	 * operate late in the transformation sequence, on HTML instead of wikitext.
5404
	 *
5405
	 * @param string $text
5406
	 *
5407
	 * @return string
5408
	 */
5409
	public function replaceTransparentTags( $text ) {
5410
		$matches = [];
5411
		$elements = array_keys( $this->mTransparentTagHooks );
5412
		$text = self::extractTagsAndParams( $elements, $text, $matches );
5413
		$replacements = [];
5414
5415
		foreach ( $matches as $marker => $data ) {
5416
			list( $element, $content, $params, $tag ) = $data;
5417
			$tagName = strtolower( $element );
5418
			if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5419
				$output = call_user_func_array(
5420
					$this->mTransparentTagHooks[$tagName],
5421
					[ $content, $params, $this ]
5422
				);
5423
			} else {
5424
				$output = $tag;
5425
			}
5426
			$replacements[$marker] = $output;
5427
		}
5428
		return strtr( $text, $replacements );
5429
	}
5430
5431
	/**
5432
	 * Break wikitext input into sections, and either pull or replace
5433
	 * some particular section's text.
5434
	 *
5435
	 * External callers should use the getSection and replaceSection methods.
5436
	 *
5437
	 * @param string $text Page wikitext
5438
	 * @param string|number $sectionId A section identifier string of the form:
5439
	 *   "<flag1> - <flag2> - ... - <section number>"
5440
	 *
5441
	 * Currently the only recognised flag is "T", which means the target section number
5442
	 * was derived during a template inclusion parse, in other words this is a template
5443
	 * section edit link. If no flags are given, it was an ordinary section edit link.
5444
	 * This flag is required to avoid a section numbering mismatch when a section is
5445
	 * enclosed by "<includeonly>" (bug 6563).
5446
	 *
5447
	 * The section number 0 pulls the text before the first heading; other numbers will
5448
	 * pull the given section along with its lower-level subsections. If the section is
5449
	 * not found, $mode=get will return $newtext, and $mode=replace will return $text.
5450
	 *
5451
	 * Section 0 is always considered to exist, even if it only contains the empty
5452
	 * string. If $text is the empty string and section 0 is replaced, $newText is
5453
	 * returned.
5454
	 *
5455
	 * @param string $mode One of "get" or "replace"
5456
	 * @param string $newText Replacement text for section data.
5457
	 * @return string For "get", the extracted section text.
5458
	 *   for "replace", the whole page with the section replaced.
5459
	 */
5460
	private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5461
		global $wgTitle; # not generally used but removes an ugly failure mode
5462
5463
		$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
5464
		$this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5465
		$outText = '';
5466
		$frame = $this->getPreprocessor()->newFrame();
5467
5468
		# Process section extraction flags
5469
		$flags = 0;
5470
		$sectionParts = explode( '-', $sectionId );
5471
		$sectionIndex = array_pop( $sectionParts );
5472
		foreach ( $sectionParts as $part ) {
5473
			if ( $part === 'T' ) {
5474
				$flags |= self::PTD_FOR_INCLUSION;
5475
			}
5476
		}
5477
5478
		# Check for empty input
5479
		if ( strval( $text ) === '' ) {
5480
			# Only sections 0 and T-0 exist in an empty document
5481
			if ( $sectionIndex == 0 ) {
5482
				if ( $mode === 'get' ) {
5483
					return '';
5484
				} else {
5485
					return $newText;
5486
				}
5487
			} else {
5488
				if ( $mode === 'get' ) {
5489
					return $newText;
5490
				} else {
5491
					return $text;
5492
				}
5493
			}
5494
		}
5495
5496
		# Preprocess the text
5497
		$root = $this->preprocessToDom( $text, $flags );
5498
5499
		# <h> nodes indicate section breaks
5500
		# They can only occur at the top level, so we can find them by iterating the root's children
5501
		$node = $root->getFirstChild();
5502
5503
		# Find the target section
5504
		if ( $sectionIndex == 0 ) {
5505
			# Section zero doesn't nest, level=big
5506
			$targetLevel = 1000;
5507
		} else {
5508
			while ( $node ) {
5509 View Code Duplication
				if ( $node->getName() === 'h' ) {
5510
					$bits = $node->splitHeading();
5511
					if ( $bits['i'] == $sectionIndex ) {
5512
						$targetLevel = $bits['level'];
5513
						break;
5514
					}
5515
				}
5516
				if ( $mode === 'replace' ) {
5517
					$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5518
				}
5519
				$node = $node->getNextSibling();
5520
			}
5521
		}
5522
5523
		if ( !$node ) {
5524
			# Not found
5525
			if ( $mode === 'get' ) {
5526
				return $newText;
5527
			} else {
5528
				return $text;
5529
			}
5530
		}
5531
5532
		# Find the end of the section, including nested sections
5533
		do {
5534 View Code Duplication
			if ( $node->getName() === 'h' ) {
5535
				$bits = $node->splitHeading();
5536
				$curLevel = $bits['level'];
5537
				if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
0 ignored issues
show
Bug introduced by
The variable $targetLevel 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...
5538
					break;
5539
				}
5540
			}
5541
			if ( $mode === 'get' ) {
5542
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5543
			}
5544
			$node = $node->getNextSibling();
5545
		} while ( $node );
5546
5547
		# Write out the remainder (in replace mode only)
5548
		if ( $mode === 'replace' ) {
5549
			# Output the replacement text
5550
			# Add two newlines on -- trailing whitespace in $newText is conventionally
5551
			# stripped by the editor, so we need both newlines to restore the paragraph gap
5552
			# Only add trailing whitespace if there is newText
5553
			if ( $newText != "" ) {
5554
				$outText .= $newText . "\n\n";
5555
			}
5556
5557
			while ( $node ) {
5558
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5559
				$node = $node->getNextSibling();
5560
			}
5561
		}
5562
5563
		if ( is_string( $outText ) ) {
5564
			# Re-insert stripped tags
5565
			$outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5566
		}
5567
5568
		return $outText;
5569
	}
5570
5571
	/**
5572
	 * This function returns the text of a section, specified by a number ($section).
5573
	 * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
5574
	 * the first section before any such heading (section 0).
5575
	 *
5576
	 * If a section contains subsections, these are also returned.
5577
	 *
5578
	 * @param string $text Text to look in
5579
	 * @param string|number $sectionId Section identifier as a number or string
5580
	 * (e.g. 0, 1 or 'T-1').
5581
	 * @param string $defaultText Default to return if section is not found
5582
	 *
5583
	 * @return string Text of the requested section
5584
	 */
5585
	public function getSection( $text, $sectionId, $defaultText = '' ) {
5586
		return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5587
	}
5588
5589
	/**
5590
	 * This function returns $oldtext after the content of the section
5591
	 * specified by $section has been replaced with $text. If the target
5592
	 * section does not exist, $oldtext is returned unchanged.
5593
	 *
5594
	 * @param string $oldText Former text of the article
5595
	 * @param string|number $sectionId Section identifier as a number or string
5596
	 * (e.g. 0, 1 or 'T-1').
5597
	 * @param string $newText Replacing text
5598
	 *
5599
	 * @return string Modified text
5600
	 */
5601
	public function replaceSection( $oldText, $sectionId, $newText ) {
5602
		return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5603
	}
5604
5605
	/**
5606
	 * Get the ID of the revision we are parsing
5607
	 *
5608
	 * @return int|null
5609
	 */
5610
	public function getRevisionId() {
5611
		return $this->mRevisionId;
5612
	}
5613
5614
	/**
5615
	 * Get the revision object for $this->mRevisionId
5616
	 *
5617
	 * @return Revision|null Either a Revision object or null
5618
	 * @since 1.23 (public since 1.23)
5619
	 */
5620
	public function getRevisionObject() {
5621
		if ( !is_null( $this->mRevisionObject ) ) {
5622
			return $this->mRevisionObject;
5623
		}
5624
		if ( is_null( $this->mRevisionId ) ) {
5625
			return null;
5626
		}
5627
5628
		$rev = call_user_func(
5629
			$this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5630
		);
5631
5632
		# If the parse is for a new revision, then the callback should have
5633
		# already been set to force the object and should match mRevisionId.
5634
		# If not, try to fetch by mRevisionId for sanity.
5635
		if ( $rev && $rev->getId() != $this->mRevisionId ) {
5636
			$rev = Revision::newFromId( $this->mRevisionId );
5637
		}
5638
5639
		$this->mRevisionObject = $rev;
5640
5641
		return $this->mRevisionObject;
5642
	}
5643
5644
	/**
5645
	 * Get the timestamp associated with the current revision, adjusted for
5646
	 * the default server-local timestamp
5647
	 * @return string
5648
	 */
5649
	public function getRevisionTimestamp() {
5650
		if ( is_null( $this->mRevisionTimestamp ) ) {
5651
			global $wgContLang;
5652
5653
			$revObject = $this->getRevisionObject();
5654
			$timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5655
5656
			# The cryptic '' timezone parameter tells to use the site-default
5657
			# timezone offset instead of the user settings.
5658
			# Since this value will be saved into the parser cache, served
5659
			# to other users, and potentially even used inside links and such,
5660
			# it needs to be consistent for all visitors.
5661
			$this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5662
5663
		}
5664
		return $this->mRevisionTimestamp;
5665
	}
5666
5667
	/**
5668
	 * Get the name of the user that edited the last revision
5669
	 *
5670
	 * @return string User name
5671
	 */
5672
	public function getRevisionUser() {
5673
		if ( is_null( $this->mRevisionUser ) ) {
5674
			$revObject = $this->getRevisionObject();
5675
5676
			# if this template is subst: the revision id will be blank,
5677
			# so just use the current user's name
5678
			if ( $revObject ) {
5679
				$this->mRevisionUser = $revObject->getUserText();
5680
			} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5681
				$this->mRevisionUser = $this->getUser()->getName();
5682
			}
5683
		}
5684
		return $this->mRevisionUser;
5685
	}
5686
5687
	/**
5688
	 * Get the size of the revision
5689
	 *
5690
	 * @return int|null Revision size
5691
	 */
5692
	public function getRevisionSize() {
5693
		if ( is_null( $this->mRevisionSize ) ) {
5694
			$revObject = $this->getRevisionObject();
5695
5696
			# if this variable is subst: the revision id will be blank,
5697
			# so just use the parser input size, because the own substituation
5698
			# will change the size.
5699
			if ( $revObject ) {
5700
				$this->mRevisionSize = $revObject->getSize();
5701
			} else {
5702
				$this->mRevisionSize = $this->mInputSize;
5703
			}
5704
		}
5705
		return $this->mRevisionSize;
5706
	}
5707
5708
	/**
5709
	 * Mutator for $mDefaultSort
5710
	 *
5711
	 * @param string $sort New value
5712
	 */
5713
	public function setDefaultSort( $sort ) {
5714
		$this->mDefaultSort = $sort;
5715
		$this->mOutput->setProperty( 'defaultsort', $sort );
5716
	}
5717
5718
	/**
5719
	 * Accessor for $mDefaultSort
5720
	 * Will use the empty string if none is set.
5721
	 *
5722
	 * This value is treated as a prefix, so the
5723
	 * empty string is equivalent to sorting by
5724
	 * page name.
5725
	 *
5726
	 * @return string
5727
	 */
5728
	public function getDefaultSort() {
5729
		if ( $this->mDefaultSort !== false ) {
5730
			return $this->mDefaultSort;
5731
		} else {
5732
			return '';
5733
		}
5734
	}
5735
5736
	/**
5737
	 * Accessor for $mDefaultSort
5738
	 * Unlike getDefaultSort(), will return false if none is set
5739
	 *
5740
	 * @return string|bool
5741
	 */
5742
	public function getCustomDefaultSort() {
5743
		return $this->mDefaultSort;
5744
	}
5745
5746
	/**
5747
	 * Try to guess the section anchor name based on a wikitext fragment
5748
	 * presumably extracted from a heading, for example "Header" from
5749
	 * "== Header ==".
5750
	 *
5751
	 * @param string $text
5752
	 *
5753
	 * @return string
5754
	 */
5755
	public function guessSectionNameFromWikiText( $text ) {
5756
		# Strip out wikitext links(they break the anchor)
5757
		$text = $this->stripSectionName( $text );
5758
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5759
		return '#' . Sanitizer::escapeId( $text, 'noninitial' );
5760
	}
5761
5762
	/**
5763
	 * Same as guessSectionNameFromWikiText(), but produces legacy anchors
5764
	 * instead.  For use in redirects, since IE6 interprets Redirect: headers
5765
	 * as something other than UTF-8 (apparently?), resulting in breakage.
5766
	 *
5767
	 * @param string $text The section name
5768
	 * @return string An anchor
5769
	 */
5770
	public function guessLegacySectionNameFromWikiText( $text ) {
5771
		# Strip out wikitext links(they break the anchor)
5772
		$text = $this->stripSectionName( $text );
5773
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5774
		return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
5775
	}
5776
5777
	/**
5778
	 * Strips a text string of wikitext for use in a section anchor
5779
	 *
5780
	 * Accepts a text string and then removes all wikitext from the
5781
	 * string and leaves only the resultant text (i.e. the result of
5782
	 * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
5783
	 * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
5784
	 * to create valid section anchors by mimicing the output of the
5785
	 * parser when headings are parsed.
5786
	 *
5787
	 * @param string $text Text string to be stripped of wikitext
5788
	 * for use in a Section anchor
5789
	 * @return string Filtered text string
5790
	 */
5791
	public function stripSectionName( $text ) {
5792
		# Strip internal link markup
5793
		$text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5794
		$text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5795
5796
		# Strip external link markup
5797
		# @todo FIXME: Not tolerant to blank link text
5798
		# I.E. [https://www.mediawiki.org] will render as [1] or something depending
5799
		# on how many empty links there are on the page - need to figure that out.
5800
		$text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5801
5802
		# Parse wikitext quotes (italics & bold)
5803
		$text = $this->doQuotes( $text );
5804
5805
		# Strip HTML tags
5806
		$text = StringUtils::delimiterReplace( '<', '>', '', $text );
5807
		return $text;
5808
	}
5809
5810
	/**
5811
	 * strip/replaceVariables/unstrip for preprocessor regression testing
5812
	 *
5813
	 * @param string $text
5814
	 * @param Title $title
5815
	 * @param ParserOptions $options
5816
	 * @param int $outputType
5817
	 *
5818
	 * @return string
5819
	 */
5820
	public function testSrvus( $text, Title $title, ParserOptions $options,
5821
		$outputType = self::OT_HTML
5822
	) {
5823
		$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
5824
		$this->startParse( $title, $options, $outputType, true );
5825
5826
		$text = $this->replaceVariables( $text );
5827
		$text = $this->mStripState->unstripBoth( $text );
5828
		$text = Sanitizer::removeHTMLtags( $text );
5829
		return $text;
5830
	}
5831
5832
	/**
5833
	 * @param string $text
5834
	 * @param Title $title
5835
	 * @param ParserOptions $options
5836
	 * @return string
5837
	 */
5838
	public function testPst( $text, Title $title, ParserOptions $options ) {
5839
		return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5840
	}
5841
5842
	/**
5843
	 * @param string $text
5844
	 * @param Title $title
5845
	 * @param ParserOptions $options
5846
	 * @return string
5847
	 */
5848
	public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5849
		return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5850
	}
5851
5852
	/**
5853
	 * Call a callback function on all regions of the given text that are not
5854
	 * inside strip markers, and replace those regions with the return value
5855
	 * of the callback. For example, with input:
5856
	 *
5857
	 *  aaa<MARKER>bbb
5858
	 *
5859
	 * This will call the callback function twice, with 'aaa' and 'bbb'. Those
5860
	 * two strings will be replaced with the value returned by the callback in
5861
	 * each case.
5862
	 *
5863
	 * @param string $s
5864
	 * @param callable $callback
5865
	 *
5866
	 * @return string
5867
	 */
5868
	public function markerSkipCallback( $s, $callback ) {
5869
		$i = 0;
5870
		$out = '';
5871
		while ( $i < strlen( $s ) ) {
5872
			$markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5873
			if ( $markerStart === false ) {
5874
				$out .= call_user_func( $callback, substr( $s, $i ) );
5875
				break;
5876
			} else {
5877
				$out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5878
				$markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5879
				if ( $markerEnd === false ) {
5880
					$out .= substr( $s, $markerStart );
5881
					break;
5882
				} else {
5883
					$markerEnd += strlen( self::MARKER_SUFFIX );
5884
					$out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5885
					$i = $markerEnd;
5886
				}
5887
			}
5888
		}
5889
		return $out;
5890
	}
5891
5892
	/**
5893
	 * Remove any strip markers found in the given text.
5894
	 *
5895
	 * @param string $text Input string
5896
	 * @return string
5897
	 */
5898
	public function killMarkers( $text ) {
5899
		return $this->mStripState->killMarkers( $text );
5900
	}
5901
5902
	/**
5903
	 * Save the parser state required to convert the given half-parsed text to
5904
	 * HTML. "Half-parsed" in this context means the output of
5905
	 * recursiveTagParse() or internalParse(). This output has strip markers
5906
	 * from replaceVariables (extensionSubstitution() etc.), and link
5907
	 * placeholders from replaceLinkHolders().
5908
	 *
5909
	 * Returns an array which can be serialized and stored persistently. This
5910
	 * array can later be loaded into another parser instance with
5911
	 * unserializeHalfParsedText(). The text can then be safely incorporated into
5912
	 * the return value of a parser hook.
5913
	 *
5914
	 * @param string $text
5915
	 *
5916
	 * @return array
5917
	 */
5918
	public function serializeHalfParsedText( $text ) {
5919
		$data = [
5920
			'text' => $text,
5921
			'version' => self::HALF_PARSED_VERSION,
5922
			'stripState' => $this->mStripState->getSubState( $text ),
5923
			'linkHolders' => $this->mLinkHolders->getSubArray( $text )
5924
		];
5925
		return $data;
5926
	}
5927
5928
	/**
5929
	 * Load the parser state given in the $data array, which is assumed to
5930
	 * have been generated by serializeHalfParsedText(). The text contents is
5931
	 * extracted from the array, and its markers are transformed into markers
5932
	 * appropriate for the current Parser instance. This transformed text is
5933
	 * returned, and can be safely included in the return value of a parser
5934
	 * hook.
5935
	 *
5936
	 * If the $data array has been stored persistently, the caller should first
5937
	 * check whether it is still valid, by calling isValidHalfParsedText().
5938
	 *
5939
	 * @param array $data Serialized data
5940
	 * @throws MWException
5941
	 * @return string
5942
	 */
5943
	public function unserializeHalfParsedText( $data ) {
5944 View Code Duplication
		if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
5945
			throw new MWException( __METHOD__ . ': invalid version' );
5946
		}
5947
5948
		# First, extract the strip state.
5949
		$texts = [ $data['text'] ];
5950
		$texts = $this->mStripState->merge( $data['stripState'], $texts );
5951
5952
		# Now renumber links
5953
		$texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
5954
5955
		# Should be good to go.
5956
		return $texts[0];
5957
	}
5958
5959
	/**
5960
	 * Returns true if the given array, presumed to be generated by
5961
	 * serializeHalfParsedText(), is compatible with the current version of the
5962
	 * parser.
5963
	 *
5964
	 * @param array $data
5965
	 *
5966
	 * @return bool
5967
	 */
5968
	public function isValidHalfParsedText( $data ) {
5969
		return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
5970
	}
5971
5972
	/**
5973
	 * Parsed a width param of imagelink like 300px or 200x300px
5974
	 *
5975
	 * @param string $value
5976
	 *
5977
	 * @return array
5978
	 * @since 1.20
5979
	 */
5980
	public function parseWidthParam( $value ) {
5981
		$parsedWidthParam = [];
5982
		if ( $value === '' ) {
5983
			return $parsedWidthParam;
5984
		}
5985
		$m = [];
5986
		# (bug 13500) In both cases (width/height and width only),
5987
		# permit trailing "px" for backward compatibility.
5988
		if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
5989
			$width = intval( $m[1] );
5990
			$height = intval( $m[2] );
5991
			$parsedWidthParam['width'] = $width;
5992
			$parsedWidthParam['height'] = $height;
5993
		} elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
5994
			$width = intval( $value );
5995
			$parsedWidthParam['width'] = $width;
5996
		}
5997
		return $parsedWidthParam;
5998
	}
5999
6000
	/**
6001
	 * Lock the current instance of the parser.
6002
	 *
6003
	 * This is meant to stop someone from calling the parser
6004
	 * recursively and messing up all the strip state.
6005
	 *
6006
	 * @throws MWException If parser is in a parse
6007
	 * @return ScopedCallback The lock will be released once the return value goes out of scope.
6008
	 */
6009
	protected function lock() {
6010
		if ( $this->mInParse ) {
6011
			throw new MWException( "Parser state cleared while parsing. "
6012
				. "Did you call Parser::parse recursively?" );
6013
		}
6014
		$this->mInParse = true;
6015
6016
		$recursiveCheck = new ScopedCallback( function() {
6017
			$this->mInParse = false;
6018
		} );
6019
6020
		return $recursiveCheck;
6021
	}
6022
6023
	/**
6024
	 * Strip outer <p></p> tag from the HTML source of a single paragraph.
6025
	 *
6026
	 * Returns original HTML if the <p/> tag has any attributes, if there's no wrapping <p/> tag,
6027
	 * or if there is more than one <p/> tag in the input HTML.
6028
	 *
6029
	 * @param string $html
6030
	 * @return string
6031
	 * @since 1.24
6032
	 */
6033
	public static function stripOuterParagraph( $html ) {
6034
		$m = [];
6035
		if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6036
			if ( strpos( $m[1], '</p>' ) === false ) {
6037
				$html = $m[1];
6038
			}
6039
		}
6040
6041
		return $html;
6042
	}
6043
6044
	/**
6045
	 * Return this parser if it is not doing anything, otherwise
6046
	 * get a fresh parser. You can use this method by doing
6047
	 * $myParser = $wgParser->getFreshParser(), or more simply
6048
	 * $wgParser->getFreshParser()->parse( ... );
6049
	 * if you're unsure if $wgParser is safe to use.
6050
	 *
6051
	 * @since 1.24
6052
	 * @return Parser A parser object that is not parsing anything
6053
	 */
6054
	public function getFreshParser() {
6055
		global $wgParserConf;
6056
		if ( $this->mInParse ) {
6057
			return new $wgParserConf['class']( $wgParserConf );
6058
		} else {
6059
			return $this;
6060
		}
6061
	}
6062
6063
	/**
6064
	 * Set's up the PHP implementation of OOUI for use in this request
6065
	 * and instructs OutputPage to enable OOUI for itself.
6066
	 *
6067
	 * @since 1.26
6068
	 */
6069
	public function enableOOUI() {
6070
		OutputPage::setupOOUI();
6071
		$this->mOutput->setEnableOOUI( true );
6072
	}
6073
}
6074