Completed
Branch master (227f0c)
by
unknown
30:54
created

Parser::getLinkRenderer()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 11
rs 9.4285
c 1
b 0
f 0
1
<?php
2
/**
3
 * PHP parser that converts wiki markup to HTML.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Parser
22
 */
23
use MediaWiki\Linker\LinkRenderer;
24
use MediaWiki\MediaWikiServices;
25
26
/**
27
 * @defgroup Parser Parser
28
 */
29
30
/**
31
 * PHP Parser - Processes wiki markup (which uses a more user-friendly
32
 * syntax, such as "[[link]]" for making links), and provides a one-way
33
 * transformation of that wiki markup it into (X)HTML output / markup
34
 * (which in turn the browser understands, and can display).
35
 *
36
 * There are seven main entry points into the Parser class:
37
 *
38
 * - Parser::parse()
39
 *     produces HTML output
40
 * - Parser::preSaveTransform()
41
 *     produces altered wiki markup
42
 * - Parser::preprocess()
43
 *     removes HTML comments and expands templates
44
 * - Parser::cleanSig() and Parser::cleanSigInSig()
45
 *     cleans a signature before saving it to preferences
46
 * - Parser::getSection()
47
 *     return the content of a section from an article for section editing
48
 * - Parser::replaceSection()
49
 *     replaces a section by number inside an article
50
 * - Parser::getPreloadText()
51
 *     removes <noinclude> sections and <includeonly> tags
52
 *
53
 * Globals used:
54
 *    object: $wgContLang
55
 *
56
 * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
57
 *
58
 * @par Settings:
59
 * $wgNamespacesWithSubpages
60
 *
61
 * @par Settings only within ParserOptions:
62
 * $wgAllowExternalImages
63
 * $wgAllowSpecialInclusion
64
 * $wgInterwikiMagic
65
 * $wgMaxArticleSize
66
 *
67
 * @ingroup Parser
68
 */
69
class Parser {
70
	/**
71
	 * Update this version number when the ParserOutput format
72
	 * changes in an incompatible way, so the parser cache
73
	 * can automatically discard old data.
74
	 */
75
	const VERSION = '1.6.4';
76
77
	/**
78
	 * Update this version number when the output of serialiseHalfParsedText()
79
	 * changes in an incompatible way
80
	 */
81
	const HALF_PARSED_VERSION = 2;
82
83
	# Flags for Parser::setFunctionHook
84
	const SFH_NO_HASH = 1;
85
	const SFH_OBJECT_ARGS = 2;
86
87
	# Constants needed for external link processing
88
	# Everything except bracket, space, or control characters
89
	# \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
90
	# as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
91
	const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
92
	# Simplified expression to match an IPv4 or IPv6 address, or
93
	# at least one character of a host name (embeds EXT_LINK_URL_CLASS)
94
	const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
95
	# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
96
	// @codingStandardsIgnoreStart Generic.Files.LineLength
97
	const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
98
		\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
99
	// @codingStandardsIgnoreEnd
100
101
	# Regular expression for a non-newline space
102
	const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
103
104
	# Flags for preprocessToDom
105
	const PTD_FOR_INCLUSION = 1;
106
107
	# Allowed values for $this->mOutputType
108
	# Parameter to startExternalParse().
109
	const OT_HTML = 1; # like parse()
110
	const OT_WIKI = 2; # like preSaveTransform()
111
	const OT_PREPROCESS = 3; # like preprocess()
112
	const OT_MSG = 3;
113
	const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
114
115
	/**
116
	 * @var string Prefix and suffix for temporary replacement strings
117
	 * for the multipass parser.
118
	 *
119
	 * \x7f should never appear in input as it's disallowed in XML.
120
	 * Using it at the front also gives us a little extra robustness
121
	 * since it shouldn't match when butted up against identifier-like
122
	 * string constructs.
123
	 *
124
	 * Must not consist of all title characters, or else it will change
125
	 * the behavior of <nowiki> in a link.
126
	 *
127
	 * Must have a character that needs escaping in attributes, otherwise
128
	 * someone could put a strip marker in an attribute, to get around
129
	 * escaping quote marks, and break out of the attribute. Thus we add
130
	 * `'".
131
	 */
132
	const MARKER_SUFFIX = "-QINU`\"'\x7f";
133
	const MARKER_PREFIX = "\x7f'\"`UNIQ-";
134
135
	# Markers used for wrapping the table of contents
136
	const TOC_START = '<mw:toc>';
137
	const TOC_END = '</mw:toc>';
138
139
	# Persistent:
140
	public $mTagHooks = [];
141
	public $mTransparentTagHooks = [];
142
	public $mFunctionHooks = [];
143
	public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
144
	public $mFunctionTagHooks = [];
145
	public $mStripList = [];
146
	public $mDefaultStripList = [];
147
	public $mVarCache = [];
148
	public $mImageParams = [];
149
	public $mImageParamsMagicArray = [];
150
	public $mMarkerIndex = 0;
151
	public $mFirstCall = true;
152
153
	# Initialised by initialiseVariables()
154
155
	/**
156
	 * @var MagicWordArray
157
	 */
158
	public $mVariables;
159
160
	/**
161
	 * @var MagicWordArray
162
	 */
163
	public $mSubstWords;
164
	# Initialised in constructor
165
	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...
166
167
	# Initialized in getPreprocessor()
168
	/** @var Preprocessor */
169
	public $mPreprocessor;
170
171
	# Cleared with clearState():
172
	/**
173
	 * @var ParserOutput
174
	 */
175
	public $mOutput;
176
	public $mAutonumber;
177
178
	/**
179
	 * @var StripState
180
	 */
181
	public $mStripState;
182
183
	public $mIncludeCount;
184
	/**
185
	 * @var LinkHolderArray
186
	 */
187
	public $mLinkHolders;
188
189
	public $mLinkID;
190
	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...
191
	public $mDefaultSort;
192
	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...
193
	public $mExpensiveFunctionCount; # number of expensive parser function calls
194
	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...
195
196
	/**
197
	 * @var User
198
	 */
199
	public $mUser; # User object; only used when doing pre-save transform
200
201
	# Temporary
202
	# These are variables reset at least once per parse regardless of $clearState
203
204
	/**
205
	 * @var ParserOptions
206
	 */
207
	public $mOptions;
208
209
	/**
210
	 * @var Title
211
	 */
212
	public $mTitle;        # Title context, used for self-link rendering and similar things
213
	public $mOutputType;   # Output type, one of the OT_xxx constants
214
	public $ot;            # Shortcut alias, see setOutputType()
215
	public $mRevisionObject; # The revision object of the specified revision ID
216
	public $mRevisionId;   # ID to display in {{REVISIONID}} tags
217
	public $mRevisionTimestamp; # The timestamp of the specified revision ID
218
	public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
219
	public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
220
	public $mRevIdForTs;   # The revision ID which was used to fetch the timestamp
221
	public $mInputSize = false; # For {{PAGESIZE}} on current page.
222
223
	/**
224
	 * @var string Deprecated accessor for the strip marker prefix.
225
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
226
	 **/
227
	public $mUniqPrefix = Parser::MARKER_PREFIX;
228
229
	/**
230
	 * @var array Array with the language name of each language link (i.e. the
231
	 * interwiki prefix) in the key, value arbitrary. Used to avoid sending
232
	 * duplicate language links to the ParserOutput.
233
	 */
234
	public $mLangLinkLanguages;
235
236
	/**
237
	 * @var MapCacheLRU|null
238
	 * @since 1.24
239
	 *
240
	 * A cache of the current revisions of titles. Keys are $title->getPrefixedDbKey()
241
	 */
242
	public $currentRevisionCache;
243
244
	/**
245
	 * @var bool Recursive call protection.
246
	 * This variable should be treated as if it were private.
247
	 */
248
	public $mInParse = false;
249
250
	/** @var SectionProfiler */
251
	protected $mProfiler;
252
253
	/**
254
	 * @var LinkRenderer
255
	 */
256
	protected $mLinkRenderer;
257
258
	/**
259
	 * @param array $conf
260
	 */
261
	public function __construct( $conf = [] ) {
262
		$this->mConf = $conf;
263
		$this->mUrlProtocols = wfUrlProtocols();
264
		$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
265
			self::EXT_LINK_ADDR .
266
			self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
267
		if ( isset( $conf['preprocessorClass'] ) ) {
268
			$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...
269
		} elseif ( defined( 'HPHP_VERSION' ) ) {
270
			# Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
271
			$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...
272
		} elseif ( extension_loaded( 'domxml' ) ) {
273
			# PECL extension that conflicts with the core DOM extension (bug 13770)
274
			wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
275
			$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...
276
		} elseif ( extension_loaded( 'dom' ) ) {
277
			$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...
278
		} else {
279
			$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...
280
		}
281
		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...
282
	}
283
284
	/**
285
	 * Reduce memory usage to reduce the impact of circular references
286
	 */
287
	public function __destruct() {
288
		if ( isset( $this->mLinkHolders ) ) {
289
			unset( $this->mLinkHolders );
290
		}
291
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Parser> is not traversable.
Loading history...
292
			unset( $this->$name );
293
		}
294
	}
295
296
	/**
297
	 * Allow extensions to clean up when the parser is cloned
298
	 */
299
	public function __clone() {
300
		$this->mInParse = false;
301
302
		// Bug 56226: When you create a reference "to" an object field, that
303
		// makes the object field itself be a reference too (until the other
304
		// reference goes out of scope). When cloning, any field that's a
305
		// reference is copied as a reference in the new object. Both of these
306
		// are defined PHP5 behaviors, as inconvenient as it is for us when old
307
		// hooks from PHP4 days are passing fields by reference.
308
		foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
309
			// Make a non-reference copy of the field, then rebind the field to
310
			// reference the new copy.
311
			$tmp = $this->$k;
312
			$this->$k =& $tmp;
313
			unset( $tmp );
314
		}
315
316
		Hooks::run( 'ParserCloned', [ $this ] );
317
	}
318
319
	/**
320
	 * Do various kinds of initialisation on the first call of the parser
321
	 */
322
	public function firstCallInit() {
323
		if ( !$this->mFirstCall ) {
324
			return;
325
		}
326
		$this->mFirstCall = false;
327
328
		CoreParserFunctions::register( $this );
329
		CoreTagHooks::register( $this );
330
		$this->initialiseVariables();
331
332
		Hooks::run( 'ParserFirstCallInit', [ &$this ] );
333
	}
334
335
	/**
336
	 * Clear Parser state
337
	 *
338
	 * @private
339
	 */
340
	public function clearState() {
341
		if ( $this->mFirstCall ) {
342
			$this->firstCallInit();
343
		}
344
		$this->mOutput = new ParserOutput;
345
		$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
346
		$this->mAutonumber = 0;
347
		$this->mIncludeCount = [];
348
		$this->mLinkHolders = new LinkHolderArray( $this );
349
		$this->mLinkID = 0;
350
		$this->mRevisionObject = $this->mRevisionTimestamp =
351
			$this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
352
		$this->mVarCache = [];
353
		$this->mUser = null;
354
		$this->mLangLinkLanguages = [];
355
		$this->currentRevisionCache = null;
356
357
		$this->mStripState = new StripState;
358
359
		# Clear these on every parse, bug 4549
360
		$this->mTplRedirCache = $this->mTplDomCache = [];
361
362
		$this->mShowToc = true;
363
		$this->mForceTocPosition = false;
364
		$this->mIncludeSizes = [
365
			'post-expand' => 0,
366
			'arg' => 0,
367
		];
368
		$this->mPPNodeCount = 0;
369
		$this->mGeneratedPPNodeCount = 0;
370
		$this->mHighestExpansionDepth = 0;
371
		$this->mDefaultSort = false;
372
		$this->mHeadings = [];
373
		$this->mDoubleUnderscores = [];
374
		$this->mExpensiveFunctionCount = 0;
375
376
		# Fix cloning
377
		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...
378
			$this->mPreprocessor = null;
379
		}
380
381
		$this->mProfiler = new SectionProfiler();
382
383
		Hooks::run( 'ParserClearState', [ &$this ] );
384
	}
385
386
	/**
387
	 * Convert wikitext to HTML
388
	 * Do not call this function recursively.
389
	 *
390
	 * @param string $text Text we want to parse
391
	 * @param Title $title
392
	 * @param ParserOptions $options
393
	 * @param bool $linestart
394
	 * @param bool $clearState
395
	 * @param int $revid Number to pass in {{REVISIONID}}
396
	 * @return ParserOutput A ParserOutput
397
	 */
398
	public function parse( $text, Title $title, ParserOptions $options,
399
		$linestart = true, $clearState = true, $revid = null
400
	) {
401
		/**
402
		 * First pass--just handle <nowiki> sections, pass the rest off
403
		 * to internalParse() which does all the real work.
404
		 */
405
406
		global $wgShowHostnames;
407
408
		if ( $clearState ) {
409
			// We use U+007F DELETE to construct strip markers, so we have to make
410
			// sure that this character does not occur in the input text.
411
			$text = strtr( $text, "\x7f", "?" );
412
			$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...
413
		}
414
415
		$this->startParse( $title, $options, self::OT_HTML, $clearState );
416
417
		$this->currentRevisionCache = null;
418
		$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...
419
		if ( $this->mOptions->getEnableLimitReport() ) {
420
			$this->mOutput->resetParseStartTime();
421
		}
422
423
		$oldRevisionId = $this->mRevisionId;
424
		$oldRevisionObject = $this->mRevisionObject;
425
		$oldRevisionTimestamp = $this->mRevisionTimestamp;
426
		$oldRevisionUser = $this->mRevisionUser;
427
		$oldRevisionSize = $this->mRevisionSize;
428
		if ( $revid !== null ) {
429
			$this->mRevisionId = $revid;
430
			$this->mRevisionObject = null;
431
			$this->mRevisionTimestamp = null;
432
			$this->mRevisionUser = null;
433
			$this->mRevisionSize = null;
434
		}
435
436
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
437
		# No more strip!
438
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
439
		$text = $this->internalParse( $text );
440
		Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] );
441
442
		$text = $this->internalParseHalfParsed( $text, true, $linestart );
443
444
		/**
445
		 * A converted title will be provided in the output object if title and
446
		 * content conversion are enabled, the article text does not contain
447
		 * a conversion-suppressing double-underscore tag, and no
448
		 * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
449
		 * automatic link conversion.
450
		 */
451
		if ( !( $options->getDisableTitleConversion()
452
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] )
453
			|| isset( $this->mDoubleUnderscores['notitleconvert'] )
454
			|| $this->mOutput->getDisplayTitle() !== false )
455
		) {
456
			$convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
457
			if ( $convruletitle ) {
458
				$this->mOutput->setTitleText( $convruletitle );
459
			} else {
460
				$titleText = $this->getConverterLanguage()->convertTitle( $title );
461
				$this->mOutput->setTitleText( $titleText );
462
			}
463
		}
464
465
		if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
466
			$this->limitationWarn( 'expensive-parserfunction',
467
				$this->mExpensiveFunctionCount,
468
				$this->mOptions->getExpensiveParserFunctionLimit()
469
			);
470
		}
471
472
		# Information on include size limits, for the benefit of users who try to skirt them
473
		if ( $this->mOptions->getEnableLimitReport() ) {
474
			$max = $this->mOptions->getMaxIncludeSize();
475
476
			$cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
477
			if ( $cpuTime !== null ) {
478
				$this->mOutput->setLimitReportData( 'limitreport-cputime',
479
					sprintf( "%.3f", $cpuTime )
480
				);
481
			}
482
483
			$wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
484
			$this->mOutput->setLimitReportData( 'limitreport-walltime',
485
				sprintf( "%.3f", $wallTime )
486
			);
487
488
			$this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
489
				[ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
490
			);
491
			$this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
492
				[ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
493
			);
494
			$this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
495
				[ $this->mIncludeSizes['post-expand'], $max ]
496
			);
497
			$this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
498
				[ $this->mIncludeSizes['arg'], $max ]
499
			);
500
			$this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
501
				[ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
502
			);
503
			$this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
504
				[ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
505
			);
506
			Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
507
508
			$limitReport = "NewPP limit report\n";
509
			if ( $wgShowHostnames ) {
510
				$limitReport .= 'Parsed by ' . wfHostname() . "\n";
511
			}
512
			$limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
513
			$limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
514
			$limitReport .= 'Dynamic content: ' .
515
				( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
516
				"\n";
517
518
			foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
519
				if ( Hooks::run( 'ParserLimitReportFormat',
520
					[ $key, &$value, &$limitReport, false, false ]
521
				) ) {
522
					$keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
523
					$valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
524
						->inLanguage( 'en' )->useDatabase( false );
525
					if ( !$valueMsg->exists() ) {
526
						$valueMsg = new RawMessage( '$1' );
527
					}
528
					if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
529
						$valueMsg->params( $value );
530
						$limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
531
					}
532
				}
533
			}
534
			// Since we're not really outputting HTML, decode the entities and
535
			// then re-encode the things that need hiding inside HTML comments.
536
			$limitReport = htmlspecialchars_decode( $limitReport );
537
			Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ] );
538
539
			// Sanitize for comment. Note '‐' in the replacement is U+2010,
540
			// which looks much like the problematic '-'.
541
			$limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
542
			$text .= "\n<!-- \n$limitReport-->\n";
543
544
			// Add on template profiling data
545
			$dataByFunc = $this->mProfiler->getFunctionStats();
546
			uasort( $dataByFunc, function ( $a, $b ) {
547
				return $a['real'] < $b['real']; // descending order
548
			} );
549
			$profileReport = "Transclusion expansion time report (%,ms,calls,template)\n";
550
			foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
551
				$profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n",
552
					$item['%real'], $item['real'], $item['calls'],
553
					htmlspecialchars( $item['name'] ) );
554
			}
555
			$text .= "\n<!-- \n$profileReport-->\n";
556
557
			if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
558
				wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
559
					$this->mTitle->getPrefixedDBkey() );
560
			}
561
		}
562
		$this->mOutput->setText( $text );
563
564
		$this->mRevisionId = $oldRevisionId;
565
		$this->mRevisionObject = $oldRevisionObject;
566
		$this->mRevisionTimestamp = $oldRevisionTimestamp;
567
		$this->mRevisionUser = $oldRevisionUser;
568
		$this->mRevisionSize = $oldRevisionSize;
569
		$this->mInputSize = false;
570
		$this->currentRevisionCache = null;
571
572
		return $this->mOutput;
573
	}
574
575
	/**
576
	 * Half-parse wikitext to half-parsed HTML. This recursive parser entry point
577
	 * can be called from an extension tag hook.
578
	 *
579
	 * The output of this function IS NOT SAFE PARSED HTML; it is "half-parsed"
580
	 * instead, which means that lists and links have not been fully parsed yet,
581
	 * and strip markers are still present.
582
	 *
583
	 * Use recursiveTagParseFully() to fully parse wikitext to output-safe HTML.
584
	 *
585
	 * Use this function if you're a parser tag hook and you want to parse
586
	 * wikitext before or after applying additional transformations, and you
587
	 * intend to *return the result as hook output*, which will cause it to go
588
	 * through the rest of parsing process automatically.
589
	 *
590
	 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
591
	 * $text are not expanded
592
	 *
593
	 * @param string $text Text extension wants to have parsed
594
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
595
	 * @return string UNSAFE half-parsed HTML
596
	 */
597
	public function recursiveTagParse( $text, $frame = false ) {
598
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
599
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
600
		$text = $this->internalParse( $text, false, $frame );
601
		return $text;
602
	}
603
604
	/**
605
	 * Fully parse wikitext to fully parsed HTML. This recursive parser entry
606
	 * point can be called from an extension tag hook.
607
	 *
608
	 * The output of this function is fully-parsed HTML that is safe for output.
609
	 * If you're a parser tag hook, you might want to use recursiveTagParse()
610
	 * instead.
611
	 *
612
	 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
613
	 * $text are not expanded
614
	 *
615
	 * @since 1.25
616
	 *
617
	 * @param string $text Text extension wants to have parsed
618
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
619
	 * @return string Fully parsed HTML
620
	 */
621
	public function recursiveTagParseFully( $text, $frame = false ) {
622
		$text = $this->recursiveTagParse( $text, $frame );
623
		$text = $this->internalParseHalfParsed( $text, false );
624
		return $text;
625
	}
626
627
	/**
628
	 * Expand templates and variables in the text, producing valid, static wikitext.
629
	 * Also removes comments.
630
	 * Do not call this function recursively.
631
	 * @param string $text
632
	 * @param Title $title
633
	 * @param ParserOptions $options
634
	 * @param int|null $revid
635
	 * @param bool|PPFrame $frame
636
	 * @return mixed|string
637
	 */
638
	public function preprocess( $text, Title $title = null,
639
		ParserOptions $options, $revid = null, $frame = false
640
	) {
641
		$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...
642
		$this->startParse( $title, $options, self::OT_PREPROCESS, true );
643
		if ( $revid !== null ) {
644
			$this->mRevisionId = $revid;
645
		}
646
		Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
647
		Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
648
		$text = $this->replaceVariables( $text, $frame );
649
		$text = $this->mStripState->unstripBoth( $text );
650
		return $text;
651
	}
652
653
	/**
654
	 * Recursive parser entry point that can be called from an extension tag
655
	 * hook.
656
	 *
657
	 * @param string $text Text to be expanded
658
	 * @param bool|PPFrame $frame The frame to use for expanding any template variables
659
	 * @return string
660
	 * @since 1.19
661
	 */
662
	public function recursivePreprocess( $text, $frame = false ) {
663
		$text = $this->replaceVariables( $text, $frame );
664
		$text = $this->mStripState->unstripBoth( $text );
665
		return $text;
666
	}
667
668
	/**
669
	 * Process the wikitext for the "?preload=" feature. (bug 5210)
670
	 *
671
	 * "<noinclude>", "<includeonly>" etc. are parsed as for template
672
	 * transclusion, comments, templates, arguments, tags hooks and parser
673
	 * functions are untouched.
674
	 *
675
	 * @param string $text
676
	 * @param Title $title
677
	 * @param ParserOptions $options
678
	 * @param array $params
679
	 * @return string
680
	 */
681
	public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
682
		$msg = new RawMessage( $text );
683
		$text = $msg->params( $params )->plain();
684
685
		# Parser (re)initialisation
686
		$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...
687
		$this->startParse( $title, $options, self::OT_PLAIN, true );
688
689
		$flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
690
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
691
		$text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
692
		$text = $this->mStripState->unstripBoth( $text );
693
		return $text;
694
	}
695
696
	/**
697
	 * Get a random string
698
	 *
699
	 * @return string
700
	 * @deprecated since 1.26; use wfRandomString() instead.
701
	 */
702
	public static function getRandomString() {
703
		wfDeprecated( __METHOD__, '1.26' );
704
		return wfRandomString( 16 );
705
	}
706
707
	/**
708
	 * Set the current user.
709
	 * Should only be used when doing pre-save transform.
710
	 *
711
	 * @param User|null $user User object or null (to reset)
712
	 */
713
	public function setUser( $user ) {
714
		$this->mUser = $user;
715
	}
716
717
	/**
718
	 * Accessor for mUniqPrefix.
719
	 *
720
	 * @return string
721
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
722
	 */
723
	public function uniqPrefix() {
724
		wfDeprecated( __METHOD__, '1.26' );
725
		return self::MARKER_PREFIX;
726
	}
727
728
	/**
729
	 * Set the context title
730
	 *
731
	 * @param Title $t
732
	 */
733
	public function setTitle( $t ) {
734
		if ( !$t ) {
735
			$t = Title::newFromText( 'NO TITLE' );
736
		}
737
738
		if ( $t->hasFragment() ) {
739
			# Strip the fragment to avoid various odd effects
740
			$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...
741
		} else {
742
			$this->mTitle = $t;
743
		}
744
	}
745
746
	/**
747
	 * Accessor for the Title object
748
	 *
749
	 * @return Title
750
	 */
751
	public function getTitle() {
752
		return $this->mTitle;
753
	}
754
755
	/**
756
	 * Accessor/mutator for the Title object
757
	 *
758
	 * @param Title $x Title object or null to just get the current one
759
	 * @return Title
760
	 */
761
	public function Title( $x = null ) {
762
		return wfSetVar( $this->mTitle, $x );
763
	}
764
765
	/**
766
	 * Set the output type
767
	 *
768
	 * @param int $ot New value
769
	 */
770
	public function setOutputType( $ot ) {
771
		$this->mOutputType = $ot;
772
		# Shortcut alias
773
		$this->ot = [
774
			'html' => $ot == self::OT_HTML,
775
			'wiki' => $ot == self::OT_WIKI,
776
			'pre' => $ot == self::OT_PREPROCESS,
777
			'plain' => $ot == self::OT_PLAIN,
778
		];
779
	}
780
781
	/**
782
	 * Accessor/mutator for the output type
783
	 *
784
	 * @param int|null $x New value or null to just get the current one
785
	 * @return int
786
	 */
787
	public function OutputType( $x = null ) {
788
		return wfSetVar( $this->mOutputType, $x );
789
	}
790
791
	/**
792
	 * Get the ParserOutput object
793
	 *
794
	 * @return ParserOutput
795
	 */
796
	public function getOutput() {
797
		return $this->mOutput;
798
	}
799
800
	/**
801
	 * Get the ParserOptions object
802
	 *
803
	 * @return ParserOptions
804
	 */
805
	public function getOptions() {
806
		return $this->mOptions;
807
	}
808
809
	/**
810
	 * Accessor/mutator for the ParserOptions object
811
	 *
812
	 * @param ParserOptions $x New value or null to just get the current one
813
	 * @return ParserOptions Current ParserOptions object
814
	 */
815
	public function Options( $x = null ) {
816
		return wfSetVar( $this->mOptions, $x );
817
	}
818
819
	/**
820
	 * @return int
821
	 */
822
	public function nextLinkID() {
823
		return $this->mLinkID++;
824
	}
825
826
	/**
827
	 * @param int $id
828
	 */
829
	public function setLinkID( $id ) {
830
		$this->mLinkID = $id;
831
	}
832
833
	/**
834
	 * Get a language object for use in parser functions such as {{FORMATNUM:}}
835
	 * @return Language
836
	 */
837
	public function getFunctionLang() {
838
		return $this->getTargetLanguage();
839
	}
840
841
	/**
842
	 * Get the target language for the content being parsed. This is usually the
843
	 * language that the content is in.
844
	 *
845
	 * @since 1.19
846
	 *
847
	 * @throws MWException
848
	 * @return Language
849
	 */
850
	public function getTargetLanguage() {
851
		$target = $this->mOptions->getTargetLanguage();
852
853
		if ( $target !== null ) {
854
			return $target;
855
		} elseif ( $this->mOptions->getInterfaceMessage() ) {
856
			return $this->mOptions->getUserLangObj();
857
		} elseif ( is_null( $this->mTitle ) ) {
858
			throw new MWException( __METHOD__ . ': $this->mTitle is null' );
859
		}
860
861
		return $this->mTitle->getPageLanguage();
862
	}
863
864
	/**
865
	 * Get the language object for language conversion
866
	 * @return Language|null
867
	 */
868
	public function getConverterLanguage() {
869
		return $this->getTargetLanguage();
870
	}
871
872
	/**
873
	 * Get a User object either from $this->mUser, if set, or from the
874
	 * ParserOptions object otherwise
875
	 *
876
	 * @return User
877
	 */
878
	public function getUser() {
879
		if ( !is_null( $this->mUser ) ) {
880
			return $this->mUser;
881
		}
882
		return $this->mOptions->getUser();
883
	}
884
885
	/**
886
	 * Get a preprocessor object
887
	 *
888
	 * @return Preprocessor
889
	 */
890
	public function getPreprocessor() {
891
		if ( !isset( $this->mPreprocessor ) ) {
892
			$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...
893
			$this->mPreprocessor = new $class( $this );
894
		}
895
		return $this->mPreprocessor;
896
	}
897
898
	/**
899
	 * Get a LinkRenderer instance to make links with
900
	 *
901
	 * @since 1.28
902
	 * @return LinkRenderer
903
	 */
904
	public function getLinkRenderer() {
905
		if ( !$this->mLinkRenderer ) {
906
			$this->mLinkRenderer = MediaWikiServices::getInstance()
907
				->getLinkRendererFactory()->create();
908
			$this->mLinkRenderer->setStubThreshold(
909
				$this->getOptions()->getStubThreshold()
910
			);
911
		}
912
913
		return $this->mLinkRenderer;
914
	}
915
916
	/**
917
	 * Replaces all occurrences of HTML-style comments and the given tags
918
	 * in the text with a random marker and returns the next text. The output
919
	 * parameter $matches will be an associative array filled with data in
920
	 * the form:
921
	 *
922
	 * @code
923
	 *   'UNIQ-xxxxx' => array(
924
	 *     'element',
925
	 *     'tag content',
926
	 *     array( 'param' => 'x' ),
927
	 *     '<element param="x">tag content</element>' ) )
928
	 * @endcode
929
	 *
930
	 * @param array $elements List of element names. Comments are always extracted.
931
	 * @param string $text Source text string.
932
	 * @param array $matches Out parameter, Array: extracted tags
933
	 * @param string|null $uniq_prefix
934
	 * @return string Stripped text
935
	 * @since 1.26 The uniq_prefix argument is deprecated.
936
	 */
937
	public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
938
		if ( $uniq_prefix !== null ) {
939
			wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
940
		}
941
		static $n = 1;
942
		$stripped = '';
943
		$matches = [];
944
945
		$taglist = implode( '|', $elements );
946
		$start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
947
948
		while ( $text != '' ) {
949
			$p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
950
			$stripped .= $p[0];
951
			if ( count( $p ) < 5 ) {
952
				break;
953
			}
954
			if ( count( $p ) > 5 ) {
955
				# comment
956
				$element = $p[4];
957
				$attributes = '';
958
				$close = '';
959
				$inside = $p[5];
960
			} else {
961
				# tag
962
				$element = $p[1];
963
				$attributes = $p[2];
964
				$close = $p[3];
965
				$inside = $p[4];
966
			}
967
968
			$marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
969
			$stripped .= $marker;
970
971
			if ( $close === '/>' ) {
972
				# Empty element tag, <tag />
973
				$content = null;
974
				$text = $inside;
975
				$tail = null;
976
			} else {
977
				if ( $element === '!--' ) {
978
					$end = '/(-->)/';
979
				} else {
980
					$end = "/(<\\/$element\\s*>)/i";
981
				}
982
				$q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
983
				$content = $q[0];
984
				if ( count( $q ) < 3 ) {
985
					# No end tag -- let it run out to the end of the text.
986
					$tail = '';
987
					$text = '';
988
				} else {
989
					$tail = $q[1];
990
					$text = $q[2];
991
				}
992
			}
993
994
			$matches[$marker] = [ $element,
995
				$content,
996
				Sanitizer::decodeTagAttributes( $attributes ),
997
				"<$element$attributes$close$content$tail" ];
998
		}
999
		return $stripped;
1000
	}
1001
1002
	/**
1003
	 * Get a list of strippable XML-like elements
1004
	 *
1005
	 * @return array
1006
	 */
1007
	public function getStripList() {
1008
		return $this->mStripList;
1009
	}
1010
1011
	/**
1012
	 * Add an item to the strip state
1013
	 * Returns the unique tag which must be inserted into the stripped text
1014
	 * The tag will be replaced with the original text in unstrip()
1015
	 *
1016
	 * @param string $text
1017
	 *
1018
	 * @return string
1019
	 */
1020
	public function insertStripItem( $text ) {
1021
		$marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1022
		$this->mMarkerIndex++;
1023
		$this->mStripState->addGeneral( $marker, $text );
1024
		return $marker;
1025
	}
1026
1027
	/**
1028
	 * parse the wiki syntax used to render tables
1029
	 *
1030
	 * @private
1031
	 * @param string $text
1032
	 * @return string
1033
	 */
1034
	public function doTableStuff( $text ) {
1035
1036
		$lines = StringUtils::explode( "\n", $text );
1037
		$out = '';
1038
		$td_history = []; # Is currently a td tag open?
1039
		$last_tag_history = []; # Save history of last lag activated (td, th or caption)
1040
		$tr_history = []; # Is currently a tr tag open?
1041
		$tr_attributes = []; # history of tr attributes
1042
		$has_opened_tr = []; # Did this table open a <tr> element?
1043
		$indent_level = 0; # indent level of the table
1044
1045
		foreach ( $lines as $outLine ) {
1046
			$line = trim( $outLine );
1047
1048
			if ( $line === '' ) { # empty line, go to next line
1049
				$out .= $outLine . "\n";
1050
				continue;
1051
			}
1052
1053
			$first_character = $line[0];
1054
			$first_two = substr( $line, 0, 2 );
1055
			$matches = [];
1056
1057
			if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1058
				# First check if we are starting a new table
1059
				$indent_level = strlen( $matches[1] );
1060
1061
				$attributes = $this->mStripState->unstripBoth( $matches[2] );
1062
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1063
1064
				$outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1065
				array_push( $td_history, false );
1066
				array_push( $last_tag_history, '' );
1067
				array_push( $tr_history, false );
1068
				array_push( $tr_attributes, '' );
1069
				array_push( $has_opened_tr, false );
1070
			} elseif ( count( $td_history ) == 0 ) {
1071
				# Don't do any of the following
1072
				$out .= $outLine . "\n";
1073
				continue;
1074
			} elseif ( $first_two === '|}' ) {
1075
				# We are ending a table
1076
				$line = '</table>' . substr( $line, 2 );
1077
				$last_tag = array_pop( $last_tag_history );
1078
1079
				if ( !array_pop( $has_opened_tr ) ) {
1080
					$line = "<tr><td></td></tr>{$line}";
1081
				}
1082
1083
				if ( array_pop( $tr_history ) ) {
1084
					$line = "</tr>{$line}";
1085
				}
1086
1087
				if ( array_pop( $td_history ) ) {
1088
					$line = "</{$last_tag}>{$line}";
1089
				}
1090
				array_pop( $tr_attributes );
1091
				$outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1092
			} elseif ( $first_two === '|-' ) {
1093
				# Now we have a table row
1094
				$line = preg_replace( '#^\|-+#', '', $line );
1095
1096
				# Whats after the tag is now only attributes
1097
				$attributes = $this->mStripState->unstripBoth( $line );
1098
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1099
				array_pop( $tr_attributes );
1100
				array_push( $tr_attributes, $attributes );
1101
1102
				$line = '';
1103
				$last_tag = array_pop( $last_tag_history );
1104
				array_pop( $has_opened_tr );
1105
				array_push( $has_opened_tr, true );
1106
1107
				if ( array_pop( $tr_history ) ) {
1108
					$line = '</tr>';
1109
				}
1110
1111
				if ( array_pop( $td_history ) ) {
1112
					$line = "</{$last_tag}>{$line}";
1113
				}
1114
1115
				$outLine = $line;
1116
				array_push( $tr_history, false );
1117
				array_push( $td_history, false );
1118
				array_push( $last_tag_history, '' );
1119
			} elseif ( $first_character === '|'
1120
				|| $first_character === '!'
1121
				|| $first_two === '|+'
1122
			) {
1123
				# This might be cell elements, td, th or captions
1124
				if ( $first_two === '|+' ) {
1125
					$first_character = '+';
1126
					$line = substr( $line, 2 );
1127
				} else {
1128
					$line = substr( $line, 1 );
1129
				}
1130
1131
				// Implies both are valid for table headings.
1132
				if ( $first_character === '!' ) {
1133
					$line = StringUtils::replaceMarkup( '!!', '||', $line );
1134
				}
1135
1136
				# Split up multiple cells on the same line.
1137
				# FIXME : This can result in improper nesting of tags processed
1138
				# by earlier parser steps.
1139
				$cells = explode( '||', $line );
1140
1141
				$outLine = '';
1142
1143
				# Loop through each table cell
1144
				foreach ( $cells as $cell ) {
1145
					$previous = '';
1146
					if ( $first_character !== '+' ) {
1147
						$tr_after = array_pop( $tr_attributes );
1148
						if ( !array_pop( $tr_history ) ) {
1149
							$previous = "<tr{$tr_after}>\n";
1150
						}
1151
						array_push( $tr_history, true );
1152
						array_push( $tr_attributes, '' );
1153
						array_pop( $has_opened_tr );
1154
						array_push( $has_opened_tr, true );
1155
					}
1156
1157
					$last_tag = array_pop( $last_tag_history );
1158
1159
					if ( array_pop( $td_history ) ) {
1160
						$previous = "</{$last_tag}>\n{$previous}";
1161
					}
1162
1163
					if ( $first_character === '|' ) {
1164
						$last_tag = 'td';
1165
					} elseif ( $first_character === '!' ) {
1166
						$last_tag = 'th';
1167
					} elseif ( $first_character === '+' ) {
1168
						$last_tag = 'caption';
1169
					} else {
1170
						$last_tag = '';
1171
					}
1172
1173
					array_push( $last_tag_history, $last_tag );
1174
1175
					# A cell could contain both parameters and data
1176
					$cell_data = explode( '|', $cell, 2 );
1177
1178
					# Bug 553: Note that a '|' inside an invalid link should not
1179
					# be mistaken as delimiting cell parameters
1180
					if ( strpos( $cell_data[0], '[[' ) !== false ) {
1181
						$cell = "{$previous}<{$last_tag}>{$cell}";
1182
					} elseif ( count( $cell_data ) == 1 ) {
1183
						$cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1184
					} else {
1185
						$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1186
						$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1187
						$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1188
					}
1189
1190
					$outLine .= $cell;
1191
					array_push( $td_history, true );
1192
				}
1193
			}
1194
			$out .= $outLine . "\n";
1195
		}
1196
1197
		# Closing open td, tr && table
1198
		while ( count( $td_history ) > 0 ) {
1199
			if ( array_pop( $td_history ) ) {
1200
				$out .= "</td>\n";
1201
			}
1202
			if ( array_pop( $tr_history ) ) {
1203
				$out .= "</tr>\n";
1204
			}
1205
			if ( !array_pop( $has_opened_tr ) ) {
1206
				$out .= "<tr><td></td></tr>\n";
1207
			}
1208
1209
			$out .= "</table>\n";
1210
		}
1211
1212
		# Remove trailing line-ending (b/c)
1213 View Code Duplication
		if ( substr( $out, -1 ) === "\n" ) {
1214
			$out = substr( $out, 0, -1 );
1215
		}
1216
1217
		# special case: don't return empty table
1218
		if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1219
			$out = '';
1220
		}
1221
1222
		return $out;
1223
	}
1224
1225
	/**
1226
	 * Helper function for parse() that transforms wiki markup into half-parsed
1227
	 * HTML. Only called for $mOutputType == self::OT_HTML.
1228
	 *
1229
	 * @private
1230
	 *
1231
	 * @param string $text The text to parse
1232
	 * @param bool $isMain Whether this is being called from the main parse() function
1233
	 * @param PPFrame|bool $frame A pre-processor frame
1234
	 *
1235
	 * @return string
1236
	 */
1237
	public function internalParse( $text, $isMain = true, $frame = false ) {
1238
1239
		$origText = $text;
1240
1241
		# Hook to suspend the parser in this state
1242
		if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) {
1243
			return $text;
1244
		}
1245
1246
		# if $frame is provided, then use $frame for replacing any variables
1247
		if ( $frame ) {
1248
			# use frame depth to infer how include/noinclude tags should be handled
1249
			# depth=0 means this is the top-level document; otherwise it's an included document
1250
			if ( !$frame->depth ) {
1251
				$flag = 0;
1252
			} else {
1253
				$flag = Parser::PTD_FOR_INCLUSION;
1254
			}
1255
			$dom = $this->preprocessToDom( $text, $flag );
1256
			$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...
1257
		} else {
1258
			# if $frame is not provided, then use old-style replaceVariables
1259
			$text = $this->replaceVariables( $text );
1260
		}
1261
1262
		Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] );
1263
		$text = Sanitizer::removeHTMLtags(
1264
			$text,
1265
			[ &$this, 'attributeStripCallback' ],
1266
			false,
1267
			array_keys( $this->mTransparentTagHooks )
1268
		);
1269
		Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
1270
1271
		# Tables need to come after variable replacement for things to work
1272
		# properly; putting them before other transformations should keep
1273
		# exciting things like link expansions from showing up in surprising
1274
		# places.
1275
		$text = $this->doTableStuff( $text );
1276
1277
		$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1278
1279
		$text = $this->doDoubleUnderscore( $text );
1280
1281
		$text = $this->doHeadings( $text );
1282
		$text = $this->replaceInternalLinks( $text );
1283
		$text = $this->doAllQuotes( $text );
1284
		$text = $this->replaceExternalLinks( $text );
1285
1286
		# replaceInternalLinks may sometimes leave behind
1287
		# absolute URLs, which have to be masked to hide them from replaceExternalLinks
1288
		$text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1289
1290
		$text = $this->doMagicLinks( $text );
1291
		$text = $this->formatHeadings( $text, $origText, $isMain );
1292
1293
		return $text;
1294
	}
1295
1296
	/**
1297
	 * Helper function for parse() that transforms half-parsed HTML into fully
1298
	 * parsed HTML.
1299
	 *
1300
	 * @param string $text
1301
	 * @param bool $isMain
1302
	 * @param bool $linestart
1303
	 * @return string
1304
	 */
1305
	private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1306
		$text = $this->mStripState->unstripGeneral( $text );
1307
1308
		if ( $isMain ) {
1309
			Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
1310
		}
1311
1312
		# Clean up special characters, only run once, next-to-last before doBlockLevels
1313
		$fixtags = [
1314
			# french spaces, last one Guillemet-left
1315
			# only if there is something before the space
1316
			'/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1317
			# french spaces, Guillemet-right
1318
			'/(\\302\\253) /' => '\\1&#160;',
1319
			'/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1320
		];
1321
		$text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1322
1323
		$text = $this->doBlockLevels( $text, $linestart );
1324
1325
		$this->replaceLinkHolders( $text );
1326
1327
		/**
1328
		 * The input doesn't get language converted if
1329
		 * a) It's disabled
1330
		 * b) Content isn't converted
1331
		 * c) It's a conversion table
1332
		 * d) it is an interface message (which is in the user language)
1333
		 */
1334
		if ( !( $this->mOptions->getDisableContentConversion()
1335
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1336
		) {
1337
			if ( !$this->mOptions->getInterfaceMessage() ) {
1338
				# The position of the convert() call should not be changed. it
1339
				# assumes that the links are all replaced and the only thing left
1340
				# is the <nowiki> mark.
1341
				$text = $this->getConverterLanguage()->convert( $text );
1342
			}
1343
		}
1344
1345
		$text = $this->mStripState->unstripNoWiki( $text );
1346
1347
		if ( $isMain ) {
1348
			Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
1349
		}
1350
1351
		$text = $this->replaceTransparentTags( $text );
1352
		$text = $this->mStripState->unstripGeneral( $text );
1353
1354
		$text = Sanitizer::normalizeCharReferences( $text );
1355
1356
		if ( MWTidy::isEnabled() && $this->mOptions->getTidy() ) {
1357
			$text = MWTidy::tidy( $text );
1358
			$this->mOutput->addModuleStyles( MWTidy::getModuleStyles() );
1359
		} else {
1360
			# attempt to sanitize at least some nesting problems
1361
			# (bug #2702 and quite a few others)
1362
			$tidyregs = [
1363
				# ''Something [http://www.cool.com cool''] -->
1364
				# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1365
				'/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1366
				'\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1367
				# fix up an anchor inside another anchor, only
1368
				# at least for a single single nested link (bug 3695)
1369
				'/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1370
				'\\1\\2</a>\\3</a>\\1\\4</a>',
1371
				# fix div inside inline elements- doBlockLevels won't wrap a line which
1372
				# contains a div, so fix it up here; replace
1373
				# div with escaped text
1374
				'/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1375
				'\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1376
				# remove empty italic or bold tag pairs, some
1377
				# introduced by rules above
1378
				'/<([bi])><\/\\1>/' => '',
1379
			];
1380
1381
			$text = preg_replace(
1382
				array_keys( $tidyregs ),
1383
				array_values( $tidyregs ),
1384
				$text );
1385
		}
1386
1387
		if ( $isMain ) {
1388
			Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
1389
		}
1390
1391
		return $text;
1392
	}
1393
1394
	/**
1395
	 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1396
	 * magic external links.
1397
	 *
1398
	 * DML
1399
	 * @private
1400
	 *
1401
	 * @param string $text
1402
	 *
1403
	 * @return string
1404
	 */
1405
	public function doMagicLinks( $text ) {
1406
		$prots = wfUrlProtocolsWithoutProtRel();
1407
		$urlChar = self::EXT_LINK_URL_CLASS;
1408
		$addr = self::EXT_LINK_ADDR;
1409
		$space = self::SPACE_NOT_NL; #  non-newline space
1410
		$spdash = "(?:-|$space)"; # a dash or a non-newline space
1411
		$spaces = "$space++"; # possessive match of 1 or more spaces
1412
		$text = preg_replace_callback(
1413
			'!(?:                            # Start cases
1414
				(<a[ \t\r\n>].*?</a>) |      # m[1]: Skip link text
1415
				(<.*?>) |                    # m[2]: Skip stuff inside
1416
				                             #       HTML elements' . "
1417
				(\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1418
				                             # m[4]: Post-protocol path
1419
				\b(?:RFC|PMID) $spaces       # m[5]: RFC or PMID, capture number
1420
					([0-9]+)\b |
1421
				\bISBN $spaces (             # m[6]: ISBN, capture number
1422
					(?: 97[89] $spdash? )?   #  optional 13-digit ISBN prefix
1423
					(?: [0-9]  $spdash? ){9} #  9 digits with opt. delimiters
1424
					[0-9Xx]                  #  check digit
1425
				)\b
1426
			)!xu", [ &$this, 'magicLinkCallback' ], $text );
1427
		return $text;
1428
	}
1429
1430
	/**
1431
	 * @throws MWException
1432
	 * @param array $m
1433
	 * @return HTML|string
1434
	 */
1435
	public function magicLinkCallback( $m ) {
1436
		if ( isset( $m[1] ) && $m[1] !== '' ) {
1437
			# Skip anchor
1438
			return $m[0];
1439
		} elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1440
			# Skip HTML element
1441
			return $m[0];
1442
		} elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1443
			# Free external link
1444
			return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1445
		} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1446
			# RFC or PMID
1447
			if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1448
				$keyword = 'RFC';
1449
				$urlmsg = 'rfcurl';
1450
				$cssClass = 'mw-magiclink-rfc';
1451
				$id = $m[5];
1452
			} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1453
				$keyword = 'PMID';
1454
				$urlmsg = 'pubmedurl';
1455
				$cssClass = 'mw-magiclink-pmid';
1456
				$id = $m[5];
1457
			} else {
1458
				throw new MWException( __METHOD__ . ': unrecognised match type "' .
1459
					substr( $m[0], 0, 20 ) . '"' );
1460
			}
1461
			$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1462
			return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass );
1463
		} elseif ( isset( $m[6] ) && $m[6] !== '' ) {
1464
			# ISBN
1465
			$isbn = $m[6];
1466
			$space = self::SPACE_NOT_NL; #  non-newline space
1467
			$isbn = preg_replace( "/$space/", ' ', $isbn );
1468
			$num = strtr( $isbn, [
1469
				'-' => '',
1470
				' ' => '',
1471
				'x' => 'X',
1472
			] );
1473
			$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
1474
			return '<a href="' .
1475
				htmlspecialchars( $titleObj->getLocalURL() ) .
1476
				"\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1477
		} else {
1478
			return $m[0];
1479
		}
1480
	}
1481
1482
	/**
1483
	 * Make a free external link, given a user-supplied URL
1484
	 *
1485
	 * @param string $url
1486
	 * @param int $numPostProto
1487
	 *   The number of characters after the protocol.
1488
	 * @return string HTML
1489
	 * @private
1490
	 */
1491
	public function makeFreeExternalLink( $url, $numPostProto ) {
1492
		$trail = '';
1493
1494
		# The characters '<' and '>' (which were escaped by
1495
		# removeHTMLtags()) should not be included in
1496
		# URLs, per RFC 2396.
1497
		# Make &nbsp; terminate a URL as well (bug T84937)
1498
		$m2 = [];
1499 View Code Duplication
		if ( preg_match(
1500
			'/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1501
			$url,
1502
			$m2,
1503
			PREG_OFFSET_CAPTURE
1504
		) ) {
1505
			$trail = substr( $url, $m2[0][1] ) . $trail;
1506
			$url = substr( $url, 0, $m2[0][1] );
1507
		}
1508
1509
		# Move trailing punctuation to $trail
1510
		$sep = ',;\.:!?';
1511
		# If there is no left bracket, then consider right brackets fair game too
1512
		if ( strpos( $url, '(' ) === false ) {
1513
			$sep .= ')';
1514
		}
1515
1516
		$urlRev = strrev( $url );
1517
		$numSepChars = strspn( $urlRev, $sep );
1518
		# Don't break a trailing HTML entity by moving the ; into $trail
1519
		# This is in hot code, so use substr_compare to avoid having to
1520
		# create a new string object for the comparison
1521
		if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1522
			# more optimization: instead of running preg_match with a $
1523
			# anchor, which can be slow, do the match on the reversed
1524
			# string starting at the desired offset.
1525
			# un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1526
			if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1527
				$numSepChars--;
1528
			}
1529
		}
1530
		if ( $numSepChars ) {
1531
			$trail = substr( $url, -$numSepChars ) . $trail;
1532
			$url = substr( $url, 0, -$numSepChars );
1533
		}
1534
1535
		# Verify that we still have a real URL after trail removal, and
1536
		# not just lone protocol
1537
		if ( strlen( $trail ) >= $numPostProto ) {
1538
			return $url . $trail;
1539
		}
1540
1541
		$url = Sanitizer::cleanUrl( $url );
1542
1543
		# Is this an external image?
1544
		$text = $this->maybeMakeExternalImage( $url );
1545
		if ( $text === false ) {
1546
			# Not an image, make a link
1547
			$text = Linker::makeExternalLink( $url,
1548
				$this->getConverterLanguage()->markNoConversion( $url, true ),
1549
				true, 'free',
1550
				$this->getExternalLinkAttribs( $url ) );
1551
			# Register it in the output object...
1552
			# Replace unnecessary URL escape codes with their equivalent characters
1553
			$pasteurized = self::normalizeLinkUrl( $url );
1554
			$this->mOutput->addExternalLink( $pasteurized );
1555
		}
1556
		return $text . $trail;
1557
	}
1558
1559
	/**
1560
	 * Parse headers and return html
1561
	 *
1562
	 * @private
1563
	 *
1564
	 * @param string $text
1565
	 *
1566
	 * @return string
1567
	 */
1568
	public function doHeadings( $text ) {
1569
		for ( $i = 6; $i >= 1; --$i ) {
1570
			$h = str_repeat( '=', $i );
1571
			$text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1572
		}
1573
		return $text;
1574
	}
1575
1576
	/**
1577
	 * Replace single quotes with HTML markup
1578
	 * @private
1579
	 *
1580
	 * @param string $text
1581
	 *
1582
	 * @return string The altered text
1583
	 */
1584
	public function doAllQuotes( $text ) {
1585
		$outtext = '';
1586
		$lines = StringUtils::explode( "\n", $text );
1587
		foreach ( $lines as $line ) {
1588
			$outtext .= $this->doQuotes( $line ) . "\n";
1589
		}
1590
		$outtext = substr( $outtext, 0, -1 );
1591
		return $outtext;
1592
	}
1593
1594
	/**
1595
	 * Helper function for doAllQuotes()
1596
	 *
1597
	 * @param string $text
1598
	 *
1599
	 * @return string
1600
	 */
1601
	public function doQuotes( $text ) {
1602
		$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1603
		$countarr = count( $arr );
1604
		if ( $countarr == 1 ) {
1605
			return $text;
1606
		}
1607
1608
		// First, do some preliminary work. This may shift some apostrophes from
1609
		// being mark-up to being text. It also counts the number of occurrences
1610
		// of bold and italics mark-ups.
1611
		$numbold = 0;
1612
		$numitalics = 0;
1613
		for ( $i = 1; $i < $countarr; $i += 2 ) {
1614
			$thislen = strlen( $arr[$i] );
1615
			// If there are ever four apostrophes, assume the first is supposed to
1616
			// be text, and the remaining three constitute mark-up for bold text.
1617
			// (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1618
			if ( $thislen == 4 ) {
1619
				$arr[$i - 1] .= "'";
1620
				$arr[$i] = "'''";
1621
				$thislen = 3;
1622
			} elseif ( $thislen > 5 ) {
1623
				// If there are more than 5 apostrophes in a row, assume they're all
1624
				// text except for the last 5.
1625
				// (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1626
				$arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1627
				$arr[$i] = "'''''";
1628
				$thislen = 5;
1629
			}
1630
			// Count the number of occurrences of bold and italics mark-ups.
1631
			if ( $thislen == 2 ) {
1632
				$numitalics++;
1633
			} elseif ( $thislen == 3 ) {
1634
				$numbold++;
1635
			} elseif ( $thislen == 5 ) {
1636
				$numitalics++;
1637
				$numbold++;
1638
			}
1639
		}
1640
1641
		// If there is an odd number of both bold and italics, it is likely
1642
		// that one of the bold ones was meant to be an apostrophe followed
1643
		// by italics. Which one we cannot know for certain, but it is more
1644
		// likely to be one that has a single-letter word before it.
1645
		if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1646
			$firstsingleletterword = -1;
1647
			$firstmultiletterword = -1;
1648
			$firstspace = -1;
1649
			for ( $i = 1; $i < $countarr; $i += 2 ) {
1650
				if ( strlen( $arr[$i] ) == 3 ) {
1651
					$x1 = substr( $arr[$i - 1], -1 );
1652
					$x2 = substr( $arr[$i - 1], -2, 1 );
1653
					if ( $x1 === ' ' ) {
1654
						if ( $firstspace == -1 ) {
1655
							$firstspace = $i;
1656
						}
1657
					} elseif ( $x2 === ' ' ) {
1658
						$firstsingleletterword = $i;
1659
						// if $firstsingleletterword is set, we don't
1660
						// look at the other options, so we can bail early.
1661
						break;
1662
					} else {
1663
						if ( $firstmultiletterword == -1 ) {
1664
							$firstmultiletterword = $i;
1665
						}
1666
					}
1667
				}
1668
			}
1669
1670
			// If there is a single-letter word, use it!
1671
			if ( $firstsingleletterword > -1 ) {
1672
				$arr[$firstsingleletterword] = "''";
1673
				$arr[$firstsingleletterword - 1] .= "'";
1674
			} elseif ( $firstmultiletterword > -1 ) {
1675
				// If not, but there's a multi-letter word, use that one.
1676
				$arr[$firstmultiletterword] = "''";
1677
				$arr[$firstmultiletterword - 1] .= "'";
1678
			} elseif ( $firstspace > -1 ) {
1679
				// ... otherwise use the first one that has neither.
1680
				// (notice that it is possible for all three to be -1 if, for example,
1681
				// there is only one pentuple-apostrophe in the line)
1682
				$arr[$firstspace] = "''";
1683
				$arr[$firstspace - 1] .= "'";
1684
			}
1685
		}
1686
1687
		// Now let's actually convert our apostrophic mush to HTML!
1688
		$output = '';
1689
		$buffer = '';
1690
		$state = '';
1691
		$i = 0;
1692
		foreach ( $arr as $r ) {
1693
			if ( ( $i % 2 ) == 0 ) {
1694
				if ( $state === 'both' ) {
1695
					$buffer .= $r;
1696
				} else {
1697
					$output .= $r;
1698
				}
1699
			} else {
1700
				$thislen = strlen( $r );
1701
				if ( $thislen == 2 ) {
1702 View Code Duplication
					if ( $state === 'i' ) {
1703
						$output .= '</i>';
1704
						$state = '';
1705
					} elseif ( $state === 'bi' ) {
1706
						$output .= '</i>';
1707
						$state = 'b';
1708
					} elseif ( $state === 'ib' ) {
1709
						$output .= '</b></i><b>';
1710
						$state = 'b';
1711
					} elseif ( $state === 'both' ) {
1712
						$output .= '<b><i>' . $buffer . '</i>';
1713
						$state = 'b';
1714
					} else { // $state can be 'b' or ''
1715
						$output .= '<i>';
1716
						$state .= 'i';
1717
					}
1718 View Code Duplication
				} elseif ( $thislen == 3 ) {
1719
					if ( $state === 'b' ) {
1720
						$output .= '</b>';
1721
						$state = '';
1722
					} elseif ( $state === 'bi' ) {
1723
						$output .= '</i></b><i>';
1724
						$state = 'i';
1725
					} elseif ( $state === 'ib' ) {
1726
						$output .= '</b>';
1727
						$state = 'i';
1728
					} elseif ( $state === 'both' ) {
1729
						$output .= '<i><b>' . $buffer . '</b>';
1730
						$state = 'i';
1731
					} else { // $state can be 'i' or ''
1732
						$output .= '<b>';
1733
						$state .= 'b';
1734
					}
1735
				} elseif ( $thislen == 5 ) {
1736
					if ( $state === 'b' ) {
1737
						$output .= '</b><i>';
1738
						$state = 'i';
1739
					} elseif ( $state === 'i' ) {
1740
						$output .= '</i><b>';
1741
						$state = 'b';
1742
					} elseif ( $state === 'bi' ) {
1743
						$output .= '</i></b>';
1744
						$state = '';
1745
					} elseif ( $state === 'ib' ) {
1746
						$output .= '</b></i>';
1747
						$state = '';
1748
					} elseif ( $state === 'both' ) {
1749
						$output .= '<i><b>' . $buffer . '</b></i>';
1750
						$state = '';
1751
					} else { // ($state == '')
1752
						$buffer = '';
1753
						$state = 'both';
1754
					}
1755
				}
1756
			}
1757
			$i++;
1758
		}
1759
		// Now close all remaining tags.  Notice that the order is important.
1760
		if ( $state === 'b' || $state === 'ib' ) {
1761
			$output .= '</b>';
1762
		}
1763
		if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1764
			$output .= '</i>';
1765
		}
1766
		if ( $state === 'bi' ) {
1767
			$output .= '</b>';
1768
		}
1769
		// There might be lonely ''''', so make sure we have a buffer
1770
		if ( $state === 'both' && $buffer ) {
1771
			$output .= '<b><i>' . $buffer . '</i></b>';
1772
		}
1773
		return $output;
1774
	}
1775
1776
	/**
1777
	 * Replace external links (REL)
1778
	 *
1779
	 * Note: this is all very hackish and the order of execution matters a lot.
1780
	 * Make sure to run tests/parserTests.php if you change this code.
1781
	 *
1782
	 * @private
1783
	 *
1784
	 * @param string $text
1785
	 *
1786
	 * @throws MWException
1787
	 * @return string
1788
	 */
1789
	public function replaceExternalLinks( $text ) {
1790
1791
		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1792
		if ( $bits === false ) {
1793
			throw new MWException( "PCRE needs to be compiled with "
1794
				. "--enable-unicode-properties in order for MediaWiki to function" );
1795
		}
1796
		$s = array_shift( $bits );
1797
1798
		$i = 0;
1799
		while ( $i < count( $bits ) ) {
1800
			$url = $bits[$i++];
1801
			$i++; // protocol
1802
			$text = $bits[$i++];
1803
			$trail = $bits[$i++];
1804
1805
			# The characters '<' and '>' (which were escaped by
1806
			# removeHTMLtags()) should not be included in
1807
			# URLs, per RFC 2396.
1808
			$m2 = [];
1809 View Code Duplication
			if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1810
				$text = substr( $url, $m2[0][1] ) . ' ' . $text;
1811
				$url = substr( $url, 0, $m2[0][1] );
1812
			}
1813
1814
			# If the link text is an image URL, replace it with an <img> tag
1815
			# This happened by accident in the original parser, but some people used it extensively
1816
			$img = $this->maybeMakeExternalImage( $text );
1817
			if ( $img !== false ) {
1818
				$text = $img;
1819
			}
1820
1821
			$dtrail = '';
1822
1823
			# Set linktype for CSS - if URL==text, link is essentially free
1824
			$linktype = ( $text === $url ) ? 'free' : 'text';
1825
1826
			# No link text, e.g. [http://domain.tld/some.link]
1827
			if ( $text == '' ) {
1828
				# Autonumber
1829
				$langObj = $this->getTargetLanguage();
1830
				$text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1831
				$linktype = 'autonumber';
1832
			} else {
1833
				# Have link text, e.g. [http://domain.tld/some.link text]s
1834
				# Check for trail
1835
				list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1836
			}
1837
1838
			$text = $this->getConverterLanguage()->markNoConversion( $text );
1839
1840
			$url = Sanitizer::cleanUrl( $url );
1841
1842
			# Use the encoded URL
1843
			# This means that users can paste URLs directly into the text
1844
			# Funny characters like ö aren't valid in URLs anyway
1845
			# This was changed in August 2004
1846
			$s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1847
				$this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1848
1849
			# Register link in the output object.
1850
			# Replace unnecessary URL escape codes with the referenced character
1851
			# This prevents spammers from hiding links from the filters
1852
			$pasteurized = self::normalizeLinkUrl( $url );
1853
			$this->mOutput->addExternalLink( $pasteurized );
1854
		}
1855
1856
		return $s;
1857
	}
1858
1859
	/**
1860
	 * Get the rel attribute for a particular external link.
1861
	 *
1862
	 * @since 1.21
1863
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1864
	 *   nofollow if appropriate
1865
	 * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups
1866
	 * @return string|null Rel attribute for $url
1867
	 */
1868
	public static function getExternalLinkRel( $url = false, $title = null ) {
1869
		global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1870
		$ns = $title ? $title->getNamespace() : false;
1871
		if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1872
			&& !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
0 ignored issues
show
Bug introduced by
It seems like $url defined by parameter $url on line 1868 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...
1873
		) {
1874
			return 'nofollow';
1875
		}
1876
		return null;
1877
	}
1878
1879
	/**
1880
	 * Get an associative array of additional HTML attributes appropriate for a
1881
	 * particular external link.  This currently may include rel => nofollow
1882
	 * (depending on configuration, namespace, and the URL's domain) and/or a
1883
	 * target attribute (depending on configuration).
1884
	 *
1885
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1886
	 *   nofollow if appropriate
1887
	 * @return array Associative array of HTML attributes
1888
	 */
1889
	public function getExternalLinkAttribs( $url = false ) {
1890
		$attribs = [];
1891
		$rel = self::getExternalLinkRel( $url, $this->mTitle );
1892
1893
		$target = $this->mOptions->getExternalLinkTarget();
1894
		if ( $target ) {
1895
			$attribs['target'] = $target;
1896
			if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1897
				// T133507. New windows can navigate parent cross-origin.
1898
				// Including noreferrer due to lacking browser
1899
				// support of noopener. Eventually noreferrer should be removed.
1900
				if ( $rel !== '' ) {
1901
					$rel .= ' ';
1902
				}
1903
				$rel .= 'noreferrer noopener';
1904
			}
1905
		}
1906
		$attribs['rel'] = $rel;
1907
		return $attribs;
1908
	}
1909
1910
	/**
1911
	 * Replace unusual escape codes in a URL with their equivalent characters
1912
	 *
1913
	 * @deprecated since 1.24, use normalizeLinkUrl
1914
	 * @param string $url
1915
	 * @return string
1916
	 */
1917
	public static function replaceUnusualEscapes( $url ) {
1918
		wfDeprecated( __METHOD__, '1.24' );
1919
		return self::normalizeLinkUrl( $url );
1920
	}
1921
1922
	/**
1923
	 * Replace unusual escape codes in a URL with their equivalent characters
1924
	 *
1925
	 * This generally follows the syntax defined in RFC 3986, with special
1926
	 * consideration for HTTP query strings.
1927
	 *
1928
	 * @param string $url
1929
	 * @return string
1930
	 */
1931
	public static function normalizeLinkUrl( $url ) {
1932
		# First, make sure unsafe characters are encoded
1933
		$url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1934
			function ( $m ) {
1935
				return rawurlencode( $m[0] );
1936
			},
1937
			$url
1938
		);
1939
1940
		$ret = '';
1941
		$end = strlen( $url );
1942
1943
		# Fragment part - 'fragment'
1944
		$start = strpos( $url, '#' );
1945 View Code Duplication
		if ( $start !== false && $start < $end ) {
1946
			$ret = self::normalizeUrlComponent(
1947
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1948
			$end = $start;
1949
		}
1950
1951
		# Query part - 'query' minus &=+;
1952
		$start = strpos( $url, '?' );
1953 View Code Duplication
		if ( $start !== false && $start < $end ) {
1954
			$ret = self::normalizeUrlComponent(
1955
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1956
			$end = $start;
1957
		}
1958
1959
		# Scheme and path part - 'pchar'
1960
		# (we assume no userinfo or encoded colons in the host)
1961
		$ret = self::normalizeUrlComponent(
1962
			substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1963
1964
		return $ret;
1965
	}
1966
1967
	private static function normalizeUrlComponent( $component, $unsafe ) {
1968
		$callback = function ( $matches ) use ( $unsafe ) {
1969
			$char = urldecode( $matches[0] );
1970
			$ord = ord( $char );
1971
			if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1972
				# Unescape it
1973
				return $char;
1974
			} else {
1975
				# Leave it escaped, but use uppercase for a-f
1976
				return strtoupper( $matches[0] );
1977
			}
1978
		};
1979
		return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
1980
	}
1981
1982
	/**
1983
	 * make an image if it's allowed, either through the global
1984
	 * option, through the exception, or through the on-wiki whitelist
1985
	 *
1986
	 * @param string $url
1987
	 *
1988
	 * @return string
1989
	 */
1990
	private function maybeMakeExternalImage( $url ) {
1991
		$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1992
		$imagesexception = !empty( $imagesfrom );
1993
		$text = false;
1994
		# $imagesfrom could be either a single string or an array of strings, parse out the latter
1995
		if ( $imagesexception && is_array( $imagesfrom ) ) {
1996
			$imagematch = false;
1997
			foreach ( $imagesfrom as $match ) {
1998
				if ( strpos( $url, $match ) === 0 ) {
1999
					$imagematch = true;
2000
					break;
2001
				}
2002
			}
2003
		} elseif ( $imagesexception ) {
2004
			$imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2005
		} else {
2006
			$imagematch = false;
2007
		}
2008
2009
		if ( $this->mOptions->getAllowExternalImages()
2010
			|| ( $imagesexception && $imagematch )
2011
		) {
2012
			if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2013
				# Image found
2014
				$text = Linker::makeExternalImage( $url );
2015
			}
2016
		}
2017
		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...
2018
			&& preg_match( self::EXT_IMAGE_REGEX, $url )
2019
		) {
2020
			$whitelist = explode(
2021
				"\n",
2022
				wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2023
			);
2024
2025
			foreach ( $whitelist as $entry ) {
2026
				# Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2027
				if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2028
					continue;
2029
				}
2030
				if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2031
					# Image matches a whitelist entry
2032
					$text = Linker::makeExternalImage( $url );
2033
					break;
2034
				}
2035
			}
2036
		}
2037
		return $text;
2038
	}
2039
2040
	/**
2041
	 * Process [[ ]] wikilinks
2042
	 *
2043
	 * @param string $s
2044
	 *
2045
	 * @return string Processed text
2046
	 *
2047
	 * @private
2048
	 */
2049
	public function replaceInternalLinks( $s ) {
2050
		$this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2051
		return $s;
2052
	}
2053
2054
	/**
2055
	 * Process [[ ]] wikilinks (RIL)
2056
	 * @param string $s
2057
	 * @throws MWException
2058
	 * @return LinkHolderArray
2059
	 *
2060
	 * @private
2061
	 */
2062
	public function replaceInternalLinks2( &$s ) {
2063
		global $wgExtraInterlanguageLinkPrefixes;
2064
2065
		static $tc = false, $e1, $e1_img;
2066
		# the % is needed to support urlencoded titles as well
2067
		if ( !$tc ) {
2068
			$tc = Title::legalChars() . '#%';
2069
			# Match a link having the form [[namespace:link|alternate]]trail
2070
			$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2071
			# Match cases where there is no "]]", which might still be images
2072
			$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2073
		}
2074
2075
		$holders = new LinkHolderArray( $this );
2076
2077
		# split the entire text string on occurrences of [[
2078
		$a = StringUtils::explode( '[[', ' ' . $s );
2079
		# get the first element (all text up to first [[), and remove the space we added
2080
		$s = $a->current();
2081
		$a->next();
2082
		$line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2083
		$s = substr( $s, 1 );
2084
2085
		$useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2086
		$e2 = null;
2087
		if ( $useLinkPrefixExtension ) {
2088
			# Match the end of a line for a word that's not followed by whitespace,
2089
			# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2090
			global $wgContLang;
2091
			$charset = $wgContLang->linkPrefixCharset();
2092
			$e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2093
		}
2094
2095
		if ( is_null( $this->mTitle ) ) {
2096
			throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2097
		}
2098
		$nottalk = !$this->mTitle->isTalkPage();
2099
2100 View Code Duplication
		if ( $useLinkPrefixExtension ) {
2101
			$m = [];
2102
			if ( preg_match( $e2, $s, $m ) ) {
2103
				$first_prefix = $m[2];
2104
			} else {
2105
				$first_prefix = false;
2106
			}
2107
		} else {
2108
			$prefix = '';
2109
		}
2110
2111
		$useSubpages = $this->areSubpagesAllowed();
2112
2113
		// @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2114
		# Loop for each link
2115
		for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2116
			// @codingStandardsIgnoreEnd
2117
2118
			# Check for excessive memory usage
2119
			if ( $holders->isBig() ) {
2120
				# Too big
2121
				# Do the existence check, replace the link holders and clear the array
2122
				$holders->replace( $s );
2123
				$holders->clear();
2124
			}
2125
2126
			if ( $useLinkPrefixExtension ) {
2127 View Code Duplication
				if ( preg_match( $e2, $s, $m ) ) {
2128
					$prefix = $m[2];
2129
					$s = $m[1];
2130
				} else {
2131
					$prefix = '';
2132
				}
2133
				# first link
2134
				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...
2135
					$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...
2136
					$first_prefix = false;
2137
				}
2138
			}
2139
2140
			$might_be_img = false;
2141
2142
			if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2143
				$text = $m[2];
2144
				# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2145
				# [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2146
				# the real problem is with the $e1 regex
2147
				# See bug 1300.
2148
				# Still some problems for cases where the ] is meant to be outside punctuation,
2149
				# and no image is in sight. See bug 2095.
2150
				if ( $text !== ''
2151
					&& substr( $m[3], 0, 1 ) === ']'
2152
					&& strpos( $text, '[' ) !== false
2153
				) {
2154
					$text .= ']'; # so that replaceExternalLinks($text) works later
2155
					$m[3] = substr( $m[3], 1 );
2156
				}
2157
				# fix up urlencoded title texts
2158
				if ( strpos( $m[1], '%' ) !== false ) {
2159
					# Should anchors '#' also be rejected?
2160
					$m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2161
				}
2162
				$trail = $m[3];
2163
			} elseif ( preg_match( $e1_img, $line, $m ) ) {
2164
				# Invalid, but might be an image with a link in its caption
2165
				$might_be_img = true;
2166
				$text = $m[2];
2167
				if ( strpos( $m[1], '%' ) !== false ) {
2168
					$m[1] = rawurldecode( $m[1] );
2169
				}
2170
				$trail = "";
2171
			} else { # Invalid form; output directly
2172
				$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...
2173
				continue;
2174
			}
2175
2176
			$origLink = $m[1];
2177
2178
			# Don't allow internal links to pages containing
2179
			# PROTO: where PROTO is a valid URL protocol; these
2180
			# should be external links.
2181
			if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2182
				$s .= $prefix . '[[' . $line;
2183
				continue;
2184
			}
2185
2186
			# Make subpage if necessary
2187
			if ( $useSubpages ) {
2188
				$link = $this->maybeDoSubpageLink( $origLink, $text );
2189
			} else {
2190
				$link = $origLink;
2191
			}
2192
2193
			$noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2194
			if ( !$noforce ) {
2195
				# Strip off leading ':'
2196
				$link = substr( $link, 1 );
2197
			}
2198
2199
			$unstrip = $this->mStripState->unstripNoWiki( $link );
2200
			$nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2201
			if ( $nt === null ) {
2202
				$s .= $prefix . '[[' . $line;
2203
				continue;
2204
			}
2205
2206
			$ns = $nt->getNamespace();
2207
			$iw = $nt->getInterwiki();
2208
2209
			if ( $might_be_img ) { # if this is actually an invalid link
2210
				if ( $ns == NS_FILE && $noforce ) { # but might be an image
2211
					$found = false;
2212
					while ( true ) {
2213
						# look at the next 'line' to see if we can close it there
2214
						$a->next();
2215
						$next_line = $a->current();
2216
						if ( $next_line === false || $next_line === null ) {
2217
							break;
2218
						}
2219
						$m = explode( ']]', $next_line, 3 );
2220
						if ( count( $m ) == 3 ) {
2221
							# the first ]] closes the inner link, the second the image
2222
							$found = true;
2223
							$text .= "[[{$m[0]}]]{$m[1]}";
2224
							$trail = $m[2];
2225
							break;
2226
						} elseif ( count( $m ) == 2 ) {
2227
							# if there's exactly one ]] that's fine, we'll keep looking
2228
							$text .= "[[{$m[0]}]]{$m[1]}";
2229
						} else {
2230
							# if $next_line is invalid too, we need look no further
2231
							$text .= '[[' . $next_line;
2232
							break;
2233
						}
2234
					}
2235
					if ( !$found ) {
2236
						# we couldn't find the end of this imageLink, so output it raw
2237
						# but don't ignore what might be perfectly normal links in the text we've examined
2238
						$holders->merge( $this->replaceInternalLinks2( $text ) );
2239
						$s .= "{$prefix}[[$link|$text";
2240
						# note: no $trail, because without an end, there *is* no trail
2241
						continue;
2242
					}
2243
				} else { # it's not an image, so output it raw
2244
					$s .= "{$prefix}[[$link|$text";
2245
					# note: no $trail, because without an end, there *is* no trail
2246
					continue;
2247
				}
2248
			}
2249
2250
			$wasblank = ( $text == '' );
2251
			if ( $wasblank ) {
2252
				$text = $link;
2253
			} else {
2254
				# Bug 4598 madness. Handle the quotes only if they come from the alternate part
2255
				# [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2256
				# [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2257
				#    -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2258
				$text = $this->doQuotes( $text );
2259
			}
2260
2261
			# Link not escaped by : , create the various objects
2262
			if ( $noforce && !$nt->wasLocalInterwiki() ) {
2263
				# Interwikis
2264
				if (
2265
					$iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2266
						Language::fetchLanguageName( $iw, null, 'mw' ) ||
2267
						in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2268
					)
2269
				) {
2270
					# Bug 24502: filter duplicates
2271
					if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2272
						$this->mLangLinkLanguages[$iw] = true;
2273
						$this->mOutput->addLanguageLink( $nt->getFullText() );
2274
					}
2275
2276
					$s = rtrim( $s . $prefix );
2277
					$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2278
					continue;
2279
				}
2280
2281
				if ( $ns == NS_FILE ) {
2282
					if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2283
						if ( $wasblank ) {
2284
							# if no parameters were passed, $text
2285
							# becomes something like "File:Foo.png",
2286
							# which we don't want to pass on to the
2287
							# image generator
2288
							$text = '';
2289
						} else {
2290
							# recursively parse links inside the image caption
2291
							# actually, this will parse them in any other parameters, too,
2292
							# but it might be hard to fix that, and it doesn't matter ATM
2293
							$text = $this->replaceExternalLinks( $text );
2294
							$holders->merge( $this->replaceInternalLinks2( $text ) );
2295
						}
2296
						# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2297
						$s .= $prefix . $this->armorLinks(
2298
							$this->makeImage( $nt, $text, $holders ) ) . $trail;
2299
					} else {
2300
						$s .= $prefix . $trail;
2301
					}
2302
					continue;
2303
				}
2304
2305
				if ( $ns == NS_CATEGORY ) {
2306
					$s = rtrim( $s . "\n" ); # bug 87
2307
2308
					if ( $wasblank ) {
2309
						$sortkey = $this->getDefaultSort();
2310
					} else {
2311
						$sortkey = $text;
2312
					}
2313
					$sortkey = Sanitizer::decodeCharReferences( $sortkey );
2314
					$sortkey = str_replace( "\n", '', $sortkey );
2315
					$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2316
					$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2317
2318
					/**
2319
					 * Strip the whitespace Category links produce, see bug 87
2320
					 */
2321
					$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2322
2323
					continue;
2324
				}
2325
			}
2326
2327
			# Self-link checking. For some languages, variants of the title are checked in
2328
			# LinkHolderArray::doVariants() to allow batching the existence checks necessary
2329
			# for linking to a different variant.
2330
			if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2331
				$s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2332
				continue;
2333
			}
2334
2335
			# NS_MEDIA is a pseudo-namespace for linking directly to a file
2336
			# @todo FIXME: Should do batch file existence checks, see comment below
2337
			if ( $ns == NS_MEDIA ) {
2338
				# Give extensions a chance to select the file revision for us
2339
				$options = [];
2340
				$descQuery = false;
2341
				Hooks::run( 'BeforeParserFetchFileAndTitle',
2342
					[ $this, $nt, &$options, &$descQuery ] );
2343
				# Fetch and register the file (file title may be different via hooks)
2344
				list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2345
				# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2346
				$s .= $prefix . $this->armorLinks(
2347
					Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2348
				continue;
2349
			}
2350
2351
			# Some titles, such as valid special pages or files in foreign repos, should
2352
			# be shown as bluelinks even though they're not included in the page table
2353
			# @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2354
			# batch file existence checks for NS_FILE and NS_MEDIA
2355
			if ( $iw == '' && $nt->isAlwaysKnown() ) {
2356
				$this->mOutput->addLink( $nt );
2357
				$s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2358
			} else {
2359
				# Links will be added to the output link list after checking
2360
				$s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2361
			}
2362
		}
2363
		return $holders;
2364
	}
2365
2366
	/**
2367
	 * Render a forced-blue link inline; protect against double expansion of
2368
	 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2369
	 * Since this little disaster has to split off the trail text to avoid
2370
	 * breaking URLs in the following text without breaking trails on the
2371
	 * wiki links, it's been made into a horrible function.
2372
	 *
2373
	 * @param Title $nt
2374
	 * @param string $text
2375
	 * @param string $trail
2376
	 * @param string $prefix
2377
	 * @return string HTML-wikitext mix oh yuck
2378
	 */
2379
	protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2380
		list( $inside, $trail ) = Linker::splitTrail( $trail );
2381
2382
		if ( $text == '' ) {
2383
			$text = htmlspecialchars( $nt->getPrefixedText() );
2384
		}
2385
2386
		$link = $this->getLinkRenderer()->makeKnownLink(
2387
			$nt, new HtmlArmor( "$prefix$text$inside" )
2388
		);
2389
2390
		return $this->armorLinks( $link ) . $trail;
2391
	}
2392
2393
	/**
2394
	 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2395
	 * going to go through further parsing steps before inline URL expansion.
2396
	 *
2397
	 * Not needed quite as much as it used to be since free links are a bit
2398
	 * more sensible these days. But bracketed links are still an issue.
2399
	 *
2400
	 * @param string $text More-or-less HTML
2401
	 * @return string Less-or-more HTML with NOPARSE bits
2402
	 */
2403
	public function armorLinks( $text ) {
2404
		return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2405
			self::MARKER_PREFIX . "NOPARSE$1", $text );
2406
	}
2407
2408
	/**
2409
	 * Return true if subpage links should be expanded on this page.
2410
	 * @return bool
2411
	 */
2412
	public function areSubpagesAllowed() {
2413
		# Some namespaces don't allow subpages
2414
		return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2415
	}
2416
2417
	/**
2418
	 * Handle link to subpage if necessary
2419
	 *
2420
	 * @param string $target The source of the link
2421
	 * @param string &$text The link text, modified as necessary
2422
	 * @return string The full name of the link
2423
	 * @private
2424
	 */
2425
	public function maybeDoSubpageLink( $target, &$text ) {
2426
		return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2427
	}
2428
2429
	/**
2430
	 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
2431
	 *
2432
	 * @param string $text
2433
	 * @param bool $linestart Whether or not this is at the start of a line.
2434
	 * @private
2435
	 * @return string The lists rendered as HTML
2436
	 */
2437
	public function doBlockLevels( $text, $linestart ) {
2438
		return BlockLevelPass::doBlockLevels( $text, $linestart );
2439
	}
2440
2441
	/**
2442
	 * Return value of a magic variable (like PAGENAME)
2443
	 *
2444
	 * @private
2445
	 *
2446
	 * @param int $index
2447
	 * @param bool|PPFrame $frame
2448
	 *
2449
	 * @throws MWException
2450
	 * @return string
2451
	 */
2452
	public function getVariableValue( $index, $frame = false ) {
2453
		global $wgContLang, $wgSitename, $wgServer, $wgServerName;
2454
		global $wgArticlePath, $wgScriptPath, $wgStylePath;
2455
2456
		if ( is_null( $this->mTitle ) ) {
2457
			// If no title set, bad things are going to happen
2458
			// later. Title should always be set since this
2459
			// should only be called in the middle of a parse
2460
			// operation (but the unit-tests do funky stuff)
2461
			throw new MWException( __METHOD__ . ' Should only be '
2462
				. ' called while parsing (no title set)' );
2463
		}
2464
2465
		/**
2466
		 * Some of these require message or data lookups and can be
2467
		 * expensive to check many times.
2468
		 */
2469
		if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
2470
			if ( isset( $this->mVarCache[$index] ) ) {
2471
				return $this->mVarCache[$index];
2472
			}
2473
		}
2474
2475
		$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2476
		Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
2477
2478
		$pageLang = $this->getFunctionLang();
2479
2480
		switch ( $index ) {
2481
			case '!':
2482
				$value = '|';
2483
				break;
2484
			case 'currentmonth':
2485
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2486
				break;
2487
			case 'currentmonth1':
2488
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2489
				break;
2490
			case 'currentmonthname':
2491
				$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2492
				break;
2493
			case 'currentmonthnamegen':
2494
				$value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2495
				break;
2496
			case 'currentmonthabbrev':
2497
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2498
				break;
2499
			case 'currentday':
2500
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2501
				break;
2502
			case 'currentday2':
2503
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2504
				break;
2505
			case 'localmonth':
2506
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2507
				break;
2508
			case 'localmonth1':
2509
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2510
				break;
2511
			case 'localmonthname':
2512
				$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2513
				break;
2514
			case 'localmonthnamegen':
2515
				$value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2516
				break;
2517
			case 'localmonthabbrev':
2518
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2519
				break;
2520
			case 'localday':
2521
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2522
				break;
2523
			case 'localday2':
2524
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2525
				break;
2526
			case 'pagename':
2527
				$value = wfEscapeWikiText( $this->mTitle->getText() );
2528
				break;
2529
			case 'pagenamee':
2530
				$value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2531
				break;
2532
			case 'fullpagename':
2533
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2534
				break;
2535
			case 'fullpagenamee':
2536
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2537
				break;
2538
			case 'subpagename':
2539
				$value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2540
				break;
2541
			case 'subpagenamee':
2542
				$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2543
				break;
2544
			case 'rootpagename':
2545
				$value = wfEscapeWikiText( $this->mTitle->getRootText() );
2546
				break;
2547 View Code Duplication
			case 'rootpagenamee':
2548
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2549
					' ',
2550
					'_',
2551
					$this->mTitle->getRootText()
2552
				) ) );
2553
				break;
2554
			case 'basepagename':
2555
				$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2556
				break;
2557 View Code Duplication
			case 'basepagenamee':
2558
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2559
					' ',
2560
					'_',
2561
					$this->mTitle->getBaseText()
2562
				) ) );
2563
				break;
2564 View Code Duplication
			case 'talkpagename':
2565
				if ( $this->mTitle->canTalk() ) {
2566
					$talkPage = $this->mTitle->getTalkPage();
2567
					$value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2568
				} else {
2569
					$value = '';
2570
				}
2571
				break;
2572 View Code Duplication
			case 'talkpagenamee':
2573
				if ( $this->mTitle->canTalk() ) {
2574
					$talkPage = $this->mTitle->getTalkPage();
2575
					$value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2576
				} else {
2577
					$value = '';
2578
				}
2579
				break;
2580
			case 'subjectpagename':
2581
				$subjPage = $this->mTitle->getSubjectPage();
2582
				$value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2583
				break;
2584
			case 'subjectpagenamee':
2585
				$subjPage = $this->mTitle->getSubjectPage();
2586
				$value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2587
				break;
2588
			case 'pageid': // requested in bug 23427
2589
				$pageid = $this->getTitle()->getArticleID();
2590
				if ( $pageid == 0 ) {
2591
					# 0 means the page doesn't exist in the database,
2592
					# which means the user is previewing a new page.
2593
					# The vary-revision flag must be set, because the magic word
2594
					# will have a different value once the page is saved.
2595
					$this->mOutput->setFlag( 'vary-revision' );
2596
					wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2597
				}
2598
				$value = $pageid ? $pageid : null;
2599
				break;
2600
			case 'revisionid':
2601
				# Let the edit saving system know we should parse the page
2602
				# *after* a revision ID has been assigned.
2603
				$this->mOutput->setFlag( 'vary-revision' );
2604
				wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
2605
				$value = $this->mRevisionId;
2606
				break;
2607 View Code Duplication
			case 'revisionday':
2608
				# Let the edit saving system know we should parse the page
2609
				# *after* a revision ID has been assigned. This is for null edits.
2610
				$this->mOutput->setFlag( 'vary-revision' );
2611
				wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2612
				$value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2613
				break;
2614 View Code Duplication
			case 'revisionday2':
2615
				# Let the edit saving system know we should parse the page
2616
				# *after* a revision ID has been assigned. This is for null edits.
2617
				$this->mOutput->setFlag( 'vary-revision' );
2618
				wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2619
				$value = substr( $this->getRevisionTimestamp(), 6, 2 );
2620
				break;
2621 View Code Duplication
			case 'revisionmonth':
2622
				# Let the edit saving system know we should parse the page
2623
				# *after* a revision ID has been assigned. This is for null edits.
2624
				$this->mOutput->setFlag( 'vary-revision' );
2625
				wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2626
				$value = substr( $this->getRevisionTimestamp(), 4, 2 );
2627
				break;
2628 View Code Duplication
			case 'revisionmonth1':
2629
				# Let the edit saving system know we should parse the page
2630
				# *after* a revision ID has been assigned. This is for null edits.
2631
				$this->mOutput->setFlag( 'vary-revision' );
2632
				wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2633
				$value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2634
				break;
2635 View Code Duplication
			case 'revisionyear':
2636
				# Let the edit saving system know we should parse the page
2637
				# *after* a revision ID has been assigned. This is for null edits.
2638
				$this->mOutput->setFlag( 'vary-revision' );
2639
				wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2640
				$value = substr( $this->getRevisionTimestamp(), 0, 4 );
2641
				break;
2642
			case 'revisiontimestamp':
2643
				# Let the edit saving system know we should parse the page
2644
				# *after* a revision ID has been assigned. This is for null edits.
2645
				$this->mOutput->setFlag( 'vary-revision' );
2646
				wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2647
				$value = $this->getRevisionTimestamp();
2648
				break;
2649
			case 'revisionuser':
2650
				# Let the edit saving system know we should parse the page
2651
				# *after* a revision ID has been assigned. This is for null edits.
2652
				$this->mOutput->setFlag( 'vary-revision' );
2653
				wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
2654
				$value = $this->getRevisionUser();
2655
				break;
2656
			case 'revisionsize':
2657
				# Let the edit saving system know we should parse the page
2658
				# *after* a revision ID has been assigned. This is for null edits.
2659
				$this->mOutput->setFlag( 'vary-revision' );
2660
				wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
2661
				$value = $this->getRevisionSize();
2662
				break;
2663
			case 'namespace':
2664
				$value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2665
				break;
2666
			case 'namespacee':
2667
				$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2668
				break;
2669
			case 'namespacenumber':
2670
				$value = $this->mTitle->getNamespace();
2671
				break;
2672
			case 'talkspace':
2673
				$value = $this->mTitle->canTalk()
2674
					? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2675
					: '';
2676
				break;
2677
			case 'talkspacee':
2678
				$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2679
				break;
2680
			case 'subjectspace':
2681
				$value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2682
				break;
2683
			case 'subjectspacee':
2684
				$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2685
				break;
2686
			case 'currentdayname':
2687
				$value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2688
				break;
2689
			case 'currentyear':
2690
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2691
				break;
2692
			case 'currenttime':
2693
				$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...
2694
				break;
2695
			case 'currenthour':
2696
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2697
				break;
2698
			case 'currentweek':
2699
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2700
				# int to remove the padding
2701
				$value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2702
				break;
2703
			case 'currentdow':
2704
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2705
				break;
2706
			case 'localdayname':
2707
				$value = $pageLang->getWeekdayName(
2708
					(int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2709
				);
2710
				break;
2711
			case 'localyear':
2712
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2713
				break;
2714
			case 'localtime':
2715
				$value = $pageLang->time(
2716
					MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2717
					false,
2718
					false
2719
				);
2720
				break;
2721
			case 'localhour':
2722
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2723
				break;
2724
			case 'localweek':
2725
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2726
				# int to remove the padding
2727
				$value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2728
				break;
2729
			case 'localdow':
2730
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2731
				break;
2732
			case 'numberofarticles':
2733
				$value = $pageLang->formatNum( SiteStats::articles() );
2734
				break;
2735
			case 'numberoffiles':
2736
				$value = $pageLang->formatNum( SiteStats::images() );
2737
				break;
2738
			case 'numberofusers':
2739
				$value = $pageLang->formatNum( SiteStats::users() );
2740
				break;
2741
			case 'numberofactiveusers':
2742
				$value = $pageLang->formatNum( SiteStats::activeUsers() );
2743
				break;
2744
			case 'numberofpages':
2745
				$value = $pageLang->formatNum( SiteStats::pages() );
2746
				break;
2747
			case 'numberofadmins':
2748
				$value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2749
				break;
2750
			case 'numberofedits':
2751
				$value = $pageLang->formatNum( SiteStats::edits() );
2752
				break;
2753
			case 'currenttimestamp':
2754
				$value = wfTimestamp( TS_MW, $ts );
2755
				break;
2756
			case 'localtimestamp':
2757
				$value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2758
				break;
2759
			case 'currentversion':
2760
				$value = SpecialVersion::getVersion();
2761
				break;
2762
			case 'articlepath':
2763
				return $wgArticlePath;
2764
			case 'sitename':
2765
				return $wgSitename;
2766
			case 'server':
2767
				return $wgServer;
2768
			case 'servername':
2769
				return $wgServerName;
2770
			case 'scriptpath':
2771
				return $wgScriptPath;
2772
			case 'stylepath':
2773
				return $wgStylePath;
2774
			case 'directionmark':
2775
				return $pageLang->getDirMark();
2776
			case 'contentlanguage':
2777
				global $wgLanguageCode;
2778
				return $wgLanguageCode;
2779
			case 'cascadingsources':
2780
				$value = CoreParserFunctions::cascadingsources( $this );
2781
				break;
2782
			default:
2783
				$ret = null;
2784
				Hooks::run(
2785
					'ParserGetVariableValueSwitch',
2786
					[ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
2787
				);
2788
2789
				return $ret;
2790
		}
2791
2792
		if ( $index ) {
2793
			$this->mVarCache[$index] = $value;
2794
		}
2795
2796
		return $value;
2797
	}
2798
2799
	/**
2800
	 * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
2801
	 *
2802
	 * @private
2803
	 */
2804
	public function initialiseVariables() {
2805
		$variableIDs = MagicWord::getVariableIDs();
2806
		$substIDs = MagicWord::getSubstIDs();
2807
2808
		$this->mVariables = new MagicWordArray( $variableIDs );
2809
		$this->mSubstWords = new MagicWordArray( $substIDs );
2810
	}
2811
2812
	/**
2813
	 * Preprocess some wikitext and return the document tree.
2814
	 * This is the ghost of replace_variables().
2815
	 *
2816
	 * @param string $text The text to parse
2817
	 * @param int $flags Bitwise combination of:
2818
	 *   - self::PTD_FOR_INCLUSION: Handle "<noinclude>" and "<includeonly>" as if the text is being
2819
	 *     included. Default is to assume a direct page view.
2820
	 *
2821
	 * The generated DOM tree must depend only on the input text and the flags.
2822
	 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
2823
	 *
2824
	 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
2825
	 * change in the DOM tree for a given text, must be passed through the section identifier
2826
	 * in the section edit link and thus back to extractSections().
2827
	 *
2828
	 * The output of this function is currently only cached in process memory, but a persistent
2829
	 * cache may be implemented at a later date which takes further advantage of these strict
2830
	 * dependency requirements.
2831
	 *
2832
	 * @return PPNode
2833
	 */
2834
	public function preprocessToDom( $text, $flags = 0 ) {
2835
		$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2836
		return $dom;
2837
	}
2838
2839
	/**
2840
	 * Return a three-element array: leading whitespace, string contents, trailing whitespace
2841
	 *
2842
	 * @param string $s
2843
	 *
2844
	 * @return array
2845
	 */
2846
	public static function splitWhitespace( $s ) {
2847
		$ltrimmed = ltrim( $s );
2848
		$w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2849
		$trimmed = rtrim( $ltrimmed );
2850
		$diff = strlen( $ltrimmed ) - strlen( $trimmed );
2851
		if ( $diff > 0 ) {
2852
			$w2 = substr( $ltrimmed, -$diff );
2853
		} else {
2854
			$w2 = '';
2855
		}
2856
		return [ $w1, $trimmed, $w2 ];
2857
	}
2858
2859
	/**
2860
	 * Replace magic variables, templates, and template arguments
2861
	 * with the appropriate text. Templates are substituted recursively,
2862
	 * taking care to avoid infinite loops.
2863
	 *
2864
	 * Note that the substitution depends on value of $mOutputType:
2865
	 *  self::OT_WIKI: only {{subst:}} templates
2866
	 *  self::OT_PREPROCESS: templates but not extension tags
2867
	 *  self::OT_HTML: all templates and extension tags
2868
	 *
2869
	 * @param string $text The text to transform
2870
	 * @param bool|PPFrame $frame Object describing the arguments passed to the
2871
	 *   template. Arguments may also be provided as an associative array, as
2872
	 *   was the usual case before MW1.12. Providing arguments this way may be
2873
	 *   useful for extensions wishing to perform variable replacement
2874
	 *   explicitly.
2875
	 * @param bool $argsOnly Only do argument (triple-brace) expansion, not
2876
	 *   double-brace expansion.
2877
	 * @return string
2878
	 */
2879
	public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2880
		# Is there any text? Also, Prevent too big inclusions!
2881
		$textSize = strlen( $text );
2882
		if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2883
			return $text;
2884
		}
2885
2886
		if ( $frame === false ) {
2887
			$frame = $this->getPreprocessor()->newFrame();
2888
		} elseif ( !( $frame instanceof PPFrame ) ) {
2889
			wfDebug( __METHOD__ . " called using plain parameters instead of "
2890
				. "a PPFrame instance. Creating custom frame.\n" );
2891
			$frame = $this->getPreprocessor()->newCustomFrame( $frame );
2892
		}
2893
2894
		$dom = $this->preprocessToDom( $text );
2895
		$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2896
		$text = $frame->expand( $dom, $flags );
2897
2898
		return $text;
2899
	}
2900
2901
	/**
2902
	 * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
2903
	 *
2904
	 * @param array $args
2905
	 *
2906
	 * @return array
2907
	 */
2908
	public static function createAssocArgs( $args ) {
2909
		$assocArgs = [];
2910
		$index = 1;
2911
		foreach ( $args as $arg ) {
2912
			$eqpos = strpos( $arg, '=' );
2913
			if ( $eqpos === false ) {
2914
				$assocArgs[$index++] = $arg;
2915
			} else {
2916
				$name = trim( substr( $arg, 0, $eqpos ) );
2917
				$value = trim( substr( $arg, $eqpos + 1 ) );
2918
				if ( $value === false ) {
2919
					$value = '';
2920
				}
2921
				if ( $name !== false ) {
2922
					$assocArgs[$name] = $value;
2923
				}
2924
			}
2925
		}
2926
2927
		return $assocArgs;
2928
	}
2929
2930
	/**
2931
	 * Warn the user when a parser limitation is reached
2932
	 * Will warn at most once the user per limitation type
2933
	 *
2934
	 * The results are shown during preview and run through the Parser (See EditPage.php)
2935
	 *
2936
	 * @param string $limitationType Should be one of:
2937
	 *   'expensive-parserfunction' (corresponding messages:
2938
	 *       'expensive-parserfunction-warning',
2939
	 *       'expensive-parserfunction-category')
2940
	 *   'post-expand-template-argument' (corresponding messages:
2941
	 *       'post-expand-template-argument-warning',
2942
	 *       'post-expand-template-argument-category')
2943
	 *   'post-expand-template-inclusion' (corresponding messages:
2944
	 *       'post-expand-template-inclusion-warning',
2945
	 *       'post-expand-template-inclusion-category')
2946
	 *   'node-count-exceeded' (corresponding messages:
2947
	 *       'node-count-exceeded-warning',
2948
	 *       'node-count-exceeded-category')
2949
	 *   'expansion-depth-exceeded' (corresponding messages:
2950
	 *       'expansion-depth-exceeded-warning',
2951
	 *       'expansion-depth-exceeded-category')
2952
	 * @param string|int|null $current Current value
2953
	 * @param string|int|null $max Maximum allowed, when an explicit limit has been
2954
	 *	 exceeded, provide the values (optional)
2955
	 */
2956
	public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2957
		# does no harm if $current and $max are present but are unnecessary for the message
2958
		# Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2959
		# only during preview, and that would split the parser cache unnecessarily.
2960
		$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2961
			->text();
2962
		$this->mOutput->addWarning( $warning );
2963
		$this->addTrackingCategory( "$limitationType-category" );
2964
	}
2965
2966
	/**
2967
	 * Return the text of a template, after recursively
2968
	 * replacing any variables or templates within the template.
2969
	 *
2970
	 * @param array $piece The parts of the template
2971
	 *   $piece['title']: the title, i.e. the part before the |
2972
	 *   $piece['parts']: the parameter array
2973
	 *   $piece['lineStart']: whether the brace was at the start of a line
2974
	 * @param PPFrame $frame The current frame, contains template arguments
2975
	 * @throws Exception
2976
	 * @return string The text of the template
2977
	 */
2978
	public function braceSubstitution( $piece, $frame ) {
2979
2980
		// Flags
2981
2982
		// $text has been filled
2983
		$found = false;
2984
		// wiki markup in $text should be escaped
2985
		$nowiki = false;
2986
		// $text is HTML, armour it against wikitext transformation
2987
		$isHTML = false;
2988
		// Force interwiki transclusion to be done in raw mode not rendered
2989
		$forceRawInterwiki = false;
2990
		// $text is a DOM node needing expansion in a child frame
2991
		$isChildObj = false;
2992
		// $text is a DOM node needing expansion in the current frame
2993
		$isLocalObj = false;
2994
2995
		# Title object, where $text came from
2996
		$title = false;
2997
2998
		# $part1 is the bit before the first |, and must contain only title characters.
2999
		# Various prefixes will be stripped from it later.
3000
		$titleWithSpaces = $frame->expand( $piece['title'] );
3001
		$part1 = trim( $titleWithSpaces );
3002
		$titleText = false;
3003
3004
		# Original title text preserved for various purposes
3005
		$originalTitle = $part1;
3006
3007
		# $args is a list of argument nodes, starting from index 0, not including $part1
3008
		# @todo FIXME: If piece['parts'] is null then the call to getLength()
3009
		# below won't work b/c this $args isn't an object
3010
		$args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3011
3012
		$profileSection = null; // profile templates
3013
3014
		# SUBST
3015
		if ( !$found ) {
3016
			$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3017
3018
			# Possibilities for substMatch: "subst", "safesubst" or FALSE
3019
			# Decide whether to expand template or keep wikitext as-is.
3020
			if ( $this->ot['wiki'] ) {
3021
				if ( $substMatch === false ) {
3022
					$literal = true;  # literal when in PST with no prefix
3023
				} else {
3024
					$literal = false; # expand when in PST with subst: or safesubst:
3025
				}
3026
			} else {
3027
				if ( $substMatch == 'subst' ) {
3028
					$literal = true;  # literal when not in PST with plain subst:
3029
				} else {
3030
					$literal = false; # expand when not in PST with safesubst: or no prefix
3031
				}
3032
			}
3033
			if ( $literal ) {
3034
				$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3035
				$isLocalObj = true;
3036
				$found = true;
3037
			}
3038
		}
3039
3040
		# Variables
3041
		if ( !$found && $args->getLength() == 0 ) {
3042
			$id = $this->mVariables->matchStartToEnd( $part1 );
3043
			if ( $id !== false ) {
3044
				$text = $this->getVariableValue( $id, $frame );
3045
				if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3046
					$this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3047
				}
3048
				$found = true;
3049
			}
3050
		}
3051
3052
		# MSG, MSGNW and RAW
3053
		if ( !$found ) {
3054
			# Check for MSGNW:
3055
			$mwMsgnw = MagicWord::get( 'msgnw' );
3056
			if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3057
				$nowiki = true;
3058
			} else {
3059
				# Remove obsolete MSG:
3060
				$mwMsg = MagicWord::get( 'msg' );
3061
				$mwMsg->matchStartAndRemove( $part1 );
3062
			}
3063
3064
			# Check for RAW:
3065
			$mwRaw = MagicWord::get( 'raw' );
3066
			if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3067
				$forceRawInterwiki = true;
3068
			}
3069
		}
3070
3071
		# Parser functions
3072
		if ( !$found ) {
3073
			$colonPos = strpos( $part1, ':' );
3074
			if ( $colonPos !== false ) {
3075
				$func = substr( $part1, 0, $colonPos );
3076
				$funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3077
				$argsLength = $args->getLength();
3078
				for ( $i = 0; $i < $argsLength; $i++ ) {
3079
					$funcArgs[] = $args->item( $i );
3080
				}
3081
				try {
3082
					$result = $this->callParserFunction( $frame, $func, $funcArgs );
3083
				} catch ( Exception $ex ) {
3084
					throw $ex;
3085
				}
3086
3087
				# The interface for parser functions allows for extracting
3088
				# flags into the local scope. Extract any forwarded flags
3089
				# here.
3090
				extract( $result );
3091
			}
3092
		}
3093
3094
		# Finish mangling title and then check for loops.
3095
		# Set $title to a Title object and $titleText to the PDBK
3096
		if ( !$found ) {
3097
			$ns = NS_TEMPLATE;
3098
			# Split the title into page and subpage
3099
			$subpage = '';
3100
			$relative = $this->maybeDoSubpageLink( $part1, $subpage );
3101
			if ( $part1 !== $relative ) {
3102
				$part1 = $relative;
3103
				$ns = $this->mTitle->getNamespace();
3104
			}
3105
			$title = Title::newFromText( $part1, $ns );
3106
			if ( $title ) {
3107
				$titleText = $title->getPrefixedText();
3108
				# Check for language variants if the template is not found
3109
				if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3110
					$this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3111
				}
3112
				# Do recursion depth check
3113
				$limit = $this->mOptions->getMaxTemplateDepth();
3114 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...
3115
					$found = true;
3116
					$text = '<span class="error">'
3117
						. wfMessage( 'parser-template-recursion-depth-warning' )
3118
							->numParams( $limit )->inContentLanguage()->text()
3119
						. '</span>';
3120
				}
3121
			}
3122
		}
3123
3124
		# Load from database
3125
		if ( !$found && $title ) {
3126
			$profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3127
			if ( !$title->isExternal() ) {
3128
				if ( $title->isSpecialPage()
3129
					&& $this->mOptions->getAllowSpecialInclusion()
3130
					&& $this->ot['html']
3131
				) {
3132
					// Pass the template arguments as URL parameters.
3133
					// "uselang" will have no effect since the Language object
3134
					// is forced to the one defined in ParserOptions.
3135
					$pageArgs = [];
3136
					$argsLength = $args->getLength();
3137
					for ( $i = 0; $i < $argsLength; $i++ ) {
3138
						$bits = $args->item( $i )->splitArg();
3139
						if ( strval( $bits['index'] ) === '' ) {
3140
							$name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3141
							$value = trim( $frame->expand( $bits['value'] ) );
3142
							$pageArgs[$name] = $value;
3143
						}
3144
					}
3145
3146
					// Create a new context to execute the special page
3147
					$context = new RequestContext;
3148
					$context->setTitle( $title );
3149
					$context->setRequest( new FauxRequest( $pageArgs ) );
3150
					$context->setUser( $this->getUser() );
3151
					$context->setLanguage( $this->mOptions->getUserLangObj() );
3152
					$ret = SpecialPageFactory::capturePath( $title, $context );
3153
					if ( $ret ) {
3154
						$text = $context->getOutput()->getHTML();
3155
						$this->mOutput->addOutputPageMetadata( $context->getOutput() );
3156
						$found = true;
3157
						$isHTML = true;
3158
						$this->disableCache();
3159
					}
3160
				} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3161
					$found = false; # access denied
3162
					wfDebug( __METHOD__ . ": template inclusion denied for " .
3163
						$title->getPrefixedDBkey() . "\n" );
3164
				} else {
3165
					list( $text, $title ) = $this->getTemplateDom( $title );
3166
					if ( $text !== false ) {
3167
						$found = true;
3168
						$isChildObj = true;
3169
					}
3170
				}
3171
3172
				# If the title is valid but undisplayable, make a link to it
3173
				if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3174
					$text = "[[:$titleText]]";
3175
					$found = true;
3176
				}
3177
			} elseif ( $title->isTrans() ) {
3178
				# Interwiki transclusion
3179
				if ( $this->ot['html'] && !$forceRawInterwiki ) {
3180
					$text = $this->interwikiTransclude( $title, 'render' );
3181
					$isHTML = true;
3182
				} else {
3183
					$text = $this->interwikiTransclude( $title, 'raw' );
3184
					# Preprocess it like a template
3185
					$text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3186
					$isChildObj = true;
3187
				}
3188
				$found = true;
3189
			}
3190
3191
			# Do infinite loop check
3192
			# This has to be done after redirect resolution to avoid infinite loops via redirects
3193
			if ( !$frame->loopCheck( $title ) ) {
3194
				$found = true;
3195
				$text = '<span class="error">'
3196
					. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3197
					. '</span>';
3198
				wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3199
			}
3200
		}
3201
3202
		# If we haven't found text to substitute by now, we're done
3203
		# Recover the source wikitext and return it
3204
		if ( !$found ) {
3205
			$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3206
			if ( $profileSection ) {
3207
				$this->mProfiler->scopedProfileOut( $profileSection );
3208
			}
3209
			return [ 'object' => $text ];
3210
		}
3211
3212
		# Expand DOM-style return values in a child frame
3213
		if ( $isChildObj ) {
3214
			# Clean up argument array
3215
			$newFrame = $frame->newChild( $args, $title );
3216
3217
			if ( $nowiki ) {
3218
				$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3219
			} elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3220
				# Expansion is eligible for the empty-frame cache
3221
				$text = $newFrame->cachedExpand( $titleText, $text );
3222
			} else {
3223
				# Uncached expansion
3224
				$text = $newFrame->expand( $text );
3225
			}
3226
		}
3227
		if ( $isLocalObj && $nowiki ) {
3228
			$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3229
			$isLocalObj = false;
3230
		}
3231
3232
		if ( $profileSection ) {
3233
			$this->mProfiler->scopedProfileOut( $profileSection );
3234
		}
3235
3236
		# Replace raw HTML by a placeholder
3237
		if ( $isHTML ) {
3238
			$text = $this->insertStripItem( $text );
3239
		} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3240
			# Escape nowiki-style return values
3241
			$text = wfEscapeWikiText( $text );
3242
		} elseif ( is_string( $text )
3243
			&& !$piece['lineStart']
3244
			&& preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3245
		) {
3246
			# Bug 529: if the template begins with a table or block-level
3247
			# element, it should be treated as beginning a new line.
3248
			# This behavior is somewhat controversial.
3249
			$text = "\n" . $text;
3250
		}
3251
3252
		if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3253
			# Error, oversize inclusion
3254
			if ( $titleText !== false ) {
3255
				# Make a working, properly escaped link if possible (bug 23588)
3256
				$text = "[[:$titleText]]";
3257
			} else {
3258
				# This will probably not be a working link, but at least it may
3259
				# provide some hint of where the problem is
3260
				preg_replace( '/^:/', '', $originalTitle );
3261
				$text = "[[:$originalTitle]]";
3262
			}
3263
			$text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3264
				. 'post-expand include size too large -->' );
3265
			$this->limitationWarn( 'post-expand-template-inclusion' );
3266
		}
3267
3268
		if ( $isLocalObj ) {
3269
			$ret = [ 'object' => $text ];
3270
		} else {
3271
			$ret = [ 'text' => $text ];
3272
		}
3273
3274
		return $ret;
3275
	}
3276
3277
	/**
3278
	 * Call a parser function and return an array with text and flags.
3279
	 *
3280
	 * The returned array will always contain a boolean 'found', indicating
3281
	 * whether the parser function was found or not. It may also contain the
3282
	 * following:
3283
	 *  text: string|object, resulting wikitext or PP DOM object
3284
	 *  isHTML: bool, $text is HTML, armour it against wikitext transformation
3285
	 *  isChildObj: bool, $text is a DOM node needing expansion in a child frame
3286
	 *  isLocalObj: bool, $text is a DOM node needing expansion in the current frame
3287
	 *  nowiki: bool, wiki markup in $text should be escaped
3288
	 *
3289
	 * @since 1.21
3290
	 * @param PPFrame $frame The current frame, contains template arguments
3291
	 * @param string $function Function name
3292
	 * @param array $args Arguments to the function
3293
	 * @throws MWException
3294
	 * @return array
3295
	 */
3296
	public function callParserFunction( $frame, $function, array $args = [] ) {
3297
		global $wgContLang;
3298
3299
		# Case sensitive functions
3300
		if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3301
			$function = $this->mFunctionSynonyms[1][$function];
3302
		} else {
3303
			# Case insensitive functions
3304
			$function = $wgContLang->lc( $function );
3305
			if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3306
				$function = $this->mFunctionSynonyms[0][$function];
3307
			} else {
3308
				return [ 'found' => false ];
3309
			}
3310
		}
3311
3312
		list( $callback, $flags ) = $this->mFunctionHooks[$function];
3313
3314
		# Workaround for PHP bug 35229 and similar
3315
		if ( !is_callable( $callback ) ) {
3316
			throw new MWException( "Tag hook for $function is not callable\n" );
3317
		}
3318
3319
		$allArgs = [ &$this ];
3320
		if ( $flags & self::SFH_OBJECT_ARGS ) {
3321
			# Convert arguments to PPNodes and collect for appending to $allArgs
3322
			$funcArgs = [];
3323
			foreach ( $args as $k => $v ) {
3324
				if ( $v instanceof PPNode || $k === 0 ) {
3325
					$funcArgs[] = $v;
3326
				} else {
3327
					$funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3328
				}
3329
			}
3330
3331
			# Add a frame parameter, and pass the arguments as an array
3332
			$allArgs[] = $frame;
3333
			$allArgs[] = $funcArgs;
3334
		} else {
3335
			# Convert arguments to plain text and append to $allArgs
3336
			foreach ( $args as $k => $v ) {
3337
				if ( $v instanceof PPNode ) {
3338
					$allArgs[] = trim( $frame->expand( $v ) );
3339
				} elseif ( is_int( $k ) && $k >= 0 ) {
3340
					$allArgs[] = trim( $v );
3341
				} else {
3342
					$allArgs[] = trim( "$k=$v" );
3343
				}
3344
			}
3345
		}
3346
3347
		$result = call_user_func_array( $callback, $allArgs );
3348
3349
		# The interface for function hooks allows them to return a wikitext
3350
		# string or an array containing the string and any flags. This mungs
3351
		# things around to match what this method should return.
3352
		if ( !is_array( $result ) ) {
3353
			$result =[
3354
				'found' => true,
3355
				'text' => $result,
3356
			];
3357
		} else {
3358
			if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3359
				$result['text'] = $result[0];
3360
			}
3361
			unset( $result[0] );
3362
			$result += [
3363
				'found' => true,
3364
			];
3365
		}
3366
3367
		$noparse = true;
3368
		$preprocessFlags = 0;
3369
		if ( isset( $result['noparse'] ) ) {
3370
			$noparse = $result['noparse'];
3371
		}
3372
		if ( isset( $result['preprocessFlags'] ) ) {
3373
			$preprocessFlags = $result['preprocessFlags'];
3374
		}
3375
3376
		if ( !$noparse ) {
3377
			$result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3378
			$result['isChildObj'] = true;
3379
		}
3380
3381
		return $result;
3382
	}
3383
3384
	/**
3385
	 * Get the semi-parsed DOM representation of a template with a given title,
3386
	 * and its redirect destination title. Cached.
3387
	 *
3388
	 * @param Title $title
3389
	 *
3390
	 * @return array
3391
	 */
3392
	public function getTemplateDom( $title ) {
3393
		$cacheTitle = $title;
3394
		$titleText = $title->getPrefixedDBkey();
3395
3396
		if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3397
			list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3398
			$title = Title::makeTitle( $ns, $dbk );
3399
			$titleText = $title->getPrefixedDBkey();
3400
		}
3401
		if ( isset( $this->mTplDomCache[$titleText] ) ) {
3402
			return [ $this->mTplDomCache[$titleText], $title ];
3403
		}
3404
3405
		# Cache miss, go to the database
3406
		list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3407
3408
		if ( $text === false ) {
3409
			$this->mTplDomCache[$titleText] = false;
3410
			return [ false, $title ];
3411
		}
3412
3413
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3414
		$this->mTplDomCache[$titleText] = $dom;
3415
3416
		if ( !$title->equals( $cacheTitle ) ) {
3417
			$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3418
				[ $title->getNamespace(), $cdb = $title->getDBkey() ];
3419
		}
3420
3421
		return [ $dom, $title ];
3422
	}
3423
3424
	/**
3425
	 * Fetch the current revision of a given title. Note that the revision
3426
	 * (and even the title) may not exist in the database, so everything
3427
	 * contributing to the output of the parser should use this method
3428
	 * where possible, rather than getting the revisions themselves. This
3429
	 * method also caches its results, so using it benefits performance.
3430
	 *
3431
	 * @since 1.24
3432
	 * @param Title $title
3433
	 * @return Revision
3434
	 */
3435
	public function fetchCurrentRevisionOfTitle( $title ) {
3436
		$cacheKey = $title->getPrefixedDBkey();
3437
		if ( !$this->currentRevisionCache ) {
3438
			$this->currentRevisionCache = new MapCacheLRU( 100 );
3439
		}
3440
		if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3441
			$this->currentRevisionCache->set( $cacheKey,
3442
				// Defaults to Parser::statelessFetchRevision()
3443
				call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3444
			);
3445
		}
3446
		return $this->currentRevisionCache->get( $cacheKey );
3447
	}
3448
3449
	/**
3450
	 * Wrapper around Revision::newFromTitle to allow passing additional parameters
3451
	 * without passing them on to it.
3452
	 *
3453
	 * @since 1.24
3454
	 * @param Title $title
3455
	 * @param Parser|bool $parser
3456
	 * @return Revision
3457
	 */
3458
	public static function statelessFetchRevision( $title, $parser = false ) {
3459
		return Revision::newFromTitle( $title );
3460
	}
3461
3462
	/**
3463
	 * Fetch the unparsed text of a template and register a reference to it.
3464
	 * @param Title $title
3465
	 * @return array ( string or false, Title )
3466
	 */
3467
	public function fetchTemplateAndTitle( $title ) {
3468
		// Defaults to Parser::statelessFetchTemplate()
3469
		$templateCb = $this->mOptions->getTemplateCallback();
3470
		$stuff = call_user_func( $templateCb, $title, $this );
3471
		// We use U+007F DELETE to distinguish strip markers from regular text.
3472
		$text = $stuff['text'];
3473
		if ( is_string( $stuff['text'] ) ) {
3474
			$text = strtr( $text, "\x7f", "?" );
3475
		}
3476
		$finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3477
		if ( isset( $stuff['deps'] ) ) {
3478
			foreach ( $stuff['deps'] as $dep ) {
3479
				$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3480
				if ( $dep['title']->equals( $this->getTitle() ) ) {
3481
					// If we transclude ourselves, the final result
3482
					// will change based on the new version of the page
3483
					$this->mOutput->setFlag( 'vary-revision' );
3484
				}
3485
			}
3486
		}
3487
		return [ $text, $finalTitle ];
3488
	}
3489
3490
	/**
3491
	 * Fetch the unparsed text of a template and register a reference to it.
3492
	 * @param Title $title
3493
	 * @return string|bool
3494
	 */
3495
	public function fetchTemplate( $title ) {
3496
		return $this->fetchTemplateAndTitle( $title )[0];
3497
	}
3498
3499
	/**
3500
	 * Static function to get a template
3501
	 * Can be overridden via ParserOptions::setTemplateCallback().
3502
	 *
3503
	 * @param Title $title
3504
	 * @param bool|Parser $parser
3505
	 *
3506
	 * @return array
3507
	 */
3508
	public static function statelessFetchTemplate( $title, $parser = false ) {
3509
		$text = $skip = false;
3510
		$finalTitle = $title;
3511
		$deps = [];
3512
3513
		# Loop to fetch the article, with up to 1 redirect
3514
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3515
		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3516
			// @codingStandardsIgnoreEnd
3517
			# Give extensions a chance to select the revision instead
3518
			$id = false; # Assume current
3519
			Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3520
				[ $parser, $title, &$skip, &$id ] );
3521
3522
			if ( $skip ) {
3523
				$text = false;
3524
				$deps[] = [
3525
					'title' => $title,
3526
					'page_id' => $title->getArticleID(),
3527
					'rev_id' => null
3528
				];
3529
				break;
3530
			}
3531
			# Get the revision
3532
			if ( $id ) {
3533
				$rev = Revision::newFromId( $id );
3534
			} elseif ( $parser ) {
3535
				$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...
3536
			} else {
3537
				$rev = Revision::newFromTitle( $title );
3538
			}
3539
			$rev_id = $rev ? $rev->getId() : 0;
3540
			# If there is no current revision, there is no page
3541
			if ( $id === false && !$rev ) {
3542
				$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...
3543
				$linkCache->addBadLinkObj( $title );
3544
			}
3545
3546
			$deps[] = [
3547
				'title' => $title,
3548
				'page_id' => $title->getArticleID(),
3549
				'rev_id' => $rev_id ];
3550
			if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3551
				# We fetched a rev from a different title; register it too...
3552
				$deps[] = [
3553
					'title' => $rev->getTitle(),
3554
					'page_id' => $rev->getPage(),
3555
					'rev_id' => $rev_id ];
3556
			}
3557
3558
			if ( $rev ) {
3559
				$content = $rev->getContent();
3560
				$text = $content ? $content->getWikitextForTransclusion() : null;
3561
3562
				if ( $text === false || $text === null ) {
3563
					$text = false;
3564
					break;
3565
				}
3566
			} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3567
				global $wgContLang;
3568
				$message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3569
				if ( !$message->exists() ) {
3570
					$text = false;
3571
					break;
3572
				}
3573
				$content = $message->content();
3574
				$text = $message->plain();
3575
			} else {
3576
				break;
3577
			}
3578
			if ( !$content ) {
3579
				break;
3580
			}
3581
			# Redirect?
3582
			$finalTitle = $title;
3583
			$title = $content->getRedirectTarget();
3584
		}
3585
		return [
3586
			'text' => $text,
3587
			'finalTitle' => $finalTitle,
3588
			'deps' => $deps ];
3589
	}
3590
3591
	/**
3592
	 * Fetch a file and its title and register a reference to it.
3593
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3594
	 * @param Title $title
3595
	 * @param array $options Array of options to RepoGroup::findFile
3596
	 * @return File|bool
3597
	 */
3598
	public function fetchFile( $title, $options = [] ) {
3599
		return $this->fetchFileAndTitle( $title, $options )[0];
3600
	}
3601
3602
	/**
3603
	 * Fetch a file and its title and register a reference to it.
3604
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3605
	 * @param Title $title
3606
	 * @param array $options Array of options to RepoGroup::findFile
3607
	 * @return array ( File or false, Title of file )
3608
	 */
3609
	public function fetchFileAndTitle( $title, $options = [] ) {
3610
		$file = $this->fetchFileNoRegister( $title, $options );
3611
3612
		$time = $file ? $file->getTimestamp() : false;
3613
		$sha1 = $file ? $file->getSha1() : false;
3614
		# Register the file as a dependency...
3615
		$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3616
		if ( $file && !$title->equals( $file->getTitle() ) ) {
3617
			# Update fetched file title
3618
			$title = $file->getTitle();
3619
			$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3620
		}
3621
		return [ $file, $title ];
3622
	}
3623
3624
	/**
3625
	 * Helper function for fetchFileAndTitle.
3626
	 *
3627
	 * Also useful if you need to fetch a file but not use it yet,
3628
	 * for example to get the file's handler.
3629
	 *
3630
	 * @param Title $title
3631
	 * @param array $options Array of options to RepoGroup::findFile
3632
	 * @return File|bool
3633
	 */
3634
	protected function fetchFileNoRegister( $title, $options = [] ) {
3635
		if ( isset( $options['broken'] ) ) {
3636
			$file = false; // broken thumbnail forced by hook
3637
		} elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3638
			$file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3639
		} else { // get by (name,timestamp)
3640
			$file = wfFindFile( $title, $options );
3641
		}
3642
		return $file;
3643
	}
3644
3645
	/**
3646
	 * Transclude an interwiki link.
3647
	 *
3648
	 * @param Title $title
3649
	 * @param string $action
3650
	 *
3651
	 * @return string
3652
	 */
3653
	public function interwikiTransclude( $title, $action ) {
3654
		global $wgEnableScaryTranscluding;
3655
3656
		if ( !$wgEnableScaryTranscluding ) {
3657
			return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3658
		}
3659
3660
		$url = $title->getFullURL( [ 'action' => $action ] );
3661
3662
		if ( strlen( $url ) > 255 ) {
3663
			return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3664
		}
3665
		return $this->fetchScaryTemplateMaybeFromCache( $url );
3666
	}
3667
3668
	/**
3669
	 * @param string $url
3670
	 * @return mixed|string
3671
	 */
3672
	public function fetchScaryTemplateMaybeFromCache( $url ) {
3673
		global $wgTranscludeCacheExpiry;
3674
		$dbr = wfGetDB( DB_SLAVE );
3675
		$tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3676
		$obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3677
				[ '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 3675 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...
3678
		if ( $obj ) {
3679
			return $obj->tc_contents;
3680
		}
3681
3682
		$req = MWHttpRequest::factory( $url, [], __METHOD__ );
3683
		$status = $req->execute(); // Status object
3684
		if ( $status->isOK() ) {
3685
			$text = $req->getContent();
3686
		} elseif ( $req->getStatus() != 200 ) {
3687
			// Though we failed to fetch the content, this status is useless.
3688
			return wfMessage( 'scarytranscludefailed-httpstatus' )
3689
				->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3690
		} else {
3691
			return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3692
		}
3693
3694
		$dbw = wfGetDB( DB_MASTER );
3695
		$dbw->replace( 'transcache', [ 'tc_url' ], [
3696
			'tc_url' => $url,
3697
			'tc_time' => $dbw->timestamp( time() ),
3698
			'tc_contents' => $text
3699
		] );
3700
		return $text;
3701
	}
3702
3703
	/**
3704
	 * Triple brace replacement -- used for template arguments
3705
	 * @private
3706
	 *
3707
	 * @param array $piece
3708
	 * @param PPFrame $frame
3709
	 *
3710
	 * @return array
3711
	 */
3712
	public function argSubstitution( $piece, $frame ) {
3713
3714
		$error = false;
3715
		$parts = $piece['parts'];
3716
		$nameWithSpaces = $frame->expand( $piece['title'] );
3717
		$argName = trim( $nameWithSpaces );
3718
		$object = false;
3719
		$text = $frame->getArgument( $argName );
3720
		if ( $text === false && $parts->getLength() > 0
3721
			&& ( $this->ot['html']
3722
				|| $this->ot['pre']
3723
				|| ( $this->ot['wiki'] && $frame->isTemplate() )
3724
			)
3725
		) {
3726
			# No match in frame, use the supplied default
3727
			$object = $parts->item( 0 )->getChildren();
3728
		}
3729
		if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3730
			$error = '<!-- WARNING: argument omitted, expansion size too large -->';
3731
			$this->limitationWarn( 'post-expand-template-argument' );
3732
		}
3733
3734
		if ( $text === false && $object === false ) {
3735
			# No match anywhere
3736
			$object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3737
		}
3738
		if ( $error !== false ) {
3739
			$text .= $error;
3740
		}
3741
		if ( $object !== false ) {
3742
			$ret = [ 'object' => $object ];
3743
		} else {
3744
			$ret = [ 'text' => $text ];
3745
		}
3746
3747
		return $ret;
3748
	}
3749
3750
	/**
3751
	 * Return the text to be used for a given extension tag.
3752
	 * This is the ghost of strip().
3753
	 *
3754
	 * @param array $params Associative array of parameters:
3755
	 *     name       PPNode for the tag name
3756
	 *     attr       PPNode for unparsed text where tag attributes are thought to be
3757
	 *     attributes Optional associative array of parsed attributes
3758
	 *     inner      Contents of extension element
3759
	 *     noClose    Original text did not have a close tag
3760
	 * @param PPFrame $frame
3761
	 *
3762
	 * @throws MWException
3763
	 * @return string
3764
	 */
3765
	public function extensionSubstitution( $params, $frame ) {
3766
		$name = $frame->expand( $params['name'] );
3767
		$attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3768
		$content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3769
		$marker = self::MARKER_PREFIX . "-$name-"
3770
			. sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3771
3772
		$isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3773
			( $this->ot['html'] || $this->ot['pre'] );
3774
		if ( $isFunctionTag ) {
3775
			$markerType = 'none';
3776
		} else {
3777
			$markerType = 'general';
3778
		}
3779
		if ( $this->ot['html'] || $isFunctionTag ) {
3780
			$name = strtolower( $name );
3781
			$attributes = Sanitizer::decodeTagAttributes( $attrText );
3782
			if ( isset( $params['attributes'] ) ) {
3783
				$attributes = $attributes + $params['attributes'];
3784
			}
3785
3786
			if ( isset( $this->mTagHooks[$name] ) ) {
3787
				# Workaround for PHP bug 35229 and similar
3788
				if ( !is_callable( $this->mTagHooks[$name] ) ) {
3789
					throw new MWException( "Tag hook for $name is not callable\n" );
3790
				}
3791
				$output = call_user_func_array( $this->mTagHooks[$name],
3792
					[ $content, $attributes, $this, $frame ] );
3793
			} elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3794
				list( $callback, ) = $this->mFunctionTagHooks[$name];
3795
				if ( !is_callable( $callback ) ) {
3796
					throw new MWException( "Tag hook for $name is not callable\n" );
3797
				}
3798
3799
				$output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] );
3800
			} else {
3801
				$output = '<span class="error">Invalid tag extension name: ' .
3802
					htmlspecialchars( $name ) . '</span>';
3803
			}
3804
3805
			if ( is_array( $output ) ) {
3806
				# Extract flags to local scope (to override $markerType)
3807
				$flags = $output;
3808
				$output = $flags[0];
3809
				unset( $flags[0] );
3810
				extract( $flags );
3811
			}
3812
		} else {
3813
			if ( is_null( $attrText ) ) {
3814
				$attrText = '';
3815
			}
3816
			if ( isset( $params['attributes'] ) ) {
3817
				foreach ( $params['attributes'] as $attrName => $attrValue ) {
3818
					$attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3819
						htmlspecialchars( $attrValue ) . '"';
3820
				}
3821
			}
3822
			if ( $content === null ) {
3823
				$output = "<$name$attrText/>";
3824
			} else {
3825
				$close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3826
				$output = "<$name$attrText>$content$close";
3827
			}
3828
		}
3829
3830
		if ( $markerType === 'none' ) {
3831
			return $output;
3832
		} elseif ( $markerType === 'nowiki' ) {
3833
			$this->mStripState->addNoWiki( $marker, $output );
3834
		} elseif ( $markerType === 'general' ) {
3835
			$this->mStripState->addGeneral( $marker, $output );
3836
		} else {
3837
			throw new MWException( __METHOD__ . ': invalid marker type' );
3838
		}
3839
		return $marker;
3840
	}
3841
3842
	/**
3843
	 * Increment an include size counter
3844
	 *
3845
	 * @param string $type The type of expansion
3846
	 * @param int $size The size of the text
3847
	 * @return bool False if this inclusion would take it over the maximum, true otherwise
3848
	 */
3849
	public function incrementIncludeSize( $type, $size ) {
3850
		if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3851
			return false;
3852
		} else {
3853
			$this->mIncludeSizes[$type] += $size;
3854
			return true;
3855
		}
3856
	}
3857
3858
	/**
3859
	 * Increment the expensive function count
3860
	 *
3861
	 * @return bool False if the limit has been exceeded
3862
	 */
3863
	public function incrementExpensiveFunctionCount() {
3864
		$this->mExpensiveFunctionCount++;
3865
		return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3866
	}
3867
3868
	/**
3869
	 * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
3870
	 * Fills $this->mDoubleUnderscores, returns the modified text
3871
	 *
3872
	 * @param string $text
3873
	 *
3874
	 * @return string
3875
	 */
3876
	public function doDoubleUnderscore( $text ) {
3877
3878
		# The position of __TOC__ needs to be recorded
3879
		$mw = MagicWord::get( 'toc' );
3880
		if ( $mw->match( $text ) ) {
3881
			$this->mShowToc = true;
3882
			$this->mForceTocPosition = true;
3883
3884
			# Set a placeholder. At the end we'll fill it in with the TOC.
3885
			$text = $mw->replace( '<!--MWTOC-->', $text, 1 );
3886
3887
			# Only keep the first one.
3888
			$text = $mw->replace( '', $text );
3889
		}
3890
3891
		# Now match and remove the rest of them
3892
		$mwa = MagicWord::getDoubleUnderscoreArray();
3893
		$this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3894
3895
		if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3896
			$this->mOutput->mNoGallery = true;
3897
		}
3898
		if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3899
			$this->mShowToc = false;
3900
		}
3901
		if ( isset( $this->mDoubleUnderscores['hiddencat'] )
3902
			&& $this->mTitle->getNamespace() == NS_CATEGORY
3903
		) {
3904
			$this->addTrackingCategory( 'hidden-category-category' );
3905
		}
3906
		# (bug 8068) Allow control over whether robots index a page.
3907
		# @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here!  This
3908
		# is not desirable, the last one on the page should win.
3909 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
3910
			$this->mOutput->setIndexPolicy( 'noindex' );
3911
			$this->addTrackingCategory( 'noindex-category' );
3912
		}
3913 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
3914
			$this->mOutput->setIndexPolicy( 'index' );
3915
			$this->addTrackingCategory( 'index-category' );
3916
		}
3917
3918
		# Cache all double underscores in the database
3919
		foreach ( $this->mDoubleUnderscores as $key => $val ) {
3920
			$this->mOutput->setProperty( $key, '' );
3921
		}
3922
3923
		return $text;
3924
	}
3925
3926
	/**
3927
	 * @see ParserOutput::addTrackingCategory()
3928
	 * @param string $msg Message key
3929
	 * @return bool Whether the addition was successful
3930
	 */
3931
	public function addTrackingCategory( $msg ) {
3932
		return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
3933
	}
3934
3935
	/**
3936
	 * This function accomplishes several tasks:
3937
	 * 1) Auto-number headings if that option is enabled
3938
	 * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
3939
	 * 3) Add a Table of contents on the top for users who have enabled the option
3940
	 * 4) Auto-anchor headings
3941
	 *
3942
	 * It loops through all headlines, collects the necessary data, then splits up the
3943
	 * string and re-inserts the newly formatted headlines.
3944
	 *
3945
	 * @param string $text
3946
	 * @param string $origText Original, untouched wikitext
3947
	 * @param bool $isMain
3948
	 * @return mixed|string
3949
	 * @private
3950
	 */
3951
	public function formatHeadings( $text, $origText, $isMain = true ) {
3952
		global $wgMaxTocLevel, $wgExperimentalHtmlIds;
3953
3954
		# Inhibit editsection links if requested in the page
3955
		if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
3956
			$maybeShowEditLink = $showEditLink = false;
3957
		} else {
3958
			$maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
3959
			$showEditLink = $this->mOptions->getEditSection();
3960
		}
3961
		if ( $showEditLink ) {
3962
			$this->mOutput->setEditSectionTokens( true );
3963
		}
3964
3965
		# Get all headlines for numbering them and adding funky stuff like [edit]
3966
		# links - this is for later, but we need the number of headlines right now
3967
		$matches = [];
3968
		$numMatches = preg_match_all(
3969
			'/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
3970
			$text,
3971
			$matches
3972
		);
3973
3974
		# if there are fewer than 4 headlines in the article, do not show TOC
3975
		# unless it's been explicitly enabled.
3976
		$enoughToc = $this->mShowToc &&
3977
			( ( $numMatches >= 4 ) || $this->mForceTocPosition );
3978
3979
		# Allow user to stipulate that a page should have a "new section"
3980
		# link added via __NEWSECTIONLINK__
3981
		if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
3982
			$this->mOutput->setNewSection( true );
3983
		}
3984
3985
		# Allow user to remove the "new section"
3986
		# link via __NONEWSECTIONLINK__
3987
		if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
3988
			$this->mOutput->hideNewSection( true );
3989
		}
3990
3991
		# if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
3992
		# override above conditions and always show TOC above first header
3993
		if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
3994
			$this->mShowToc = true;
3995
			$enoughToc = true;
3996
		}
3997
3998
		# headline counter
3999
		$headlineCount = 0;
4000
		$numVisible = 0;
4001
4002
		# Ugh .. the TOC should have neat indentation levels which can be
4003
		# passed to the skin functions. These are determined here
4004
		$toc = '';
4005
		$full = '';
4006
		$head = [];
4007
		$sublevelCount = [];
4008
		$levelCount = [];
4009
		$level = 0;
4010
		$prevlevel = 0;
4011
		$toclevel = 0;
4012
		$prevtoclevel = 0;
4013
		$markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4014
		$baseTitleText = $this->mTitle->getPrefixedDBkey();
4015
		$oldType = $this->mOutputType;
4016
		$this->setOutputType( self::OT_WIKI );
4017
		$frame = $this->getPreprocessor()->newFrame();
4018
		$root = $this->preprocessToDom( $origText );
4019
		$node = $root->getFirstChild();
4020
		$byteOffset = 0;
4021
		$tocraw = [];
4022
		$refers = [];
4023
4024
		$headlines = $numMatches !== false ? $matches[3] : [];
4025
4026
		foreach ( $headlines as $headline ) {
4027
			$isTemplate = false;
4028
			$titleText = false;
4029
			$sectionIndex = false;
4030
			$numbering = '';
4031
			$markerMatches = [];
4032
			if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4033
				$serial = $markerMatches[1];
4034
				list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4035
				$isTemplate = ( $titleText != $baseTitleText );
4036
				$headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4037
			}
4038
4039
			if ( $toclevel ) {
4040
				$prevlevel = $level;
4041
			}
4042
			$level = $matches[1][$headlineCount];
4043
4044
			if ( $level > $prevlevel ) {
4045
				# Increase TOC level
4046
				$toclevel++;
4047
				$sublevelCount[$toclevel] = 0;
4048
				if ( $toclevel < $wgMaxTocLevel ) {
4049
					$prevtoclevel = $toclevel;
4050
					$toc .= Linker::tocIndent();
4051
					$numVisible++;
4052
				}
4053
			} elseif ( $level < $prevlevel && $toclevel > 1 ) {
4054
				# Decrease TOC level, find level to jump to
4055
4056
				for ( $i = $toclevel; $i > 0; $i-- ) {
4057
					if ( $levelCount[$i] == $level ) {
4058
						# Found last matching level
4059
						$toclevel = $i;
4060
						break;
4061
					} elseif ( $levelCount[$i] < $level ) {
4062
						# Found first matching level below current level
4063
						$toclevel = $i + 1;
4064
						break;
4065
					}
4066
				}
4067
				if ( $i == 0 ) {
4068
					$toclevel = 1;
4069
				}
4070
				if ( $toclevel < $wgMaxTocLevel ) {
4071
					if ( $prevtoclevel < $wgMaxTocLevel ) {
4072
						# Unindent only if the previous toc level was shown :p
4073
						$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4074
						$prevtoclevel = $toclevel;
4075
					} else {
4076
						$toc .= Linker::tocLineEnd();
4077
					}
4078
				}
4079
			} else {
4080
				# No change in level, end TOC line
4081
				if ( $toclevel < $wgMaxTocLevel ) {
4082
					$toc .= Linker::tocLineEnd();
4083
				}
4084
			}
4085
4086
			$levelCount[$toclevel] = $level;
4087
4088
			# count number of headlines for each level
4089
			$sublevelCount[$toclevel]++;
4090
			$dot = 0;
4091
			for ( $i = 1; $i <= $toclevel; $i++ ) {
4092
				if ( !empty( $sublevelCount[$i] ) ) {
4093
					if ( $dot ) {
4094
						$numbering .= '.';
4095
					}
4096
					$numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4097
					$dot = 1;
4098
				}
4099
			}
4100
4101
			# The safe header is a version of the header text safe to use for links
4102
4103
			# Remove link placeholders by the link text.
4104
			#     <!--LINK number-->
4105
			# turns into
4106
			#     link text with suffix
4107
			# Do this before unstrip since link text can contain strip markers
4108
			$safeHeadline = $this->replaceLinkHoldersText( $headline );
4109
4110
			# Avoid insertion of weird stuff like <math> by expanding the relevant sections
4111
			$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4112
4113
			# Strip out HTML (first regex removes any tag not allowed)
4114
			# Allowed tags are:
4115
			# * <sup> and <sub> (bug 8393)
4116
			# * <i> (bug 26375)
4117
			# * <b> (r105284)
4118
			# * <bdi> (bug 72884)
4119
			# * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4120
			# We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4121
			# to allow setting directionality in toc items.
4122
			$tocline = preg_replace(
4123
				[
4124
					'#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4125
					'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4126
				],
4127
				[ '', '<$1>' ],
4128
				$safeHeadline
4129
			);
4130
4131
			# Strip '<span></span>', which is the result from the above if
4132
			# <span id="foo"></span> is used to produce an additional anchor
4133
			# for a section.
4134
			$tocline = str_replace( '<span></span>', '', $tocline );
4135
4136
			$tocline = trim( $tocline );
4137
4138
			# For the anchor, strip out HTML-y stuff period
4139
			$safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4140
			$safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4141
4142
			# Save headline for section edit hint before it's escaped
4143
			$headlineHint = $safeHeadline;
4144
4145
			if ( $wgExperimentalHtmlIds ) {
4146
				# For reverse compatibility, provide an id that's
4147
				# HTML4-compatible, like we used to.
4148
				# It may be worth noting, academically, that it's possible for
4149
				# the legacy anchor to conflict with a non-legacy headline
4150
				# anchor on the page.  In this case likely the "correct" thing
4151
				# would be to either drop the legacy anchors or make sure
4152
				# they're numbered first.  However, this would require people
4153
				# to type in section names like "abc_.D7.93.D7.90.D7.A4"
4154
				# manually, so let's not bother worrying about it.
4155
				$legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4156
					[ 'noninitial', 'legacy' ] );
4157
				$safeHeadline = Sanitizer::escapeId( $safeHeadline );
4158
4159
				if ( $legacyHeadline == $safeHeadline ) {
4160
					# No reason to have both (in fact, we can't)
4161
					$legacyHeadline = false;
4162
				}
4163
			} else {
4164
				$legacyHeadline = false;
4165
				$safeHeadline = Sanitizer::escapeId( $safeHeadline,
4166
					'noninitial' );
4167
			}
4168
4169
			# HTML names must be case-insensitively unique (bug 10721).
4170
			# This does not apply to Unicode characters per
4171
			# http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4172
			# @todo FIXME: We may be changing them depending on the current locale.
4173
			$arrayKey = strtolower( $safeHeadline );
4174
			if ( $legacyHeadline === false ) {
4175
				$legacyArrayKey = false;
4176
			} else {
4177
				$legacyArrayKey = strtolower( $legacyHeadline );
4178
			}
4179
4180
			# Create the anchor for linking from the TOC to the section
4181
			$anchor = $safeHeadline;
4182
			$legacyAnchor = $legacyHeadline;
4183 View Code Duplication
			if ( isset( $refers[$arrayKey] ) ) {
4184
				// @codingStandardsIgnoreStart
4185
				for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4186
				// @codingStandardsIgnoreEnd
4187
				$anchor .= "_$i";
4188
				$refers["${arrayKey}_$i"] = true;
4189
			} else {
4190
				$refers[$arrayKey] = true;
4191
			}
4192 View Code Duplication
			if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4193
				// @codingStandardsIgnoreStart
4194
				for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4195
				// @codingStandardsIgnoreEnd
4196
				$legacyAnchor .= "_$i";
4197
				$refers["${legacyArrayKey}_$i"] = true;
4198
			} else {
4199
				$refers[$legacyArrayKey] = true;
4200
			}
4201
4202
			# Don't number the heading if it is the only one (looks silly)
4203
			if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4204
				# the two are different if the line contains a link
4205
				$headline = Html::element(
4206
					'span',
4207
					[ 'class' => 'mw-headline-number' ],
4208
					$numbering
4209
				) . ' ' . $headline;
4210
			}
4211
4212
			if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4213
				$toc .= Linker::tocLine( $anchor, $tocline,
4214
					$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4215
			}
4216
4217
			# Add the section to the section tree
4218
			# Find the DOM node for this header
4219
			$noOffset = ( $isTemplate || $sectionIndex === false );
4220
			while ( $node && !$noOffset ) {
4221
				if ( $node->getName() === 'h' ) {
4222
					$bits = $node->splitHeading();
4223
					if ( $bits['i'] == $sectionIndex ) {
4224
						break;
4225
					}
4226
				}
4227
				$byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4228
					$frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4229
				$node = $node->getNextSibling();
4230
			}
4231
			$tocraw[] = [
4232
				'toclevel' => $toclevel,
4233
				'level' => $level,
4234
				'line' => $tocline,
4235
				'number' => $numbering,
4236
				'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4237
				'fromtitle' => $titleText,
4238
				'byteoffset' => ( $noOffset ? null : $byteOffset ),
4239
				'anchor' => $anchor,
4240
			];
4241
4242
			# give headline the correct <h#> tag
4243
			if ( $maybeShowEditLink && $sectionIndex !== false ) {
4244
				// Output edit section links as markers with styles that can be customized by skins
4245
				if ( $isTemplate ) {
4246
					# Put a T flag in the section identifier, to indicate to extractSections()
4247
					# that sections inside <includeonly> should be counted.
4248
					$editsectionPage = $titleText;
4249
					$editsectionSection = "T-$sectionIndex";
4250
					$editsectionContent = null;
4251
				} else {
4252
					$editsectionPage = $this->mTitle->getPrefixedText();
4253
					$editsectionSection = $sectionIndex;
4254
					$editsectionContent = $headlineHint;
4255
				}
4256
				// We use a bit of pesudo-xml for editsection markers. The
4257
				// language converter is run later on. Using a UNIQ style marker
4258
				// leads to the converter screwing up the tokens when it
4259
				// converts stuff. And trying to insert strip tags fails too. At
4260
				// this point all real inputted tags have already been escaped,
4261
				// so we don't have to worry about a user trying to input one of
4262
				// these markers directly. We use a page and section attribute
4263
				// to stop the language converter from converting these
4264
				// important bits of data, but put the headline hint inside a
4265
				// content block because the language converter is supposed to
4266
				// be able to convert that piece of data.
4267
				// Gets replaced with html in ParserOutput::getText
4268
				$editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4269
				$editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4270
				if ( $editsectionContent !== null ) {
4271
					$editlink .= '>' . $editsectionContent . '</mw:editsection>';
4272
				} else {
4273
					$editlink .= '/>';
4274
				}
4275
			} else {
4276
				$editlink = '';
4277
			}
4278
			$head[$headlineCount] = Linker::makeHeadline( $level,
4279
				$matches['attrib'][$headlineCount], $anchor, $headline,
4280
				$editlink, $legacyAnchor );
4281
4282
			$headlineCount++;
4283
		}
4284
4285
		$this->setOutputType( $oldType );
4286
4287
		# Never ever show TOC if no headers
4288
		if ( $numVisible < 1 ) {
4289
			$enoughToc = false;
4290
		}
4291
4292
		if ( $enoughToc ) {
4293
			if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4294
				$toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4295
			}
4296
			$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4297
			$this->mOutput->setTOCHTML( $toc );
4298
			$toc = self::TOC_START . $toc . self::TOC_END;
4299
			$this->mOutput->addModules( 'mediawiki.toc' );
4300
		}
4301
4302
		if ( $isMain ) {
4303
			$this->mOutput->setSections( $tocraw );
4304
		}
4305
4306
		# split up and insert constructed headlines
4307
		$blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4308
		$i = 0;
4309
4310
		// build an array of document sections
4311
		$sections = [];
4312
		foreach ( $blocks as $block ) {
4313
			// $head is zero-based, sections aren't.
4314
			if ( empty( $head[$i - 1] ) ) {
4315
				$sections[$i] = $block;
4316
			} else {
4317
				$sections[$i] = $head[$i - 1] . $block;
4318
			}
4319
4320
			/**
4321
			 * Send a hook, one per section.
4322
			 * The idea here is to be able to make section-level DIVs, but to do so in a
4323
			 * lower-impact, more correct way than r50769
4324
			 *
4325
			 * $this : caller
4326
			 * $section : the section number
4327
			 * &$sectionContent : ref to the content of the section
4328
			 * $showEditLinks : boolean describing whether this section has an edit link
4329
			 */
4330
			Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4331
4332
			$i++;
4333
		}
4334
4335
		if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4336
			// append the TOC at the beginning
4337
			// Top anchor now in skin
4338
			$sections[0] = $sections[0] . $toc . "\n";
4339
		}
4340
4341
		$full .= implode( '', $sections );
4342
4343
		if ( $this->mForceTocPosition ) {
4344
			return str_replace( '<!--MWTOC-->', $toc, $full );
4345
		} else {
4346
			return $full;
4347
		}
4348
	}
4349
4350
	/**
4351
	 * Transform wiki markup when saving a page by doing "\r\n" -> "\n"
4352
	 * conversion, substituting signatures, {{subst:}} templates, etc.
4353
	 *
4354
	 * @param string $text The text to transform
4355
	 * @param Title $title The Title object for the current article
4356
	 * @param User $user The User object describing the current user
4357
	 * @param ParserOptions $options Parsing options
4358
	 * @param bool $clearState Whether to clear the parser state first
4359
	 * @return string The altered wiki markup
4360
	 */
4361
	public function preSaveTransform( $text, Title $title, User $user,
4362
		ParserOptions $options, $clearState = true
4363
	) {
4364
		if ( $clearState ) {
4365
			$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...
4366
		}
4367
		$this->startParse( $title, $options, self::OT_WIKI, $clearState );
4368
		$this->setUser( $user );
4369
4370
		$pairs = [
4371
			"\r\n" => "\n",
4372
			"\r" => "\n",
4373
		];
4374
		$text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4375
		if ( $options->getPreSaveTransform() ) {
4376
			$text = $this->pstPass2( $text, $user );
4377
		}
4378
		$text = $this->mStripState->unstripBoth( $text );
4379
4380
		$this->setUser( null ); # Reset
4381
4382
		return $text;
4383
	}
4384
4385
	/**
4386
	 * Pre-save transform helper function
4387
	 *
4388
	 * @param string $text
4389
	 * @param User $user
4390
	 *
4391
	 * @return string
4392
	 */
4393
	private function pstPass2( $text, $user ) {
4394
		global $wgContLang;
4395
4396
		# Note: This is the timestamp saved as hardcoded wikitext to
4397
		# the database, we use $wgContLang here in order to give
4398
		# everyone the same signature and use the default one rather
4399
		# than the one selected in each user's preferences.
4400
		# (see also bug 12815)
4401
		$ts = $this->mOptions->getTimestamp();
4402
		$timestamp = MWTimestamp::getLocalInstance( $ts );
4403
		$ts = $timestamp->format( 'YmdHis' );
4404
		$tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4405
4406
		$d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4407
4408
		# Variable replacement
4409
		# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4410
		$text = $this->replaceVariables( $text );
4411
4412
		# This works almost by chance, as the replaceVariables are done before the getUserSig(),
4413
		# which may corrupt this parser instance via its wfMessage()->text() call-
4414
4415
		# Signatures
4416
		$sigText = $this->getUserSig( $user );
4417
		$text = strtr( $text, [
4418
			'~~~~~' => $d,
4419
			'~~~~' => "$sigText $d",
4420
			'~~~' => $sigText
4421
		] );
4422
4423
		# Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4424
		$tc = '[' . Title::legalChars() . ']';
4425
		$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4426
4427
		// [[ns:page (context)|]]
4428
		$p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4429
		// [[ns:page(context)|]] (double-width brackets, added in r40257)
4430
		$p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4431
		// [[ns:page (context), context|]] (using either single or double-width comma)
4432
		$p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4433
		// [[|page]] (reverse pipe trick: add context from page title)
4434
		$p2 = "/\[\[\\|($tc+)]]/";
4435
4436
		# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4437
		$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4438
		$text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4439
		$text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4440
4441
		$t = $this->mTitle->getText();
4442
		$m = [];
4443
		if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4444
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4445
		} elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4446
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4447
		} else {
4448
			# if there's no context, don't bother duplicating the title
4449
			$text = preg_replace( $p2, '[[\\1]]', $text );
4450
		}
4451
4452
		# Trim trailing whitespace
4453
		$text = rtrim( $text );
4454
4455
		return $text;
4456
	}
4457
4458
	/**
4459
	 * Fetch the user's signature text, if any, and normalize to
4460
	 * validated, ready-to-insert wikitext.
4461
	 * If you have pre-fetched the nickname or the fancySig option, you can
4462
	 * specify them here to save a database query.
4463
	 * Do not reuse this parser instance after calling getUserSig(),
4464
	 * as it may have changed if it's the $wgParser.
4465
	 *
4466
	 * @param User $user
4467
	 * @param string|bool $nickname Nickname to use or false to use user's default nickname
4468
	 * @param bool|null $fancySig whether the nicknname is the complete signature
4469
	 *    or null to use default value
4470
	 * @return string
4471
	 */
4472
	public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4473
		global $wgMaxSigChars;
4474
4475
		$username = $user->getName();
4476
4477
		# If not given, retrieve from the user object.
4478
		if ( $nickname === false ) {
4479
			$nickname = $user->getOption( 'nickname' );
4480
		}
4481
4482
		if ( is_null( $fancySig ) ) {
4483
			$fancySig = $user->getBoolOption( 'fancysig' );
4484
		}
4485
4486
		$nickname = $nickname == null ? $username : $nickname;
4487
4488
		if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4489
			$nickname = $username;
4490
			wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4491
		} elseif ( $fancySig !== false ) {
4492
			# Sig. might contain markup; validate this
4493
			if ( $this->validateSig( $nickname ) !== false ) {
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4486 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...
4494
				# Validated; clean up (if needed) and return it
4495
				return $this->cleanSig( $nickname, true );
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4486 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...
4496
			} else {
4497
				# Failed to validate; fall back to the default
4498
				$nickname = $username;
4499
				wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4500
			}
4501
		}
4502
4503
		# Make sure nickname doesnt get a sig in a sig
4504
		$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...
4505
4506
		# If we're still here, make it a link to the user page
4507
		$userText = wfEscapeWikiText( $username );
4508
		$nickText = wfEscapeWikiText( $nickname );
4509
		$msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4510
4511
		return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4512
			->title( $this->getTitle() )->text();
4513
	}
4514
4515
	/**
4516
	 * Check that the user's signature contains no bad XML
4517
	 *
4518
	 * @param string $text
4519
	 * @return string|bool An expanded string, or false if invalid.
4520
	 */
4521
	public function validateSig( $text ) {
4522
		return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4523
	}
4524
4525
	/**
4526
	 * Clean up signature text
4527
	 *
4528
	 * 1) Strip 3, 4 or 5 tildes out of signatures @see cleanSigInSig
4529
	 * 2) Substitute all transclusions
4530
	 *
4531
	 * @param string $text
4532
	 * @param bool $parsing Whether we're cleaning (preferences save) or parsing
4533
	 * @return string Signature text
4534
	 */
4535
	public function cleanSig( $text, $parsing = false ) {
4536
		if ( !$parsing ) {
4537
			global $wgTitle;
4538
			$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...
4539
			$this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4540
		}
4541
4542
		# Option to disable this feature
4543
		if ( !$this->mOptions->getCleanSignatures() ) {
4544
			return $text;
4545
		}
4546
4547
		# @todo FIXME: Regex doesn't respect extension tags or nowiki
4548
		#  => Move this logic to braceSubstitution()
4549
		$substWord = MagicWord::get( 'subst' );
4550
		$substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4551
		$substText = '{{' . $substWord->getSynonym( 0 );
4552
4553
		$text = preg_replace( $substRegex, $substText, $text );
4554
		$text = self::cleanSigInSig( $text );
4555
		$dom = $this->preprocessToDom( $text );
4556
		$frame = $this->getPreprocessor()->newFrame();
4557
		$text = $frame->expand( $dom );
4558
4559
		if ( !$parsing ) {
4560
			$text = $this->mStripState->unstripBoth( $text );
4561
		}
4562
4563
		return $text;
4564
	}
4565
4566
	/**
4567
	 * Strip 3, 4 or 5 tildes out of signatures.
4568
	 *
4569
	 * @param string $text
4570
	 * @return string Signature text with /~{3,5}/ removed
4571
	 */
4572
	public static function cleanSigInSig( $text ) {
4573
		$text = preg_replace( '/~{3,5}/', '', $text );
4574
		return $text;
4575
	}
4576
4577
	/**
4578
	 * Set up some variables which are usually set up in parse()
4579
	 * so that an external function can call some class members with confidence
4580
	 *
4581
	 * @param Title|null $title
4582
	 * @param ParserOptions $options
4583
	 * @param int $outputType
4584
	 * @param bool $clearState
4585
	 */
4586
	public function startExternalParse( Title $title = null, ParserOptions $options,
4587
		$outputType, $clearState = true
4588
	) {
4589
		$this->startParse( $title, $options, $outputType, $clearState );
4590
	}
4591
4592
	/**
4593
	 * @param Title|null $title
4594
	 * @param ParserOptions $options
4595
	 * @param int $outputType
4596
	 * @param bool $clearState
4597
	 */
4598
	private function startParse( Title $title = null, ParserOptions $options,
4599
		$outputType, $clearState = true
4600
	) {
4601
		$this->setTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 4598 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...
4602
		$this->mOptions = $options;
4603
		$this->setOutputType( $outputType );
4604
		if ( $clearState ) {
4605
			$this->clearState();
4606
		}
4607
	}
4608
4609
	/**
4610
	 * Wrapper for preprocess()
4611
	 *
4612
	 * @param string $text The text to preprocess
4613
	 * @param ParserOptions $options Options
4614
	 * @param Title|null $title Title object or null to use $wgTitle
4615
	 * @return string
4616
	 */
4617
	public function transformMsg( $text, $options, $title = null ) {
4618
		static $executing = false;
4619
4620
		# Guard against infinite recursion
4621
		if ( $executing ) {
4622
			return $text;
4623
		}
4624
		$executing = true;
4625
4626
		if ( !$title ) {
4627
			global $wgTitle;
4628
			$title = $wgTitle;
4629
		}
4630
4631
		$text = $this->preprocess( $text, $title, $options );
4632
4633
		$executing = false;
4634
		return $text;
4635
	}
4636
4637
	/**
4638
	 * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
4639
	 * The callback should have the following form:
4640
	 *    function myParserHook( $text, $params, $parser, $frame ) { ... }
4641
	 *
4642
	 * Transform and return $text. Use $parser for any required context, e.g. use
4643
	 * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
4644
	 *
4645
	 * Hooks may return extended information by returning an array, of which the
4646
	 * first numbered element (index 0) must be the return string, and all other
4647
	 * entries are extracted into local variables within an internal function
4648
	 * in the Parser class.
4649
	 *
4650
	 * This interface (introduced r61913) appears to be undocumented, but
4651
	 * 'markerType' is used by some core tag hooks to override which strip
4652
	 * array their results are placed in. **Use great caution if attempting
4653
	 * this interface, as it is not documented and injudicious use could smash
4654
	 * private variables.**
4655
	 *
4656
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4657
	 * @param callable $callback The callback function (and object) to use for the tag
4658
	 * @throws MWException
4659
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4660
	 */
4661 View Code Duplication
	public function setHook( $tag, $callback ) {
4662
		$tag = strtolower( $tag );
4663
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4664
			throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4665
		}
4666
		$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4667
		$this->mTagHooks[$tag] = $callback;
4668
		if ( !in_array( $tag, $this->mStripList ) ) {
4669
			$this->mStripList[] = $tag;
4670
		}
4671
4672
		return $oldVal;
4673
	}
4674
4675
	/**
4676
	 * As setHook(), but letting the contents be parsed.
4677
	 *
4678
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
4679
	 * operate late in the transformation sequence, on HTML instead of wikitext.
4680
	 *
4681
	 * This is probably obsoleted by things dealing with parser frames?
4682
	 * The only extension currently using it is geoserver.
4683
	 *
4684
	 * @since 1.10
4685
	 * @todo better document or deprecate this
4686
	 *
4687
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4688
	 * @param callable $callback The callback function (and object) to use for the tag
4689
	 * @throws MWException
4690
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4691
	 */
4692
	public function setTransparentTagHook( $tag, $callback ) {
4693
		$tag = strtolower( $tag );
4694
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4695
			throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4696
		}
4697
		$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4698
		$this->mTransparentTagHooks[$tag] = $callback;
4699
4700
		return $oldVal;
4701
	}
4702
4703
	/**
4704
	 * Remove all tag hooks
4705
	 */
4706
	public function clearTagHooks() {
4707
		$this->mTagHooks = [];
4708
		$this->mFunctionTagHooks = [];
4709
		$this->mStripList = $this->mDefaultStripList;
4710
	}
4711
4712
	/**
4713
	 * Create a function, e.g. {{sum:1|2|3}}
4714
	 * The callback function should have the form:
4715
	 *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
4716
	 *
4717
	 * Or with Parser::SFH_OBJECT_ARGS:
4718
	 *    function myParserFunction( $parser, $frame, $args ) { ... }
4719
	 *
4720
	 * The callback may either return the text result of the function, or an array with the text
4721
	 * in element 0, and a number of flags in the other elements. The names of the flags are
4722
	 * specified in the keys. Valid flags are:
4723
	 *   found                     The text returned is valid, stop processing the template. This
4724
	 *                             is on by default.
4725
	 *   nowiki                    Wiki markup in the return value should be escaped
4726
	 *   isHTML                    The returned text is HTML, armour it against wikitext transformation
4727
	 *
4728
	 * @param string $id The magic word ID
4729
	 * @param callable $callback The callback function (and object) to use
4730
	 * @param int $flags A combination of the following flags:
4731
	 *     Parser::SFH_NO_HASH      No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
4732
	 *
4733
	 *     Parser::SFH_OBJECT_ARGS  Pass the template arguments as PPNode objects instead of text.
4734
	 *     This allows for conditional expansion of the parse tree, allowing you to eliminate dead
4735
	 *     branches and thus speed up parsing. It is also possible to analyse the parse tree of
4736
	 *     the arguments, and to control the way they are expanded.
4737
	 *
4738
	 *     The $frame parameter is a PPFrame. This can be used to produce expanded text from the
4739
	 *     arguments, for instance:
4740
	 *         $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
4741
	 *
4742
	 *     For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
4743
	 *     future versions. Please call $frame->expand() on it anyway so that your code keeps
4744
	 *     working if/when this is changed.
4745
	 *
4746
	 *     If you want whitespace to be trimmed from $args, you need to do it yourself, post-
4747
	 *     expansion.
4748
	 *
4749
	 *     Please read the documentation in includes/parser/Preprocessor.php for more information
4750
	 *     about the methods available in PPFrame and PPNode.
4751
	 *
4752
	 * @throws MWException
4753
	 * @return string|callable The old callback function for this name, if any
4754
	 */
4755
	public function setFunctionHook( $id, $callback, $flags = 0 ) {
4756
		global $wgContLang;
4757
4758
		$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4759
		$this->mFunctionHooks[$id] = [ $callback, $flags ];
4760
4761
		# Add to function cache
4762
		$mw = MagicWord::get( $id );
4763
		if ( !$mw ) {
4764
			throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4765
		}
4766
4767
		$synonyms = $mw->getSynonyms();
4768
		$sensitive = intval( $mw->isCaseSensitive() );
4769
4770
		foreach ( $synonyms as $syn ) {
4771
			# Case
4772
			if ( !$sensitive ) {
4773
				$syn = $wgContLang->lc( $syn );
4774
			}
4775
			# Add leading hash
4776
			if ( !( $flags & self::SFH_NO_HASH ) ) {
4777
				$syn = '#' . $syn;
4778
			}
4779
			# Remove trailing colon
4780
			if ( substr( $syn, -1, 1 ) === ':' ) {
4781
				$syn = substr( $syn, 0, -1 );
4782
			}
4783
			$this->mFunctionSynonyms[$sensitive][$syn] = $id;
4784
		}
4785
		return $oldVal;
4786
	}
4787
4788
	/**
4789
	 * Get all registered function hook identifiers
4790
	 *
4791
	 * @return array
4792
	 */
4793
	public function getFunctionHooks() {
4794
		return array_keys( $this->mFunctionHooks );
4795
	}
4796
4797
	/**
4798
	 * Create a tag function, e.g. "<test>some stuff</test>".
4799
	 * Unlike tag hooks, tag functions are parsed at preprocessor level.
4800
	 * Unlike parser functions, their content is not preprocessed.
4801
	 * @param string $tag
4802
	 * @param callable $callback
4803
	 * @param int $flags
4804
	 * @throws MWException
4805
	 * @return null
4806
	 */
4807 View Code Duplication
	public function setFunctionTagHook( $tag, $callback, $flags ) {
4808
		$tag = strtolower( $tag );
4809
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4810
			throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4811
		}
4812
		$old = isset( $this->mFunctionTagHooks[$tag] ) ?
4813
			$this->mFunctionTagHooks[$tag] : null;
4814
		$this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4815
4816
		if ( !in_array( $tag, $this->mStripList ) ) {
4817
			$this->mStripList[] = $tag;
4818
		}
4819
4820
		return $old;
4821
	}
4822
4823
	/**
4824
	 * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
4825
	 * Placeholders created in Linker::link()
4826
	 *
4827
	 * @param string $text
4828
	 * @param int $options
4829
	 */
4830
	public function replaceLinkHolders( &$text, $options = 0 ) {
4831
		$this->mLinkHolders->replace( $text );
4832
	}
4833
4834
	/**
4835
	 * Replace "<!--LINK-->" link placeholders with plain text of links
4836
	 * (not HTML-formatted).
4837
	 *
4838
	 * @param string $text
4839
	 * @return string
4840
	 */
4841
	public function replaceLinkHoldersText( $text ) {
4842
		return $this->mLinkHolders->replaceText( $text );
4843
	}
4844
4845
	/**
4846
	 * Renders an image gallery from a text with one line per image.
4847
	 * text labels may be given by using |-style alternative text. E.g.
4848
	 *   Image:one.jpg|The number "1"
4849
	 *   Image:tree.jpg|A tree
4850
	 * given as text will return the HTML of a gallery with two images,
4851
	 * labeled 'The number "1"' and
4852
	 * 'A tree'.
4853
	 *
4854
	 * @param string $text
4855
	 * @param array $params
4856
	 * @return string HTML
4857
	 */
4858
	public function renderImageGallery( $text, $params ) {
4859
4860
		$mode = false;
4861
		if ( isset( $params['mode'] ) ) {
4862
			$mode = $params['mode'];
4863
		}
4864
4865
		try {
4866
			$ig = ImageGalleryBase::factory( $mode );
4867
		} catch ( Exception $e ) {
4868
			// If invalid type set, fallback to default.
4869
			$ig = ImageGalleryBase::factory( false );
4870
		}
4871
4872
		$ig->setContextTitle( $this->mTitle );
4873
		$ig->setShowBytes( false );
4874
		$ig->setShowFilename( false );
4875
		$ig->setParser( $this );
4876
		$ig->setHideBadImages();
4877
		$ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
4878
4879
		if ( isset( $params['showfilename'] ) ) {
4880
			$ig->setShowFilename( true );
4881
		} else {
4882
			$ig->setShowFilename( false );
4883
		}
4884
		if ( isset( $params['caption'] ) ) {
4885
			$caption = $params['caption'];
4886
			$caption = htmlspecialchars( $caption );
4887
			$caption = $this->replaceInternalLinks( $caption );
4888
			$ig->setCaptionHtml( $caption );
4889
		}
4890
		if ( isset( $params['perrow'] ) ) {
4891
			$ig->setPerRow( $params['perrow'] );
4892
		}
4893
		if ( isset( $params['widths'] ) ) {
4894
			$ig->setWidths( $params['widths'] );
4895
		}
4896
		if ( isset( $params['heights'] ) ) {
4897
			$ig->setHeights( $params['heights'] );
4898
		}
4899
		$ig->setAdditionalOptions( $params );
4900
4901
		Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] );
4902
4903
		$lines = StringUtils::explode( "\n", $text );
4904
		foreach ( $lines as $line ) {
4905
			# match lines like these:
4906
			# Image:someimage.jpg|This is some image
4907
			$matches = [];
4908
			preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4909
			# Skip empty lines
4910
			if ( count( $matches ) == 0 ) {
4911
				continue;
4912
			}
4913
4914
			if ( strpos( $matches[0], '%' ) !== false ) {
4915
				$matches[1] = rawurldecode( $matches[1] );
4916
			}
4917
			$title = Title::newFromText( $matches[1], NS_FILE );
4918
			if ( is_null( $title ) ) {
4919
				# Bogus title. Ignore these so we don't bomb out later.
4920
				continue;
4921
			}
4922
4923
			# We need to get what handler the file uses, to figure out parameters.
4924
			# Note, a hook can overide the file name, and chose an entirely different
4925
			# file (which potentially could be of a different type and have different handler).
4926
			$options = [];
4927
			$descQuery = false;
4928
			Hooks::run( 'BeforeParserFetchFileAndTitle',
4929
				[ $this, $title, &$options, &$descQuery ] );
4930
			# Don't register it now, as ImageGallery does that later.
4931
			$file = $this->fetchFileNoRegister( $title, $options );
4932
			$handler = $file ? $file->getHandler() : false;
4933
4934
			$paramMap = [
4935
				'img_alt' => 'gallery-internal-alt',
4936
				'img_link' => 'gallery-internal-link',
4937
			];
4938
			if ( $handler ) {
4939
				$paramMap = $paramMap + $handler->getParamMap();
4940
				// We don't want people to specify per-image widths.
4941
				// Additionally the width parameter would need special casing anyhow.
4942
				unset( $paramMap['img_width'] );
4943
			}
4944
4945
			$mwArray = new MagicWordArray( array_keys( $paramMap ) );
4946
4947
			$label = '';
4948
			$alt = '';
4949
			$link = '';
4950
			$handlerOptions = [];
4951
			if ( isset( $matches[3] ) ) {
4952
				// look for an |alt= definition while trying not to break existing
4953
				// captions with multiple pipes (|) in it, until a more sensible grammar
4954
				// is defined for images in galleries
4955
4956
				// FIXME: Doing recursiveTagParse at this stage, and the trim before
4957
				// splitting on '|' is a bit odd, and different from makeImage.
4958
				$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
4959
				$parameterMatches = StringUtils::explode( '|', $matches[3] );
4960
4961
				foreach ( $parameterMatches as $parameterMatch ) {
4962
					list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
4963
					if ( $magicName ) {
4964
						$paramName = $paramMap[$magicName];
4965
4966
						switch ( $paramName ) {
4967
						case 'gallery-internal-alt':
4968
							$alt = $this->stripAltText( $match, false );
4969
							break;
4970
						case 'gallery-internal-link':
4971
							$linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
4972
							$chars = self::EXT_LINK_URL_CLASS;
4973
							$addr = self::EXT_LINK_ADDR;
4974
							$prots = $this->mUrlProtocols;
4975
							// check to see if link matches an absolute url, if not then it must be a wiki link.
4976
							if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
4977
								$link = $linkValue;
4978
							} else {
4979
								$localLinkTitle = Title::newFromText( $linkValue );
4980
								if ( $localLinkTitle !== null ) {
4981
									$link = $localLinkTitle->getLinkURL();
4982
								}
4983
							}
4984
							break;
4985
						default:
4986
							// Must be a handler specific parameter.
4987
							if ( $handler->validateParam( $paramName, $match ) ) {
4988
								$handlerOptions[$paramName] = $match;
4989
							} else {
4990
								// Guess not, consider it as caption.
4991
								wfDebug( "$parameterMatch failed parameter validation\n" );
4992
								$label = '|' . $parameterMatch;
4993
							}
4994
						}
4995
4996
					} else {
4997
						// Last pipe wins.
4998
						$label = '|' . $parameterMatch;
4999
					}
5000
				}
5001
				// Remove the pipe.
5002
				$label = substr( $label, 1 );
5003
			}
5004
5005
			$ig->add( $title, $label, $alt, $link, $handlerOptions );
5006
		}
5007
		$html = $ig->toHTML();
5008
		Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5009
		return $html;
5010
	}
5011
5012
	/**
5013
	 * @param MediaHandler $handler
5014
	 * @return array
5015
	 */
5016
	public function getImageParams( $handler ) {
5017
		if ( $handler ) {
5018
			$handlerClass = get_class( $handler );
5019
		} else {
5020
			$handlerClass = '';
5021
		}
5022
		if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5023
			# Initialise static lists
5024
			static $internalParamNames = [
5025
				'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5026
				'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5027
					'bottom', 'text-bottom' ],
5028
				'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5029
					'upright', 'border', 'link', 'alt', 'class' ],
5030
			];
5031
			static $internalParamMap;
5032
			if ( !$internalParamMap ) {
5033
				$internalParamMap = [];
5034
				foreach ( $internalParamNames as $type => $names ) {
5035
					foreach ( $names as $name ) {
5036
						$magicName = str_replace( '-', '_', "img_$name" );
5037
						$internalParamMap[$magicName] = [ $type, $name ];
5038
					}
5039
				}
5040
			}
5041
5042
			# Add handler params
5043
			$paramMap = $internalParamMap;
5044
			if ( $handler ) {
5045
				$handlerParamMap = $handler->getParamMap();
5046
				foreach ( $handlerParamMap as $magic => $paramName ) {
5047
					$paramMap[$magic] = [ 'handler', $paramName ];
5048
				}
5049
			}
5050
			$this->mImageParams[$handlerClass] = $paramMap;
5051
			$this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5052
		}
5053
		return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5054
	}
5055
5056
	/**
5057
	 * Parse image options text and use it to make an image
5058
	 *
5059
	 * @param Title $title
5060
	 * @param string $options
5061
	 * @param LinkHolderArray|bool $holders
5062
	 * @return string HTML
5063
	 */
5064
	public function makeImage( $title, $options, $holders = false ) {
5065
		# Check if the options text is of the form "options|alt text"
5066
		# Options are:
5067
		#  * thumbnail  make a thumbnail with enlarge-icon and caption, alignment depends on lang
5068
		#  * left       no resizing, just left align. label is used for alt= only
5069
		#  * right      same, but right aligned
5070
		#  * none       same, but not aligned
5071
		#  * ___px      scale to ___ pixels width, no aligning. e.g. use in taxobox
5072
		#  * center     center the image
5073
		#  * frame      Keep original image size, no magnify-button.
5074
		#  * framed     Same as "frame"
5075
		#  * frameless  like 'thumb' but without a frame. Keeps user preferences for width
5076
		#  * upright    reduce width for upright images, rounded to full __0 px
5077
		#  * border     draw a 1px border around the image
5078
		#  * alt        Text for HTML alt attribute (defaults to empty)
5079
		#  * class      Set a class for img node
5080
		#  * link       Set the target of the image link. Can be external, interwiki, or local
5081
		# vertical-align values (no % or length right now):
5082
		#  * baseline
5083
		#  * sub
5084
		#  * super
5085
		#  * top
5086
		#  * text-top
5087
		#  * middle
5088
		#  * bottom
5089
		#  * text-bottom
5090
5091
		$parts = StringUtils::explode( "|", $options );
5092
5093
		# Give extensions a chance to select the file revision for us
5094
		$options = [];
5095
		$descQuery = false;
5096
		Hooks::run( 'BeforeParserFetchFileAndTitle',
5097
			[ $this, $title, &$options, &$descQuery ] );
5098
		# Fetch and register the file (file title may be different via hooks)
5099
		list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5100
5101
		# Get parameter map
5102
		$handler = $file ? $file->getHandler() : false;
5103
5104
		list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5105
5106
		if ( !$file ) {
5107
			$this->addTrackingCategory( 'broken-file-category' );
5108
		}
5109
5110
		# Process the input parameters
5111
		$caption = '';
5112
		$params = [ 'frame' => [], 'handler' => [],
5113
			'horizAlign' => [], 'vertAlign' => [] ];
5114
		$seenformat = false;
5115
		foreach ( $parts as $part ) {
5116
			$part = trim( $part );
5117
			list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5118
			$validated = false;
5119
			if ( isset( $paramMap[$magicName] ) ) {
5120
				list( $type, $paramName ) = $paramMap[$magicName];
5121
5122
				# Special case; width and height come in one variable together
5123
				if ( $type === 'handler' && $paramName === 'width' ) {
5124
					$parsedWidthParam = $this->parseWidthParam( $value );
5125 View Code Duplication
					if ( isset( $parsedWidthParam['width'] ) ) {
5126
						$width = $parsedWidthParam['width'];
5127
						if ( $handler->validateParam( 'width', $width ) ) {
5128
							$params[$type]['width'] = $width;
5129
							$validated = true;
5130
						}
5131
					}
5132 View Code Duplication
					if ( isset( $parsedWidthParam['height'] ) ) {
5133
						$height = $parsedWidthParam['height'];
5134
						if ( $handler->validateParam( 'height', $height ) ) {
5135
							$params[$type]['height'] = $height;
5136
							$validated = true;
5137
						}
5138
					}
5139
					# else no validation -- bug 13436
5140
				} else {
5141
					if ( $type === 'handler' ) {
5142
						# Validate handler parameter
5143
						$validated = $handler->validateParam( $paramName, $value );
5144
					} else {
5145
						# Validate internal parameters
5146
						switch ( $paramName ) {
5147
						case 'manualthumb':
5148
						case 'alt':
5149
						case 'class':
5150
							# @todo FIXME: Possibly check validity here for
5151
							# manualthumb? downstream behavior seems odd with
5152
							# missing manual thumbs.
5153
							$validated = true;
5154
							$value = $this->stripAltText( $value, $holders );
5155
							break;
5156
						case 'link':
5157
							$chars = self::EXT_LINK_URL_CLASS;
5158
							$addr = self::EXT_LINK_ADDR;
5159
							$prots = $this->mUrlProtocols;
5160
							if ( $value === '' ) {
5161
								$paramName = 'no-link';
5162
								$value = true;
5163
								$validated = true;
5164
							} elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5165
								if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5166
									$paramName = 'link-url';
5167
									$this->mOutput->addExternalLink( $value );
5168
									if ( $this->mOptions->getExternalLinkTarget() ) {
5169
										$params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5170
									}
5171
									$validated = true;
5172
								}
5173
							} else {
5174
								$linkTitle = Title::newFromText( $value );
5175
								if ( $linkTitle ) {
5176
									$paramName = 'link-title';
5177
									$value = $linkTitle;
5178
									$this->mOutput->addLink( $linkTitle );
5179
									$validated = true;
5180
								}
5181
							}
5182
							break;
5183
						case 'frameless':
5184
						case 'framed':
5185
						case 'thumbnail':
5186
							// use first appearing option, discard others.
5187
							$validated = ! $seenformat;
5188
							$seenformat = true;
5189
							break;
5190
						default:
5191
							# Most other things appear to be empty or numeric...
5192
							$validated = ( $value === false || is_numeric( trim( $value ) ) );
5193
						}
5194
					}
5195
5196
					if ( $validated ) {
5197
						$params[$type][$paramName] = $value;
5198
					}
5199
				}
5200
			}
5201
			if ( !$validated ) {
5202
				$caption = $part;
5203
			}
5204
		}
5205
5206
		# Process alignment parameters
5207
		if ( $params['horizAlign'] ) {
5208
			$params['frame']['align'] = key( $params['horizAlign'] );
5209
		}
5210
		if ( $params['vertAlign'] ) {
5211
			$params['frame']['valign'] = key( $params['vertAlign'] );
5212
		}
5213
5214
		$params['frame']['caption'] = $caption;
5215
5216
		# Will the image be presented in a frame, with the caption below?
5217
		$imageIsFramed = isset( $params['frame']['frame'] )
5218
			|| isset( $params['frame']['framed'] )
5219
			|| isset( $params['frame']['thumbnail'] )
5220
			|| isset( $params['frame']['manualthumb'] );
5221
5222
		# In the old days, [[Image:Foo|text...]] would set alt text.  Later it
5223
		# came to also set the caption, ordinary text after the image -- which
5224
		# makes no sense, because that just repeats the text multiple times in
5225
		# screen readers.  It *also* came to set the title attribute.
5226
		# Now that we have an alt attribute, we should not set the alt text to
5227
		# equal the caption: that's worse than useless, it just repeats the
5228
		# text.  This is the framed/thumbnail case.  If there's no caption, we
5229
		# use the unnamed parameter for alt text as well, just for the time be-
5230
		# ing, if the unnamed param is set and the alt param is not.
5231
		# For the future, we need to figure out if we want to tweak this more,
5232
		# e.g., introducing a title= parameter for the title; ignoring the un-
5233
		# named parameter entirely for images without a caption; adding an ex-
5234
		# plicit caption= parameter and preserving the old magic unnamed para-
5235
		# meter for BC; ...
5236
		if ( $imageIsFramed ) { # Framed image
5237
			if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5238
				# No caption or alt text, add the filename as the alt text so
5239
				# that screen readers at least get some description of the image
5240
				$params['frame']['alt'] = $title->getText();
5241
			}
5242
			# Do not set $params['frame']['title'] because tooltips don't make sense
5243
			# for framed images
5244
		} else { # Inline image
5245
			if ( !isset( $params['frame']['alt'] ) ) {
5246
				# No alt text, use the "caption" for the alt text
5247
				if ( $caption !== '' ) {
5248
					$params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5249
				} else {
5250
					# No caption, fall back to using the filename for the
5251
					# alt text
5252
					$params['frame']['alt'] = $title->getText();
5253
				}
5254
			}
5255
			# Use the "caption" for the tooltip text
5256
			$params['frame']['title'] = $this->stripAltText( $caption, $holders );
5257
		}
5258
5259
		Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5260
5261
		# Linker does the rest
5262
		$time = isset( $options['time'] ) ? $options['time'] : false;
5263
		$ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5264
			$time, $descQuery, $this->mOptions->getThumbSize() );
5265
5266
		# Give the handler a chance to modify the parser object
5267
		if ( $handler ) {
5268
			$handler->parserTransformHook( $this, $file );
5269
		}
5270
5271
		return $ret;
5272
	}
5273
5274
	/**
5275
	 * @param string $caption
5276
	 * @param LinkHolderArray|bool $holders
5277
	 * @return mixed|string
5278
	 */
5279
	protected function stripAltText( $caption, $holders ) {
5280
		# Strip bad stuff out of the title (tooltip).  We can't just use
5281
		# replaceLinkHoldersText() here, because if this function is called
5282
		# from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5283
		if ( $holders ) {
5284
			$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...
5285
		} else {
5286
			$tooltip = $this->replaceLinkHoldersText( $caption );
5287
		}
5288
5289
		# make sure there are no placeholders in thumbnail attributes
5290
		# that are later expanded to html- so expand them now and
5291
		# remove the tags
5292
		$tooltip = $this->mStripState->unstripBoth( $tooltip );
5293
		$tooltip = Sanitizer::stripAllTags( $tooltip );
5294
5295
		return $tooltip;
5296
	}
5297
5298
	/**
5299
	 * Set a flag in the output object indicating that the content is dynamic and
5300
	 * shouldn't be cached.
5301
	 */
5302
	public function disableCache() {
5303
		wfDebug( "Parser output marked as uncacheable.\n" );
5304
		if ( !$this->mOutput ) {
5305
			throw new MWException( __METHOD__ .
5306
				" can only be called when actually parsing something" );
5307
		}
5308
		$this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5309
	}
5310
5311
	/**
5312
	 * Callback from the Sanitizer for expanding items found in HTML attribute
5313
	 * values, so they can be safely tested and escaped.
5314
	 *
5315
	 * @param string $text
5316
	 * @param bool|PPFrame $frame
5317
	 * @return string
5318
	 */
5319
	public function attributeStripCallback( &$text, $frame = false ) {
5320
		$text = $this->replaceVariables( $text, $frame );
5321
		$text = $this->mStripState->unstripBoth( $text );
5322
		return $text;
5323
	}
5324
5325
	/**
5326
	 * Accessor
5327
	 *
5328
	 * @return array
5329
	 */
5330
	public function getTags() {
5331
		return array_merge(
5332
			array_keys( $this->mTransparentTagHooks ),
5333
			array_keys( $this->mTagHooks ),
5334
			array_keys( $this->mFunctionTagHooks )
5335
		);
5336
	}
5337
5338
	/**
5339
	 * Replace transparent tags in $text with the values given by the callbacks.
5340
	 *
5341
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
5342
	 * operate late in the transformation sequence, on HTML instead of wikitext.
5343
	 *
5344
	 * @param string $text
5345
	 *
5346
	 * @return string
5347
	 */
5348
	public function replaceTransparentTags( $text ) {
5349
		$matches = [];
5350
		$elements = array_keys( $this->mTransparentTagHooks );
5351
		$text = self::extractTagsAndParams( $elements, $text, $matches );
5352
		$replacements = [];
5353
5354
		foreach ( $matches as $marker => $data ) {
5355
			list( $element, $content, $params, $tag ) = $data;
5356
			$tagName = strtolower( $element );
5357
			if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5358
				$output = call_user_func_array(
5359
					$this->mTransparentTagHooks[$tagName],
5360
					[ $content, $params, $this ]
5361
				);
5362
			} else {
5363
				$output = $tag;
5364
			}
5365
			$replacements[$marker] = $output;
5366
		}
5367
		return strtr( $text, $replacements );
5368
	}
5369
5370
	/**
5371
	 * Break wikitext input into sections, and either pull or replace
5372
	 * some particular section's text.
5373
	 *
5374
	 * External callers should use the getSection and replaceSection methods.
5375
	 *
5376
	 * @param string $text Page wikitext
5377
	 * @param string|number $sectionId A section identifier string of the form:
5378
	 *   "<flag1> - <flag2> - ... - <section number>"
5379
	 *
5380
	 * Currently the only recognised flag is "T", which means the target section number
5381
	 * was derived during a template inclusion parse, in other words this is a template
5382
	 * section edit link. If no flags are given, it was an ordinary section edit link.
5383
	 * This flag is required to avoid a section numbering mismatch when a section is
5384
	 * enclosed by "<includeonly>" (bug 6563).
5385
	 *
5386
	 * The section number 0 pulls the text before the first heading; other numbers will
5387
	 * pull the given section along with its lower-level subsections. If the section is
5388
	 * not found, $mode=get will return $newtext, and $mode=replace will return $text.
5389
	 *
5390
	 * Section 0 is always considered to exist, even if it only contains the empty
5391
	 * string. If $text is the empty string and section 0 is replaced, $newText is
5392
	 * returned.
5393
	 *
5394
	 * @param string $mode One of "get" or "replace"
5395
	 * @param string $newText Replacement text for section data.
5396
	 * @return string For "get", the extracted section text.
5397
	 *   for "replace", the whole page with the section replaced.
5398
	 */
5399
	private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5400
		global $wgTitle; # not generally used but removes an ugly failure mode
5401
5402
		$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...
5403
		$this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5404
		$outText = '';
5405
		$frame = $this->getPreprocessor()->newFrame();
5406
5407
		# Process section extraction flags
5408
		$flags = 0;
5409
		$sectionParts = explode( '-', $sectionId );
5410
		$sectionIndex = array_pop( $sectionParts );
5411
		foreach ( $sectionParts as $part ) {
5412
			if ( $part === 'T' ) {
5413
				$flags |= self::PTD_FOR_INCLUSION;
5414
			}
5415
		}
5416
5417
		# Check for empty input
5418
		if ( strval( $text ) === '' ) {
5419
			# Only sections 0 and T-0 exist in an empty document
5420
			if ( $sectionIndex == 0 ) {
5421
				if ( $mode === 'get' ) {
5422
					return '';
5423
				} else {
5424
					return $newText;
5425
				}
5426
			} else {
5427
				if ( $mode === 'get' ) {
5428
					return $newText;
5429
				} else {
5430
					return $text;
5431
				}
5432
			}
5433
		}
5434
5435
		# Preprocess the text
5436
		$root = $this->preprocessToDom( $text, $flags );
5437
5438
		# <h> nodes indicate section breaks
5439
		# They can only occur at the top level, so we can find them by iterating the root's children
5440
		$node = $root->getFirstChild();
5441
5442
		# Find the target section
5443
		if ( $sectionIndex == 0 ) {
5444
			# Section zero doesn't nest, level=big
5445
			$targetLevel = 1000;
5446
		} else {
5447
			while ( $node ) {
5448 View Code Duplication
				if ( $node->getName() === 'h' ) {
5449
					$bits = $node->splitHeading();
5450
					if ( $bits['i'] == $sectionIndex ) {
5451
						$targetLevel = $bits['level'];
5452
						break;
5453
					}
5454
				}
5455
				if ( $mode === 'replace' ) {
5456
					$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5457
				}
5458
				$node = $node->getNextSibling();
5459
			}
5460
		}
5461
5462
		if ( !$node ) {
5463
			# Not found
5464
			if ( $mode === 'get' ) {
5465
				return $newText;
5466
			} else {
5467
				return $text;
5468
			}
5469
		}
5470
5471
		# Find the end of the section, including nested sections
5472
		do {
5473 View Code Duplication
			if ( $node->getName() === 'h' ) {
5474
				$bits = $node->splitHeading();
5475
				$curLevel = $bits['level'];
5476
				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...
5477
					break;
5478
				}
5479
			}
5480
			if ( $mode === 'get' ) {
5481
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5482
			}
5483
			$node = $node->getNextSibling();
5484
		} while ( $node );
5485
5486
		# Write out the remainder (in replace mode only)
5487
		if ( $mode === 'replace' ) {
5488
			# Output the replacement text
5489
			# Add two newlines on -- trailing whitespace in $newText is conventionally
5490
			# stripped by the editor, so we need both newlines to restore the paragraph gap
5491
			# Only add trailing whitespace if there is newText
5492
			if ( $newText != "" ) {
5493
				$outText .= $newText . "\n\n";
5494
			}
5495
5496
			while ( $node ) {
5497
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5498
				$node = $node->getNextSibling();
5499
			}
5500
		}
5501
5502
		if ( is_string( $outText ) ) {
5503
			# Re-insert stripped tags
5504
			$outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5505
		}
5506
5507
		return $outText;
5508
	}
5509
5510
	/**
5511
	 * This function returns the text of a section, specified by a number ($section).
5512
	 * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
5513
	 * the first section before any such heading (section 0).
5514
	 *
5515
	 * If a section contains subsections, these are also returned.
5516
	 *
5517
	 * @param string $text Text to look in
5518
	 * @param string|number $sectionId Section identifier as a number or string
5519
	 * (e.g. 0, 1 or 'T-1').
5520
	 * @param string $defaultText Default to return if section is not found
5521
	 *
5522
	 * @return string Text of the requested section
5523
	 */
5524
	public function getSection( $text, $sectionId, $defaultText = '' ) {
5525
		return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5526
	}
5527
5528
	/**
5529
	 * This function returns $oldtext after the content of the section
5530
	 * specified by $section has been replaced with $text. If the target
5531
	 * section does not exist, $oldtext is returned unchanged.
5532
	 *
5533
	 * @param string $oldText Former text of the article
5534
	 * @param string|number $sectionId Section identifier as a number or string
5535
	 * (e.g. 0, 1 or 'T-1').
5536
	 * @param string $newText Replacing text
5537
	 *
5538
	 * @return string Modified text
5539
	 */
5540
	public function replaceSection( $oldText, $sectionId, $newText ) {
5541
		return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5542
	}
5543
5544
	/**
5545
	 * Get the ID of the revision we are parsing
5546
	 *
5547
	 * @return int|null
5548
	 */
5549
	public function getRevisionId() {
5550
		return $this->mRevisionId;
5551
	}
5552
5553
	/**
5554
	 * Get the revision object for $this->mRevisionId
5555
	 *
5556
	 * @return Revision|null Either a Revision object or null
5557
	 * @since 1.23 (public since 1.23)
5558
	 */
5559
	public function getRevisionObject() {
5560
		if ( !is_null( $this->mRevisionObject ) ) {
5561
			return $this->mRevisionObject;
5562
		}
5563
		if ( is_null( $this->mRevisionId ) ) {
5564
			return null;
5565
		}
5566
5567
		$rev = call_user_func(
5568
			$this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5569
		);
5570
5571
		# If the parse is for a new revision, then the callback should have
5572
		# already been set to force the object and should match mRevisionId.
5573
		# If not, try to fetch by mRevisionId for sanity.
5574
		if ( $rev && $rev->getId() != $this->mRevisionId ) {
5575
			$rev = Revision::newFromId( $this->mRevisionId );
5576
		}
5577
5578
		$this->mRevisionObject = $rev;
5579
5580
		return $this->mRevisionObject;
5581
	}
5582
5583
	/**
5584
	 * Get the timestamp associated with the current revision, adjusted for
5585
	 * the default server-local timestamp
5586
	 * @return string
5587
	 */
5588
	public function getRevisionTimestamp() {
5589
		if ( is_null( $this->mRevisionTimestamp ) ) {
5590
			global $wgContLang;
5591
5592
			$revObject = $this->getRevisionObject();
5593
			$timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5594
5595
			# The cryptic '' timezone parameter tells to use the site-default
5596
			# timezone offset instead of the user settings.
5597
			# Since this value will be saved into the parser cache, served
5598
			# to other users, and potentially even used inside links and such,
5599
			# it needs to be consistent for all visitors.
5600
			$this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5601
5602
		}
5603
		return $this->mRevisionTimestamp;
5604
	}
5605
5606
	/**
5607
	 * Get the name of the user that edited the last revision
5608
	 *
5609
	 * @return string User name
5610
	 */
5611 View Code Duplication
	public function getRevisionUser() {
5612
		if ( is_null( $this->mRevisionUser ) ) {
5613
			$revObject = $this->getRevisionObject();
5614
5615
			# if this template is subst: the revision id will be blank,
5616
			# so just use the current user's name
5617
			if ( $revObject ) {
5618
				$this->mRevisionUser = $revObject->getUserText();
5619
			} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5620
				$this->mRevisionUser = $this->getUser()->getName();
5621
			}
5622
		}
5623
		return $this->mRevisionUser;
5624
	}
5625
5626
	/**
5627
	 * Get the size of the revision
5628
	 *
5629
	 * @return int|null Revision size
5630
	 */
5631 View Code Duplication
	public function getRevisionSize() {
5632
		if ( is_null( $this->mRevisionSize ) ) {
5633
			$revObject = $this->getRevisionObject();
5634
5635
			# if this variable is subst: the revision id will be blank,
5636
			# so just use the parser input size, because the own substituation
5637
			# will change the size.
5638
			if ( $revObject ) {
5639
				$this->mRevisionSize = $revObject->getSize();
5640
			} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5641
				$this->mRevisionSize = $this->mInputSize;
5642
			}
5643
		}
5644
		return $this->mRevisionSize;
5645
	}
5646
5647
	/**
5648
	 * Mutator for $mDefaultSort
5649
	 *
5650
	 * @param string $sort New value
5651
	 */
5652
	public function setDefaultSort( $sort ) {
5653
		$this->mDefaultSort = $sort;
5654
		$this->mOutput->setProperty( 'defaultsort', $sort );
5655
	}
5656
5657
	/**
5658
	 * Accessor for $mDefaultSort
5659
	 * Will use the empty string if none is set.
5660
	 *
5661
	 * This value is treated as a prefix, so the
5662
	 * empty string is equivalent to sorting by
5663
	 * page name.
5664
	 *
5665
	 * @return string
5666
	 */
5667
	public function getDefaultSort() {
5668
		if ( $this->mDefaultSort !== false ) {
5669
			return $this->mDefaultSort;
5670
		} else {
5671
			return '';
5672
		}
5673
	}
5674
5675
	/**
5676
	 * Accessor for $mDefaultSort
5677
	 * Unlike getDefaultSort(), will return false if none is set
5678
	 *
5679
	 * @return string|bool
5680
	 */
5681
	public function getCustomDefaultSort() {
5682
		return $this->mDefaultSort;
5683
	}
5684
5685
	/**
5686
	 * Try to guess the section anchor name based on a wikitext fragment
5687
	 * presumably extracted from a heading, for example "Header" from
5688
	 * "== Header ==".
5689
	 *
5690
	 * @param string $text
5691
	 *
5692
	 * @return string
5693
	 */
5694
	public function guessSectionNameFromWikiText( $text ) {
5695
		# Strip out wikitext links(they break the anchor)
5696
		$text = $this->stripSectionName( $text );
5697
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5698
		return '#' . Sanitizer::escapeId( $text, 'noninitial' );
5699
	}
5700
5701
	/**
5702
	 * Same as guessSectionNameFromWikiText(), but produces legacy anchors
5703
	 * instead.  For use in redirects, since IE6 interprets Redirect: headers
5704
	 * as something other than UTF-8 (apparently?), resulting in breakage.
5705
	 *
5706
	 * @param string $text The section name
5707
	 * @return string An anchor
5708
	 */
5709
	public function guessLegacySectionNameFromWikiText( $text ) {
5710
		# Strip out wikitext links(they break the anchor)
5711
		$text = $this->stripSectionName( $text );
5712
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5713
		return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
5714
	}
5715
5716
	/**
5717
	 * Strips a text string of wikitext for use in a section anchor
5718
	 *
5719
	 * Accepts a text string and then removes all wikitext from the
5720
	 * string and leaves only the resultant text (i.e. the result of
5721
	 * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
5722
	 * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
5723
	 * to create valid section anchors by mimicing the output of the
5724
	 * parser when headings are parsed.
5725
	 *
5726
	 * @param string $text Text string to be stripped of wikitext
5727
	 * for use in a Section anchor
5728
	 * @return string Filtered text string
5729
	 */
5730
	public function stripSectionName( $text ) {
5731
		# Strip internal link markup
5732
		$text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5733
		$text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5734
5735
		# Strip external link markup
5736
		# @todo FIXME: Not tolerant to blank link text
5737
		# I.E. [https://www.mediawiki.org] will render as [1] or something depending
5738
		# on how many empty links there are on the page - need to figure that out.
5739
		$text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5740
5741
		# Parse wikitext quotes (italics & bold)
5742
		$text = $this->doQuotes( $text );
5743
5744
		# Strip HTML tags
5745
		$text = StringUtils::delimiterReplace( '<', '>', '', $text );
5746
		return $text;
5747
	}
5748
5749
	/**
5750
	 * strip/replaceVariables/unstrip for preprocessor regression testing
5751
	 *
5752
	 * @param string $text
5753
	 * @param Title $title
5754
	 * @param ParserOptions $options
5755
	 * @param int $outputType
5756
	 *
5757
	 * @return string
5758
	 */
5759
	public function testSrvus( $text, Title $title, ParserOptions $options,
5760
		$outputType = self::OT_HTML
5761
	) {
5762
		$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...
5763
		$this->startParse( $title, $options, $outputType, true );
5764
5765
		$text = $this->replaceVariables( $text );
5766
		$text = $this->mStripState->unstripBoth( $text );
5767
		$text = Sanitizer::removeHTMLtags( $text );
5768
		return $text;
5769
	}
5770
5771
	/**
5772
	 * @param string $text
5773
	 * @param Title $title
5774
	 * @param ParserOptions $options
5775
	 * @return string
5776
	 */
5777
	public function testPst( $text, Title $title, ParserOptions $options ) {
5778
		return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5779
	}
5780
5781
	/**
5782
	 * @param string $text
5783
	 * @param Title $title
5784
	 * @param ParserOptions $options
5785
	 * @return string
5786
	 */
5787
	public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5788
		return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5789
	}
5790
5791
	/**
5792
	 * Call a callback function on all regions of the given text that are not
5793
	 * inside strip markers, and replace those regions with the return value
5794
	 * of the callback. For example, with input:
5795
	 *
5796
	 *  aaa<MARKER>bbb
5797
	 *
5798
	 * This will call the callback function twice, with 'aaa' and 'bbb'. Those
5799
	 * two strings will be replaced with the value returned by the callback in
5800
	 * each case.
5801
	 *
5802
	 * @param string $s
5803
	 * @param callable $callback
5804
	 *
5805
	 * @return string
5806
	 */
5807
	public function markerSkipCallback( $s, $callback ) {
5808
		$i = 0;
5809
		$out = '';
5810
		while ( $i < strlen( $s ) ) {
5811
			$markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5812
			if ( $markerStart === false ) {
5813
				$out .= call_user_func( $callback, substr( $s, $i ) );
5814
				break;
5815
			} else {
5816
				$out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5817
				$markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5818
				if ( $markerEnd === false ) {
5819
					$out .= substr( $s, $markerStart );
5820
					break;
5821
				} else {
5822
					$markerEnd += strlen( self::MARKER_SUFFIX );
5823
					$out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5824
					$i = $markerEnd;
5825
				}
5826
			}
5827
		}
5828
		return $out;
5829
	}
5830
5831
	/**
5832
	 * Remove any strip markers found in the given text.
5833
	 *
5834
	 * @param string $text Input string
5835
	 * @return string
5836
	 */
5837
	public function killMarkers( $text ) {
5838
		return $this->mStripState->killMarkers( $text );
5839
	}
5840
5841
	/**
5842
	 * Save the parser state required to convert the given half-parsed text to
5843
	 * HTML. "Half-parsed" in this context means the output of
5844
	 * recursiveTagParse() or internalParse(). This output has strip markers
5845
	 * from replaceVariables (extensionSubstitution() etc.), and link
5846
	 * placeholders from replaceLinkHolders().
5847
	 *
5848
	 * Returns an array which can be serialized and stored persistently. This
5849
	 * array can later be loaded into another parser instance with
5850
	 * unserializeHalfParsedText(). The text can then be safely incorporated into
5851
	 * the return value of a parser hook.
5852
	 *
5853
	 * @param string $text
5854
	 *
5855
	 * @return array
5856
	 */
5857
	public function serializeHalfParsedText( $text ) {
5858
		$data = [
5859
			'text' => $text,
5860
			'version' => self::HALF_PARSED_VERSION,
5861
			'stripState' => $this->mStripState->getSubState( $text ),
5862
			'linkHolders' => $this->mLinkHolders->getSubArray( $text )
5863
		];
5864
		return $data;
5865
	}
5866
5867
	/**
5868
	 * Load the parser state given in the $data array, which is assumed to
5869
	 * have been generated by serializeHalfParsedText(). The text contents is
5870
	 * extracted from the array, and its markers are transformed into markers
5871
	 * appropriate for the current Parser instance. This transformed text is
5872
	 * returned, and can be safely included in the return value of a parser
5873
	 * hook.
5874
	 *
5875
	 * If the $data array has been stored persistently, the caller should first
5876
	 * check whether it is still valid, by calling isValidHalfParsedText().
5877
	 *
5878
	 * @param array $data Serialized data
5879
	 * @throws MWException
5880
	 * @return string
5881
	 */
5882
	public function unserializeHalfParsedText( $data ) {
5883 View Code Duplication
		if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
5884
			throw new MWException( __METHOD__ . ': invalid version' );
5885
		}
5886
5887
		# First, extract the strip state.
5888
		$texts = [ $data['text'] ];
5889
		$texts = $this->mStripState->merge( $data['stripState'], $texts );
5890
5891
		# Now renumber links
5892
		$texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
5893
5894
		# Should be good to go.
5895
		return $texts[0];
5896
	}
5897
5898
	/**
5899
	 * Returns true if the given array, presumed to be generated by
5900
	 * serializeHalfParsedText(), is compatible with the current version of the
5901
	 * parser.
5902
	 *
5903
	 * @param array $data
5904
	 *
5905
	 * @return bool
5906
	 */
5907
	public function isValidHalfParsedText( $data ) {
5908
		return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
5909
	}
5910
5911
	/**
5912
	 * Parsed a width param of imagelink like 300px or 200x300px
5913
	 *
5914
	 * @param string $value
5915
	 *
5916
	 * @return array
5917
	 * @since 1.20
5918
	 */
5919
	public function parseWidthParam( $value ) {
5920
		$parsedWidthParam = [];
5921
		if ( $value === '' ) {
5922
			return $parsedWidthParam;
5923
		}
5924
		$m = [];
5925
		# (bug 13500) In both cases (width/height and width only),
5926
		# permit trailing "px" for backward compatibility.
5927
		if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
5928
			$width = intval( $m[1] );
5929
			$height = intval( $m[2] );
5930
			$parsedWidthParam['width'] = $width;
5931
			$parsedWidthParam['height'] = $height;
5932
		} elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
5933
			$width = intval( $value );
5934
			$parsedWidthParam['width'] = $width;
5935
		}
5936
		return $parsedWidthParam;
5937
	}
5938
5939
	/**
5940
	 * Lock the current instance of the parser.
5941
	 *
5942
	 * This is meant to stop someone from calling the parser
5943
	 * recursively and messing up all the strip state.
5944
	 *
5945
	 * @throws MWException If parser is in a parse
5946
	 * @return ScopedCallback The lock will be released once the return value goes out of scope.
5947
	 */
5948
	protected function lock() {
5949
		if ( $this->mInParse ) {
5950
			throw new MWException( "Parser state cleared while parsing. "
5951
				. "Did you call Parser::parse recursively?" );
5952
		}
5953
		$this->mInParse = true;
5954
5955
		$recursiveCheck = new ScopedCallback( function() {
5956
			$this->mInParse = false;
5957
		} );
5958
5959
		return $recursiveCheck;
5960
	}
5961
5962
	/**
5963
	 * Strip outer <p></p> tag from the HTML source of a single paragraph.
5964
	 *
5965
	 * Returns original HTML if the <p/> tag has any attributes, if there's no wrapping <p/> tag,
5966
	 * or if there is more than one <p/> tag in the input HTML.
5967
	 *
5968
	 * @param string $html
5969
	 * @return string
5970
	 * @since 1.24
5971
	 */
5972
	public static function stripOuterParagraph( $html ) {
5973
		$m = [];
5974
		if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
5975
			if ( strpos( $m[1], '</p>' ) === false ) {
5976
				$html = $m[1];
5977
			}
5978
		}
5979
5980
		return $html;
5981
	}
5982
5983
	/**
5984
	 * Return this parser if it is not doing anything, otherwise
5985
	 * get a fresh parser. You can use this method by doing
5986
	 * $myParser = $wgParser->getFreshParser(), or more simply
5987
	 * $wgParser->getFreshParser()->parse( ... );
5988
	 * if you're unsure if $wgParser is safe to use.
5989
	 *
5990
	 * @since 1.24
5991
	 * @return Parser A parser object that is not parsing anything
5992
	 */
5993
	public function getFreshParser() {
5994
		global $wgParserConf;
5995
		if ( $this->mInParse ) {
5996
			return new $wgParserConf['class']( $wgParserConf );
5997
		} else {
5998
			return $this;
5999
		}
6000
	}
6001
6002
	/**
6003
	 * Set's up the PHP implementation of OOUI for use in this request
6004
	 * and instructs OutputPage to enable OOUI for itself.
6005
	 *
6006
	 * @since 1.26
6007
	 */
6008
	public function enableOOUI() {
6009
		OutputPage::setupOOUI();
6010
		$this->mOutput->setEnableOOUI( true );
6011
	}
6012
}
6013