Completed
Branch master (5cbada)
by
unknown
28:59
created

Parser::braceSubstitution()   F

Complexity

Conditions 62
Paths > 20000

Size

Total Lines 298
Code Lines 172

Duplication

Lines 7
Ratio 2.35 %

Importance

Changes 0
Metric Value
cc 62
eloc 172
nc 123813720
nop 2
dl 7
loc 298
rs 2
c 0
b 0
f 0

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
24
/**
25
 * @defgroup Parser Parser
26
 */
27
28
/**
29
 * PHP Parser - Processes wiki markup (which uses a more user-friendly
30
 * syntax, such as "[[link]]" for making links), and provides a one-way
31
 * transformation of that wiki markup it into (X)HTML output / markup
32
 * (which in turn the browser understands, and can display).
33
 *
34
 * There are seven main entry points into the Parser class:
35
 *
36
 * - Parser::parse()
37
 *     produces HTML output
38
 * - Parser::preSaveTransform()
39
 *     produces altered wiki markup
40
 * - Parser::preprocess()
41
 *     removes HTML comments and expands templates
42
 * - Parser::cleanSig() and Parser::cleanSigInSig()
43
 *     cleans a signature before saving it to preferences
44
 * - Parser::getSection()
45
 *     return the content of a section from an article for section editing
46
 * - Parser::replaceSection()
47
 *     replaces a section by number inside an article
48
 * - Parser::getPreloadText()
49
 *     removes <noinclude> sections and <includeonly> tags
50
 *
51
 * Globals used:
52
 *    object: $wgContLang
53
 *
54
 * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
55
 *
56
 * @par Settings:
57
 * $wgNamespacesWithSubpages
58
 *
59
 * @par Settings only within ParserOptions:
60
 * $wgAllowExternalImages
61
 * $wgAllowSpecialInclusion
62
 * $wgInterwikiMagic
63
 * $wgMaxArticleSize
64
 *
65
 * @ingroup Parser
66
 */
67
class Parser {
68
	/**
69
	 * Update this version number when the ParserOutput format
70
	 * changes in an incompatible way, so the parser cache
71
	 * can automatically discard old data.
72
	 */
73
	const VERSION = '1.6.4';
74
75
	/**
76
	 * Update this version number when the output of serialiseHalfParsedText()
77
	 * changes in an incompatible way
78
	 */
79
	const HALF_PARSED_VERSION = 2;
80
81
	# Flags for Parser::setFunctionHook
82
	const SFH_NO_HASH = 1;
83
	const SFH_OBJECT_ARGS = 2;
84
85
	# Constants needed for external link processing
86
	# Everything except bracket, space, or control characters
87
	# \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
88
	# as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
89
	const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
90
	# Simplified expression to match an IPv4 or IPv6 address, or
91
	# at least one character of a host name (embeds EXT_LINK_URL_CLASS)
92
	const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
93
	# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
94
	// @codingStandardsIgnoreStart Generic.Files.LineLength
95
	const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
96
		\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
97
	// @codingStandardsIgnoreEnd
98
99
	# Regular expression for a non-newline space
100
	const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
101
102
	# Flags for preprocessToDom
103
	const PTD_FOR_INCLUSION = 1;
104
105
	# Allowed values for $this->mOutputType
106
	# Parameter to startExternalParse().
107
	const OT_HTML = 1; # like parse()
108
	const OT_WIKI = 2; # like preSaveTransform()
109
	const OT_PREPROCESS = 3; # like preprocess()
110
	const OT_MSG = 3;
111
	const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
112
113
	/**
114
	 * @var string Prefix and suffix for temporary replacement strings
115
	 * for the multipass parser.
116
	 *
117
	 * \x7f should never appear in input as it's disallowed in XML.
118
	 * Using it at the front also gives us a little extra robustness
119
	 * since it shouldn't match when butted up against identifier-like
120
	 * string constructs.
121
	 *
122
	 * Must not consist of all title characters, or else it will change
123
	 * the behavior of <nowiki> in a link.
124
	 *
125
	 * Must have a character that needs escaping in attributes, otherwise
126
	 * someone could put a strip marker in an attribute, to get around
127
	 * escaping quote marks, and break out of the attribute. Thus we add
128
	 * `'".
129
	 */
130
	const MARKER_SUFFIX = "-QINU`\"'\x7f";
131
	const MARKER_PREFIX = "\x7f'\"`UNIQ-";
132
133
	# Markers used for wrapping the table of contents
134
	const TOC_START = '<mw:toc>';
135
	const TOC_END = '</mw:toc>';
136
137
	# Persistent:
138
	public $mTagHooks = [];
139
	public $mTransparentTagHooks = [];
140
	public $mFunctionHooks = [];
141
	public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
142
	public $mFunctionTagHooks = [];
143
	public $mStripList = [];
144
	public $mDefaultStripList = [];
145
	public $mVarCache = [];
146
	public $mImageParams = [];
147
	public $mImageParamsMagicArray = [];
148
	public $mMarkerIndex = 0;
149
	public $mFirstCall = true;
150
151
	# Initialised by initialiseVariables()
152
153
	/**
154
	 * @var MagicWordArray
155
	 */
156
	public $mVariables;
157
158
	/**
159
	 * @var MagicWordArray
160
	 */
161
	public $mSubstWords;
162
	# Initialised in constructor
163
	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...
164
165
	# Initialized in getPreprocessor()
166
	/** @var Preprocessor */
167
	public $mPreprocessor;
168
169
	# Cleared with clearState():
170
	/**
171
	 * @var ParserOutput
172
	 */
173
	public $mOutput;
174
	public $mAutonumber;
175
176
	/**
177
	 * @var StripState
178
	 */
179
	public $mStripState;
180
181
	public $mIncludeCount;
182
	/**
183
	 * @var LinkHolderArray
184
	 */
185
	public $mLinkHolders;
186
187
	public $mLinkID;
188
	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...
189
	public $mDefaultSort;
190
	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...
191
	public $mExpensiveFunctionCount; # number of expensive parser function calls
192
	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...
193
194
	/**
195
	 * @var User
196
	 */
197
	public $mUser; # User object; only used when doing pre-save transform
198
199
	# Temporary
200
	# These are variables reset at least once per parse regardless of $clearState
201
202
	/**
203
	 * @var ParserOptions
204
	 */
205
	public $mOptions;
206
207
	/**
208
	 * @var Title
209
	 */
210
	public $mTitle;        # Title context, used for self-link rendering and similar things
211
	public $mOutputType;   # Output type, one of the OT_xxx constants
212
	public $ot;            # Shortcut alias, see setOutputType()
213
	public $mRevisionObject; # The revision object of the specified revision ID
214
	public $mRevisionId;   # ID to display in {{REVISIONID}} tags
215
	public $mRevisionTimestamp; # The timestamp of the specified revision ID
216
	public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
217
	public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
218
	public $mRevIdForTs;   # The revision ID which was used to fetch the timestamp
219
	public $mInputSize = false; # For {{PAGESIZE}} on current page.
220
221
	/**
222
	 * @var string Deprecated accessor for the strip marker prefix.
223
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
224
	 **/
225
	public $mUniqPrefix = Parser::MARKER_PREFIX;
226
227
	/**
228
	 * @var array Array with the language name of each language link (i.e. the
229
	 * interwiki prefix) in the key, value arbitrary. Used to avoid sending
230
	 * duplicate language links to the ParserOutput.
231
	 */
232
	public $mLangLinkLanguages;
233
234
	/**
235
	 * @var MapCacheLRU|null
236
	 * @since 1.24
237
	 *
238
	 * A cache of the current revisions of titles. Keys are $title->getPrefixedDbKey()
239
	 */
240
	public $currentRevisionCache;
241
242
	/**
243
	 * @var bool Recursive call protection.
244
	 * This variable should be treated as if it were private.
245
	 */
246
	public $mInParse = false;
247
248
	/** @var SectionProfiler */
249
	protected $mProfiler;
250
251
	/**
252
	 * @param array $conf
253
	 */
254
	public function __construct( $conf = [] ) {
255
		$this->mConf = $conf;
256
		$this->mUrlProtocols = wfUrlProtocols();
257
		$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
258
			self::EXT_LINK_ADDR .
259
			self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
260
		if ( isset( $conf['preprocessorClass'] ) ) {
261
			$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...
262
		} elseif ( defined( 'HPHP_VERSION' ) ) {
263
			# Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
264
			$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...
265
		} elseif ( extension_loaded( 'domxml' ) ) {
266
			# PECL extension that conflicts with the core DOM extension (bug 13770)
267
			wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
268
			$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...
269
		} elseif ( extension_loaded( 'dom' ) ) {
270
			$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...
271
		} else {
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
		}
274
		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...
275
	}
276
277
	/**
278
	 * Reduce memory usage to reduce the impact of circular references
279
	 */
280
	public function __destruct() {
281
		if ( isset( $this->mLinkHolders ) ) {
282
			unset( $this->mLinkHolders );
283
		}
284
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Parser> is not traversable.
Loading history...
285
			unset( $this->$name );
286
		}
287
	}
288
289
	/**
290
	 * Allow extensions to clean up when the parser is cloned
291
	 */
292
	public function __clone() {
293
		$this->mInParse = false;
294
295
		// Bug 56226: When you create a reference "to" an object field, that
296
		// makes the object field itself be a reference too (until the other
297
		// reference goes out of scope). When cloning, any field that's a
298
		// reference is copied as a reference in the new object. Both of these
299
		// are defined PHP5 behaviors, as inconvenient as it is for us when old
300
		// hooks from PHP4 days are passing fields by reference.
301
		foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
302
			// Make a non-reference copy of the field, then rebind the field to
303
			// reference the new copy.
304
			$tmp = $this->$k;
305
			$this->$k =& $tmp;
306
			unset( $tmp );
307
		}
308
309
		Hooks::run( 'ParserCloned', [ $this ] );
310
	}
311
312
	/**
313
	 * Do various kinds of initialisation on the first call of the parser
314
	 */
315
	public function firstCallInit() {
316
		if ( !$this->mFirstCall ) {
317
			return;
318
		}
319
		$this->mFirstCall = false;
320
321
		CoreParserFunctions::register( $this );
322
		CoreTagHooks::register( $this );
323
		$this->initialiseVariables();
324
325
		Hooks::run( 'ParserFirstCallInit', [ &$this ] );
326
	}
327
328
	/**
329
	 * Clear Parser state
330
	 *
331
	 * @private
332
	 */
333
	public function clearState() {
334
		if ( $this->mFirstCall ) {
335
			$this->firstCallInit();
336
		}
337
		$this->mOutput = new ParserOutput;
338
		$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
339
		$this->mAutonumber = 0;
340
		$this->mIncludeCount = [];
341
		$this->mLinkHolders = new LinkHolderArray( $this );
342
		$this->mLinkID = 0;
343
		$this->mRevisionObject = $this->mRevisionTimestamp =
344
			$this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
345
		$this->mVarCache = [];
346
		$this->mUser = null;
347
		$this->mLangLinkLanguages = [];
348
		$this->currentRevisionCache = null;
349
350
		$this->mStripState = new StripState;
351
352
		# Clear these on every parse, bug 4549
353
		$this->mTplRedirCache = $this->mTplDomCache = [];
354
355
		$this->mShowToc = true;
356
		$this->mForceTocPosition = false;
357
		$this->mIncludeSizes = [
358
			'post-expand' => 0,
359
			'arg' => 0,
360
		];
361
		$this->mPPNodeCount = 0;
362
		$this->mGeneratedPPNodeCount = 0;
363
		$this->mHighestExpansionDepth = 0;
364
		$this->mDefaultSort = false;
365
		$this->mHeadings = [];
366
		$this->mDoubleUnderscores = [];
367
		$this->mExpensiveFunctionCount = 0;
368
369
		# Fix cloning
370
		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...
371
			$this->mPreprocessor = null;
372
		}
373
374
		$this->mProfiler = new SectionProfiler();
375
376
		Hooks::run( 'ParserClearState', [ &$this ] );
377
	}
378
379
	/**
380
	 * Convert wikitext to HTML
381
	 * Do not call this function recursively.
382
	 *
383
	 * @param string $text Text we want to parse
384
	 * @param Title $title
385
	 * @param ParserOptions $options
386
	 * @param bool $linestart
387
	 * @param bool $clearState
388
	 * @param int $revid Number to pass in {{REVISIONID}}
389
	 * @return ParserOutput A ParserOutput
390
	 */
391
	public function parse( $text, Title $title, ParserOptions $options,
392
		$linestart = true, $clearState = true, $revid = null
393
	) {
394
		/**
395
		 * First pass--just handle <nowiki> sections, pass the rest off
396
		 * to internalParse() which does all the real work.
397
		 */
398
399
		global $wgShowHostnames;
400
401
		if ( $clearState ) {
402
			// We use U+007F DELETE to construct strip markers, so we have to make
403
			// sure that this character does not occur in the input text.
404
			$text = strtr( $text, "\x7f", "?" );
405
			$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...
406
		}
407
408
		$this->startParse( $title, $options, self::OT_HTML, $clearState );
409
410
		$this->currentRevisionCache = null;
411
		$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...
412
		if ( $this->mOptions->getEnableLimitReport() ) {
413
			$this->mOutput->resetParseStartTime();
414
		}
415
416
		$oldRevisionId = $this->mRevisionId;
417
		$oldRevisionObject = $this->mRevisionObject;
418
		$oldRevisionTimestamp = $this->mRevisionTimestamp;
419
		$oldRevisionUser = $this->mRevisionUser;
420
		$oldRevisionSize = $this->mRevisionSize;
421
		if ( $revid !== null ) {
422
			$this->mRevisionId = $revid;
423
			$this->mRevisionObject = null;
424
			$this->mRevisionTimestamp = null;
425
			$this->mRevisionUser = null;
426
			$this->mRevisionSize = null;
427
		}
428
429
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
430
		# No more strip!
431
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
432
		$text = $this->internalParse( $text );
433
		Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] );
434
435
		$text = $this->internalParseHalfParsed( $text, true, $linestart );
436
437
		/**
438
		 * A converted title will be provided in the output object if title and
439
		 * content conversion are enabled, the article text does not contain
440
		 * a conversion-suppressing double-underscore tag, and no
441
		 * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
442
		 * automatic link conversion.
443
		 */
444
		if ( !( $options->getDisableTitleConversion()
445
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] )
446
			|| isset( $this->mDoubleUnderscores['notitleconvert'] )
447
			|| $this->mOutput->getDisplayTitle() !== false )
448
		) {
449
			$convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
450
			if ( $convruletitle ) {
451
				$this->mOutput->setTitleText( $convruletitle );
452
			} else {
453
				$titleText = $this->getConverterLanguage()->convertTitle( $title );
454
				$this->mOutput->setTitleText( $titleText );
455
			}
456
		}
457
458
		if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
459
			$this->limitationWarn( 'expensive-parserfunction',
460
				$this->mExpensiveFunctionCount,
461
				$this->mOptions->getExpensiveParserFunctionLimit()
462
			);
463
		}
464
465
		# Information on include size limits, for the benefit of users who try to skirt them
466
		if ( $this->mOptions->getEnableLimitReport() ) {
467
			$max = $this->mOptions->getMaxIncludeSize();
468
469
			$cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
470
			if ( $cpuTime !== null ) {
471
				$this->mOutput->setLimitReportData( 'limitreport-cputime',
472
					sprintf( "%.3f", $cpuTime )
473
				);
474
			}
475
476
			$wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
477
			$this->mOutput->setLimitReportData( 'limitreport-walltime',
478
				sprintf( "%.3f", $wallTime )
479
			);
480
481
			$this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
482
				[ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
483
			);
484
			$this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
485
				[ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
486
			);
487
			$this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
488
				[ $this->mIncludeSizes['post-expand'], $max ]
489
			);
490
			$this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
491
				[ $this->mIncludeSizes['arg'], $max ]
492
			);
493
			$this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
494
				[ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
495
			);
496
			$this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
497
				[ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
498
			);
499
			Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
500
501
			$limitReport = "NewPP limit report\n";
502
			if ( $wgShowHostnames ) {
503
				$limitReport .= 'Parsed by ' . wfHostname() . "\n";
504
			}
505
			$limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
506
			$limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
507
			$limitReport .= 'Dynamic content: ' .
508
				( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
509
				"\n";
510
511
			foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
512
				if ( Hooks::run( 'ParserLimitReportFormat',
513
					[ $key, &$value, &$limitReport, false, false ]
514
				) ) {
515
					$keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
516
					$valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
517
						->inLanguage( 'en' )->useDatabase( false );
518
					if ( !$valueMsg->exists() ) {
519
						$valueMsg = new RawMessage( '$1' );
520
					}
521
					if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
522
						$valueMsg->params( $value );
523
						$limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
524
					}
525
				}
526
			}
527
			// Since we're not really outputting HTML, decode the entities and
528
			// then re-encode the things that need hiding inside HTML comments.
529
			$limitReport = htmlspecialchars_decode( $limitReport );
530
			Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ] );
531
532
			// Sanitize for comment. Note '‐' in the replacement is U+2010,
533
			// which looks much like the problematic '-'.
534
			$limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
535
			$text .= "\n<!-- \n$limitReport-->\n";
536
537
			// Add on template profiling data
538
			$dataByFunc = $this->mProfiler->getFunctionStats();
539
			uasort( $dataByFunc, function ( $a, $b ) {
540
				return $a['real'] < $b['real']; // descending order
541
			} );
542
			$profileReport = "Transclusion expansion time report (%,ms,calls,template)\n";
543
			foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
544
				$profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n",
545
					$item['%real'], $item['real'], $item['calls'],
546
					htmlspecialchars( $item['name'] ) );
547
			}
548
			$text .= "\n<!-- \n$profileReport-->\n";
549
550
			if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
551
				wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
552
					$this->mTitle->getPrefixedDBkey() );
553
			}
554
		}
555
		$this->mOutput->setText( $text );
556
557
		$this->mRevisionId = $oldRevisionId;
558
		$this->mRevisionObject = $oldRevisionObject;
559
		$this->mRevisionTimestamp = $oldRevisionTimestamp;
560
		$this->mRevisionUser = $oldRevisionUser;
561
		$this->mRevisionSize = $oldRevisionSize;
562
		$this->mInputSize = false;
563
		$this->currentRevisionCache = null;
564
565
		return $this->mOutput;
566
	}
567
568
	/**
569
	 * Half-parse wikitext to half-parsed HTML. This recursive parser entry point
570
	 * can be called from an extension tag hook.
571
	 *
572
	 * The output of this function IS NOT SAFE PARSED HTML; it is "half-parsed"
573
	 * instead, which means that lists and links have not been fully parsed yet,
574
	 * and strip markers are still present.
575
	 *
576
	 * Use recursiveTagParseFully() to fully parse wikitext to output-safe HTML.
577
	 *
578
	 * Use this function if you're a parser tag hook and you want to parse
579
	 * wikitext before or after applying additional transformations, and you
580
	 * intend to *return the result as hook output*, which will cause it to go
581
	 * through the rest of parsing process automatically.
582
	 *
583
	 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
584
	 * $text are not expanded
585
	 *
586
	 * @param string $text Text extension wants to have parsed
587
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
588
	 * @return string UNSAFE half-parsed HTML
589
	 */
590
	public function recursiveTagParse( $text, $frame = false ) {
591
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
592
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
593
		$text = $this->internalParse( $text, false, $frame );
594
		return $text;
595
	}
596
597
	/**
598
	 * Fully parse wikitext to fully parsed HTML. This recursive parser entry
599
	 * point can be called from an extension tag hook.
600
	 *
601
	 * The output of this function is fully-parsed HTML that is safe for output.
602
	 * If you're a parser tag hook, you might want to use recursiveTagParse()
603
	 * instead.
604
	 *
605
	 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
606
	 * $text are not expanded
607
	 *
608
	 * @since 1.25
609
	 *
610
	 * @param string $text Text extension wants to have parsed
611
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
612
	 * @return string Fully parsed HTML
613
	 */
614
	public function recursiveTagParseFully( $text, $frame = false ) {
615
		$text = $this->recursiveTagParse( $text, $frame );
616
		$text = $this->internalParseHalfParsed( $text, false );
617
		return $text;
618
	}
619
620
	/**
621
	 * Expand templates and variables in the text, producing valid, static wikitext.
622
	 * Also removes comments.
623
	 * Do not call this function recursively.
624
	 * @param string $text
625
	 * @param Title $title
626
	 * @param ParserOptions $options
627
	 * @param int|null $revid
628
	 * @param bool|PPFrame $frame
629
	 * @return mixed|string
630
	 */
631
	public function preprocess( $text, Title $title = null,
632
		ParserOptions $options, $revid = null, $frame = false
633
	) {
634
		$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...
635
		$this->startParse( $title, $options, self::OT_PREPROCESS, true );
636
		if ( $revid !== null ) {
637
			$this->mRevisionId = $revid;
638
		}
639
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
640
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
641
		$text = $this->replaceVariables( $text, $frame );
642
		$text = $this->mStripState->unstripBoth( $text );
643
		return $text;
644
	}
645
646
	/**
647
	 * Recursive parser entry point that can be called from an extension tag
648
	 * hook.
649
	 *
650
	 * @param string $text Text to be expanded
651
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
652
	 * @return string
653
	 * @since 1.19
654
	 */
655
	public function recursivePreprocess( $text, $frame = false ) {
656
		$text = $this->replaceVariables( $text, $frame );
657
		$text = $this->mStripState->unstripBoth( $text );
658
		return $text;
659
	}
660
661
	/**
662
	 * Process the wikitext for the "?preload=" feature. (bug 5210)
663
	 *
664
	 * "<noinclude>", "<includeonly>" etc. are parsed as for template
665
	 * transclusion, comments, templates, arguments, tags hooks and parser
666
	 * functions are untouched.
667
	 *
668
	 * @param string $text
669
	 * @param Title $title
670
	 * @param ParserOptions $options
671
	 * @param array $params
672
	 * @return string
673
	 */
674
	public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
675
		$msg = new RawMessage( $text );
676
		$text = $msg->params( $params )->plain();
677
678
		# Parser (re)initialisation
679
		$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...
680
		$this->startParse( $title, $options, self::OT_PLAIN, true );
681
682
		$flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
683
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
684
		$text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
685
		$text = $this->mStripState->unstripBoth( $text );
686
		return $text;
687
	}
688
689
	/**
690
	 * Get a random string
691
	 *
692
	 * @return string
693
	 * @deprecated since 1.26; use wfRandomString() instead.
694
	 */
695
	public static function getRandomString() {
696
		wfDeprecated( __METHOD__, '1.26' );
697
		return wfRandomString( 16 );
698
	}
699
700
	/**
701
	 * Set the current user.
702
	 * Should only be used when doing pre-save transform.
703
	 *
704
	 * @param User|null $user User object or null (to reset)
705
	 */
706
	public function setUser( $user ) {
707
		$this->mUser = $user;
708
	}
709
710
	/**
711
	 * Accessor for mUniqPrefix.
712
	 *
713
	 * @return string
714
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
715
	 */
716
	public function uniqPrefix() {
717
		wfDeprecated( __METHOD__, '1.26' );
718
		return self::MARKER_PREFIX;
719
	}
720
721
	/**
722
	 * Set the context title
723
	 *
724
	 * @param Title $t
725
	 */
726
	public function setTitle( $t ) {
727
		if ( !$t ) {
728
			$t = Title::newFromText( 'NO TITLE' );
729
		}
730
731
		if ( $t->hasFragment() ) {
732
			# Strip the fragment to avoid various odd effects
733
			$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...
734
		} else {
735
			$this->mTitle = $t;
736
		}
737
	}
738
739
	/**
740
	 * Accessor for the Title object
741
	 *
742
	 * @return Title
743
	 */
744
	public function getTitle() {
745
		return $this->mTitle;
746
	}
747
748
	/**
749
	 * Accessor/mutator for the Title object
750
	 *
751
	 * @param Title $x Title object or null to just get the current one
752
	 * @return Title
753
	 */
754
	public function Title( $x = null ) {
755
		return wfSetVar( $this->mTitle, $x );
756
	}
757
758
	/**
759
	 * Set the output type
760
	 *
761
	 * @param int $ot New value
762
	 */
763
	public function setOutputType( $ot ) {
764
		$this->mOutputType = $ot;
765
		# Shortcut alias
766
		$this->ot = [
767
			'html' => $ot == self::OT_HTML,
768
			'wiki' => $ot == self::OT_WIKI,
769
			'pre' => $ot == self::OT_PREPROCESS,
770
			'plain' => $ot == self::OT_PLAIN,
771
		];
772
	}
773
774
	/**
775
	 * Accessor/mutator for the output type
776
	 *
777
	 * @param int|null $x New value or null to just get the current one
778
	 * @return int
779
	 */
780
	public function OutputType( $x = null ) {
781
		return wfSetVar( $this->mOutputType, $x );
782
	}
783
784
	/**
785
	 * Get the ParserOutput object
786
	 *
787
	 * @return ParserOutput
788
	 */
789
	public function getOutput() {
790
		return $this->mOutput;
791
	}
792
793
	/**
794
	 * Get the ParserOptions object
795
	 *
796
	 * @return ParserOptions
797
	 */
798
	public function getOptions() {
799
		return $this->mOptions;
800
	}
801
802
	/**
803
	 * Accessor/mutator for the ParserOptions object
804
	 *
805
	 * @param ParserOptions $x New value or null to just get the current one
806
	 * @return ParserOptions Current ParserOptions object
807
	 */
808
	public function Options( $x = null ) {
809
		return wfSetVar( $this->mOptions, $x );
810
	}
811
812
	/**
813
	 * @return int
814
	 */
815
	public function nextLinkID() {
816
		return $this->mLinkID++;
817
	}
818
819
	/**
820
	 * @param int $id
821
	 */
822
	public function setLinkID( $id ) {
823
		$this->mLinkID = $id;
824
	}
825
826
	/**
827
	 * Get a language object for use in parser functions such as {{FORMATNUM:}}
828
	 * @return Language
829
	 */
830
	public function getFunctionLang() {
831
		return $this->getTargetLanguage();
832
	}
833
834
	/**
835
	 * Get the target language for the content being parsed. This is usually the
836
	 * language that the content is in.
837
	 *
838
	 * @since 1.19
839
	 *
840
	 * @throws MWException
841
	 * @return Language
842
	 */
843
	public function getTargetLanguage() {
844
		$target = $this->mOptions->getTargetLanguage();
845
846
		if ( $target !== null ) {
847
			return $target;
848
		} elseif ( $this->mOptions->getInterfaceMessage() ) {
849
			return $this->mOptions->getUserLangObj();
850
		} elseif ( is_null( $this->mTitle ) ) {
851
			throw new MWException( __METHOD__ . ': $this->mTitle is null' );
852
		}
853
854
		return $this->mTitle->getPageLanguage();
855
	}
856
857
	/**
858
	 * Get the language object for language conversion
859
	 * @return Language|null
860
	 */
861
	public function getConverterLanguage() {
862
		return $this->getTargetLanguage();
863
	}
864
865
	/**
866
	 * Get a User object either from $this->mUser, if set, or from the
867
	 * ParserOptions object otherwise
868
	 *
869
	 * @return User
870
	 */
871
	public function getUser() {
872
		if ( !is_null( $this->mUser ) ) {
873
			return $this->mUser;
874
		}
875
		return $this->mOptions->getUser();
876
	}
877
878
	/**
879
	 * Get a preprocessor object
880
	 *
881
	 * @return Preprocessor
882
	 */
883
	public function getPreprocessor() {
884
		if ( !isset( $this->mPreprocessor ) ) {
885
			$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...
886
			$this->mPreprocessor = new $class( $this );
887
		}
888
		return $this->mPreprocessor;
889
	}
890
891
	/**
892
	 * Replaces all occurrences of HTML-style comments and the given tags
893
	 * in the text with a random marker and returns the next text. The output
894
	 * parameter $matches will be an associative array filled with data in
895
	 * the form:
896
	 *
897
	 * @code
898
	 *   'UNIQ-xxxxx' => array(
899
	 *     'element',
900
	 *     'tag content',
901
	 *     array( 'param' => 'x' ),
902
	 *     '<element param="x">tag content</element>' ) )
903
	 * @endcode
904
	 *
905
	 * @param array $elements List of element names. Comments are always extracted.
906
	 * @param string $text Source text string.
907
	 * @param array $matches Out parameter, Array: extracted tags
908
	 * @param string|null $uniq_prefix
909
	 * @return string Stripped text
910
	 * @since 1.26 The uniq_prefix argument is deprecated.
911
	 */
912
	public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
913
		if ( $uniq_prefix !== null ) {
914
			wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
915
		}
916
		static $n = 1;
917
		$stripped = '';
918
		$matches = [];
919
920
		$taglist = implode( '|', $elements );
921
		$start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
922
923
		while ( $text != '' ) {
924
			$p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
925
			$stripped .= $p[0];
926
			if ( count( $p ) < 5 ) {
927
				break;
928
			}
929
			if ( count( $p ) > 5 ) {
930
				# comment
931
				$element = $p[4];
932
				$attributes = '';
933
				$close = '';
934
				$inside = $p[5];
935
			} else {
936
				# tag
937
				$element = $p[1];
938
				$attributes = $p[2];
939
				$close = $p[3];
940
				$inside = $p[4];
941
			}
942
943
			$marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
944
			$stripped .= $marker;
945
946
			if ( $close === '/>' ) {
947
				# Empty element tag, <tag />
948
				$content = null;
949
				$text = $inside;
950
				$tail = null;
951
			} else {
952
				if ( $element === '!--' ) {
953
					$end = '/(-->)/';
954
				} else {
955
					$end = "/(<\\/$element\\s*>)/i";
956
				}
957
				$q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
958
				$content = $q[0];
959
				if ( count( $q ) < 3 ) {
960
					# No end tag -- let it run out to the end of the text.
961
					$tail = '';
962
					$text = '';
963
				} else {
964
					$tail = $q[1];
965
					$text = $q[2];
966
				}
967
			}
968
969
			$matches[$marker] = [ $element,
970
				$content,
971
				Sanitizer::decodeTagAttributes( $attributes ),
972
				"<$element$attributes$close$content$tail" ];
973
		}
974
		return $stripped;
975
	}
976
977
	/**
978
	 * Get a list of strippable XML-like elements
979
	 *
980
	 * @return array
981
	 */
982
	public function getStripList() {
983
		return $this->mStripList;
984
	}
985
986
	/**
987
	 * Add an item to the strip state
988
	 * Returns the unique tag which must be inserted into the stripped text
989
	 * The tag will be replaced with the original text in unstrip()
990
	 *
991
	 * @param string $text
992
	 *
993
	 * @return string
994
	 */
995
	public function insertStripItem( $text ) {
996
		$marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
997
		$this->mMarkerIndex++;
998
		$this->mStripState->addGeneral( $marker, $text );
999
		return $marker;
1000
	}
1001
1002
	/**
1003
	 * parse the wiki syntax used to render tables
1004
	 *
1005
	 * @private
1006
	 * @param string $text
1007
	 * @return string
1008
	 */
1009
	public function doTableStuff( $text ) {
1010
1011
		$lines = StringUtils::explode( "\n", $text );
1012
		$out = '';
1013
		$td_history = []; # Is currently a td tag open?
1014
		$last_tag_history = []; # Save history of last lag activated (td, th or caption)
1015
		$tr_history = []; # Is currently a tr tag open?
1016
		$tr_attributes = []; # history of tr attributes
1017
		$has_opened_tr = []; # Did this table open a <tr> element?
1018
		$indent_level = 0; # indent level of the table
1019
1020
		foreach ( $lines as $outLine ) {
1021
			$line = trim( $outLine );
1022
1023
			if ( $line === '' ) { # empty line, go to next line
1024
				$out .= $outLine . "\n";
1025
				continue;
1026
			}
1027
1028
			$first_character = $line[0];
1029
			$first_two = substr( $line, 0, 2 );
1030
			$matches = [];
1031
1032
			if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1033
				# First check if we are starting a new table
1034
				$indent_level = strlen( $matches[1] );
1035
1036
				$attributes = $this->mStripState->unstripBoth( $matches[2] );
1037
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1038
1039
				$outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1040
				array_push( $td_history, false );
1041
				array_push( $last_tag_history, '' );
1042
				array_push( $tr_history, false );
1043
				array_push( $tr_attributes, '' );
1044
				array_push( $has_opened_tr, false );
1045
			} elseif ( count( $td_history ) == 0 ) {
1046
				# Don't do any of the following
1047
				$out .= $outLine . "\n";
1048
				continue;
1049
			} elseif ( $first_two === '|}' ) {
1050
				# We are ending a table
1051
				$line = '</table>' . substr( $line, 2 );
1052
				$last_tag = array_pop( $last_tag_history );
1053
1054
				if ( !array_pop( $has_opened_tr ) ) {
1055
					$line = "<tr><td></td></tr>{$line}";
1056
				}
1057
1058
				if ( array_pop( $tr_history ) ) {
1059
					$line = "</tr>{$line}";
1060
				}
1061
1062
				if ( array_pop( $td_history ) ) {
1063
					$line = "</{$last_tag}>{$line}";
1064
				}
1065
				array_pop( $tr_attributes );
1066
				$outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1067
			} elseif ( $first_two === '|-' ) {
1068
				# Now we have a table row
1069
				$line = preg_replace( '#^\|-+#', '', $line );
1070
1071
				# Whats after the tag is now only attributes
1072
				$attributes = $this->mStripState->unstripBoth( $line );
1073
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1074
				array_pop( $tr_attributes );
1075
				array_push( $tr_attributes, $attributes );
1076
1077
				$line = '';
1078
				$last_tag = array_pop( $last_tag_history );
1079
				array_pop( $has_opened_tr );
1080
				array_push( $has_opened_tr, true );
1081
1082
				if ( array_pop( $tr_history ) ) {
1083
					$line = '</tr>';
1084
				}
1085
1086
				if ( array_pop( $td_history ) ) {
1087
					$line = "</{$last_tag}>{$line}";
1088
				}
1089
1090
				$outLine = $line;
1091
				array_push( $tr_history, false );
1092
				array_push( $td_history, false );
1093
				array_push( $last_tag_history, '' );
1094
			} elseif ( $first_character === '|'
1095
				|| $first_character === '!'
1096
				|| $first_two === '|+'
1097
			) {
1098
				# This might be cell elements, td, th or captions
1099
				if ( $first_two === '|+' ) {
1100
					$first_character = '+';
1101
					$line = substr( $line, 2 );
1102
				} else {
1103
					$line = substr( $line, 1 );
1104
				}
1105
1106
				// Implies both are valid for table headings.
1107
				if ( $first_character === '!' ) {
1108
					$line = StringUtils::replaceMarkup( '!!', '||', $line );
1109
				}
1110
1111
				# Split up multiple cells on the same line.
1112
				# FIXME : This can result in improper nesting of tags processed
1113
				# by earlier parser steps.
1114
				$cells = explode( '||', $line );
1115
1116
				$outLine = '';
1117
1118
				# Loop through each table cell
1119
				foreach ( $cells as $cell ) {
1120
					$previous = '';
1121
					if ( $first_character !== '+' ) {
1122
						$tr_after = array_pop( $tr_attributes );
1123
						if ( !array_pop( $tr_history ) ) {
1124
							$previous = "<tr{$tr_after}>\n";
1125
						}
1126
						array_push( $tr_history, true );
1127
						array_push( $tr_attributes, '' );
1128
						array_pop( $has_opened_tr );
1129
						array_push( $has_opened_tr, true );
1130
					}
1131
1132
					$last_tag = array_pop( $last_tag_history );
1133
1134
					if ( array_pop( $td_history ) ) {
1135
						$previous = "</{$last_tag}>\n{$previous}";
1136
					}
1137
1138
					if ( $first_character === '|' ) {
1139
						$last_tag = 'td';
1140
					} elseif ( $first_character === '!' ) {
1141
						$last_tag = 'th';
1142
					} elseif ( $first_character === '+' ) {
1143
						$last_tag = 'caption';
1144
					} else {
1145
						$last_tag = '';
1146
					}
1147
1148
					array_push( $last_tag_history, $last_tag );
1149
1150
					# A cell could contain both parameters and data
1151
					$cell_data = explode( '|', $cell, 2 );
1152
1153
					# Bug 553: Note that a '|' inside an invalid link should not
1154
					# be mistaken as delimiting cell parameters
1155
					if ( strpos( $cell_data[0], '[[' ) !== false ) {
1156
						$cell = "{$previous}<{$last_tag}>{$cell}";
1157
					} elseif ( count( $cell_data ) == 1 ) {
1158
						$cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1159
					} else {
1160
						$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1161
						$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1162
						$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1163
					}
1164
1165
					$outLine .= $cell;
1166
					array_push( $td_history, true );
1167
				}
1168
			}
1169
			$out .= $outLine . "\n";
1170
		}
1171
1172
		# Closing open td, tr && table
1173
		while ( count( $td_history ) > 0 ) {
1174
			if ( array_pop( $td_history ) ) {
1175
				$out .= "</td>\n";
1176
			}
1177
			if ( array_pop( $tr_history ) ) {
1178
				$out .= "</tr>\n";
1179
			}
1180
			if ( !array_pop( $has_opened_tr ) ) {
1181
				$out .= "<tr><td></td></tr>\n";
1182
			}
1183
1184
			$out .= "</table>\n";
1185
		}
1186
1187
		# Remove trailing line-ending (b/c)
1188 View Code Duplication
		if ( substr( $out, -1 ) === "\n" ) {
1189
			$out = substr( $out, 0, -1 );
1190
		}
1191
1192
		# special case: don't return empty table
1193
		if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1194
			$out = '';
1195
		}
1196
1197
		return $out;
1198
	}
1199
1200
	/**
1201
	 * Helper function for parse() that transforms wiki markup into half-parsed
1202
	 * HTML. Only called for $mOutputType == self::OT_HTML.
1203
	 *
1204
	 * @private
1205
	 *
1206
	 * @param string $text The text to parse
1207
	 * @param bool $isMain Whether this is being called from the main parse() function
1208
	 * @param PPFrame|bool $frame A pre-processor frame
1209
	 *
1210
	 * @return string
1211
	 */
1212
	public function internalParse( $text, $isMain = true, $frame = false ) {
1213
1214
		$origText = $text;
1215
1216
		# Hook to suspend the parser in this state
1217
		if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) {
1218
			return $text;
1219
		}
1220
1221
		# if $frame is provided, then use $frame for replacing any variables
1222
		if ( $frame ) {
1223
			# use frame depth to infer how include/noinclude tags should be handled
1224
			# depth=0 means this is the top-level document; otherwise it's an included document
1225
			if ( !$frame->depth ) {
1226
				$flag = 0;
1227
			} else {
1228
				$flag = Parser::PTD_FOR_INCLUSION;
1229
			}
1230
			$dom = $this->preprocessToDom( $text, $flag );
1231
			$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...
1232
		} else {
1233
			# if $frame is not provided, then use old-style replaceVariables
1234
			$text = $this->replaceVariables( $text );
1235
		}
1236
1237
		Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] );
1238
		$text = Sanitizer::removeHTMLtags(
1239
			$text,
1240
			[ &$this, 'attributeStripCallback' ],
1241
			false,
1242
			array_keys( $this->mTransparentTagHooks )
1243
		);
1244
		Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
1245
1246
		# Tables need to come after variable replacement for things to work
1247
		# properly; putting them before other transformations should keep
1248
		# exciting things like link expansions from showing up in surprising
1249
		# places.
1250
		$text = $this->doTableStuff( $text );
1251
1252
		$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1253
1254
		$text = $this->doDoubleUnderscore( $text );
1255
1256
		$text = $this->doHeadings( $text );
1257
		$text = $this->replaceInternalLinks( $text );
1258
		$text = $this->doAllQuotes( $text );
1259
		$text = $this->replaceExternalLinks( $text );
1260
1261
		# replaceInternalLinks may sometimes leave behind
1262
		# absolute URLs, which have to be masked to hide them from replaceExternalLinks
1263
		$text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1264
1265
		$text = $this->doMagicLinks( $text );
1266
		$text = $this->formatHeadings( $text, $origText, $isMain );
1267
1268
		return $text;
1269
	}
1270
1271
	/**
1272
	 * Helper function for parse() that transforms half-parsed HTML into fully
1273
	 * parsed HTML.
1274
	 *
1275
	 * @param string $text
1276
	 * @param bool $isMain
1277
	 * @param bool $linestart
1278
	 * @return string
1279
	 */
1280
	private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1281
		$text = $this->mStripState->unstripGeneral( $text );
1282
1283
		if ( $isMain ) {
1284
			Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
1285
		}
1286
1287
		# Clean up special characters, only run once, next-to-last before doBlockLevels
1288
		$fixtags = [
1289
			# french spaces, last one Guillemet-left
1290
			# only if there is something before the space
1291
			'/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1292
			# french spaces, Guillemet-right
1293
			'/(\\302\\253) /' => '\\1&#160;',
1294
			'/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1295
		];
1296
		$text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1297
1298
		$text = $this->doBlockLevels( $text, $linestart );
1299
1300
		$this->replaceLinkHolders( $text );
1301
1302
		/**
1303
		 * The input doesn't get language converted if
1304
		 * a) It's disabled
1305
		 * b) Content isn't converted
1306
		 * c) It's a conversion table
1307
		 * d) it is an interface message (which is in the user language)
1308
		 */
1309
		if ( !( $this->mOptions->getDisableContentConversion()
1310
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1311
		) {
1312
			if ( !$this->mOptions->getInterfaceMessage() ) {
1313
				# The position of the convert() call should not be changed. it
1314
				# assumes that the links are all replaced and the only thing left
1315
				# is the <nowiki> mark.
1316
				$text = $this->getConverterLanguage()->convert( $text );
1317
			}
1318
		}
1319
1320
		$text = $this->mStripState->unstripNoWiki( $text );
1321
1322
		if ( $isMain ) {
1323
			Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
1324
		}
1325
1326
		$text = $this->replaceTransparentTags( $text );
1327
		$text = $this->mStripState->unstripGeneral( $text );
1328
1329
		$text = Sanitizer::normalizeCharReferences( $text );
1330
1331
		if ( MWTidy::isEnabled() && $this->mOptions->getTidy() ) {
1332
			$text = MWTidy::tidy( $text );
1333
			$this->mOutput->addModuleStyles( MWTidy::getModuleStyles() );
1334
		} else {
1335
			# attempt to sanitize at least some nesting problems
1336
			# (bug #2702 and quite a few others)
1337
			$tidyregs = [
1338
				# ''Something [http://www.cool.com cool''] -->
1339
				# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1340
				'/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1341
				'\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1342
				# fix up an anchor inside another anchor, only
1343
				# at least for a single single nested link (bug 3695)
1344
				'/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1345
				'\\1\\2</a>\\3</a>\\1\\4</a>',
1346
				# fix div inside inline elements- doBlockLevels won't wrap a line which
1347
				# contains a div, so fix it up here; replace
1348
				# div with escaped text
1349
				'/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1350
				'\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1351
				# remove empty italic or bold tag pairs, some
1352
				# introduced by rules above
1353
				'/<([bi])><\/\\1>/' => '',
1354
			];
1355
1356
			$text = preg_replace(
1357
				array_keys( $tidyregs ),
1358
				array_values( $tidyregs ),
1359
				$text );
1360
		}
1361
1362
		if ( $isMain ) {
1363
			Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
1364
		}
1365
1366
		return $text;
1367
	}
1368
1369
	/**
1370
	 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1371
	 * magic external links.
1372
	 *
1373
	 * DML
1374
	 * @private
1375
	 *
1376
	 * @param string $text
1377
	 *
1378
	 * @return string
1379
	 */
1380
	public function doMagicLinks( $text ) {
1381
		$prots = wfUrlProtocolsWithoutProtRel();
1382
		$urlChar = self::EXT_LINK_URL_CLASS;
1383
		$addr = self::EXT_LINK_ADDR;
1384
		$space = self::SPACE_NOT_NL; #  non-newline space
1385
		$spdash = "(?:-|$space)"; # a dash or a non-newline space
1386
		$spaces = "$space++"; # possessive match of 1 or more spaces
1387
		$text = preg_replace_callback(
1388
			'!(?:                            # Start cases
1389
				(<a[ \t\r\n>].*?</a>) |      # m[1]: Skip link text
1390
				(<.*?>) |                    # m[2]: Skip stuff inside
1391
				                             #       HTML elements' . "
1392
				(\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1393
				                             # m[4]: Post-protocol path
1394
				\b(?:RFC|PMID) $spaces       # m[5]: RFC or PMID, capture number
1395
					([0-9]+)\b |
1396
				\bISBN $spaces (             # m[6]: ISBN, capture number
1397
					(?: 97[89] $spdash? )?   #  optional 13-digit ISBN prefix
1398
					(?: [0-9]  $spdash? ){9} #  9 digits with opt. delimiters
1399
					[0-9Xx]                  #  check digit
1400
				)\b
1401
			)!xu", [ &$this, 'magicLinkCallback' ], $text );
1402
		return $text;
1403
	}
1404
1405
	/**
1406
	 * @throws MWException
1407
	 * @param array $m
1408
	 * @return HTML|string
1409
	 */
1410
	public function magicLinkCallback( $m ) {
1411
		if ( isset( $m[1] ) && $m[1] !== '' ) {
1412
			# Skip anchor
1413
			return $m[0];
1414
		} elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1415
			# Skip HTML element
1416
			return $m[0];
1417
		} elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1418
			# Free external link
1419
			return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1420
		} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1421
			# RFC or PMID
1422
			if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1423
				$keyword = 'RFC';
1424
				$urlmsg = 'rfcurl';
1425
				$cssClass = 'mw-magiclink-rfc';
1426
				$id = $m[5];
1427
			} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1428
				$keyword = 'PMID';
1429
				$urlmsg = 'pubmedurl';
1430
				$cssClass = 'mw-magiclink-pmid';
1431
				$id = $m[5];
1432
			} else {
1433
				throw new MWException( __METHOD__ . ': unrecognised match type "' .
1434
					substr( $m[0], 0, 20 ) . '"' );
1435
			}
1436
			$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1437
			return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass );
1438
		} elseif ( isset( $m[6] ) && $m[6] !== '' ) {
1439
			# ISBN
1440
			$isbn = $m[6];
1441
			$space = self::SPACE_NOT_NL; #  non-newline space
1442
			$isbn = preg_replace( "/$space/", ' ', $isbn );
1443
			$num = strtr( $isbn, [
1444
				'-' => '',
1445
				' ' => '',
1446
				'x' => 'X',
1447
			] );
1448
			$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
1449
			return '<a href="' .
1450
				htmlspecialchars( $titleObj->getLocalURL() ) .
1451
				"\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1452
		} else {
1453
			return $m[0];
1454
		}
1455
	}
1456
1457
	/**
1458
	 * Make a free external link, given a user-supplied URL
1459
	 *
1460
	 * @param string $url
1461
	 * @param int $numPostProto
1462
	 *   The number of characters after the protocol.
1463
	 * @return string HTML
1464
	 * @private
1465
	 */
1466
	public function makeFreeExternalLink( $url, $numPostProto ) {
1467
		$trail = '';
1468
1469
		# The characters '<' and '>' (which were escaped by
1470
		# removeHTMLtags()) should not be included in
1471
		# URLs, per RFC 2396.
1472
		# Make &nbsp; terminate a URL as well (bug T84937)
1473
		$m2 = [];
1474 View Code Duplication
		if ( preg_match(
1475
			'/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1476
			$url,
1477
			$m2,
1478
			PREG_OFFSET_CAPTURE
1479
		) ) {
1480
			$trail = substr( $url, $m2[0][1] ) . $trail;
1481
			$url = substr( $url, 0, $m2[0][1] );
1482
		}
1483
1484
		# Move trailing punctuation to $trail
1485
		$sep = ',;\.:!?';
1486
		# If there is no left bracket, then consider right brackets fair game too
1487
		if ( strpos( $url, '(' ) === false ) {
1488
			$sep .= ')';
1489
		}
1490
1491
		$urlRev = strrev( $url );
1492
		$numSepChars = strspn( $urlRev, $sep );
1493
		# Don't break a trailing HTML entity by moving the ; into $trail
1494
		# This is in hot code, so use substr_compare to avoid having to
1495
		# create a new string object for the comparison
1496
		if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1497
			# more optimization: instead of running preg_match with a $
1498
			# anchor, which can be slow, do the match on the reversed
1499
			# string starting at the desired offset.
1500
			# un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1501
			if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1502
				$numSepChars--;
1503
			}
1504
		}
1505
		if ( $numSepChars ) {
1506
			$trail = substr( $url, -$numSepChars ) . $trail;
1507
			$url = substr( $url, 0, -$numSepChars );
1508
		}
1509
1510
		# Verify that we still have a real URL after trail removal, and
1511
		# not just lone protocol
1512
		if ( strlen( $trail ) >= $numPostProto ) {
1513
			return $url . $trail;
1514
		}
1515
1516
		$url = Sanitizer::cleanUrl( $url );
1517
1518
		# Is this an external image?
1519
		$text = $this->maybeMakeExternalImage( $url );
1520
		if ( $text === false ) {
1521
			# Not an image, make a link
1522
			$text = Linker::makeExternalLink( $url,
1523
				$this->getConverterLanguage()->markNoConversion( $url, true ),
1524
				true, 'free',
1525
				$this->getExternalLinkAttribs( $url ) );
1526
			# Register it in the output object...
1527
			# Replace unnecessary URL escape codes with their equivalent characters
1528
			$pasteurized = self::normalizeLinkUrl( $url );
1529
			$this->mOutput->addExternalLink( $pasteurized );
1530
		}
1531
		return $text . $trail;
1532
	}
1533
1534
	/**
1535
	 * Parse headers and return html
1536
	 *
1537
	 * @private
1538
	 *
1539
	 * @param string $text
1540
	 *
1541
	 * @return string
1542
	 */
1543
	public function doHeadings( $text ) {
1544
		for ( $i = 6; $i >= 1; --$i ) {
1545
			$h = str_repeat( '=', $i );
1546
			$text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1547
		}
1548
		return $text;
1549
	}
1550
1551
	/**
1552
	 * Replace single quotes with HTML markup
1553
	 * @private
1554
	 *
1555
	 * @param string $text
1556
	 *
1557
	 * @return string The altered text
1558
	 */
1559
	public function doAllQuotes( $text ) {
1560
		$outtext = '';
1561
		$lines = StringUtils::explode( "\n", $text );
1562
		foreach ( $lines as $line ) {
1563
			$outtext .= $this->doQuotes( $line ) . "\n";
1564
		}
1565
		$outtext = substr( $outtext, 0, -1 );
1566
		return $outtext;
1567
	}
1568
1569
	/**
1570
	 * Helper function for doAllQuotes()
1571
	 *
1572
	 * @param string $text
1573
	 *
1574
	 * @return string
1575
	 */
1576
	public function doQuotes( $text ) {
1577
		$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1578
		$countarr = count( $arr );
1579
		if ( $countarr == 1 ) {
1580
			return $text;
1581
		}
1582
1583
		// First, do some preliminary work. This may shift some apostrophes from
1584
		// being mark-up to being text. It also counts the number of occurrences
1585
		// of bold and italics mark-ups.
1586
		$numbold = 0;
1587
		$numitalics = 0;
1588
		for ( $i = 1; $i < $countarr; $i += 2 ) {
1589
			$thislen = strlen( $arr[$i] );
1590
			// If there are ever four apostrophes, assume the first is supposed to
1591
			// be text, and the remaining three constitute mark-up for bold text.
1592
			// (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1593
			if ( $thislen == 4 ) {
1594
				$arr[$i - 1] .= "'";
1595
				$arr[$i] = "'''";
1596
				$thislen = 3;
1597
			} elseif ( $thislen > 5 ) {
1598
				// If there are more than 5 apostrophes in a row, assume they're all
1599
				// text except for the last 5.
1600
				// (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1601
				$arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1602
				$arr[$i] = "'''''";
1603
				$thislen = 5;
1604
			}
1605
			// Count the number of occurrences of bold and italics mark-ups.
1606
			if ( $thislen == 2 ) {
1607
				$numitalics++;
1608
			} elseif ( $thislen == 3 ) {
1609
				$numbold++;
1610
			} elseif ( $thislen == 5 ) {
1611
				$numitalics++;
1612
				$numbold++;
1613
			}
1614
		}
1615
1616
		// If there is an odd number of both bold and italics, it is likely
1617
		// that one of the bold ones was meant to be an apostrophe followed
1618
		// by italics. Which one we cannot know for certain, but it is more
1619
		// likely to be one that has a single-letter word before it.
1620
		if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1621
			$firstsingleletterword = -1;
1622
			$firstmultiletterword = -1;
1623
			$firstspace = -1;
1624
			for ( $i = 1; $i < $countarr; $i += 2 ) {
1625
				if ( strlen( $arr[$i] ) == 3 ) {
1626
					$x1 = substr( $arr[$i - 1], -1 );
1627
					$x2 = substr( $arr[$i - 1], -2, 1 );
1628
					if ( $x1 === ' ' ) {
1629
						if ( $firstspace == -1 ) {
1630
							$firstspace = $i;
1631
						}
1632
					} elseif ( $x2 === ' ' ) {
1633
						$firstsingleletterword = $i;
1634
						// if $firstsingleletterword is set, we don't
1635
						// look at the other options, so we can bail early.
1636
						break;
1637
					} else {
1638
						if ( $firstmultiletterword == -1 ) {
1639
							$firstmultiletterword = $i;
1640
						}
1641
					}
1642
				}
1643
			}
1644
1645
			// If there is a single-letter word, use it!
1646
			if ( $firstsingleletterword > -1 ) {
1647
				$arr[$firstsingleletterword] = "''";
1648
				$arr[$firstsingleletterword - 1] .= "'";
1649
			} elseif ( $firstmultiletterword > -1 ) {
1650
				// If not, but there's a multi-letter word, use that one.
1651
				$arr[$firstmultiletterword] = "''";
1652
				$arr[$firstmultiletterword - 1] .= "'";
1653
			} elseif ( $firstspace > -1 ) {
1654
				// ... otherwise use the first one that has neither.
1655
				// (notice that it is possible for all three to be -1 if, for example,
1656
				// there is only one pentuple-apostrophe in the line)
1657
				$arr[$firstspace] = "''";
1658
				$arr[$firstspace - 1] .= "'";
1659
			}
1660
		}
1661
1662
		// Now let's actually convert our apostrophic mush to HTML!
1663
		$output = '';
1664
		$buffer = '';
1665
		$state = '';
1666
		$i = 0;
1667
		foreach ( $arr as $r ) {
1668
			if ( ( $i % 2 ) == 0 ) {
1669
				if ( $state === 'both' ) {
1670
					$buffer .= $r;
1671
				} else {
1672
					$output .= $r;
1673
				}
1674
			} else {
1675
				$thislen = strlen( $r );
1676
				if ( $thislen == 2 ) {
1677 View Code Duplication
					if ( $state === 'i' ) {
1678
						$output .= '</i>';
1679
						$state = '';
1680
					} elseif ( $state === 'bi' ) {
1681
						$output .= '</i>';
1682
						$state = 'b';
1683
					} elseif ( $state === 'ib' ) {
1684
						$output .= '</b></i><b>';
1685
						$state = 'b';
1686
					} elseif ( $state === 'both' ) {
1687
						$output .= '<b><i>' . $buffer . '</i>';
1688
						$state = 'b';
1689
					} else { // $state can be 'b' or ''
1690
						$output .= '<i>';
1691
						$state .= 'i';
1692
					}
1693 View Code Duplication
				} elseif ( $thislen == 3 ) {
1694
					if ( $state === 'b' ) {
1695
						$output .= '</b>';
1696
						$state = '';
1697
					} elseif ( $state === 'bi' ) {
1698
						$output .= '</i></b><i>';
1699
						$state = 'i';
1700
					} elseif ( $state === 'ib' ) {
1701
						$output .= '</b>';
1702
						$state = 'i';
1703
					} elseif ( $state === 'both' ) {
1704
						$output .= '<i><b>' . $buffer . '</b>';
1705
						$state = 'i';
1706
					} else { // $state can be 'i' or ''
1707
						$output .= '<b>';
1708
						$state .= 'b';
1709
					}
1710
				} elseif ( $thislen == 5 ) {
1711
					if ( $state === 'b' ) {
1712
						$output .= '</b><i>';
1713
						$state = 'i';
1714
					} elseif ( $state === 'i' ) {
1715
						$output .= '</i><b>';
1716
						$state = 'b';
1717
					} elseif ( $state === 'bi' ) {
1718
						$output .= '</i></b>';
1719
						$state = '';
1720
					} elseif ( $state === 'ib' ) {
1721
						$output .= '</b></i>';
1722
						$state = '';
1723
					} elseif ( $state === 'both' ) {
1724
						$output .= '<i><b>' . $buffer . '</b></i>';
1725
						$state = '';
1726
					} else { // ($state == '')
1727
						$buffer = '';
1728
						$state = 'both';
1729
					}
1730
				}
1731
			}
1732
			$i++;
1733
		}
1734
		// Now close all remaining tags.  Notice that the order is important.
1735
		if ( $state === 'b' || $state === 'ib' ) {
1736
			$output .= '</b>';
1737
		}
1738
		if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1739
			$output .= '</i>';
1740
		}
1741
		if ( $state === 'bi' ) {
1742
			$output .= '</b>';
1743
		}
1744
		// There might be lonely ''''', so make sure we have a buffer
1745
		if ( $state === 'both' && $buffer ) {
1746
			$output .= '<b><i>' . $buffer . '</i></b>';
1747
		}
1748
		return $output;
1749
	}
1750
1751
	/**
1752
	 * Replace external links (REL)
1753
	 *
1754
	 * Note: this is all very hackish and the order of execution matters a lot.
1755
	 * Make sure to run tests/parserTests.php if you change this code.
1756
	 *
1757
	 * @private
1758
	 *
1759
	 * @param string $text
1760
	 *
1761
	 * @throws MWException
1762
	 * @return string
1763
	 */
1764
	public function replaceExternalLinks( $text ) {
1765
1766
		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1767
		if ( $bits === false ) {
1768
			throw new MWException( "PCRE needs to be compiled with "
1769
				. "--enable-unicode-properties in order for MediaWiki to function" );
1770
		}
1771
		$s = array_shift( $bits );
1772
1773
		$i = 0;
1774
		while ( $i < count( $bits ) ) {
1775
			$url = $bits[$i++];
1776
			$i++; // protocol
1777
			$text = $bits[$i++];
1778
			$trail = $bits[$i++];
1779
1780
			# The characters '<' and '>' (which were escaped by
1781
			# removeHTMLtags()) should not be included in
1782
			# URLs, per RFC 2396.
1783
			$m2 = [];
1784 View Code Duplication
			if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1785
				$text = substr( $url, $m2[0][1] ) . ' ' . $text;
1786
				$url = substr( $url, 0, $m2[0][1] );
1787
			}
1788
1789
			# If the link text is an image URL, replace it with an <img> tag
1790
			# This happened by accident in the original parser, but some people used it extensively
1791
			$img = $this->maybeMakeExternalImage( $text );
1792
			if ( $img !== false ) {
1793
				$text = $img;
1794
			}
1795
1796
			$dtrail = '';
1797
1798
			# Set linktype for CSS - if URL==text, link is essentially free
1799
			$linktype = ( $text === $url ) ? 'free' : 'text';
1800
1801
			# No link text, e.g. [http://domain.tld/some.link]
1802
			if ( $text == '' ) {
1803
				# Autonumber
1804
				$langObj = $this->getTargetLanguage();
1805
				$text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1806
				$linktype = 'autonumber';
1807
			} else {
1808
				# Have link text, e.g. [http://domain.tld/some.link text]s
1809
				# Check for trail
1810
				list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1811
			}
1812
1813
			$text = $this->getConverterLanguage()->markNoConversion( $text );
1814
1815
			$url = Sanitizer::cleanUrl( $url );
1816
1817
			# Use the encoded URL
1818
			# This means that users can paste URLs directly into the text
1819
			# Funny characters like ö aren't valid in URLs anyway
1820
			# This was changed in August 2004
1821
			$s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1822
				$this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1823
1824
			# Register link in the output object.
1825
			# Replace unnecessary URL escape codes with the referenced character
1826
			# This prevents spammers from hiding links from the filters
1827
			$pasteurized = self::normalizeLinkUrl( $url );
1828
			$this->mOutput->addExternalLink( $pasteurized );
1829
		}
1830
1831
		return $s;
1832
	}
1833
1834
	/**
1835
	 * Get the rel attribute for a particular external link.
1836
	 *
1837
	 * @since 1.21
1838
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1839
	 *   nofollow if appropriate
1840
	 * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups
1841
	 * @return string|null Rel attribute for $url
1842
	 */
1843
	public static function getExternalLinkRel( $url = false, $title = null ) {
1844
		global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1845
		$ns = $title ? $title->getNamespace() : false;
1846
		if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1847
			&& !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
0 ignored issues
show
Bug introduced by
It seems like $url defined by parameter $url on line 1843 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...
1848
		) {
1849
			return 'nofollow';
1850
		}
1851
		return null;
1852
	}
1853
1854
	/**
1855
	 * Get an associative array of additional HTML attributes appropriate for a
1856
	 * particular external link.  This currently may include rel => nofollow
1857
	 * (depending on configuration, namespace, and the URL's domain) and/or a
1858
	 * target attribute (depending on configuration).
1859
	 *
1860
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1861
	 *   nofollow if appropriate
1862
	 * @return array Associative array of HTML attributes
1863
	 */
1864
	public function getExternalLinkAttribs( $url = false ) {
1865
		$attribs = [];
1866
		$rel = self::getExternalLinkRel( $url, $this->mTitle );
1867
1868
		$target = $this->mOptions->getExternalLinkTarget();
1869
		if ( $target ) {
1870
			$attribs['target'] = $target;
1871
			if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1872
				// T133507. New windows can navigate parent cross-origin.
1873
				// Including noreferrer due to lacking browser
1874
				// support of noopener. Eventually noreferrer should be removed.
1875
				if ( $rel !== '' ) {
1876
					$rel .= ' ';
1877
				}
1878
				$rel .= 'noreferrer noopener';
1879
			}
1880
		}
1881
		$attribs['rel'] = $rel;
1882
		return $attribs;
1883
	}
1884
1885
	/**
1886
	 * Replace unusual escape codes in a URL with their equivalent characters
1887
	 *
1888
	 * @deprecated since 1.24, use normalizeLinkUrl
1889
	 * @param string $url
1890
	 * @return string
1891
	 */
1892
	public static function replaceUnusualEscapes( $url ) {
1893
		wfDeprecated( __METHOD__, '1.24' );
1894
		return self::normalizeLinkUrl( $url );
1895
	}
1896
1897
	/**
1898
	 * Replace unusual escape codes in a URL with their equivalent characters
1899
	 *
1900
	 * This generally follows the syntax defined in RFC 3986, with special
1901
	 * consideration for HTTP query strings.
1902
	 *
1903
	 * @param string $url
1904
	 * @return string
1905
	 */
1906
	public static function normalizeLinkUrl( $url ) {
1907
		# First, make sure unsafe characters are encoded
1908
		$url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1909
			function ( $m ) {
1910
				return rawurlencode( $m[0] );
1911
			},
1912
			$url
1913
		);
1914
1915
		$ret = '';
1916
		$end = strlen( $url );
1917
1918
		# Fragment part - 'fragment'
1919
		$start = strpos( $url, '#' );
1920 View Code Duplication
		if ( $start !== false && $start < $end ) {
1921
			$ret = self::normalizeUrlComponent(
1922
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1923
			$end = $start;
1924
		}
1925
1926
		# Query part - 'query' minus &=+;
1927
		$start = strpos( $url, '?' );
1928 View Code Duplication
		if ( $start !== false && $start < $end ) {
1929
			$ret = self::normalizeUrlComponent(
1930
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1931
			$end = $start;
1932
		}
1933
1934
		# Scheme and path part - 'pchar'
1935
		# (we assume no userinfo or encoded colons in the host)
1936
		$ret = self::normalizeUrlComponent(
1937
			substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1938
1939
		return $ret;
1940
	}
1941
1942
	private static function normalizeUrlComponent( $component, $unsafe ) {
1943
		$callback = function ( $matches ) use ( $unsafe ) {
1944
			$char = urldecode( $matches[0] );
1945
			$ord = ord( $char );
1946
			if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1947
				# Unescape it
1948
				return $char;
1949
			} else {
1950
				# Leave it escaped, but use uppercase for a-f
1951
				return strtoupper( $matches[0] );
1952
			}
1953
		};
1954
		return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
1955
	}
1956
1957
	/**
1958
	 * make an image if it's allowed, either through the global
1959
	 * option, through the exception, or through the on-wiki whitelist
1960
	 *
1961
	 * @param string $url
1962
	 *
1963
	 * @return string
1964
	 */
1965
	private function maybeMakeExternalImage( $url ) {
1966
		$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1967
		$imagesexception = !empty( $imagesfrom );
1968
		$text = false;
1969
		# $imagesfrom could be either a single string or an array of strings, parse out the latter
1970
		if ( $imagesexception && is_array( $imagesfrom ) ) {
1971
			$imagematch = false;
1972
			foreach ( $imagesfrom as $match ) {
1973
				if ( strpos( $url, $match ) === 0 ) {
1974
					$imagematch = true;
1975
					break;
1976
				}
1977
			}
1978
		} elseif ( $imagesexception ) {
1979
			$imagematch = ( strpos( $url, $imagesfrom ) === 0 );
1980
		} else {
1981
			$imagematch = false;
1982
		}
1983
1984
		if ( $this->mOptions->getAllowExternalImages()
1985
			|| ( $imagesexception && $imagematch )
1986
		) {
1987
			if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1988
				# Image found
1989
				$text = Linker::makeExternalImage( $url );
1990
			}
1991
		}
1992
		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...
1993
			&& preg_match( self::EXT_IMAGE_REGEX, $url )
1994
		) {
1995
			$whitelist = explode(
1996
				"\n",
1997
				wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
1998
			);
1999
2000
			foreach ( $whitelist as $entry ) {
2001
				# Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2002
				if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2003
					continue;
2004
				}
2005
				if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2006
					# Image matches a whitelist entry
2007
					$text = Linker::makeExternalImage( $url );
2008
					break;
2009
				}
2010
			}
2011
		}
2012
		return $text;
2013
	}
2014
2015
	/**
2016
	 * Process [[ ]] wikilinks
2017
	 *
2018
	 * @param string $s
2019
	 *
2020
	 * @return string Processed text
2021
	 *
2022
	 * @private
2023
	 */
2024
	public function replaceInternalLinks( $s ) {
2025
		$this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2026
		return $s;
2027
	}
2028
2029
	/**
2030
	 * Process [[ ]] wikilinks (RIL)
2031
	 * @param string $s
2032
	 * @throws MWException
2033
	 * @return LinkHolderArray
2034
	 *
2035
	 * @private
2036
	 */
2037
	public function replaceInternalLinks2( &$s ) {
2038
		global $wgExtraInterlanguageLinkPrefixes;
2039
2040
		static $tc = false, $e1, $e1_img;
2041
		# the % is needed to support urlencoded titles as well
2042
		if ( !$tc ) {
2043
			$tc = Title::legalChars() . '#%';
2044
			# Match a link having the form [[namespace:link|alternate]]trail
2045
			$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2046
			# Match cases where there is no "]]", which might still be images
2047
			$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2048
		}
2049
2050
		$holders = new LinkHolderArray( $this );
2051
2052
		# split the entire text string on occurrences of [[
2053
		$a = StringUtils::explode( '[[', ' ' . $s );
2054
		# get the first element (all text up to first [[), and remove the space we added
2055
		$s = $a->current();
2056
		$a->next();
2057
		$line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2058
		$s = substr( $s, 1 );
2059
2060
		$useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2061
		$e2 = null;
2062
		if ( $useLinkPrefixExtension ) {
2063
			# Match the end of a line for a word that's not followed by whitespace,
2064
			# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2065
			global $wgContLang;
2066
			$charset = $wgContLang->linkPrefixCharset();
2067
			$e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2068
		}
2069
2070
		if ( is_null( $this->mTitle ) ) {
2071
			throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2072
		}
2073
		$nottalk = !$this->mTitle->isTalkPage();
2074
2075 View Code Duplication
		if ( $useLinkPrefixExtension ) {
2076
			$m = [];
2077
			if ( preg_match( $e2, $s, $m ) ) {
2078
				$first_prefix = $m[2];
2079
			} else {
2080
				$first_prefix = false;
2081
			}
2082
		} else {
2083
			$prefix = '';
2084
		}
2085
2086
		$useSubpages = $this->areSubpagesAllowed();
2087
2088
		// @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2089
		# Loop for each link
2090
		for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2091
			// @codingStandardsIgnoreEnd
2092
2093
			# Check for excessive memory usage
2094
			if ( $holders->isBig() ) {
2095
				# Too big
2096
				# Do the existence check, replace the link holders and clear the array
2097
				$holders->replace( $s );
2098
				$holders->clear();
2099
			}
2100
2101
			if ( $useLinkPrefixExtension ) {
2102 View Code Duplication
				if ( preg_match( $e2, $s, $m ) ) {
2103
					$prefix = $m[2];
2104
					$s = $m[1];
2105
				} else {
2106
					$prefix = '';
2107
				}
2108
				# first link
2109
				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...
2110
					$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...
2111
					$first_prefix = false;
2112
				}
2113
			}
2114
2115
			$might_be_img = false;
2116
2117
			if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2118
				$text = $m[2];
2119
				# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2120
				# [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2121
				# the real problem is with the $e1 regex
2122
				# See bug 1300.
2123
				# Still some problems for cases where the ] is meant to be outside punctuation,
2124
				# and no image is in sight. See bug 2095.
2125
				if ( $text !== ''
2126
					&& substr( $m[3], 0, 1 ) === ']'
2127
					&& strpos( $text, '[' ) !== false
2128
				) {
2129
					$text .= ']'; # so that replaceExternalLinks($text) works later
2130
					$m[3] = substr( $m[3], 1 );
2131
				}
2132
				# fix up urlencoded title texts
2133
				if ( strpos( $m[1], '%' ) !== false ) {
2134
					# Should anchors '#' also be rejected?
2135
					$m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2136
				}
2137
				$trail = $m[3];
2138
			} elseif ( preg_match( $e1_img, $line, $m ) ) {
2139
				# Invalid, but might be an image with a link in its caption
2140
				$might_be_img = true;
2141
				$text = $m[2];
2142
				if ( strpos( $m[1], '%' ) !== false ) {
2143
					$m[1] = rawurldecode( $m[1] );
2144
				}
2145
				$trail = "";
2146
			} else { # Invalid form; output directly
2147
				$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...
2148
				continue;
2149
			}
2150
2151
			$origLink = $m[1];
2152
2153
			# Don't allow internal links to pages containing
2154
			# PROTO: where PROTO is a valid URL protocol; these
2155
			# should be external links.
2156
			if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2157
				$s .= $prefix . '[[' . $line;
2158
				continue;
2159
			}
2160
2161
			# Make subpage if necessary
2162
			if ( $useSubpages ) {
2163
				$link = $this->maybeDoSubpageLink( $origLink, $text );
2164
			} else {
2165
				$link = $origLink;
2166
			}
2167
2168
			$noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2169
			if ( !$noforce ) {
2170
				# Strip off leading ':'
2171
				$link = substr( $link, 1 );
2172
			}
2173
2174
			$unstrip = $this->mStripState->unstripNoWiki( $link );
2175
			$nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2176
			if ( $nt === null ) {
2177
				$s .= $prefix . '[[' . $line;
2178
				continue;
2179
			}
2180
2181
			$ns = $nt->getNamespace();
2182
			$iw = $nt->getInterwiki();
2183
2184
			if ( $might_be_img ) { # if this is actually an invalid link
2185
				if ( $ns == NS_FILE && $noforce ) { # but might be an image
2186
					$found = false;
2187
					while ( true ) {
2188
						# look at the next 'line' to see if we can close it there
2189
						$a->next();
2190
						$next_line = $a->current();
2191
						if ( $next_line === false || $next_line === null ) {
2192
							break;
2193
						}
2194
						$m = explode( ']]', $next_line, 3 );
2195
						if ( count( $m ) == 3 ) {
2196
							# the first ]] closes the inner link, the second the image
2197
							$found = true;
2198
							$text .= "[[{$m[0]}]]{$m[1]}";
2199
							$trail = $m[2];
2200
							break;
2201
						} elseif ( count( $m ) == 2 ) {
2202
							# if there's exactly one ]] that's fine, we'll keep looking
2203
							$text .= "[[{$m[0]}]]{$m[1]}";
2204
						} else {
2205
							# if $next_line is invalid too, we need look no further
2206
							$text .= '[[' . $next_line;
2207
							break;
2208
						}
2209
					}
2210
					if ( !$found ) {
2211
						# we couldn't find the end of this imageLink, so output it raw
2212
						# but don't ignore what might be perfectly normal links in the text we've examined
2213
						$holders->merge( $this->replaceInternalLinks2( $text ) );
2214
						$s .= "{$prefix}[[$link|$text";
2215
						# note: no $trail, because without an end, there *is* no trail
2216
						continue;
2217
					}
2218
				} else { # it's not an image, so output it raw
2219
					$s .= "{$prefix}[[$link|$text";
2220
					# note: no $trail, because without an end, there *is* no trail
2221
					continue;
2222
				}
2223
			}
2224
2225
			$wasblank = ( $text == '' );
2226
			if ( $wasblank ) {
2227
				$text = $link;
2228
			} else {
2229
				# Bug 4598 madness. Handle the quotes only if they come from the alternate part
2230
				# [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2231
				# [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2232
				#    -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2233
				$text = $this->doQuotes( $text );
2234
			}
2235
2236
			# Link not escaped by : , create the various objects
2237
			if ( $noforce && !$nt->wasLocalInterwiki() ) {
2238
				# Interwikis
2239
				if (
2240
					$iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2241
						Language::fetchLanguageName( $iw, null, 'mw' ) ||
2242
						in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2243
					)
2244
				) {
2245
					# Bug 24502: filter duplicates
2246
					if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2247
						$this->mLangLinkLanguages[$iw] = true;
2248
						$this->mOutput->addLanguageLink( $nt->getFullText() );
2249
					}
2250
2251
					$s = rtrim( $s . $prefix );
2252
					$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2253
					continue;
2254
				}
2255
2256
				if ( $ns == NS_FILE ) {
2257
					if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2258
						if ( $wasblank ) {
2259
							# if no parameters were passed, $text
2260
							# becomes something like "File:Foo.png",
2261
							# which we don't want to pass on to the
2262
							# image generator
2263
							$text = '';
2264
						} else {
2265
							# recursively parse links inside the image caption
2266
							# actually, this will parse them in any other parameters, too,
2267
							# but it might be hard to fix that, and it doesn't matter ATM
2268
							$text = $this->replaceExternalLinks( $text );
2269
							$holders->merge( $this->replaceInternalLinks2( $text ) );
2270
						}
2271
						# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2272
						$s .= $prefix . $this->armorLinks(
2273
							$this->makeImage( $nt, $text, $holders ) ) . $trail;
2274
					} else {
2275
						$s .= $prefix . $trail;
2276
					}
2277
					continue;
2278
				}
2279
2280
				if ( $ns == NS_CATEGORY ) {
2281
					$s = rtrim( $s . "\n" ); # bug 87
2282
2283
					if ( $wasblank ) {
2284
						$sortkey = $this->getDefaultSort();
2285
					} else {
2286
						$sortkey = $text;
2287
					}
2288
					$sortkey = Sanitizer::decodeCharReferences( $sortkey );
2289
					$sortkey = str_replace( "\n", '', $sortkey );
2290
					$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2291
					$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2292
2293
					/**
2294
					 * Strip the whitespace Category links produce, see bug 87
2295
					 */
2296
					$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2297
2298
					continue;
2299
				}
2300
			}
2301
2302
			# Self-link checking. For some languages, variants of the title are checked in
2303
			# LinkHolderArray::doVariants() to allow batching the existence checks necessary
2304
			# for linking to a different variant.
2305
			if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2306
				$s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2307
				continue;
2308
			}
2309
2310
			# NS_MEDIA is a pseudo-namespace for linking directly to a file
2311
			# @todo FIXME: Should do batch file existence checks, see comment below
2312
			if ( $ns == NS_MEDIA ) {
2313
				# Give extensions a chance to select the file revision for us
2314
				$options = [];
2315
				$descQuery = false;
2316
				Hooks::run( 'BeforeParserFetchFileAndTitle',
2317
					[ $this, $nt, &$options, &$descQuery ] );
2318
				# Fetch and register the file (file title may be different via hooks)
2319
				list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2320
				# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2321
				$s .= $prefix . $this->armorLinks(
2322
					Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2323
				continue;
2324
			}
2325
2326
			# Some titles, such as valid special pages or files in foreign repos, should
2327
			# be shown as bluelinks even though they're not included in the page table
2328
			# @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2329
			# batch file existence checks for NS_FILE and NS_MEDIA
2330
			if ( $iw == '' && $nt->isAlwaysKnown() ) {
2331
				$this->mOutput->addLink( $nt );
2332
				$s .= $this->makeKnownLinkHolder( $nt, $text, [], $trail, $prefix );
2333
			} else {
2334
				# Links will be added to the output link list after checking
2335
				$s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2336
			}
2337
		}
2338
		return $holders;
2339
	}
2340
2341
	/**
2342
	 * Render a forced-blue link inline; protect against double expansion of
2343
	 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2344
	 * Since this little disaster has to split off the trail text to avoid
2345
	 * breaking URLs in the following text without breaking trails on the
2346
	 * wiki links, it's been made into a horrible function.
2347
	 *
2348
	 * @param Title $nt
2349
	 * @param string $text
2350
	 * @param array|string $query
2351
	 * @param string $trail
2352
	 * @param string $prefix
2353
	 * @return string HTML-wikitext mix oh yuck
2354
	 */
2355
	public function makeKnownLinkHolder( $nt, $text = '', $query = [], $trail = '', $prefix = '' ) {
2356
		list( $inside, $trail ) = Linker::splitTrail( $trail );
2357
2358
		if ( is_string( $query ) ) {
2359
			$query = wfCgiToArray( $query );
2360
		}
2361
		if ( $text == '' ) {
2362
			$text = htmlspecialchars( $nt->getPrefixedText() );
2363
		}
2364
2365
		$link = Linker::linkKnown( $nt, "$prefix$text$inside", [], $query );
2366
2367
		return $this->armorLinks( $link ) . $trail;
2368
	}
2369
2370
	/**
2371
	 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2372
	 * going to go through further parsing steps before inline URL expansion.
2373
	 *
2374
	 * Not needed quite as much as it used to be since free links are a bit
2375
	 * more sensible these days. But bracketed links are still an issue.
2376
	 *
2377
	 * @param string $text More-or-less HTML
2378
	 * @return string Less-or-more HTML with NOPARSE bits
2379
	 */
2380
	public function armorLinks( $text ) {
2381
		return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2382
			self::MARKER_PREFIX . "NOPARSE$1", $text );
2383
	}
2384
2385
	/**
2386
	 * Return true if subpage links should be expanded on this page.
2387
	 * @return bool
2388
	 */
2389
	public function areSubpagesAllowed() {
2390
		# Some namespaces don't allow subpages
2391
		return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2392
	}
2393
2394
	/**
2395
	 * Handle link to subpage if necessary
2396
	 *
2397
	 * @param string $target The source of the link
2398
	 * @param string &$text The link text, modified as necessary
2399
	 * @return string The full name of the link
2400
	 * @private
2401
	 */
2402
	public function maybeDoSubpageLink( $target, &$text ) {
2403
		return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2404
	}
2405
2406
	/**
2407
	 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
2408
	 *
2409
	 * @param string $text
2410
	 * @param bool $linestart Whether or not this is at the start of a line.
2411
	 * @private
2412
	 * @return string The lists rendered as HTML
2413
	 */
2414
	public function doBlockLevels( $text, $linestart ) {
2415
		return BlockLevelPass::doBlockLevels( $text, $linestart );
2416
	}
2417
2418
	/**
2419
	 * Return value of a magic variable (like PAGENAME)
2420
	 *
2421
	 * @private
2422
	 *
2423
	 * @param int $index
2424
	 * @param bool|PPFrame $frame
2425
	 *
2426
	 * @throws MWException
2427
	 * @return string
2428
	 */
2429
	public function getVariableValue( $index, $frame = false ) {
2430
		global $wgContLang, $wgSitename, $wgServer, $wgServerName;
2431
		global $wgArticlePath, $wgScriptPath, $wgStylePath;
2432
2433
		if ( is_null( $this->mTitle ) ) {
2434
			// If no title set, bad things are going to happen
2435
			// later. Title should always be set since this
2436
			// should only be called in the middle of a parse
2437
			// operation (but the unit-tests do funky stuff)
2438
			throw new MWException( __METHOD__ . ' Should only be '
2439
				. ' called while parsing (no title set)' );
2440
		}
2441
2442
		/**
2443
		 * Some of these require message or data lookups and can be
2444
		 * expensive to check many times.
2445
		 */
2446
		if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
2447
			if ( isset( $this->mVarCache[$index] ) ) {
2448
				return $this->mVarCache[$index];
2449
			}
2450
		}
2451
2452
		$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2453
		Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
2454
2455
		$pageLang = $this->getFunctionLang();
2456
2457
		switch ( $index ) {
2458
			case '!':
2459
				$value = '|';
2460
				break;
2461
			case 'currentmonth':
2462
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2463
				break;
2464
			case 'currentmonth1':
2465
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2466
				break;
2467
			case 'currentmonthname':
2468
				$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2469
				break;
2470
			case 'currentmonthnamegen':
2471
				$value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2472
				break;
2473
			case 'currentmonthabbrev':
2474
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2475
				break;
2476
			case 'currentday':
2477
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2478
				break;
2479
			case 'currentday2':
2480
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2481
				break;
2482
			case 'localmonth':
2483
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2484
				break;
2485
			case 'localmonth1':
2486
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2487
				break;
2488
			case 'localmonthname':
2489
				$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2490
				break;
2491
			case 'localmonthnamegen':
2492
				$value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2493
				break;
2494
			case 'localmonthabbrev':
2495
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2496
				break;
2497
			case 'localday':
2498
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2499
				break;
2500
			case 'localday2':
2501
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2502
				break;
2503
			case 'pagename':
2504
				$value = wfEscapeWikiText( $this->mTitle->getText() );
2505
				break;
2506
			case 'pagenamee':
2507
				$value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2508
				break;
2509
			case 'fullpagename':
2510
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2511
				break;
2512
			case 'fullpagenamee':
2513
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2514
				break;
2515
			case 'subpagename':
2516
				$value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2517
				break;
2518
			case 'subpagenamee':
2519
				$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2520
				break;
2521
			case 'rootpagename':
2522
				$value = wfEscapeWikiText( $this->mTitle->getRootText() );
2523
				break;
2524 View Code Duplication
			case 'rootpagenamee':
2525
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2526
					' ',
2527
					'_',
2528
					$this->mTitle->getRootText()
2529
				) ) );
2530
				break;
2531
			case 'basepagename':
2532
				$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2533
				break;
2534 View Code Duplication
			case 'basepagenamee':
2535
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2536
					' ',
2537
					'_',
2538
					$this->mTitle->getBaseText()
2539
				) ) );
2540
				break;
2541 View Code Duplication
			case 'talkpagename':
2542
				if ( $this->mTitle->canTalk() ) {
2543
					$talkPage = $this->mTitle->getTalkPage();
2544
					$value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2545
				} else {
2546
					$value = '';
2547
				}
2548
				break;
2549 View Code Duplication
			case 'talkpagenamee':
2550
				if ( $this->mTitle->canTalk() ) {
2551
					$talkPage = $this->mTitle->getTalkPage();
2552
					$value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2553
				} else {
2554
					$value = '';
2555
				}
2556
				break;
2557
			case 'subjectpagename':
2558
				$subjPage = $this->mTitle->getSubjectPage();
2559
				$value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2560
				break;
2561
			case 'subjectpagenamee':
2562
				$subjPage = $this->mTitle->getSubjectPage();
2563
				$value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2564
				break;
2565
			case 'pageid': // requested in bug 23427
2566
				$pageid = $this->getTitle()->getArticleID();
2567
				if ( $pageid == 0 ) {
2568
					# 0 means the page doesn't exist in the database,
2569
					# which means the user is previewing a new page.
2570
					# The vary-revision flag must be set, because the magic word
2571
					# will have a different value once the page is saved.
2572
					$this->mOutput->setFlag( 'vary-revision' );
2573
					wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2574
				}
2575
				$value = $pageid ? $pageid : null;
2576
				break;
2577
			case 'revisionid':
2578
				# Let the edit saving system know we should parse the page
2579
				# *after* a revision ID has been assigned.
2580
				$this->mOutput->setFlag( 'vary-revision' );
2581
				wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
2582
				$value = $this->mRevisionId;
2583
				break;
2584 View Code Duplication
			case 'revisionday':
2585
				# Let the edit saving system know we should parse the page
2586
				# *after* a revision ID has been assigned. This is for null edits.
2587
				$this->mOutput->setFlag( 'vary-revision' );
2588
				wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2589
				$value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2590
				break;
2591 View Code Duplication
			case 'revisionday2':
2592
				# Let the edit saving system know we should parse the page
2593
				# *after* a revision ID has been assigned. This is for null edits.
2594
				$this->mOutput->setFlag( 'vary-revision' );
2595
				wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2596
				$value = substr( $this->getRevisionTimestamp(), 6, 2 );
2597
				break;
2598 View Code Duplication
			case 'revisionmonth':
2599
				# Let the edit saving system know we should parse the page
2600
				# *after* a revision ID has been assigned. This is for null edits.
2601
				$this->mOutput->setFlag( 'vary-revision' );
2602
				wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2603
				$value = substr( $this->getRevisionTimestamp(), 4, 2 );
2604
				break;
2605 View Code Duplication
			case 'revisionmonth1':
2606
				# Let the edit saving system know we should parse the page
2607
				# *after* a revision ID has been assigned. This is for null edits.
2608
				$this->mOutput->setFlag( 'vary-revision' );
2609
				wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2610
				$value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2611
				break;
2612 View Code Duplication
			case 'revisionyear':
2613
				# Let the edit saving system know we should parse the page
2614
				# *after* a revision ID has been assigned. This is for null edits.
2615
				$this->mOutput->setFlag( 'vary-revision' );
2616
				wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2617
				$value = substr( $this->getRevisionTimestamp(), 0, 4 );
2618
				break;
2619
			case 'revisiontimestamp':
2620
				# Let the edit saving system know we should parse the page
2621
				# *after* a revision ID has been assigned. This is for null edits.
2622
				$this->mOutput->setFlag( 'vary-revision' );
2623
				wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2624
				$value = $this->getRevisionTimestamp();
2625
				break;
2626
			case 'revisionuser':
2627
				# Let the edit saving system know we should parse the page
2628
				# *after* a revision ID has been assigned. This is for null edits.
2629
				$this->mOutput->setFlag( 'vary-revision' );
2630
				wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
2631
				$value = $this->getRevisionUser();
2632
				break;
2633
			case 'revisionsize':
2634
				# Let the edit saving system know we should parse the page
2635
				# *after* a revision ID has been assigned. This is for null edits.
2636
				$this->mOutput->setFlag( 'vary-revision' );
2637
				wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
2638
				$value = $this->getRevisionSize();
2639
				break;
2640
			case 'namespace':
2641
				$value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2642
				break;
2643
			case 'namespacee':
2644
				$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2645
				break;
2646
			case 'namespacenumber':
2647
				$value = $this->mTitle->getNamespace();
2648
				break;
2649
			case 'talkspace':
2650
				$value = $this->mTitle->canTalk()
2651
					? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2652
					: '';
2653
				break;
2654
			case 'talkspacee':
2655
				$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2656
				break;
2657
			case 'subjectspace':
2658
				$value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2659
				break;
2660
			case 'subjectspacee':
2661
				$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2662
				break;
2663
			case 'currentdayname':
2664
				$value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2665
				break;
2666
			case 'currentyear':
2667
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2668
				break;
2669
			case 'currenttime':
2670
				$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...
2671
				break;
2672
			case 'currenthour':
2673
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2674
				break;
2675
			case 'currentweek':
2676
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2677
				# int to remove the padding
2678
				$value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2679
				break;
2680
			case 'currentdow':
2681
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2682
				break;
2683
			case 'localdayname':
2684
				$value = $pageLang->getWeekdayName(
2685
					(int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2686
				);
2687
				break;
2688
			case 'localyear':
2689
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2690
				break;
2691
			case 'localtime':
2692
				$value = $pageLang->time(
2693
					MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2694
					false,
2695
					false
2696
				);
2697
				break;
2698
			case 'localhour':
2699
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2700
				break;
2701
			case 'localweek':
2702
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2703
				# int to remove the padding
2704
				$value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2705
				break;
2706
			case 'localdow':
2707
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2708
				break;
2709
			case 'numberofarticles':
2710
				$value = $pageLang->formatNum( SiteStats::articles() );
2711
				break;
2712
			case 'numberoffiles':
2713
				$value = $pageLang->formatNum( SiteStats::images() );
2714
				break;
2715
			case 'numberofusers':
2716
				$value = $pageLang->formatNum( SiteStats::users() );
2717
				break;
2718
			case 'numberofactiveusers':
2719
				$value = $pageLang->formatNum( SiteStats::activeUsers() );
2720
				break;
2721
			case 'numberofpages':
2722
				$value = $pageLang->formatNum( SiteStats::pages() );
2723
				break;
2724
			case 'numberofadmins':
2725
				$value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2726
				break;
2727
			case 'numberofedits':
2728
				$value = $pageLang->formatNum( SiteStats::edits() );
2729
				break;
2730
			case 'currenttimestamp':
2731
				$value = wfTimestamp( TS_MW, $ts );
2732
				break;
2733
			case 'localtimestamp':
2734
				$value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2735
				break;
2736
			case 'currentversion':
2737
				$value = SpecialVersion::getVersion();
2738
				break;
2739
			case 'articlepath':
2740
				return $wgArticlePath;
2741
			case 'sitename':
2742
				return $wgSitename;
2743
			case 'server':
2744
				return $wgServer;
2745
			case 'servername':
2746
				return $wgServerName;
2747
			case 'scriptpath':
2748
				return $wgScriptPath;
2749
			case 'stylepath':
2750
				return $wgStylePath;
2751
			case 'directionmark':
2752
				return $pageLang->getDirMark();
2753
			case 'contentlanguage':
2754
				global $wgLanguageCode;
2755
				return $wgLanguageCode;
2756
			case 'cascadingsources':
2757
				$value = CoreParserFunctions::cascadingsources( $this );
2758
				break;
2759
			default:
2760
				$ret = null;
2761
				Hooks::run(
2762
					'ParserGetVariableValueSwitch',
2763
					[ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
2764
				);
2765
2766
				return $ret;
2767
		}
2768
2769
		if ( $index ) {
2770
			$this->mVarCache[$index] = $value;
2771
		}
2772
2773
		return $value;
2774
	}
2775
2776
	/**
2777
	 * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
2778
	 *
2779
	 * @private
2780
	 */
2781
	public function initialiseVariables() {
2782
		$variableIDs = MagicWord::getVariableIDs();
2783
		$substIDs = MagicWord::getSubstIDs();
2784
2785
		$this->mVariables = new MagicWordArray( $variableIDs );
2786
		$this->mSubstWords = new MagicWordArray( $substIDs );
2787
	}
2788
2789
	/**
2790
	 * Preprocess some wikitext and return the document tree.
2791
	 * This is the ghost of replace_variables().
2792
	 *
2793
	 * @param string $text The text to parse
2794
	 * @param int $flags Bitwise combination of:
2795
	 *   - self::PTD_FOR_INCLUSION: Handle "<noinclude>" and "<includeonly>" as if the text is being
2796
	 *     included. Default is to assume a direct page view.
2797
	 *
2798
	 * The generated DOM tree must depend only on the input text and the flags.
2799
	 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
2800
	 *
2801
	 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
2802
	 * change in the DOM tree for a given text, must be passed through the section identifier
2803
	 * in the section edit link and thus back to extractSections().
2804
	 *
2805
	 * The output of this function is currently only cached in process memory, but a persistent
2806
	 * cache may be implemented at a later date which takes further advantage of these strict
2807
	 * dependency requirements.
2808
	 *
2809
	 * @return PPNode
2810
	 */
2811
	public function preprocessToDom( $text, $flags = 0 ) {
2812
		$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2813
		return $dom;
2814
	}
2815
2816
	/**
2817
	 * Return a three-element array: leading whitespace, string contents, trailing whitespace
2818
	 *
2819
	 * @param string $s
2820
	 *
2821
	 * @return array
2822
	 */
2823
	public static function splitWhitespace( $s ) {
2824
		$ltrimmed = ltrim( $s );
2825
		$w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2826
		$trimmed = rtrim( $ltrimmed );
2827
		$diff = strlen( $ltrimmed ) - strlen( $trimmed );
2828
		if ( $diff > 0 ) {
2829
			$w2 = substr( $ltrimmed, -$diff );
2830
		} else {
2831
			$w2 = '';
2832
		}
2833
		return [ $w1, $trimmed, $w2 ];
2834
	}
2835
2836
	/**
2837
	 * Replace magic variables, templates, and template arguments
2838
	 * with the appropriate text. Templates are substituted recursively,
2839
	 * taking care to avoid infinite loops.
2840
	 *
2841
	 * Note that the substitution depends on value of $mOutputType:
2842
	 *  self::OT_WIKI: only {{subst:}} templates
2843
	 *  self::OT_PREPROCESS: templates but not extension tags
2844
	 *  self::OT_HTML: all templates and extension tags
2845
	 *
2846
	 * @param string $text The text to transform
2847
	 * @param bool|PPFrame $frame Object describing the arguments passed to the
2848
	 *   template. Arguments may also be provided as an associative array, as
2849
	 *   was the usual case before MW1.12. Providing arguments this way may be
2850
	 *   useful for extensions wishing to perform variable replacement
2851
	 *   explicitly.
2852
	 * @param bool $argsOnly Only do argument (triple-brace) expansion, not
2853
	 *   double-brace expansion.
2854
	 * @return string
2855
	 */
2856
	public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2857
		# Is there any text? Also, Prevent too big inclusions!
2858
		$textSize = strlen( $text );
2859
		if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2860
			return $text;
2861
		}
2862
2863
		if ( $frame === false ) {
2864
			$frame = $this->getPreprocessor()->newFrame();
2865
		} elseif ( !( $frame instanceof PPFrame ) ) {
2866
			wfDebug( __METHOD__ . " called using plain parameters instead of "
2867
				. "a PPFrame instance. Creating custom frame.\n" );
2868
			$frame = $this->getPreprocessor()->newCustomFrame( $frame );
2869
		}
2870
2871
		$dom = $this->preprocessToDom( $text );
2872
		$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2873
		$text = $frame->expand( $dom, $flags );
2874
2875
		return $text;
2876
	}
2877
2878
	/**
2879
	 * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
2880
	 *
2881
	 * @param array $args
2882
	 *
2883
	 * @return array
2884
	 */
2885
	public static function createAssocArgs( $args ) {
2886
		$assocArgs = [];
2887
		$index = 1;
2888
		foreach ( $args as $arg ) {
2889
			$eqpos = strpos( $arg, '=' );
2890
			if ( $eqpos === false ) {
2891
				$assocArgs[$index++] = $arg;
2892
			} else {
2893
				$name = trim( substr( $arg, 0, $eqpos ) );
2894
				$value = trim( substr( $arg, $eqpos + 1 ) );
2895
				if ( $value === false ) {
2896
					$value = '';
2897
				}
2898
				if ( $name !== false ) {
2899
					$assocArgs[$name] = $value;
2900
				}
2901
			}
2902
		}
2903
2904
		return $assocArgs;
2905
	}
2906
2907
	/**
2908
	 * Warn the user when a parser limitation is reached
2909
	 * Will warn at most once the user per limitation type
2910
	 *
2911
	 * The results are shown during preview and run through the Parser (See EditPage.php)
2912
	 *
2913
	 * @param string $limitationType Should be one of:
2914
	 *   'expensive-parserfunction' (corresponding messages:
2915
	 *       'expensive-parserfunction-warning',
2916
	 *       'expensive-parserfunction-category')
2917
	 *   'post-expand-template-argument' (corresponding messages:
2918
	 *       'post-expand-template-argument-warning',
2919
	 *       'post-expand-template-argument-category')
2920
	 *   'post-expand-template-inclusion' (corresponding messages:
2921
	 *       'post-expand-template-inclusion-warning',
2922
	 *       'post-expand-template-inclusion-category')
2923
	 *   'node-count-exceeded' (corresponding messages:
2924
	 *       'node-count-exceeded-warning',
2925
	 *       'node-count-exceeded-category')
2926
	 *   'expansion-depth-exceeded' (corresponding messages:
2927
	 *       'expansion-depth-exceeded-warning',
2928
	 *       'expansion-depth-exceeded-category')
2929
	 * @param string|int|null $current Current value
2930
	 * @param string|int|null $max Maximum allowed, when an explicit limit has been
2931
	 *	 exceeded, provide the values (optional)
2932
	 */
2933
	public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2934
		# does no harm if $current and $max are present but are unnecessary for the message
2935
		# Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2936
		# only during preview, and that would split the parser cache unnecessarily.
2937
		$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2938
			->text();
2939
		$this->mOutput->addWarning( $warning );
2940
		$this->addTrackingCategory( "$limitationType-category" );
2941
	}
2942
2943
	/**
2944
	 * Return the text of a template, after recursively
2945
	 * replacing any variables or templates within the template.
2946
	 *
2947
	 * @param array $piece The parts of the template
2948
	 *   $piece['title']: the title, i.e. the part before the |
2949
	 *   $piece['parts']: the parameter array
2950
	 *   $piece['lineStart']: whether the brace was at the start of a line
2951
	 * @param PPFrame $frame The current frame, contains template arguments
2952
	 * @throws Exception
2953
	 * @return string The text of the template
2954
	 */
2955
	public function braceSubstitution( $piece, $frame ) {
2956
2957
		// Flags
2958
2959
		// $text has been filled
2960
		$found = false;
2961
		// wiki markup in $text should be escaped
2962
		$nowiki = false;
2963
		// $text is HTML, armour it against wikitext transformation
2964
		$isHTML = false;
2965
		// Force interwiki transclusion to be done in raw mode not rendered
2966
		$forceRawInterwiki = false;
2967
		// $text is a DOM node needing expansion in a child frame
2968
		$isChildObj = false;
2969
		// $text is a DOM node needing expansion in the current frame
2970
		$isLocalObj = false;
2971
2972
		# Title object, where $text came from
2973
		$title = false;
2974
2975
		# $part1 is the bit before the first |, and must contain only title characters.
2976
		# Various prefixes will be stripped from it later.
2977
		$titleWithSpaces = $frame->expand( $piece['title'] );
2978
		$part1 = trim( $titleWithSpaces );
2979
		$titleText = false;
2980
2981
		# Original title text preserved for various purposes
2982
		$originalTitle = $part1;
2983
2984
		# $args is a list of argument nodes, starting from index 0, not including $part1
2985
		# @todo FIXME: If piece['parts'] is null then the call to getLength()
2986
		# below won't work b/c this $args isn't an object
2987
		$args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
2988
2989
		$profileSection = null; // profile templates
2990
2991
		# SUBST
2992
		if ( !$found ) {
2993
			$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2994
2995
			# Possibilities for substMatch: "subst", "safesubst" or FALSE
2996
			# Decide whether to expand template or keep wikitext as-is.
2997
			if ( $this->ot['wiki'] ) {
2998
				if ( $substMatch === false ) {
2999
					$literal = true;  # literal when in PST with no prefix
3000
				} else {
3001
					$literal = false; # expand when in PST with subst: or safesubst:
3002
				}
3003
			} else {
3004
				if ( $substMatch == 'subst' ) {
3005
					$literal = true;  # literal when not in PST with plain subst:
3006
				} else {
3007
					$literal = false; # expand when not in PST with safesubst: or no prefix
3008
				}
3009
			}
3010
			if ( $literal ) {
3011
				$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3012
				$isLocalObj = true;
3013
				$found = true;
3014
			}
3015
		}
3016
3017
		# Variables
3018
		if ( !$found && $args->getLength() == 0 ) {
3019
			$id = $this->mVariables->matchStartToEnd( $part1 );
3020
			if ( $id !== false ) {
3021
				$text = $this->getVariableValue( $id, $frame );
3022
				if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3023
					$this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3024
				}
3025
				$found = true;
3026
			}
3027
		}
3028
3029
		# MSG, MSGNW and RAW
3030
		if ( !$found ) {
3031
			# Check for MSGNW:
3032
			$mwMsgnw = MagicWord::get( 'msgnw' );
3033
			if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3034
				$nowiki = true;
3035
			} else {
3036
				# Remove obsolete MSG:
3037
				$mwMsg = MagicWord::get( 'msg' );
3038
				$mwMsg->matchStartAndRemove( $part1 );
3039
			}
3040
3041
			# Check for RAW:
3042
			$mwRaw = MagicWord::get( 'raw' );
3043
			if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3044
				$forceRawInterwiki = true;
3045
			}
3046
		}
3047
3048
		# Parser functions
3049
		if ( !$found ) {
3050
			$colonPos = strpos( $part1, ':' );
3051
			if ( $colonPos !== false ) {
3052
				$func = substr( $part1, 0, $colonPos );
3053
				$funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3054
				$argsLength = $args->getLength();
3055
				for ( $i = 0; $i < $argsLength; $i++ ) {
3056
					$funcArgs[] = $args->item( $i );
3057
				}
3058
				try {
3059
					$result = $this->callParserFunction( $frame, $func, $funcArgs );
3060
				} catch ( Exception $ex ) {
3061
					throw $ex;
3062
				}
3063
3064
				# The interface for parser functions allows for extracting
3065
				# flags into the local scope. Extract any forwarded flags
3066
				# here.
3067
				extract( $result );
3068
			}
3069
		}
3070
3071
		# Finish mangling title and then check for loops.
3072
		# Set $title to a Title object and $titleText to the PDBK
3073
		if ( !$found ) {
3074
			$ns = NS_TEMPLATE;
3075
			# Split the title into page and subpage
3076
			$subpage = '';
3077
			$relative = $this->maybeDoSubpageLink( $part1, $subpage );
3078
			if ( $part1 !== $relative ) {
3079
				$part1 = $relative;
3080
				$ns = $this->mTitle->getNamespace();
3081
			}
3082
			$title = Title::newFromText( $part1, $ns );
3083
			if ( $title ) {
3084
				$titleText = $title->getPrefixedText();
3085
				# Check for language variants if the template is not found
3086
				if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3087
					$this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3088
				}
3089
				# Do recursion depth check
3090
				$limit = $this->mOptions->getMaxTemplateDepth();
3091 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...
3092
					$found = true;
3093
					$text = '<span class="error">'
3094
						. wfMessage( 'parser-template-recursion-depth-warning' )
3095
							->numParams( $limit )->inContentLanguage()->text()
3096
						. '</span>';
3097
				}
3098
			}
3099
		}
3100
3101
		# Load from database
3102
		if ( !$found && $title ) {
3103
			$profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3104
			if ( !$title->isExternal() ) {
3105
				if ( $title->isSpecialPage()
3106
					&& $this->mOptions->getAllowSpecialInclusion()
3107
					&& $this->ot['html']
3108
				) {
3109
					// Pass the template arguments as URL parameters.
3110
					// "uselang" will have no effect since the Language object
3111
					// is forced to the one defined in ParserOptions.
3112
					$pageArgs = [];
3113
					$argsLength = $args->getLength();
3114
					for ( $i = 0; $i < $argsLength; $i++ ) {
3115
						$bits = $args->item( $i )->splitArg();
3116
						if ( strval( $bits['index'] ) === '' ) {
3117
							$name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3118
							$value = trim( $frame->expand( $bits['value'] ) );
3119
							$pageArgs[$name] = $value;
3120
						}
3121
					}
3122
3123
					// Create a new context to execute the special page
3124
					$context = new RequestContext;
3125
					$context->setTitle( $title );
3126
					$context->setRequest( new FauxRequest( $pageArgs ) );
3127
					$context->setUser( $this->getUser() );
3128
					$context->setLanguage( $this->mOptions->getUserLangObj() );
3129
					$ret = SpecialPageFactory::capturePath( $title, $context );
3130
					if ( $ret ) {
3131
						$text = $context->getOutput()->getHTML();
3132
						$this->mOutput->addOutputPageMetadata( $context->getOutput() );
3133
						$found = true;
3134
						$isHTML = true;
3135
						$this->disableCache();
3136
					}
3137
				} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3138
					$found = false; # access denied
3139
					wfDebug( __METHOD__ . ": template inclusion denied for " .
3140
						$title->getPrefixedDBkey() . "\n" );
3141
				} else {
3142
					list( $text, $title ) = $this->getTemplateDom( $title );
3143
					if ( $text !== false ) {
3144
						$found = true;
3145
						$isChildObj = true;
3146
					}
3147
				}
3148
3149
				# If the title is valid but undisplayable, make a link to it
3150
				if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3151
					$text = "[[:$titleText]]";
3152
					$found = true;
3153
				}
3154
			} elseif ( $title->isTrans() ) {
3155
				# Interwiki transclusion
3156
				if ( $this->ot['html'] && !$forceRawInterwiki ) {
3157
					$text = $this->interwikiTransclude( $title, 'render' );
3158
					$isHTML = true;
3159
				} else {
3160
					$text = $this->interwikiTransclude( $title, 'raw' );
3161
					# Preprocess it like a template
3162
					$text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3163
					$isChildObj = true;
3164
				}
3165
				$found = true;
3166
			}
3167
3168
			# Do infinite loop check
3169
			# This has to be done after redirect resolution to avoid infinite loops via redirects
3170
			if ( !$frame->loopCheck( $title ) ) {
3171
				$found = true;
3172
				$text = '<span class="error">'
3173
					. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3174
					. '</span>';
3175
				wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3176
			}
3177
		}
3178
3179
		# If we haven't found text to substitute by now, we're done
3180
		# Recover the source wikitext and return it
3181
		if ( !$found ) {
3182
			$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3183
			if ( $profileSection ) {
3184
				$this->mProfiler->scopedProfileOut( $profileSection );
3185
			}
3186
			return [ 'object' => $text ];
3187
		}
3188
3189
		# Expand DOM-style return values in a child frame
3190
		if ( $isChildObj ) {
3191
			# Clean up argument array
3192
			$newFrame = $frame->newChild( $args, $title );
3193
3194
			if ( $nowiki ) {
3195
				$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3196
			} elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3197
				# Expansion is eligible for the empty-frame cache
3198
				$text = $newFrame->cachedExpand( $titleText, $text );
3199
			} else {
3200
				# Uncached expansion
3201
				$text = $newFrame->expand( $text );
3202
			}
3203
		}
3204
		if ( $isLocalObj && $nowiki ) {
3205
			$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3206
			$isLocalObj = false;
3207
		}
3208
3209
		if ( $profileSection ) {
3210
			$this->mProfiler->scopedProfileOut( $profileSection );
3211
		}
3212
3213
		# Replace raw HTML by a placeholder
3214
		if ( $isHTML ) {
3215
			$text = $this->insertStripItem( $text );
3216
		} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3217
			# Escape nowiki-style return values
3218
			$text = wfEscapeWikiText( $text );
3219
		} elseif ( is_string( $text )
3220
			&& !$piece['lineStart']
3221
			&& preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3222
		) {
3223
			# Bug 529: if the template begins with a table or block-level
3224
			# element, it should be treated as beginning a new line.
3225
			# This behavior is somewhat controversial.
3226
			$text = "\n" . $text;
3227
		}
3228
3229
		if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3230
			# Error, oversize inclusion
3231
			if ( $titleText !== false ) {
3232
				# Make a working, properly escaped link if possible (bug 23588)
3233
				$text = "[[:$titleText]]";
3234
			} else {
3235
				# This will probably not be a working link, but at least it may
3236
				# provide some hint of where the problem is
3237
				preg_replace( '/^:/', '', $originalTitle );
3238
				$text = "[[:$originalTitle]]";
3239
			}
3240
			$text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3241
				. 'post-expand include size too large -->' );
3242
			$this->limitationWarn( 'post-expand-template-inclusion' );
3243
		}
3244
3245
		if ( $isLocalObj ) {
3246
			$ret = [ 'object' => $text ];
3247
		} else {
3248
			$ret = [ 'text' => $text ];
3249
		}
3250
3251
		return $ret;
3252
	}
3253
3254
	/**
3255
	 * Call a parser function and return an array with text and flags.
3256
	 *
3257
	 * The returned array will always contain a boolean 'found', indicating
3258
	 * whether the parser function was found or not. It may also contain the
3259
	 * following:
3260
	 *  text: string|object, resulting wikitext or PP DOM object
3261
	 *  isHTML: bool, $text is HTML, armour it against wikitext transformation
3262
	 *  isChildObj: bool, $text is a DOM node needing expansion in a child frame
3263
	 *  isLocalObj: bool, $text is a DOM node needing expansion in the current frame
3264
	 *  nowiki: bool, wiki markup in $text should be escaped
3265
	 *
3266
	 * @since 1.21
3267
	 * @param PPFrame $frame The current frame, contains template arguments
3268
	 * @param string $function Function name
3269
	 * @param array $args Arguments to the function
3270
	 * @throws MWException
3271
	 * @return array
3272
	 */
3273
	public function callParserFunction( $frame, $function, array $args = [] ) {
3274
		global $wgContLang;
3275
3276
		# Case sensitive functions
3277
		if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3278
			$function = $this->mFunctionSynonyms[1][$function];
3279
		} else {
3280
			# Case insensitive functions
3281
			$function = $wgContLang->lc( $function );
3282
			if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3283
				$function = $this->mFunctionSynonyms[0][$function];
3284
			} else {
3285
				return [ 'found' => false ];
3286
			}
3287
		}
3288
3289
		list( $callback, $flags ) = $this->mFunctionHooks[$function];
3290
3291
		# Workaround for PHP bug 35229 and similar
3292
		if ( !is_callable( $callback ) ) {
3293
			throw new MWException( "Tag hook for $function is not callable\n" );
3294
		}
3295
3296
		$allArgs = [ &$this ];
3297
		if ( $flags & self::SFH_OBJECT_ARGS ) {
3298
			# Convert arguments to PPNodes and collect for appending to $allArgs
3299
			$funcArgs = [];
3300
			foreach ( $args as $k => $v ) {
3301
				if ( $v instanceof PPNode || $k === 0 ) {
3302
					$funcArgs[] = $v;
3303
				} else {
3304
					$funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3305
				}
3306
			}
3307
3308
			# Add a frame parameter, and pass the arguments as an array
3309
			$allArgs[] = $frame;
3310
			$allArgs[] = $funcArgs;
3311
		} else {
3312
			# Convert arguments to plain text and append to $allArgs
3313
			foreach ( $args as $k => $v ) {
3314
				if ( $v instanceof PPNode ) {
3315
					$allArgs[] = trim( $frame->expand( $v ) );
3316
				} elseif ( is_int( $k ) && $k >= 0 ) {
3317
					$allArgs[] = trim( $v );
3318
				} else {
3319
					$allArgs[] = trim( "$k=$v" );
3320
				}
3321
			}
3322
		}
3323
3324
		$result = call_user_func_array( $callback, $allArgs );
3325
3326
		# The interface for function hooks allows them to return a wikitext
3327
		# string or an array containing the string and any flags. This mungs
3328
		# things around to match what this method should return.
3329
		if ( !is_array( $result ) ) {
3330
			$result =[
3331
				'found' => true,
3332
				'text' => $result,
3333
			];
3334
		} else {
3335
			if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3336
				$result['text'] = $result[0];
3337
			}
3338
			unset( $result[0] );
3339
			$result += [
3340
				'found' => true,
3341
			];
3342
		}
3343
3344
		$noparse = true;
3345
		$preprocessFlags = 0;
3346
		if ( isset( $result['noparse'] ) ) {
3347
			$noparse = $result['noparse'];
3348
		}
3349
		if ( isset( $result['preprocessFlags'] ) ) {
3350
			$preprocessFlags = $result['preprocessFlags'];
3351
		}
3352
3353
		if ( !$noparse ) {
3354
			$result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3355
			$result['isChildObj'] = true;
3356
		}
3357
3358
		return $result;
3359
	}
3360
3361
	/**
3362
	 * Get the semi-parsed DOM representation of a template with a given title,
3363
	 * and its redirect destination title. Cached.
3364
	 *
3365
	 * @param Title $title
3366
	 *
3367
	 * @return array
3368
	 */
3369
	public function getTemplateDom( $title ) {
3370
		$cacheTitle = $title;
3371
		$titleText = $title->getPrefixedDBkey();
3372
3373
		if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3374
			list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3375
			$title = Title::makeTitle( $ns, $dbk );
3376
			$titleText = $title->getPrefixedDBkey();
3377
		}
3378
		if ( isset( $this->mTplDomCache[$titleText] ) ) {
3379
			return [ $this->mTplDomCache[$titleText], $title ];
3380
		}
3381
3382
		# Cache miss, go to the database
3383
		list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3384
3385
		if ( $text === false ) {
3386
			$this->mTplDomCache[$titleText] = false;
3387
			return [ false, $title ];
3388
		}
3389
3390
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3391
		$this->mTplDomCache[$titleText] = $dom;
3392
3393
		if ( !$title->equals( $cacheTitle ) ) {
3394
			$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3395
				[ $title->getNamespace(), $cdb = $title->getDBkey() ];
3396
		}
3397
3398
		return [ $dom, $title ];
3399
	}
3400
3401
	/**
3402
	 * Fetch the current revision of a given title. Note that the revision
3403
	 * (and even the title) may not exist in the database, so everything
3404
	 * contributing to the output of the parser should use this method
3405
	 * where possible, rather than getting the revisions themselves. This
3406
	 * method also caches its results, so using it benefits performance.
3407
	 *
3408
	 * @since 1.24
3409
	 * @param Title $title
3410
	 * @return Revision
3411
	 */
3412
	public function fetchCurrentRevisionOfTitle( $title ) {
3413
		$cacheKey = $title->getPrefixedDBkey();
3414
		if ( !$this->currentRevisionCache ) {
3415
			$this->currentRevisionCache = new MapCacheLRU( 100 );
3416
		}
3417
		if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3418
			$this->currentRevisionCache->set( $cacheKey,
3419
				// Defaults to Parser::statelessFetchRevision()
3420
				call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3421
			);
3422
		}
3423
		return $this->currentRevisionCache->get( $cacheKey );
3424
	}
3425
3426
	/**
3427
	 * Wrapper around Revision::newFromTitle to allow passing additional parameters
3428
	 * without passing them on to it.
3429
	 *
3430
	 * @since 1.24
3431
	 * @param Title $title
3432
	 * @param Parser|bool $parser
3433
	 * @return Revision
3434
	 */
3435
	public static function statelessFetchRevision( $title, $parser = false ) {
3436
		return Revision::newFromTitle( $title );
3437
	}
3438
3439
	/**
3440
	 * Fetch the unparsed text of a template and register a reference to it.
3441
	 * @param Title $title
3442
	 * @return array ( string or false, Title )
3443
	 */
3444
	public function fetchTemplateAndTitle( $title ) {
3445
		// Defaults to Parser::statelessFetchTemplate()
3446
		$templateCb = $this->mOptions->getTemplateCallback();
3447
		$stuff = call_user_func( $templateCb, $title, $this );
3448
		// We use U+007F DELETE to distinguish strip markers from regular text.
3449
		$text = $stuff['text'];
3450
		if ( is_string( $stuff['text'] ) ) {
3451
			$text = strtr( $text, "\x7f", "?" );
3452
		}
3453
		$finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3454
		if ( isset( $stuff['deps'] ) ) {
3455
			foreach ( $stuff['deps'] as $dep ) {
3456
				$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3457
				if ( $dep['title']->equals( $this->getTitle() ) ) {
3458
					// If we transclude ourselves, the final result
3459
					// will change based on the new version of the page
3460
					$this->mOutput->setFlag( 'vary-revision' );
3461
				}
3462
			}
3463
		}
3464
		return [ $text, $finalTitle ];
3465
	}
3466
3467
	/**
3468
	 * Fetch the unparsed text of a template and register a reference to it.
3469
	 * @param Title $title
3470
	 * @return string|bool
3471
	 */
3472
	public function fetchTemplate( $title ) {
3473
		return $this->fetchTemplateAndTitle( $title )[0];
3474
	}
3475
3476
	/**
3477
	 * Static function to get a template
3478
	 * Can be overridden via ParserOptions::setTemplateCallback().
3479
	 *
3480
	 * @param Title $title
3481
	 * @param bool|Parser $parser
3482
	 *
3483
	 * @return array
3484
	 */
3485
	public static function statelessFetchTemplate( $title, $parser = false ) {
3486
		$text = $skip = false;
3487
		$finalTitle = $title;
3488
		$deps = [];
3489
3490
		# Loop to fetch the article, with up to 1 redirect
3491
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3492
		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3493
			// @codingStandardsIgnoreEnd
3494
			# Give extensions a chance to select the revision instead
3495
			$id = false; # Assume current
3496
			Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3497
				[ $parser, $title, &$skip, &$id ] );
3498
3499
			if ( $skip ) {
3500
				$text = false;
3501
				$deps[] = [
3502
					'title' => $title,
3503
					'page_id' => $title->getArticleID(),
3504
					'rev_id' => null
3505
				];
3506
				break;
3507
			}
3508
			# Get the revision
3509
			if ( $id ) {
3510
				$rev = Revision::newFromId( $id );
3511
			} elseif ( $parser ) {
3512
				$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...
3513
			} else {
3514
				$rev = Revision::newFromTitle( $title );
3515
			}
3516
			$rev_id = $rev ? $rev->getId() : 0;
3517
			# If there is no current revision, there is no page
3518
			if ( $id === false && !$rev ) {
3519
				$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
3520
				$linkCache->addBadLinkObj( $title );
3521
			}
3522
3523
			$deps[] = [
3524
				'title' => $title,
3525
				'page_id' => $title->getArticleID(),
3526
				'rev_id' => $rev_id ];
3527
			if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3528
				# We fetched a rev from a different title; register it too...
3529
				$deps[] = [
3530
					'title' => $rev->getTitle(),
3531
					'page_id' => $rev->getPage(),
3532
					'rev_id' => $rev_id ];
3533
			}
3534
3535
			if ( $rev ) {
3536
				$content = $rev->getContent();
3537
				$text = $content ? $content->getWikitextForTransclusion() : null;
3538
3539
				if ( $text === false || $text === null ) {
3540
					$text = false;
3541
					break;
3542
				}
3543
			} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3544
				global $wgContLang;
3545
				$message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3546
				if ( !$message->exists() ) {
3547
					$text = false;
3548
					break;
3549
				}
3550
				$content = $message->content();
3551
				$text = $message->plain();
3552
			} else {
3553
				break;
3554
			}
3555
			if ( !$content ) {
3556
				break;
3557
			}
3558
			# Redirect?
3559
			$finalTitle = $title;
3560
			$title = $content->getRedirectTarget();
3561
		}
3562
		return [
3563
			'text' => $text,
3564
			'finalTitle' => $finalTitle,
3565
			'deps' => $deps ];
3566
	}
3567
3568
	/**
3569
	 * Fetch a file and its title and register a reference to it.
3570
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3571
	 * @param Title $title
3572
	 * @param array $options Array of options to RepoGroup::findFile
3573
	 * @return File|bool
3574
	 */
3575
	public function fetchFile( $title, $options = [] ) {
3576
		return $this->fetchFileAndTitle( $title, $options )[0];
3577
	}
3578
3579
	/**
3580
	 * Fetch a file and its title and register a reference to it.
3581
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3582
	 * @param Title $title
3583
	 * @param array $options Array of options to RepoGroup::findFile
3584
	 * @return array ( File or false, Title of file )
3585
	 */
3586
	public function fetchFileAndTitle( $title, $options = [] ) {
3587
		$file = $this->fetchFileNoRegister( $title, $options );
3588
3589
		$time = $file ? $file->getTimestamp() : false;
3590
		$sha1 = $file ? $file->getSha1() : false;
3591
		# Register the file as a dependency...
3592
		$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3593
		if ( $file && !$title->equals( $file->getTitle() ) ) {
3594
			# Update fetched file title
3595
			$title = $file->getTitle();
3596
			$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3597
		}
3598
		return [ $file, $title ];
3599
	}
3600
3601
	/**
3602
	 * Helper function for fetchFileAndTitle.
3603
	 *
3604
	 * Also useful if you need to fetch a file but not use it yet,
3605
	 * for example to get the file's handler.
3606
	 *
3607
	 * @param Title $title
3608
	 * @param array $options Array of options to RepoGroup::findFile
3609
	 * @return File|bool
3610
	 */
3611
	protected function fetchFileNoRegister( $title, $options = [] ) {
3612
		if ( isset( $options['broken'] ) ) {
3613
			$file = false; // broken thumbnail forced by hook
3614
		} elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3615
			$file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3616
		} else { // get by (name,timestamp)
3617
			$file = wfFindFile( $title, $options );
3618
		}
3619
		return $file;
3620
	}
3621
3622
	/**
3623
	 * Transclude an interwiki link.
3624
	 *
3625
	 * @param Title $title
3626
	 * @param string $action
3627
	 *
3628
	 * @return string
3629
	 */
3630
	public function interwikiTransclude( $title, $action ) {
3631
		global $wgEnableScaryTranscluding;
3632
3633
		if ( !$wgEnableScaryTranscluding ) {
3634
			return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3635
		}
3636
3637
		$url = $title->getFullURL( [ 'action' => $action ] );
3638
3639
		if ( strlen( $url ) > 255 ) {
3640
			return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3641
		}
3642
		return $this->fetchScaryTemplateMaybeFromCache( $url );
3643
	}
3644
3645
	/**
3646
	 * @param string $url
3647
	 * @return mixed|string
3648
	 */
3649
	public function fetchScaryTemplateMaybeFromCache( $url ) {
3650
		global $wgTranscludeCacheExpiry;
3651
		$dbr = wfGetDB( DB_SLAVE );
3652
		$tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3653
		$obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3654
				[ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
0 ignored issues
show
Security Bug introduced by
It seems like $tsCond defined by $dbr->timestamp(time() -...gTranscludeCacheExpiry) on line 3652 can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
3655
		if ( $obj ) {
3656
			return $obj->tc_contents;
3657
		}
3658
3659
		$req = MWHttpRequest::factory( $url, [], __METHOD__ );
3660
		$status = $req->execute(); // Status object
3661
		if ( $status->isOK() ) {
3662
			$text = $req->getContent();
3663
		} elseif ( $req->getStatus() != 200 ) {
3664
			// Though we failed to fetch the content, this status is useless.
3665
			return wfMessage( 'scarytranscludefailed-httpstatus' )
3666
				->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3667
		} else {
3668
			return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3669
		}
3670
3671
		$dbw = wfGetDB( DB_MASTER );
3672
		$dbw->replace( 'transcache', [ 'tc_url' ], [
3673
			'tc_url' => $url,
3674
			'tc_time' => $dbw->timestamp( time() ),
3675
			'tc_contents' => $text
3676
		] );
3677
		return $text;
3678
	}
3679
3680
	/**
3681
	 * Triple brace replacement -- used for template arguments
3682
	 * @private
3683
	 *
3684
	 * @param array $piece
3685
	 * @param PPFrame $frame
3686
	 *
3687
	 * @return array
3688
	 */
3689
	public function argSubstitution( $piece, $frame ) {
3690
3691
		$error = false;
3692
		$parts = $piece['parts'];
3693
		$nameWithSpaces = $frame->expand( $piece['title'] );
3694
		$argName = trim( $nameWithSpaces );
3695
		$object = false;
3696
		$text = $frame->getArgument( $argName );
3697
		if ( $text === false && $parts->getLength() > 0
3698
			&& ( $this->ot['html']
3699
				|| $this->ot['pre']
3700
				|| ( $this->ot['wiki'] && $frame->isTemplate() )
3701
			)
3702
		) {
3703
			# No match in frame, use the supplied default
3704
			$object = $parts->item( 0 )->getChildren();
3705
		}
3706
		if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3707
			$error = '<!-- WARNING: argument omitted, expansion size too large -->';
3708
			$this->limitationWarn( 'post-expand-template-argument' );
3709
		}
3710
3711
		if ( $text === false && $object === false ) {
3712
			# No match anywhere
3713
			$object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3714
		}
3715
		if ( $error !== false ) {
3716
			$text .= $error;
3717
		}
3718
		if ( $object !== false ) {
3719
			$ret = [ 'object' => $object ];
3720
		} else {
3721
			$ret = [ 'text' => $text ];
3722
		}
3723
3724
		return $ret;
3725
	}
3726
3727
	/**
3728
	 * Return the text to be used for a given extension tag.
3729
	 * This is the ghost of strip().
3730
	 *
3731
	 * @param array $params Associative array of parameters:
3732
	 *     name       PPNode for the tag name
3733
	 *     attr       PPNode for unparsed text where tag attributes are thought to be
3734
	 *     attributes Optional associative array of parsed attributes
3735
	 *     inner      Contents of extension element
3736
	 *     noClose    Original text did not have a close tag
3737
	 * @param PPFrame $frame
3738
	 *
3739
	 * @throws MWException
3740
	 * @return string
3741
	 */
3742
	public function extensionSubstitution( $params, $frame ) {
3743
		$name = $frame->expand( $params['name'] );
3744
		$attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3745
		$content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3746
		$marker = self::MARKER_PREFIX . "-$name-"
3747
			. sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3748
3749
		$isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3750
			( $this->ot['html'] || $this->ot['pre'] );
3751
		if ( $isFunctionTag ) {
3752
			$markerType = 'none';
3753
		} else {
3754
			$markerType = 'general';
3755
		}
3756
		if ( $this->ot['html'] || $isFunctionTag ) {
3757
			$name = strtolower( $name );
3758
			$attributes = Sanitizer::decodeTagAttributes( $attrText );
3759
			if ( isset( $params['attributes'] ) ) {
3760
				$attributes = $attributes + $params['attributes'];
3761
			}
3762
3763
			if ( isset( $this->mTagHooks[$name] ) ) {
3764
				# Workaround for PHP bug 35229 and similar
3765
				if ( !is_callable( $this->mTagHooks[$name] ) ) {
3766
					throw new MWException( "Tag hook for $name is not callable\n" );
3767
				}
3768
				$output = call_user_func_array( $this->mTagHooks[$name],
3769
					[ $content, $attributes, $this, $frame ] );
3770
			} elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3771
				list( $callback, ) = $this->mFunctionTagHooks[$name];
3772
				if ( !is_callable( $callback ) ) {
3773
					throw new MWException( "Tag hook for $name is not callable\n" );
3774
				}
3775
3776
				$output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] );
3777
			} else {
3778
				$output = '<span class="error">Invalid tag extension name: ' .
3779
					htmlspecialchars( $name ) . '</span>';
3780
			}
3781
3782
			if ( is_array( $output ) ) {
3783
				# Extract flags to local scope (to override $markerType)
3784
				$flags = $output;
3785
				$output = $flags[0];
3786
				unset( $flags[0] );
3787
				extract( $flags );
3788
			}
3789
		} else {
3790
			if ( is_null( $attrText ) ) {
3791
				$attrText = '';
3792
			}
3793
			if ( isset( $params['attributes'] ) ) {
3794
				foreach ( $params['attributes'] as $attrName => $attrValue ) {
3795
					$attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3796
						htmlspecialchars( $attrValue ) . '"';
3797
				}
3798
			}
3799
			if ( $content === null ) {
3800
				$output = "<$name$attrText/>";
3801
			} else {
3802
				$close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3803
				$output = "<$name$attrText>$content$close";
3804
			}
3805
		}
3806
3807
		if ( $markerType === 'none' ) {
3808
			return $output;
3809
		} elseif ( $markerType === 'nowiki' ) {
3810
			$this->mStripState->addNoWiki( $marker, $output );
3811
		} elseif ( $markerType === 'general' ) {
3812
			$this->mStripState->addGeneral( $marker, $output );
3813
		} else {
3814
			throw new MWException( __METHOD__ . ': invalid marker type' );
3815
		}
3816
		return $marker;
3817
	}
3818
3819
	/**
3820
	 * Increment an include size counter
3821
	 *
3822
	 * @param string $type The type of expansion
3823
	 * @param int $size The size of the text
3824
	 * @return bool False if this inclusion would take it over the maximum, true otherwise
3825
	 */
3826
	public function incrementIncludeSize( $type, $size ) {
3827
		if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3828
			return false;
3829
		} else {
3830
			$this->mIncludeSizes[$type] += $size;
3831
			return true;
3832
		}
3833
	}
3834
3835
	/**
3836
	 * Increment the expensive function count
3837
	 *
3838
	 * @return bool False if the limit has been exceeded
3839
	 */
3840
	public function incrementExpensiveFunctionCount() {
3841
		$this->mExpensiveFunctionCount++;
3842
		return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3843
	}
3844
3845
	/**
3846
	 * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
3847
	 * Fills $this->mDoubleUnderscores, returns the modified text
3848
	 *
3849
	 * @param string $text
3850
	 *
3851
	 * @return string
3852
	 */
3853
	public function doDoubleUnderscore( $text ) {
3854
3855
		# The position of __TOC__ needs to be recorded
3856
		$mw = MagicWord::get( 'toc' );
3857
		if ( $mw->match( $text ) ) {
3858
			$this->mShowToc = true;
3859
			$this->mForceTocPosition = true;
3860
3861
			# Set a placeholder. At the end we'll fill it in with the TOC.
3862
			$text = $mw->replace( '<!--MWTOC-->', $text, 1 );
3863
3864
			# Only keep the first one.
3865
			$text = $mw->replace( '', $text );
3866
		}
3867
3868
		# Now match and remove the rest of them
3869
		$mwa = MagicWord::getDoubleUnderscoreArray();
3870
		$this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3871
3872
		if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3873
			$this->mOutput->mNoGallery = true;
3874
		}
3875
		if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3876
			$this->mShowToc = false;
3877
		}
3878
		if ( isset( $this->mDoubleUnderscores['hiddencat'] )
3879
			&& $this->mTitle->getNamespace() == NS_CATEGORY
3880
		) {
3881
			$this->addTrackingCategory( 'hidden-category-category' );
3882
		}
3883
		# (bug 8068) Allow control over whether robots index a page.
3884
		# @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here!  This
3885
		# is not desirable, the last one on the page should win.
3886 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
3887
			$this->mOutput->setIndexPolicy( 'noindex' );
3888
			$this->addTrackingCategory( 'noindex-category' );
3889
		}
3890 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
3891
			$this->mOutput->setIndexPolicy( 'index' );
3892
			$this->addTrackingCategory( 'index-category' );
3893
		}
3894
3895
		# Cache all double underscores in the database
3896
		foreach ( $this->mDoubleUnderscores as $key => $val ) {
3897
			$this->mOutput->setProperty( $key, '' );
3898
		}
3899
3900
		return $text;
3901
	}
3902
3903
	/**
3904
	 * @see ParserOutput::addTrackingCategory()
3905
	 * @param string $msg Message key
3906
	 * @return bool Whether the addition was successful
3907
	 */
3908
	public function addTrackingCategory( $msg ) {
3909
		return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
3910
	}
3911
3912
	/**
3913
	 * This function accomplishes several tasks:
3914
	 * 1) Auto-number headings if that option is enabled
3915
	 * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
3916
	 * 3) Add a Table of contents on the top for users who have enabled the option
3917
	 * 4) Auto-anchor headings
3918
	 *
3919
	 * It loops through all headlines, collects the necessary data, then splits up the
3920
	 * string and re-inserts the newly formatted headlines.
3921
	 *
3922
	 * @param string $text
3923
	 * @param string $origText Original, untouched wikitext
3924
	 * @param bool $isMain
3925
	 * @return mixed|string
3926
	 * @private
3927
	 */
3928
	public function formatHeadings( $text, $origText, $isMain = true ) {
3929
		global $wgMaxTocLevel, $wgExperimentalHtmlIds;
3930
3931
		# Inhibit editsection links if requested in the page
3932
		if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
3933
			$maybeShowEditLink = $showEditLink = false;
3934
		} else {
3935
			$maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
3936
			$showEditLink = $this->mOptions->getEditSection();
3937
		}
3938
		if ( $showEditLink ) {
3939
			$this->mOutput->setEditSectionTokens( true );
3940
		}
3941
3942
		# Get all headlines for numbering them and adding funky stuff like [edit]
3943
		# links - this is for later, but we need the number of headlines right now
3944
		$matches = [];
3945
		$numMatches = preg_match_all(
3946
			'/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
3947
			$text,
3948
			$matches
3949
		);
3950
3951
		# if there are fewer than 4 headlines in the article, do not show TOC
3952
		# unless it's been explicitly enabled.
3953
		$enoughToc = $this->mShowToc &&
3954
			( ( $numMatches >= 4 ) || $this->mForceTocPosition );
3955
3956
		# Allow user to stipulate that a page should have a "new section"
3957
		# link added via __NEWSECTIONLINK__
3958
		if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
3959
			$this->mOutput->setNewSection( true );
3960
		}
3961
3962
		# Allow user to remove the "new section"
3963
		# link via __NONEWSECTIONLINK__
3964
		if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
3965
			$this->mOutput->hideNewSection( true );
3966
		}
3967
3968
		# if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
3969
		# override above conditions and always show TOC above first header
3970
		if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
3971
			$this->mShowToc = true;
3972
			$enoughToc = true;
3973
		}
3974
3975
		# headline counter
3976
		$headlineCount = 0;
3977
		$numVisible = 0;
3978
3979
		# Ugh .. the TOC should have neat indentation levels which can be
3980
		# passed to the skin functions. These are determined here
3981
		$toc = '';
3982
		$full = '';
3983
		$head = [];
3984
		$sublevelCount = [];
3985
		$levelCount = [];
3986
		$level = 0;
3987
		$prevlevel = 0;
3988
		$toclevel = 0;
3989
		$prevtoclevel = 0;
3990
		$markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
3991
		$baseTitleText = $this->mTitle->getPrefixedDBkey();
3992
		$oldType = $this->mOutputType;
3993
		$this->setOutputType( self::OT_WIKI );
3994
		$frame = $this->getPreprocessor()->newFrame();
3995
		$root = $this->preprocessToDom( $origText );
3996
		$node = $root->getFirstChild();
3997
		$byteOffset = 0;
3998
		$tocraw = [];
3999
		$refers = [];
4000
4001
		$headlines = $numMatches !== false ? $matches[3] : [];
4002
4003
		foreach ( $headlines as $headline ) {
4004
			$isTemplate = false;
4005
			$titleText = false;
4006
			$sectionIndex = false;
4007
			$numbering = '';
4008
			$markerMatches = [];
4009
			if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4010
				$serial = $markerMatches[1];
4011
				list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4012
				$isTemplate = ( $titleText != $baseTitleText );
4013
				$headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4014
			}
4015
4016
			if ( $toclevel ) {
4017
				$prevlevel = $level;
4018
			}
4019
			$level = $matches[1][$headlineCount];
4020
4021
			if ( $level > $prevlevel ) {
4022
				# Increase TOC level
4023
				$toclevel++;
4024
				$sublevelCount[$toclevel] = 0;
4025
				if ( $toclevel < $wgMaxTocLevel ) {
4026
					$prevtoclevel = $toclevel;
4027
					$toc .= Linker::tocIndent();
4028
					$numVisible++;
4029
				}
4030
			} elseif ( $level < $prevlevel && $toclevel > 1 ) {
4031
				# Decrease TOC level, find level to jump to
4032
4033
				for ( $i = $toclevel; $i > 0; $i-- ) {
4034
					if ( $levelCount[$i] == $level ) {
4035
						# Found last matching level
4036
						$toclevel = $i;
4037
						break;
4038
					} elseif ( $levelCount[$i] < $level ) {
4039
						# Found first matching level below current level
4040
						$toclevel = $i + 1;
4041
						break;
4042
					}
4043
				}
4044
				if ( $i == 0 ) {
4045
					$toclevel = 1;
4046
				}
4047
				if ( $toclevel < $wgMaxTocLevel ) {
4048
					if ( $prevtoclevel < $wgMaxTocLevel ) {
4049
						# Unindent only if the previous toc level was shown :p
4050
						$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4051
						$prevtoclevel = $toclevel;
4052
					} else {
4053
						$toc .= Linker::tocLineEnd();
4054
					}
4055
				}
4056
			} else {
4057
				# No change in level, end TOC line
4058
				if ( $toclevel < $wgMaxTocLevel ) {
4059
					$toc .= Linker::tocLineEnd();
4060
				}
4061
			}
4062
4063
			$levelCount[$toclevel] = $level;
4064
4065
			# count number of headlines for each level
4066
			$sublevelCount[$toclevel]++;
4067
			$dot = 0;
4068
			for ( $i = 1; $i <= $toclevel; $i++ ) {
4069
				if ( !empty( $sublevelCount[$i] ) ) {
4070
					if ( $dot ) {
4071
						$numbering .= '.';
4072
					}
4073
					$numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4074
					$dot = 1;
4075
				}
4076
			}
4077
4078
			# The safe header is a version of the header text safe to use for links
4079
4080
			# Remove link placeholders by the link text.
4081
			#     <!--LINK number-->
4082
			# turns into
4083
			#     link text with suffix
4084
			# Do this before unstrip since link text can contain strip markers
4085
			$safeHeadline = $this->replaceLinkHoldersText( $headline );
4086
4087
			# Avoid insertion of weird stuff like <math> by expanding the relevant sections
4088
			$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4089
4090
			# Strip out HTML (first regex removes any tag not allowed)
4091
			# Allowed tags are:
4092
			# * <sup> and <sub> (bug 8393)
4093
			# * <i> (bug 26375)
4094
			# * <b> (r105284)
4095
			# * <bdi> (bug 72884)
4096
			# * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4097
			# We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4098
			# to allow setting directionality in toc items.
4099
			$tocline = preg_replace(
4100
				[
4101
					'#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4102
					'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4103
				],
4104
				[ '', '<$1>' ],
4105
				$safeHeadline
4106
			);
4107
4108
			# Strip '<span></span>', which is the result from the above if
4109
			# <span id="foo"></span> is used to produce an additional anchor
4110
			# for a section.
4111
			$tocline = str_replace( '<span></span>', '', $tocline );
4112
4113
			$tocline = trim( $tocline );
4114
4115
			# For the anchor, strip out HTML-y stuff period
4116
			$safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4117
			$safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4118
4119
			# Save headline for section edit hint before it's escaped
4120
			$headlineHint = $safeHeadline;
4121
4122
			if ( $wgExperimentalHtmlIds ) {
4123
				# For reverse compatibility, provide an id that's
4124
				# HTML4-compatible, like we used to.
4125
				# It may be worth noting, academically, that it's possible for
4126
				# the legacy anchor to conflict with a non-legacy headline
4127
				# anchor on the page.  In this case likely the "correct" thing
4128
				# would be to either drop the legacy anchors or make sure
4129
				# they're numbered first.  However, this would require people
4130
				# to type in section names like "abc_.D7.93.D7.90.D7.A4"
4131
				# manually, so let's not bother worrying about it.
4132
				$legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4133
					[ 'noninitial', 'legacy' ] );
4134
				$safeHeadline = Sanitizer::escapeId( $safeHeadline );
4135
4136
				if ( $legacyHeadline == $safeHeadline ) {
4137
					# No reason to have both (in fact, we can't)
4138
					$legacyHeadline = false;
4139
				}
4140
			} else {
4141
				$legacyHeadline = false;
4142
				$safeHeadline = Sanitizer::escapeId( $safeHeadline,
4143
					'noninitial' );
4144
			}
4145
4146
			# HTML names must be case-insensitively unique (bug 10721).
4147
			# This does not apply to Unicode characters per
4148
			# http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4149
			# @todo FIXME: We may be changing them depending on the current locale.
4150
			$arrayKey = strtolower( $safeHeadline );
4151
			if ( $legacyHeadline === false ) {
4152
				$legacyArrayKey = false;
4153
			} else {
4154
				$legacyArrayKey = strtolower( $legacyHeadline );
4155
			}
4156
4157
			# Create the anchor for linking from the TOC to the section
4158
			$anchor = $safeHeadline;
4159
			$legacyAnchor = $legacyHeadline;
4160 View Code Duplication
			if ( isset( $refers[$arrayKey] ) ) {
4161
				// @codingStandardsIgnoreStart
4162
				for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4163
				// @codingStandardsIgnoreEnd
4164
				$anchor .= "_$i";
4165
				$refers["${arrayKey}_$i"] = true;
4166
			} else {
4167
				$refers[$arrayKey] = true;
4168
			}
4169 View Code Duplication
			if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4170
				// @codingStandardsIgnoreStart
4171
				for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4172
				// @codingStandardsIgnoreEnd
4173
				$legacyAnchor .= "_$i";
4174
				$refers["${legacyArrayKey}_$i"] = true;
4175
			} else {
4176
				$refers[$legacyArrayKey] = true;
4177
			}
4178
4179
			# Don't number the heading if it is the only one (looks silly)
4180
			if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4181
				# the two are different if the line contains a link
4182
				$headline = Html::element(
4183
					'span',
4184
					[ 'class' => 'mw-headline-number' ],
4185
					$numbering
4186
				) . ' ' . $headline;
4187
			}
4188
4189
			if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4190
				$toc .= Linker::tocLine( $anchor, $tocline,
4191
					$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4192
			}
4193
4194
			# Add the section to the section tree
4195
			# Find the DOM node for this header
4196
			$noOffset = ( $isTemplate || $sectionIndex === false );
4197
			while ( $node && !$noOffset ) {
4198
				if ( $node->getName() === 'h' ) {
4199
					$bits = $node->splitHeading();
4200
					if ( $bits['i'] == $sectionIndex ) {
4201
						break;
4202
					}
4203
				}
4204
				$byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4205
					$frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4206
				$node = $node->getNextSibling();
4207
			}
4208
			$tocraw[] = [
4209
				'toclevel' => $toclevel,
4210
				'level' => $level,
4211
				'line' => $tocline,
4212
				'number' => $numbering,
4213
				'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4214
				'fromtitle' => $titleText,
4215
				'byteoffset' => ( $noOffset ? null : $byteOffset ),
4216
				'anchor' => $anchor,
4217
			];
4218
4219
			# give headline the correct <h#> tag
4220
			if ( $maybeShowEditLink && $sectionIndex !== false ) {
4221
				// Output edit section links as markers with styles that can be customized by skins
4222
				if ( $isTemplate ) {
4223
					# Put a T flag in the section identifier, to indicate to extractSections()
4224
					# that sections inside <includeonly> should be counted.
4225
					$editsectionPage = $titleText;
4226
					$editsectionSection = "T-$sectionIndex";
4227
					$editsectionContent = null;
4228
				} else {
4229
					$editsectionPage = $this->mTitle->getPrefixedText();
4230
					$editsectionSection = $sectionIndex;
4231
					$editsectionContent = $headlineHint;
4232
				}
4233
				// We use a bit of pesudo-xml for editsection markers. The
4234
				// language converter is run later on. Using a UNIQ style marker
4235
				// leads to the converter screwing up the tokens when it
4236
				// converts stuff. And trying to insert strip tags fails too. At
4237
				// this point all real inputted tags have already been escaped,
4238
				// so we don't have to worry about a user trying to input one of
4239
				// these markers directly. We use a page and section attribute
4240
				// to stop the language converter from converting these
4241
				// important bits of data, but put the headline hint inside a
4242
				// content block because the language converter is supposed to
4243
				// be able to convert that piece of data.
4244
				// Gets replaced with html in ParserOutput::getText
4245
				$editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4246
				$editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4247
				if ( $editsectionContent !== null ) {
4248
					$editlink .= '>' . $editsectionContent . '</mw:editsection>';
4249
				} else {
4250
					$editlink .= '/>';
4251
				}
4252
			} else {
4253
				$editlink = '';
4254
			}
4255
			$head[$headlineCount] = Linker::makeHeadline( $level,
4256
				$matches['attrib'][$headlineCount], $anchor, $headline,
4257
				$editlink, $legacyAnchor );
4258
4259
			$headlineCount++;
4260
		}
4261
4262
		$this->setOutputType( $oldType );
4263
4264
		# Never ever show TOC if no headers
4265
		if ( $numVisible < 1 ) {
4266
			$enoughToc = false;
4267
		}
4268
4269
		if ( $enoughToc ) {
4270
			if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4271
				$toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4272
			}
4273
			$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4274
			$this->mOutput->setTOCHTML( $toc );
4275
			$toc = self::TOC_START . $toc . self::TOC_END;
4276
			$this->mOutput->addModules( 'mediawiki.toc' );
4277
		}
4278
4279
		if ( $isMain ) {
4280
			$this->mOutput->setSections( $tocraw );
4281
		}
4282
4283
		# split up and insert constructed headlines
4284
		$blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4285
		$i = 0;
4286
4287
		// build an array of document sections
4288
		$sections = [];
4289
		foreach ( $blocks as $block ) {
4290
			// $head is zero-based, sections aren't.
4291
			if ( empty( $head[$i - 1] ) ) {
4292
				$sections[$i] = $block;
4293
			} else {
4294
				$sections[$i] = $head[$i - 1] . $block;
4295
			}
4296
4297
			/**
4298
			 * Send a hook, one per section.
4299
			 * The idea here is to be able to make section-level DIVs, but to do so in a
4300
			 * lower-impact, more correct way than r50769
4301
			 *
4302
			 * $this : caller
4303
			 * $section : the section number
4304
			 * &$sectionContent : ref to the content of the section
4305
			 * $showEditLinks : boolean describing whether this section has an edit link
4306
			 */
4307
			Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4308
4309
			$i++;
4310
		}
4311
4312
		if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4313
			// append the TOC at the beginning
4314
			// Top anchor now in skin
4315
			$sections[0] = $sections[0] . $toc . "\n";
4316
		}
4317
4318
		$full .= implode( '', $sections );
4319
4320
		if ( $this->mForceTocPosition ) {
4321
			return str_replace( '<!--MWTOC-->', $toc, $full );
4322
		} else {
4323
			return $full;
4324
		}
4325
	}
4326
4327
	/**
4328
	 * Transform wiki markup when saving a page by doing "\r\n" -> "\n"
4329
	 * conversion, substituting signatures, {{subst:}} templates, etc.
4330
	 *
4331
	 * @param string $text The text to transform
4332
	 * @param Title $title The Title object for the current article
4333
	 * @param User $user The User object describing the current user
4334
	 * @param ParserOptions $options Parsing options
4335
	 * @param bool $clearState Whether to clear the parser state first
4336
	 * @return string The altered wiki markup
4337
	 */
4338
	public function preSaveTransform( $text, Title $title, User $user,
4339
		ParserOptions $options, $clearState = true
4340
	) {
4341
		if ( $clearState ) {
4342
			$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...
4343
		}
4344
		$this->startParse( $title, $options, self::OT_WIKI, $clearState );
4345
		$this->setUser( $user );
4346
4347
		$pairs = [
4348
			"\r\n" => "\n",
4349
			"\r" => "\n",
4350
		];
4351
		$text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4352
		if ( $options->getPreSaveTransform() ) {
4353
			$text = $this->pstPass2( $text, $user );
4354
		}
4355
		$text = $this->mStripState->unstripBoth( $text );
4356
4357
		$this->setUser( null ); # Reset
4358
4359
		return $text;
4360
	}
4361
4362
	/**
4363
	 * Pre-save transform helper function
4364
	 *
4365
	 * @param string $text
4366
	 * @param User $user
4367
	 *
4368
	 * @return string
4369
	 */
4370
	private function pstPass2( $text, $user ) {
4371
		global $wgContLang;
4372
4373
		# Note: This is the timestamp saved as hardcoded wikitext to
4374
		# the database, we use $wgContLang here in order to give
4375
		# everyone the same signature and use the default one rather
4376
		# than the one selected in each user's preferences.
4377
		# (see also bug 12815)
4378
		$ts = $this->mOptions->getTimestamp();
4379
		$timestamp = MWTimestamp::getLocalInstance( $ts );
4380
		$ts = $timestamp->format( 'YmdHis' );
4381
		$tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4382
4383
		$d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4384
4385
		# Variable replacement
4386
		# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4387
		$text = $this->replaceVariables( $text );
4388
4389
		# This works almost by chance, as the replaceVariables are done before the getUserSig(),
4390
		# which may corrupt this parser instance via its wfMessage()->text() call-
4391
4392
		# Signatures
4393
		$sigText = $this->getUserSig( $user );
4394
		$text = strtr( $text, [
4395
			'~~~~~' => $d,
4396
			'~~~~' => "$sigText $d",
4397
			'~~~' => $sigText
4398
		] );
4399
4400
		# Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4401
		$tc = '[' . Title::legalChars() . ']';
4402
		$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4403
4404
		// [[ns:page (context)|]]
4405
		$p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4406
		// [[ns:page(context)|]] (double-width brackets, added in r40257)
4407
		$p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4408
		// [[ns:page (context), context|]] (using either single or double-width comma)
4409
		$p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4410
		// [[|page]] (reverse pipe trick: add context from page title)
4411
		$p2 = "/\[\[\\|($tc+)]]/";
4412
4413
		# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4414
		$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4415
		$text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4416
		$text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4417
4418
		$t = $this->mTitle->getText();
4419
		$m = [];
4420
		if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4421
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4422
		} elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4423
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4424
		} else {
4425
			# if there's no context, don't bother duplicating the title
4426
			$text = preg_replace( $p2, '[[\\1]]', $text );
4427
		}
4428
4429
		# Trim trailing whitespace
4430
		$text = rtrim( $text );
4431
4432
		return $text;
4433
	}
4434
4435
	/**
4436
	 * Fetch the user's signature text, if any, and normalize to
4437
	 * validated, ready-to-insert wikitext.
4438
	 * If you have pre-fetched the nickname or the fancySig option, you can
4439
	 * specify them here to save a database query.
4440
	 * Do not reuse this parser instance after calling getUserSig(),
4441
	 * as it may have changed if it's the $wgParser.
4442
	 *
4443
	 * @param User $user
4444
	 * @param string|bool $nickname Nickname to use or false to use user's default nickname
4445
	 * @param bool|null $fancySig whether the nicknname is the complete signature
4446
	 *    or null to use default value
4447
	 * @return string
4448
	 */
4449
	public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4450
		global $wgMaxSigChars;
4451
4452
		$username = $user->getName();
4453
4454
		# If not given, retrieve from the user object.
4455
		if ( $nickname === false ) {
4456
			$nickname = $user->getOption( 'nickname' );
4457
		}
4458
4459
		if ( is_null( $fancySig ) ) {
4460
			$fancySig = $user->getBoolOption( 'fancysig' );
4461
		}
4462
4463
		$nickname = $nickname == null ? $username : $nickname;
4464
4465
		if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4466
			$nickname = $username;
4467
			wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4468
		} elseif ( $fancySig !== false ) {
4469
			# Sig. might contain markup; validate this
4470
			if ( $this->validateSig( $nickname ) !== false ) {
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4463 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...
4471
				# Validated; clean up (if needed) and return it
4472
				return $this->cleanSig( $nickname, true );
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4463 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...
4473
			} else {
4474
				# Failed to validate; fall back to the default
4475
				$nickname = $username;
4476
				wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4477
			}
4478
		}
4479
4480
		# Make sure nickname doesnt get a sig in a sig
4481
		$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...
4482
4483
		# If we're still here, make it a link to the user page
4484
		$userText = wfEscapeWikiText( $username );
4485
		$nickText = wfEscapeWikiText( $nickname );
4486
		$msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4487
4488
		return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4489
			->title( $this->getTitle() )->text();
4490
	}
4491
4492
	/**
4493
	 * Check that the user's signature contains no bad XML
4494
	 *
4495
	 * @param string $text
4496
	 * @return string|bool An expanded string, or false if invalid.
4497
	 */
4498
	public function validateSig( $text ) {
4499
		return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4500
	}
4501
4502
	/**
4503
	 * Clean up signature text
4504
	 *
4505
	 * 1) Strip 3, 4 or 5 tildes out of signatures @see cleanSigInSig
4506
	 * 2) Substitute all transclusions
4507
	 *
4508
	 * @param string $text
4509
	 * @param bool $parsing Whether we're cleaning (preferences save) or parsing
4510
	 * @return string Signature text
4511
	 */
4512
	public function cleanSig( $text, $parsing = false ) {
4513
		if ( !$parsing ) {
4514
			global $wgTitle;
4515
			$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...
4516
			$this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4517
		}
4518
4519
		# Option to disable this feature
4520
		if ( !$this->mOptions->getCleanSignatures() ) {
4521
			return $text;
4522
		}
4523
4524
		# @todo FIXME: Regex doesn't respect extension tags or nowiki
4525
		#  => Move this logic to braceSubstitution()
4526
		$substWord = MagicWord::get( 'subst' );
4527
		$substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4528
		$substText = '{{' . $substWord->getSynonym( 0 );
4529
4530
		$text = preg_replace( $substRegex, $substText, $text );
4531
		$text = self::cleanSigInSig( $text );
4532
		$dom = $this->preprocessToDom( $text );
4533
		$frame = $this->getPreprocessor()->newFrame();
4534
		$text = $frame->expand( $dom );
4535
4536
		if ( !$parsing ) {
4537
			$text = $this->mStripState->unstripBoth( $text );
4538
		}
4539
4540
		return $text;
4541
	}
4542
4543
	/**
4544
	 * Strip 3, 4 or 5 tildes out of signatures.
4545
	 *
4546
	 * @param string $text
4547
	 * @return string Signature text with /~{3,5}/ removed
4548
	 */
4549
	public static function cleanSigInSig( $text ) {
4550
		$text = preg_replace( '/~{3,5}/', '', $text );
4551
		return $text;
4552
	}
4553
4554
	/**
4555
	 * Set up some variables which are usually set up in parse()
4556
	 * so that an external function can call some class members with confidence
4557
	 *
4558
	 * @param Title|null $title
4559
	 * @param ParserOptions $options
4560
	 * @param int $outputType
4561
	 * @param bool $clearState
4562
	 */
4563
	public function startExternalParse( Title $title = null, ParserOptions $options,
4564
		$outputType, $clearState = true
4565
	) {
4566
		$this->startParse( $title, $options, $outputType, $clearState );
4567
	}
4568
4569
	/**
4570
	 * @param Title|null $title
4571
	 * @param ParserOptions $options
4572
	 * @param int $outputType
4573
	 * @param bool $clearState
4574
	 */
4575
	private function startParse( Title $title = null, ParserOptions $options,
4576
		$outputType, $clearState = true
4577
	) {
4578
		$this->setTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 4575 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...
4579
		$this->mOptions = $options;
4580
		$this->setOutputType( $outputType );
4581
		if ( $clearState ) {
4582
			$this->clearState();
4583
		}
4584
	}
4585
4586
	/**
4587
	 * Wrapper for preprocess()
4588
	 *
4589
	 * @param string $text The text to preprocess
4590
	 * @param ParserOptions $options Options
4591
	 * @param Title|null $title Title object or null to use $wgTitle
4592
	 * @return string
4593
	 */
4594
	public function transformMsg( $text, $options, $title = null ) {
4595
		static $executing = false;
4596
4597
		# Guard against infinite recursion
4598
		if ( $executing ) {
4599
			return $text;
4600
		}
4601
		$executing = true;
4602
4603
		if ( !$title ) {
4604
			global $wgTitle;
4605
			$title = $wgTitle;
4606
		}
4607
4608
		$text = $this->preprocess( $text, $title, $options );
4609
4610
		$executing = false;
4611
		return $text;
4612
	}
4613
4614
	/**
4615
	 * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
4616
	 * The callback should have the following form:
4617
	 *    function myParserHook( $text, $params, $parser, $frame ) { ... }
4618
	 *
4619
	 * Transform and return $text. Use $parser for any required context, e.g. use
4620
	 * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
4621
	 *
4622
	 * Hooks may return extended information by returning an array, of which the
4623
	 * first numbered element (index 0) must be the return string, and all other
4624
	 * entries are extracted into local variables within an internal function
4625
	 * in the Parser class.
4626
	 *
4627
	 * This interface (introduced r61913) appears to be undocumented, but
4628
	 * 'markerType' is used by some core tag hooks to override which strip
4629
	 * array their results are placed in. **Use great caution if attempting
4630
	 * this interface, as it is not documented and injudicious use could smash
4631
	 * private variables.**
4632
	 *
4633
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4634
	 * @param callable $callback The callback function (and object) to use for the tag
4635
	 * @throws MWException
4636
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4637
	 */
4638 View Code Duplication
	public function setHook( $tag, $callback ) {
4639
		$tag = strtolower( $tag );
4640
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4641
			throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4642
		}
4643
		$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4644
		$this->mTagHooks[$tag] = $callback;
4645
		if ( !in_array( $tag, $this->mStripList ) ) {
4646
			$this->mStripList[] = $tag;
4647
		}
4648
4649
		return $oldVal;
4650
	}
4651
4652
	/**
4653
	 * As setHook(), but letting the contents be parsed.
4654
	 *
4655
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
4656
	 * operate late in the transformation sequence, on HTML instead of wikitext.
4657
	 *
4658
	 * This is probably obsoleted by things dealing with parser frames?
4659
	 * The only extension currently using it is geoserver.
4660
	 *
4661
	 * @since 1.10
4662
	 * @todo better document or deprecate this
4663
	 *
4664
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4665
	 * @param callable $callback The callback function (and object) to use for the tag
4666
	 * @throws MWException
4667
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4668
	 */
4669
	public function setTransparentTagHook( $tag, $callback ) {
4670
		$tag = strtolower( $tag );
4671
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4672
			throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4673
		}
4674
		$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4675
		$this->mTransparentTagHooks[$tag] = $callback;
4676
4677
		return $oldVal;
4678
	}
4679
4680
	/**
4681
	 * Remove all tag hooks
4682
	 */
4683
	public function clearTagHooks() {
4684
		$this->mTagHooks = [];
4685
		$this->mFunctionTagHooks = [];
4686
		$this->mStripList = $this->mDefaultStripList;
4687
	}
4688
4689
	/**
4690
	 * Create a function, e.g. {{sum:1|2|3}}
4691
	 * The callback function should have the form:
4692
	 *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
4693
	 *
4694
	 * Or with Parser::SFH_OBJECT_ARGS:
4695
	 *    function myParserFunction( $parser, $frame, $args ) { ... }
4696
	 *
4697
	 * The callback may either return the text result of the function, or an array with the text
4698
	 * in element 0, and a number of flags in the other elements. The names of the flags are
4699
	 * specified in the keys. Valid flags are:
4700
	 *   found                     The text returned is valid, stop processing the template. This
4701
	 *                             is on by default.
4702
	 *   nowiki                    Wiki markup in the return value should be escaped
4703
	 *   isHTML                    The returned text is HTML, armour it against wikitext transformation
4704
	 *
4705
	 * @param string $id The magic word ID
4706
	 * @param callable $callback The callback function (and object) to use
4707
	 * @param int $flags A combination of the following flags:
4708
	 *     Parser::SFH_NO_HASH      No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
4709
	 *
4710
	 *     Parser::SFH_OBJECT_ARGS  Pass the template arguments as PPNode objects instead of text.
4711
	 *     This allows for conditional expansion of the parse tree, allowing you to eliminate dead
4712
	 *     branches and thus speed up parsing. It is also possible to analyse the parse tree of
4713
	 *     the arguments, and to control the way they are expanded.
4714
	 *
4715
	 *     The $frame parameter is a PPFrame. This can be used to produce expanded text from the
4716
	 *     arguments, for instance:
4717
	 *         $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
4718
	 *
4719
	 *     For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
4720
	 *     future versions. Please call $frame->expand() on it anyway so that your code keeps
4721
	 *     working if/when this is changed.
4722
	 *
4723
	 *     If you want whitespace to be trimmed from $args, you need to do it yourself, post-
4724
	 *     expansion.
4725
	 *
4726
	 *     Please read the documentation in includes/parser/Preprocessor.php for more information
4727
	 *     about the methods available in PPFrame and PPNode.
4728
	 *
4729
	 * @throws MWException
4730
	 * @return string|callable The old callback function for this name, if any
4731
	 */
4732
	public function setFunctionHook( $id, $callback, $flags = 0 ) {
4733
		global $wgContLang;
4734
4735
		$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4736
		$this->mFunctionHooks[$id] = [ $callback, $flags ];
4737
4738
		# Add to function cache
4739
		$mw = MagicWord::get( $id );
4740
		if ( !$mw ) {
4741
			throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4742
		}
4743
4744
		$synonyms = $mw->getSynonyms();
4745
		$sensitive = intval( $mw->isCaseSensitive() );
4746
4747
		foreach ( $synonyms as $syn ) {
4748
			# Case
4749
			if ( !$sensitive ) {
4750
				$syn = $wgContLang->lc( $syn );
4751
			}
4752
			# Add leading hash
4753
			if ( !( $flags & self::SFH_NO_HASH ) ) {
4754
				$syn = '#' . $syn;
4755
			}
4756
			# Remove trailing colon
4757
			if ( substr( $syn, -1, 1 ) === ':' ) {
4758
				$syn = substr( $syn, 0, -1 );
4759
			}
4760
			$this->mFunctionSynonyms[$sensitive][$syn] = $id;
4761
		}
4762
		return $oldVal;
4763
	}
4764
4765
	/**
4766
	 * Get all registered function hook identifiers
4767
	 *
4768
	 * @return array
4769
	 */
4770
	public function getFunctionHooks() {
4771
		return array_keys( $this->mFunctionHooks );
4772
	}
4773
4774
	/**
4775
	 * Create a tag function, e.g. "<test>some stuff</test>".
4776
	 * Unlike tag hooks, tag functions are parsed at preprocessor level.
4777
	 * Unlike parser functions, their content is not preprocessed.
4778
	 * @param string $tag
4779
	 * @param callable $callback
4780
	 * @param int $flags
4781
	 * @throws MWException
4782
	 * @return null
4783
	 */
4784 View Code Duplication
	public function setFunctionTagHook( $tag, $callback, $flags ) {
4785
		$tag = strtolower( $tag );
4786
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4787
			throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4788
		}
4789
		$old = isset( $this->mFunctionTagHooks[$tag] ) ?
4790
			$this->mFunctionTagHooks[$tag] : null;
4791
		$this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4792
4793
		if ( !in_array( $tag, $this->mStripList ) ) {
4794
			$this->mStripList[] = $tag;
4795
		}
4796
4797
		return $old;
4798
	}
4799
4800
	/**
4801
	 * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
4802
	 * Placeholders created in Linker::link()
4803
	 *
4804
	 * @param string $text
4805
	 * @param int $options
4806
	 */
4807
	public function replaceLinkHolders( &$text, $options = 0 ) {
4808
		$this->mLinkHolders->replace( $text );
4809
	}
4810
4811
	/**
4812
	 * Replace "<!--LINK-->" link placeholders with plain text of links
4813
	 * (not HTML-formatted).
4814
	 *
4815
	 * @param string $text
4816
	 * @return string
4817
	 */
4818
	public function replaceLinkHoldersText( $text ) {
4819
		return $this->mLinkHolders->replaceText( $text );
4820
	}
4821
4822
	/**
4823
	 * Renders an image gallery from a text with one line per image.
4824
	 * text labels may be given by using |-style alternative text. E.g.
4825
	 *   Image:one.jpg|The number "1"
4826
	 *   Image:tree.jpg|A tree
4827
	 * given as text will return the HTML of a gallery with two images,
4828
	 * labeled 'The number "1"' and
4829
	 * 'A tree'.
4830
	 *
4831
	 * @param string $text
4832
	 * @param array $params
4833
	 * @return string HTML
4834
	 */
4835
	public function renderImageGallery( $text, $params ) {
4836
4837
		$mode = false;
4838
		if ( isset( $params['mode'] ) ) {
4839
			$mode = $params['mode'];
4840
		}
4841
4842
		try {
4843
			$ig = ImageGalleryBase::factory( $mode );
4844
		} catch ( Exception $e ) {
4845
			// If invalid type set, fallback to default.
4846
			$ig = ImageGalleryBase::factory( false );
4847
		}
4848
4849
		$ig->setContextTitle( $this->mTitle );
4850
		$ig->setShowBytes( false );
4851
		$ig->setShowFilename( false );
4852
		$ig->setParser( $this );
4853
		$ig->setHideBadImages();
4854
		$ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
4855
4856
		if ( isset( $params['showfilename'] ) ) {
4857
			$ig->setShowFilename( true );
4858
		} else {
4859
			$ig->setShowFilename( false );
4860
		}
4861
		if ( isset( $params['caption'] ) ) {
4862
			$caption = $params['caption'];
4863
			$caption = htmlspecialchars( $caption );
4864
			$caption = $this->replaceInternalLinks( $caption );
4865
			$ig->setCaptionHtml( $caption );
4866
		}
4867
		if ( isset( $params['perrow'] ) ) {
4868
			$ig->setPerRow( $params['perrow'] );
4869
		}
4870
		if ( isset( $params['widths'] ) ) {
4871
			$ig->setWidths( $params['widths'] );
4872
		}
4873
		if ( isset( $params['heights'] ) ) {
4874
			$ig->setHeights( $params['heights'] );
4875
		}
4876
		$ig->setAdditionalOptions( $params );
4877
4878
		Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] );
4879
4880
		$lines = StringUtils::explode( "\n", $text );
4881
		foreach ( $lines as $line ) {
4882
			# match lines like these:
4883
			# Image:someimage.jpg|This is some image
4884
			$matches = [];
4885
			preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4886
			# Skip empty lines
4887
			if ( count( $matches ) == 0 ) {
4888
				continue;
4889
			}
4890
4891
			if ( strpos( $matches[0], '%' ) !== false ) {
4892
				$matches[1] = rawurldecode( $matches[1] );
4893
			}
4894
			$title = Title::newFromText( $matches[1], NS_FILE );
4895
			if ( is_null( $title ) ) {
4896
				# Bogus title. Ignore these so we don't bomb out later.
4897
				continue;
4898
			}
4899
4900
			# We need to get what handler the file uses, to figure out parameters.
4901
			# Note, a hook can overide the file name, and chose an entirely different
4902
			# file (which potentially could be of a different type and have different handler).
4903
			$options = [];
4904
			$descQuery = false;
4905
			Hooks::run( 'BeforeParserFetchFileAndTitle',
4906
				[ $this, $title, &$options, &$descQuery ] );
4907
			# Don't register it now, as ImageGallery does that later.
4908
			$file = $this->fetchFileNoRegister( $title, $options );
4909
			$handler = $file ? $file->getHandler() : false;
4910
4911
			$paramMap = [
4912
				'img_alt' => 'gallery-internal-alt',
4913
				'img_link' => 'gallery-internal-link',
4914
			];
4915
			if ( $handler ) {
4916
				$paramMap = $paramMap + $handler->getParamMap();
4917
				// We don't want people to specify per-image widths.
4918
				// Additionally the width parameter would need special casing anyhow.
4919
				unset( $paramMap['img_width'] );
4920
			}
4921
4922
			$mwArray = new MagicWordArray( array_keys( $paramMap ) );
4923
4924
			$label = '';
4925
			$alt = '';
4926
			$link = '';
4927
			$handlerOptions = [];
4928
			if ( isset( $matches[3] ) ) {
4929
				// look for an |alt= definition while trying not to break existing
4930
				// captions with multiple pipes (|) in it, until a more sensible grammar
4931
				// is defined for images in galleries
4932
4933
				// FIXME: Doing recursiveTagParse at this stage, and the trim before
4934
				// splitting on '|' is a bit odd, and different from makeImage.
4935
				$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
4936
				$parameterMatches = StringUtils::explode( '|', $matches[3] );
4937
4938
				foreach ( $parameterMatches as $parameterMatch ) {
4939
					list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
4940
					if ( $magicName ) {
4941
						$paramName = $paramMap[$magicName];
4942
4943
						switch ( $paramName ) {
4944
						case 'gallery-internal-alt':
4945
							$alt = $this->stripAltText( $match, false );
4946
							break;
4947
						case 'gallery-internal-link':
4948
							$linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
4949
							$chars = self::EXT_LINK_URL_CLASS;
4950
							$addr = self::EXT_LINK_ADDR;
4951
							$prots = $this->mUrlProtocols;
4952
							// check to see if link matches an absolute url, if not then it must be a wiki link.
4953
							if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
4954
								$link = $linkValue;
4955
							} else {
4956
								$localLinkTitle = Title::newFromText( $linkValue );
4957
								if ( $localLinkTitle !== null ) {
4958
									$link = $localLinkTitle->getLinkURL();
4959
								}
4960
							}
4961
							break;
4962
						default:
4963
							// Must be a handler specific parameter.
4964
							if ( $handler->validateParam( $paramName, $match ) ) {
4965
								$handlerOptions[$paramName] = $match;
4966
							} else {
4967
								// Guess not, consider it as caption.
4968
								wfDebug( "$parameterMatch failed parameter validation\n" );
4969
								$label = '|' . $parameterMatch;
4970
							}
4971
						}
4972
4973
					} else {
4974
						// Last pipe wins.
4975
						$label = '|' . $parameterMatch;
4976
					}
4977
				}
4978
				// Remove the pipe.
4979
				$label = substr( $label, 1 );
4980
			}
4981
4982
			$ig->add( $title, $label, $alt, $link, $handlerOptions );
4983
		}
4984
		$html = $ig->toHTML();
4985
		Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
4986
		return $html;
4987
	}
4988
4989
	/**
4990
	 * @param MediaHandler $handler
4991
	 * @return array
4992
	 */
4993
	public function getImageParams( $handler ) {
4994
		if ( $handler ) {
4995
			$handlerClass = get_class( $handler );
4996
		} else {
4997
			$handlerClass = '';
4998
		}
4999
		if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5000
			# Initialise static lists
5001
			static $internalParamNames = [
5002
				'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5003
				'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5004
					'bottom', 'text-bottom' ],
5005
				'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5006
					'upright', 'border', 'link', 'alt', 'class' ],
5007
			];
5008
			static $internalParamMap;
5009
			if ( !$internalParamMap ) {
5010
				$internalParamMap = [];
5011
				foreach ( $internalParamNames as $type => $names ) {
5012
					foreach ( $names as $name ) {
5013
						$magicName = str_replace( '-', '_', "img_$name" );
5014
						$internalParamMap[$magicName] = [ $type, $name ];
5015
					}
5016
				}
5017
			}
5018
5019
			# Add handler params
5020
			$paramMap = $internalParamMap;
5021
			if ( $handler ) {
5022
				$handlerParamMap = $handler->getParamMap();
5023
				foreach ( $handlerParamMap as $magic => $paramName ) {
5024
					$paramMap[$magic] = [ 'handler', $paramName ];
5025
				}
5026
			}
5027
			$this->mImageParams[$handlerClass] = $paramMap;
5028
			$this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5029
		}
5030
		return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5031
	}
5032
5033
	/**
5034
	 * Parse image options text and use it to make an image
5035
	 *
5036
	 * @param Title $title
5037
	 * @param string $options
5038
	 * @param LinkHolderArray|bool $holders
5039
	 * @return string HTML
5040
	 */
5041
	public function makeImage( $title, $options, $holders = false ) {
5042
		# Check if the options text is of the form "options|alt text"
5043
		# Options are:
5044
		#  * thumbnail  make a thumbnail with enlarge-icon and caption, alignment depends on lang
5045
		#  * left       no resizing, just left align. label is used for alt= only
5046
		#  * right      same, but right aligned
5047
		#  * none       same, but not aligned
5048
		#  * ___px      scale to ___ pixels width, no aligning. e.g. use in taxobox
5049
		#  * center     center the image
5050
		#  * frame      Keep original image size, no magnify-button.
5051
		#  * framed     Same as "frame"
5052
		#  * frameless  like 'thumb' but without a frame. Keeps user preferences for width
5053
		#  * upright    reduce width for upright images, rounded to full __0 px
5054
		#  * border     draw a 1px border around the image
5055
		#  * alt        Text for HTML alt attribute (defaults to empty)
5056
		#  * class      Set a class for img node
5057
		#  * link       Set the target of the image link. Can be external, interwiki, or local
5058
		# vertical-align values (no % or length right now):
5059
		#  * baseline
5060
		#  * sub
5061
		#  * super
5062
		#  * top
5063
		#  * text-top
5064
		#  * middle
5065
		#  * bottom
5066
		#  * text-bottom
5067
5068
		$parts = StringUtils::explode( "|", $options );
5069
5070
		# Give extensions a chance to select the file revision for us
5071
		$options = [];
5072
		$descQuery = false;
5073
		Hooks::run( 'BeforeParserFetchFileAndTitle',
5074
			[ $this, $title, &$options, &$descQuery ] );
5075
		# Fetch and register the file (file title may be different via hooks)
5076
		list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5077
5078
		# Get parameter map
5079
		$handler = $file ? $file->getHandler() : false;
5080
5081
		list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5082
5083
		if ( !$file ) {
5084
			$this->addTrackingCategory( 'broken-file-category' );
5085
		}
5086
5087
		# Process the input parameters
5088
		$caption = '';
5089
		$params = [ 'frame' => [], 'handler' => [],
5090
			'horizAlign' => [], 'vertAlign' => [] ];
5091
		$seenformat = false;
5092
		foreach ( $parts as $part ) {
5093
			$part = trim( $part );
5094
			list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5095
			$validated = false;
5096
			if ( isset( $paramMap[$magicName] ) ) {
5097
				list( $type, $paramName ) = $paramMap[$magicName];
5098
5099
				# Special case; width and height come in one variable together
5100
				if ( $type === 'handler' && $paramName === 'width' ) {
5101
					$parsedWidthParam = $this->parseWidthParam( $value );
5102 View Code Duplication
					if ( isset( $parsedWidthParam['width'] ) ) {
5103
						$width = $parsedWidthParam['width'];
5104
						if ( $handler->validateParam( 'width', $width ) ) {
5105
							$params[$type]['width'] = $width;
5106
							$validated = true;
5107
						}
5108
					}
5109 View Code Duplication
					if ( isset( $parsedWidthParam['height'] ) ) {
5110
						$height = $parsedWidthParam['height'];
5111
						if ( $handler->validateParam( 'height', $height ) ) {
5112
							$params[$type]['height'] = $height;
5113
							$validated = true;
5114
						}
5115
					}
5116
					# else no validation -- bug 13436
5117
				} else {
5118
					if ( $type === 'handler' ) {
5119
						# Validate handler parameter
5120
						$validated = $handler->validateParam( $paramName, $value );
5121
					} else {
5122
						# Validate internal parameters
5123
						switch ( $paramName ) {
5124
						case 'manualthumb':
5125
						case 'alt':
5126
						case 'class':
5127
							# @todo FIXME: Possibly check validity here for
5128
							# manualthumb? downstream behavior seems odd with
5129
							# missing manual thumbs.
5130
							$validated = true;
5131
							$value = $this->stripAltText( $value, $holders );
5132
							break;
5133
						case 'link':
5134
							$chars = self::EXT_LINK_URL_CLASS;
5135
							$addr = self::EXT_LINK_ADDR;
5136
							$prots = $this->mUrlProtocols;
5137
							if ( $value === '' ) {
5138
								$paramName = 'no-link';
5139
								$value = true;
5140
								$validated = true;
5141
							} elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5142
								if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5143
									$paramName = 'link-url';
5144
									$this->mOutput->addExternalLink( $value );
5145
									if ( $this->mOptions->getExternalLinkTarget() ) {
5146
										$params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5147
									}
5148
									$validated = true;
5149
								}
5150
							} else {
5151
								$linkTitle = Title::newFromText( $value );
5152
								if ( $linkTitle ) {
5153
									$paramName = 'link-title';
5154
									$value = $linkTitle;
5155
									$this->mOutput->addLink( $linkTitle );
5156
									$validated = true;
5157
								}
5158
							}
5159
							break;
5160
						case 'frameless':
5161
						case 'framed':
5162
						case 'thumbnail':
5163
							// use first appearing option, discard others.
5164
							$validated = ! $seenformat;
5165
							$seenformat = true;
5166
							break;
5167
						default:
5168
							# Most other things appear to be empty or numeric...
5169
							$validated = ( $value === false || is_numeric( trim( $value ) ) );
5170
						}
5171
					}
5172
5173
					if ( $validated ) {
5174
						$params[$type][$paramName] = $value;
5175
					}
5176
				}
5177
			}
5178
			if ( !$validated ) {
5179
				$caption = $part;
5180
			}
5181
		}
5182
5183
		# Process alignment parameters
5184
		if ( $params['horizAlign'] ) {
5185
			$params['frame']['align'] = key( $params['horizAlign'] );
5186
		}
5187
		if ( $params['vertAlign'] ) {
5188
			$params['frame']['valign'] = key( $params['vertAlign'] );
5189
		}
5190
5191
		$params['frame']['caption'] = $caption;
5192
5193
		# Will the image be presented in a frame, with the caption below?
5194
		$imageIsFramed = isset( $params['frame']['frame'] )
5195
			|| isset( $params['frame']['framed'] )
5196
			|| isset( $params['frame']['thumbnail'] )
5197
			|| isset( $params['frame']['manualthumb'] );
5198
5199
		# In the old days, [[Image:Foo|text...]] would set alt text.  Later it
5200
		# came to also set the caption, ordinary text after the image -- which
5201
		# makes no sense, because that just repeats the text multiple times in
5202
		# screen readers.  It *also* came to set the title attribute.
5203
		# Now that we have an alt attribute, we should not set the alt text to
5204
		# equal the caption: that's worse than useless, it just repeats the
5205
		# text.  This is the framed/thumbnail case.  If there's no caption, we
5206
		# use the unnamed parameter for alt text as well, just for the time be-
5207
		# ing, if the unnamed param is set and the alt param is not.
5208
		# For the future, we need to figure out if we want to tweak this more,
5209
		# e.g., introducing a title= parameter for the title; ignoring the un-
5210
		# named parameter entirely for images without a caption; adding an ex-
5211
		# plicit caption= parameter and preserving the old magic unnamed para-
5212
		# meter for BC; ...
5213
		if ( $imageIsFramed ) { # Framed image
5214
			if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5215
				# No caption or alt text, add the filename as the alt text so
5216
				# that screen readers at least get some description of the image
5217
				$params['frame']['alt'] = $title->getText();
5218
			}
5219
			# Do not set $params['frame']['title'] because tooltips don't make sense
5220
			# for framed images
5221
		} else { # Inline image
5222
			if ( !isset( $params['frame']['alt'] ) ) {
5223
				# No alt text, use the "caption" for the alt text
5224
				if ( $caption !== '' ) {
5225
					$params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5226
				} else {
5227
					# No caption, fall back to using the filename for the
5228
					# alt text
5229
					$params['frame']['alt'] = $title->getText();
5230
				}
5231
			}
5232
			# Use the "caption" for the tooltip text
5233
			$params['frame']['title'] = $this->stripAltText( $caption, $holders );
5234
		}
5235
5236
		Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5237
5238
		# Linker does the rest
5239
		$time = isset( $options['time'] ) ? $options['time'] : false;
5240
		$ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5241
			$time, $descQuery, $this->mOptions->getThumbSize() );
5242
5243
		# Give the handler a chance to modify the parser object
5244
		if ( $handler ) {
5245
			$handler->parserTransformHook( $this, $file );
5246
		}
5247
5248
		return $ret;
5249
	}
5250
5251
	/**
5252
	 * @param string $caption
5253
	 * @param LinkHolderArray|bool $holders
5254
	 * @return mixed|string
5255
	 */
5256
	protected function stripAltText( $caption, $holders ) {
5257
		# Strip bad stuff out of the title (tooltip).  We can't just use
5258
		# replaceLinkHoldersText() here, because if this function is called
5259
		# from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5260
		if ( $holders ) {
5261
			$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...
5262
		} else {
5263
			$tooltip = $this->replaceLinkHoldersText( $caption );
5264
		}
5265
5266
		# make sure there are no placeholders in thumbnail attributes
5267
		# that are later expanded to html- so expand them now and
5268
		# remove the tags
5269
		$tooltip = $this->mStripState->unstripBoth( $tooltip );
5270
		$tooltip = Sanitizer::stripAllTags( $tooltip );
5271
5272
		return $tooltip;
5273
	}
5274
5275
	/**
5276
	 * Set a flag in the output object indicating that the content is dynamic and
5277
	 * shouldn't be cached.
5278
	 */
5279
	public function disableCache() {
5280
		wfDebug( "Parser output marked as uncacheable.\n" );
5281
		if ( !$this->mOutput ) {
5282
			throw new MWException( __METHOD__ .
5283
				" can only be called when actually parsing something" );
5284
		}
5285
		$this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5286
	}
5287
5288
	/**
5289
	 * Callback from the Sanitizer for expanding items found in HTML attribute
5290
	 * values, so they can be safely tested and escaped.
5291
	 *
5292
	 * @param string $text
5293
	 * @param bool|PPFrame $frame
5294
	 * @return string
5295
	 */
5296
	public function attributeStripCallback( &$text, $frame = false ) {
5297
		$text = $this->replaceVariables( $text, $frame );
5298
		$text = $this->mStripState->unstripBoth( $text );
5299
		return $text;
5300
	}
5301
5302
	/**
5303
	 * Accessor
5304
	 *
5305
	 * @return array
5306
	 */
5307
	public function getTags() {
5308
		return array_merge(
5309
			array_keys( $this->mTransparentTagHooks ),
5310
			array_keys( $this->mTagHooks ),
5311
			array_keys( $this->mFunctionTagHooks )
5312
		);
5313
	}
5314
5315
	/**
5316
	 * Replace transparent tags in $text with the values given by the callbacks.
5317
	 *
5318
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
5319
	 * operate late in the transformation sequence, on HTML instead of wikitext.
5320
	 *
5321
	 * @param string $text
5322
	 *
5323
	 * @return string
5324
	 */
5325
	public function replaceTransparentTags( $text ) {
5326
		$matches = [];
5327
		$elements = array_keys( $this->mTransparentTagHooks );
5328
		$text = self::extractTagsAndParams( $elements, $text, $matches );
5329
		$replacements = [];
5330
5331
		foreach ( $matches as $marker => $data ) {
5332
			list( $element, $content, $params, $tag ) = $data;
5333
			$tagName = strtolower( $element );
5334
			if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5335
				$output = call_user_func_array(
5336
					$this->mTransparentTagHooks[$tagName],
5337
					[ $content, $params, $this ]
5338
				);
5339
			} else {
5340
				$output = $tag;
5341
			}
5342
			$replacements[$marker] = $output;
5343
		}
5344
		return strtr( $text, $replacements );
5345
	}
5346
5347
	/**
5348
	 * Break wikitext input into sections, and either pull or replace
5349
	 * some particular section's text.
5350
	 *
5351
	 * External callers should use the getSection and replaceSection methods.
5352
	 *
5353
	 * @param string $text Page wikitext
5354
	 * @param string|number $sectionId A section identifier string of the form:
5355
	 *   "<flag1> - <flag2> - ... - <section number>"
5356
	 *
5357
	 * Currently the only recognised flag is "T", which means the target section number
5358
	 * was derived during a template inclusion parse, in other words this is a template
5359
	 * section edit link. If no flags are given, it was an ordinary section edit link.
5360
	 * This flag is required to avoid a section numbering mismatch when a section is
5361
	 * enclosed by "<includeonly>" (bug 6563).
5362
	 *
5363
	 * The section number 0 pulls the text before the first heading; other numbers will
5364
	 * pull the given section along with its lower-level subsections. If the section is
5365
	 * not found, $mode=get will return $newtext, and $mode=replace will return $text.
5366
	 *
5367
	 * Section 0 is always considered to exist, even if it only contains the empty
5368
	 * string. If $text is the empty string and section 0 is replaced, $newText is
5369
	 * returned.
5370
	 *
5371
	 * @param string $mode One of "get" or "replace"
5372
	 * @param string $newText Replacement text for section data.
5373
	 * @return string For "get", the extracted section text.
5374
	 *   for "replace", the whole page with the section replaced.
5375
	 */
5376
	private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5377
		global $wgTitle; # not generally used but removes an ugly failure mode
5378
5379
		$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...
5380
		$this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5381
		$outText = '';
5382
		$frame = $this->getPreprocessor()->newFrame();
5383
5384
		# Process section extraction flags
5385
		$flags = 0;
5386
		$sectionParts = explode( '-', $sectionId );
5387
		$sectionIndex = array_pop( $sectionParts );
5388
		foreach ( $sectionParts as $part ) {
5389
			if ( $part === 'T' ) {
5390
				$flags |= self::PTD_FOR_INCLUSION;
5391
			}
5392
		}
5393
5394
		# Check for empty input
5395
		if ( strval( $text ) === '' ) {
5396
			# Only sections 0 and T-0 exist in an empty document
5397
			if ( $sectionIndex == 0 ) {
5398
				if ( $mode === 'get' ) {
5399
					return '';
5400
				} else {
5401
					return $newText;
5402
				}
5403
			} else {
5404
				if ( $mode === 'get' ) {
5405
					return $newText;
5406
				} else {
5407
					return $text;
5408
				}
5409
			}
5410
		}
5411
5412
		# Preprocess the text
5413
		$root = $this->preprocessToDom( $text, $flags );
5414
5415
		# <h> nodes indicate section breaks
5416
		# They can only occur at the top level, so we can find them by iterating the root's children
5417
		$node = $root->getFirstChild();
5418
5419
		# Find the target section
5420
		if ( $sectionIndex == 0 ) {
5421
			# Section zero doesn't nest, level=big
5422
			$targetLevel = 1000;
5423
		} else {
5424
			while ( $node ) {
5425 View Code Duplication
				if ( $node->getName() === 'h' ) {
5426
					$bits = $node->splitHeading();
5427
					if ( $bits['i'] == $sectionIndex ) {
5428
						$targetLevel = $bits['level'];
5429
						break;
5430
					}
5431
				}
5432
				if ( $mode === 'replace' ) {
5433
					$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5434
				}
5435
				$node = $node->getNextSibling();
5436
			}
5437
		}
5438
5439
		if ( !$node ) {
5440
			# Not found
5441
			if ( $mode === 'get' ) {
5442
				return $newText;
5443
			} else {
5444
				return $text;
5445
			}
5446
		}
5447
5448
		# Find the end of the section, including nested sections
5449
		do {
5450 View Code Duplication
			if ( $node->getName() === 'h' ) {
5451
				$bits = $node->splitHeading();
5452
				$curLevel = $bits['level'];
5453
				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...
5454
					break;
5455
				}
5456
			}
5457
			if ( $mode === 'get' ) {
5458
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5459
			}
5460
			$node = $node->getNextSibling();
5461
		} while ( $node );
5462
5463
		# Write out the remainder (in replace mode only)
5464
		if ( $mode === 'replace' ) {
5465
			# Output the replacement text
5466
			# Add two newlines on -- trailing whitespace in $newText is conventionally
5467
			# stripped by the editor, so we need both newlines to restore the paragraph gap
5468
			# Only add trailing whitespace if there is newText
5469
			if ( $newText != "" ) {
5470
				$outText .= $newText . "\n\n";
5471
			}
5472
5473
			while ( $node ) {
5474
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5475
				$node = $node->getNextSibling();
5476
			}
5477
		}
5478
5479
		if ( is_string( $outText ) ) {
5480
			# Re-insert stripped tags
5481
			$outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5482
		}
5483
5484
		return $outText;
5485
	}
5486
5487
	/**
5488
	 * This function returns the text of a section, specified by a number ($section).
5489
	 * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
5490
	 * the first section before any such heading (section 0).
5491
	 *
5492
	 * If a section contains subsections, these are also returned.
5493
	 *
5494
	 * @param string $text Text to look in
5495
	 * @param string|number $sectionId Section identifier as a number or string
5496
	 * (e.g. 0, 1 or 'T-1').
5497
	 * @param string $defaultText Default to return if section is not found
5498
	 *
5499
	 * @return string Text of the requested section
5500
	 */
5501
	public function getSection( $text, $sectionId, $defaultText = '' ) {
5502
		return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5503
	}
5504
5505
	/**
5506
	 * This function returns $oldtext after the content of the section
5507
	 * specified by $section has been replaced with $text. If the target
5508
	 * section does not exist, $oldtext is returned unchanged.
5509
	 *
5510
	 * @param string $oldText Former text of the article
5511
	 * @param string|number $sectionId Section identifier as a number or string
5512
	 * (e.g. 0, 1 or 'T-1').
5513
	 * @param string $newText Replacing text
5514
	 *
5515
	 * @return string Modified text
5516
	 */
5517
	public function replaceSection( $oldText, $sectionId, $newText ) {
5518
		return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5519
	}
5520
5521
	/**
5522
	 * Get the ID of the revision we are parsing
5523
	 *
5524
	 * @return int|null
5525
	 */
5526
	public function getRevisionId() {
5527
		return $this->mRevisionId;
5528
	}
5529
5530
	/**
5531
	 * Get the revision object for $this->mRevisionId
5532
	 *
5533
	 * @return Revision|null Either a Revision object or null
5534
	 * @since 1.23 (public since 1.23)
5535
	 */
5536
	public function getRevisionObject() {
5537
		if ( !is_null( $this->mRevisionObject ) ) {
5538
			return $this->mRevisionObject;
5539
		}
5540
		if ( is_null( $this->mRevisionId ) ) {
5541
			return null;
5542
		}
5543
5544
		$rev = call_user_func(
5545
			$this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5546
		);
5547
5548
		# If the parse is for a new revision, then the callback should have
5549
		# already been set to force the object and should match mRevisionId.
5550
		# If not, try to fetch by mRevisionId for sanity.
5551
		if ( $rev && $rev->getId() != $this->mRevisionId ) {
5552
			$rev = Revision::newFromId( $this->mRevisionId );
5553
		}
5554
5555
		$this->mRevisionObject = $rev;
5556
5557
		return $this->mRevisionObject;
5558
	}
5559
5560
	/**
5561
	 * Get the timestamp associated with the current revision, adjusted for
5562
	 * the default server-local timestamp
5563
	 * @return string
5564
	 */
5565
	public function getRevisionTimestamp() {
5566
		if ( is_null( $this->mRevisionTimestamp ) ) {
5567
			global $wgContLang;
5568
5569
			$revObject = $this->getRevisionObject();
5570
			$timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5571
5572
			# The cryptic '' timezone parameter tells to use the site-default
5573
			# timezone offset instead of the user settings.
5574
			# Since this value will be saved into the parser cache, served
5575
			# to other users, and potentially even used inside links and such,
5576
			# it needs to be consistent for all visitors.
5577
			$this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5578
5579
		}
5580
		return $this->mRevisionTimestamp;
5581
	}
5582
5583
	/**
5584
	 * Get the name of the user that edited the last revision
5585
	 *
5586
	 * @return string User name
5587
	 */
5588 View Code Duplication
	public function getRevisionUser() {
5589
		if ( is_null( $this->mRevisionUser ) ) {
5590
			$revObject = $this->getRevisionObject();
5591
5592
			# if this template is subst: the revision id will be blank,
5593
			# so just use the current user's name
5594
			if ( $revObject ) {
5595
				$this->mRevisionUser = $revObject->getUserText();
5596
			} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5597
				$this->mRevisionUser = $this->getUser()->getName();
5598
			}
5599
		}
5600
		return $this->mRevisionUser;
5601
	}
5602
5603
	/**
5604
	 * Get the size of the revision
5605
	 *
5606
	 * @return int|null Revision size
5607
	 */
5608 View Code Duplication
	public function getRevisionSize() {
5609
		if ( is_null( $this->mRevisionSize ) ) {
5610
			$revObject = $this->getRevisionObject();
5611
5612
			# if this variable is subst: the revision id will be blank,
5613
			# so just use the parser input size, because the own substituation
5614
			# will change the size.
5615
			if ( $revObject ) {
5616
				$this->mRevisionSize = $revObject->getSize();
5617
			} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5618
				$this->mRevisionSize = $this->mInputSize;
5619
			}
5620
		}
5621
		return $this->mRevisionSize;
5622
	}
5623
5624
	/**
5625
	 * Mutator for $mDefaultSort
5626
	 *
5627
	 * @param string $sort New value
5628
	 */
5629
	public function setDefaultSort( $sort ) {
5630
		$this->mDefaultSort = $sort;
5631
		$this->mOutput->setProperty( 'defaultsort', $sort );
5632
	}
5633
5634
	/**
5635
	 * Accessor for $mDefaultSort
5636
	 * Will use the empty string if none is set.
5637
	 *
5638
	 * This value is treated as a prefix, so the
5639
	 * empty string is equivalent to sorting by
5640
	 * page name.
5641
	 *
5642
	 * @return string
5643
	 */
5644
	public function getDefaultSort() {
5645
		if ( $this->mDefaultSort !== false ) {
5646
			return $this->mDefaultSort;
5647
		} else {
5648
			return '';
5649
		}
5650
	}
5651
5652
	/**
5653
	 * Accessor for $mDefaultSort
5654
	 * Unlike getDefaultSort(), will return false if none is set
5655
	 *
5656
	 * @return string|bool
5657
	 */
5658
	public function getCustomDefaultSort() {
5659
		return $this->mDefaultSort;
5660
	}
5661
5662
	/**
5663
	 * Try to guess the section anchor name based on a wikitext fragment
5664
	 * presumably extracted from a heading, for example "Header" from
5665
	 * "== Header ==".
5666
	 *
5667
	 * @param string $text
5668
	 *
5669
	 * @return string
5670
	 */
5671
	public function guessSectionNameFromWikiText( $text ) {
5672
		# Strip out wikitext links(they break the anchor)
5673
		$text = $this->stripSectionName( $text );
5674
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5675
		return '#' . Sanitizer::escapeId( $text, 'noninitial' );
5676
	}
5677
5678
	/**
5679
	 * Same as guessSectionNameFromWikiText(), but produces legacy anchors
5680
	 * instead.  For use in redirects, since IE6 interprets Redirect: headers
5681
	 * as something other than UTF-8 (apparently?), resulting in breakage.
5682
	 *
5683
	 * @param string $text The section name
5684
	 * @return string An anchor
5685
	 */
5686
	public function guessLegacySectionNameFromWikiText( $text ) {
5687
		# Strip out wikitext links(they break the anchor)
5688
		$text = $this->stripSectionName( $text );
5689
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5690
		return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
5691
	}
5692
5693
	/**
5694
	 * Strips a text string of wikitext for use in a section anchor
5695
	 *
5696
	 * Accepts a text string and then removes all wikitext from the
5697
	 * string and leaves only the resultant text (i.e. the result of
5698
	 * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
5699
	 * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
5700
	 * to create valid section anchors by mimicing the output of the
5701
	 * parser when headings are parsed.
5702
	 *
5703
	 * @param string $text Text string to be stripped of wikitext
5704
	 * for use in a Section anchor
5705
	 * @return string Filtered text string
5706
	 */
5707
	public function stripSectionName( $text ) {
5708
		# Strip internal link markup
5709
		$text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5710
		$text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5711
5712
		# Strip external link markup
5713
		# @todo FIXME: Not tolerant to blank link text
5714
		# I.E. [https://www.mediawiki.org] will render as [1] or something depending
5715
		# on how many empty links there are on the page - need to figure that out.
5716
		$text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5717
5718
		# Parse wikitext quotes (italics & bold)
5719
		$text = $this->doQuotes( $text );
5720
5721
		# Strip HTML tags
5722
		$text = StringUtils::delimiterReplace( '<', '>', '', $text );
5723
		return $text;
5724
	}
5725
5726
	/**
5727
	 * strip/replaceVariables/unstrip for preprocessor regression testing
5728
	 *
5729
	 * @param string $text
5730
	 * @param Title $title
5731
	 * @param ParserOptions $options
5732
	 * @param int $outputType
5733
	 *
5734
	 * @return string
5735
	 */
5736
	public function testSrvus( $text, Title $title, ParserOptions $options,
5737
		$outputType = self::OT_HTML
5738
	) {
5739
		$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...
5740
		$this->startParse( $title, $options, $outputType, true );
5741
5742
		$text = $this->replaceVariables( $text );
5743
		$text = $this->mStripState->unstripBoth( $text );
5744
		$text = Sanitizer::removeHTMLtags( $text );
5745
		return $text;
5746
	}
5747
5748
	/**
5749
	 * @param string $text
5750
	 * @param Title $title
5751
	 * @param ParserOptions $options
5752
	 * @return string
5753
	 */
5754
	public function testPst( $text, Title $title, ParserOptions $options ) {
5755
		return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5756
	}
5757
5758
	/**
5759
	 * @param string $text
5760
	 * @param Title $title
5761
	 * @param ParserOptions $options
5762
	 * @return string
5763
	 */
5764
	public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5765
		return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5766
	}
5767
5768
	/**
5769
	 * Call a callback function on all regions of the given text that are not
5770
	 * inside strip markers, and replace those regions with the return value
5771
	 * of the callback. For example, with input:
5772
	 *
5773
	 *  aaa<MARKER>bbb
5774
	 *
5775
	 * This will call the callback function twice, with 'aaa' and 'bbb'. Those
5776
	 * two strings will be replaced with the value returned by the callback in
5777
	 * each case.
5778
	 *
5779
	 * @param string $s
5780
	 * @param callable $callback
5781
	 *
5782
	 * @return string
5783
	 */
5784
	public function markerSkipCallback( $s, $callback ) {
5785
		$i = 0;
5786
		$out = '';
5787
		while ( $i < strlen( $s ) ) {
5788
			$markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5789
			if ( $markerStart === false ) {
5790
				$out .= call_user_func( $callback, substr( $s, $i ) );
5791
				break;
5792
			} else {
5793
				$out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5794
				$markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5795
				if ( $markerEnd === false ) {
5796
					$out .= substr( $s, $markerStart );
5797
					break;
5798
				} else {
5799
					$markerEnd += strlen( self::MARKER_SUFFIX );
5800
					$out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5801
					$i = $markerEnd;
5802
				}
5803
			}
5804
		}
5805
		return $out;
5806
	}
5807
5808
	/**
5809
	 * Remove any strip markers found in the given text.
5810
	 *
5811
	 * @param string $text Input string
5812
	 * @return string
5813
	 */
5814
	public function killMarkers( $text ) {
5815
		return $this->mStripState->killMarkers( $text );
5816
	}
5817
5818
	/**
5819
	 * Save the parser state required to convert the given half-parsed text to
5820
	 * HTML. "Half-parsed" in this context means the output of
5821
	 * recursiveTagParse() or internalParse(). This output has strip markers
5822
	 * from replaceVariables (extensionSubstitution() etc.), and link
5823
	 * placeholders from replaceLinkHolders().
5824
	 *
5825
	 * Returns an array which can be serialized and stored persistently. This
5826
	 * array can later be loaded into another parser instance with
5827
	 * unserializeHalfParsedText(). The text can then be safely incorporated into
5828
	 * the return value of a parser hook.
5829
	 *
5830
	 * @param string $text
5831
	 *
5832
	 * @return array
5833
	 */
5834
	public function serializeHalfParsedText( $text ) {
5835
		$data = [
5836
			'text' => $text,
5837
			'version' => self::HALF_PARSED_VERSION,
5838
			'stripState' => $this->mStripState->getSubState( $text ),
5839
			'linkHolders' => $this->mLinkHolders->getSubArray( $text )
5840
		];
5841
		return $data;
5842
	}
5843
5844
	/**
5845
	 * Load the parser state given in the $data array, which is assumed to
5846
	 * have been generated by serializeHalfParsedText(). The text contents is
5847
	 * extracted from the array, and its markers are transformed into markers
5848
	 * appropriate for the current Parser instance. This transformed text is
5849
	 * returned, and can be safely included in the return value of a parser
5850
	 * hook.
5851
	 *
5852
	 * If the $data array has been stored persistently, the caller should first
5853
	 * check whether it is still valid, by calling isValidHalfParsedText().
5854
	 *
5855
	 * @param array $data Serialized data
5856
	 * @throws MWException
5857
	 * @return string
5858
	 */
5859
	public function unserializeHalfParsedText( $data ) {
5860 View Code Duplication
		if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
5861
			throw new MWException( __METHOD__ . ': invalid version' );
5862
		}
5863
5864
		# First, extract the strip state.
5865
		$texts = [ $data['text'] ];
5866
		$texts = $this->mStripState->merge( $data['stripState'], $texts );
5867
5868
		# Now renumber links
5869
		$texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
5870
5871
		# Should be good to go.
5872
		return $texts[0];
5873
	}
5874
5875
	/**
5876
	 * Returns true if the given array, presumed to be generated by
5877
	 * serializeHalfParsedText(), is compatible with the current version of the
5878
	 * parser.
5879
	 *
5880
	 * @param array $data
5881
	 *
5882
	 * @return bool
5883
	 */
5884
	public function isValidHalfParsedText( $data ) {
5885
		return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
5886
	}
5887
5888
	/**
5889
	 * Parsed a width param of imagelink like 300px or 200x300px
5890
	 *
5891
	 * @param string $value
5892
	 *
5893
	 * @return array
5894
	 * @since 1.20
5895
	 */
5896
	public function parseWidthParam( $value ) {
5897
		$parsedWidthParam = [];
5898
		if ( $value === '' ) {
5899
			return $parsedWidthParam;
5900
		}
5901
		$m = [];
5902
		# (bug 13500) In both cases (width/height and width only),
5903
		# permit trailing "px" for backward compatibility.
5904
		if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
5905
			$width = intval( $m[1] );
5906
			$height = intval( $m[2] );
5907
			$parsedWidthParam['width'] = $width;
5908
			$parsedWidthParam['height'] = $height;
5909
		} elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
5910
			$width = intval( $value );
5911
			$parsedWidthParam['width'] = $width;
5912
		}
5913
		return $parsedWidthParam;
5914
	}
5915
5916
	/**
5917
	 * Lock the current instance of the parser.
5918
	 *
5919
	 * This is meant to stop someone from calling the parser
5920
	 * recursively and messing up all the strip state.
5921
	 *
5922
	 * @throws MWException If parser is in a parse
5923
	 * @return ScopedCallback The lock will be released once the return value goes out of scope.
5924
	 */
5925
	protected function lock() {
5926
		if ( $this->mInParse ) {
5927
			throw new MWException( "Parser state cleared while parsing. "
5928
				. "Did you call Parser::parse recursively?" );
5929
		}
5930
		$this->mInParse = true;
5931
5932
		$recursiveCheck = new ScopedCallback( function() {
5933
			$this->mInParse = false;
5934
		} );
5935
5936
		return $recursiveCheck;
5937
	}
5938
5939
	/**
5940
	 * Strip outer <p></p> tag from the HTML source of a single paragraph.
5941
	 *
5942
	 * Returns original HTML if the <p/> tag has any attributes, if there's no wrapping <p/> tag,
5943
	 * or if there is more than one <p/> tag in the input HTML.
5944
	 *
5945
	 * @param string $html
5946
	 * @return string
5947
	 * @since 1.24
5948
	 */
5949
	public static function stripOuterParagraph( $html ) {
5950
		$m = [];
5951
		if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
5952
			if ( strpos( $m[1], '</p>' ) === false ) {
5953
				$html = $m[1];
5954
			}
5955
		}
5956
5957
		return $html;
5958
	}
5959
5960
	/**
5961
	 * Return this parser if it is not doing anything, otherwise
5962
	 * get a fresh parser. You can use this method by doing
5963
	 * $myParser = $wgParser->getFreshParser(), or more simply
5964
	 * $wgParser->getFreshParser()->parse( ... );
5965
	 * if you're unsure if $wgParser is safe to use.
5966
	 *
5967
	 * @since 1.24
5968
	 * @return Parser A parser object that is not parsing anything
5969
	 */
5970
	public function getFreshParser() {
5971
		global $wgParserConf;
5972
		if ( $this->mInParse ) {
5973
			return new $wgParserConf['class']( $wgParserConf );
5974
		} else {
5975
			return $this;
5976
		}
5977
	}
5978
5979
	/**
5980
	 * Set's up the PHP implementation of OOUI for use in this request
5981
	 * and instructs OutputPage to enable OOUI for itself.
5982
	 *
5983
	 * @since 1.26
5984
	 */
5985
	public function enableOOUI() {
5986
		OutputPage::setupOOUI();
5987
		$this->mOutput->setEnableOOUI( true );
5988
	}
5989
}
5990