Completed
Branch master (e2eefa)
by
unknown
25:58
created

Parser::doBlockLevels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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