Completed
Branch master (246348)
by
unknown
22:34
created

Parser::getRevisionSize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 15
rs 9.4285
1
<?php
2
/**
3
 * PHP parser that converts wiki markup to HTML.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Parser
22
 */
23
use MediaWiki\Linker\LinkRenderer;
24
use MediaWiki\MediaWikiServices;
25
26
/**
27
 * @defgroup Parser Parser
28
 */
29
30
/**
31
 * PHP Parser - Processes wiki markup (which uses a more user-friendly
32
 * syntax, such as "[[link]]" for making links), and provides a one-way
33
 * transformation of that wiki markup it into (X)HTML output / markup
34
 * (which in turn the browser understands, and can display).
35
 *
36
 * There are seven main entry points into the Parser class:
37
 *
38
 * - Parser::parse()
39
 *     produces HTML output
40
 * - Parser::preSaveTransform()
41
 *     produces altered wiki markup
42
 * - Parser::preprocess()
43
 *     removes HTML comments and expands templates
44
 * - Parser::cleanSig() and Parser::cleanSigInSig()
45
 *     cleans a signature before saving it to preferences
46
 * - Parser::getSection()
47
 *     return the content of a section from an article for section editing
48
 * - Parser::replaceSection()
49
 *     replaces a section by number inside an article
50
 * - Parser::getPreloadText()
51
 *     removes <noinclude> sections and <includeonly> tags
52
 *
53
 * Globals used:
54
 *    object: $wgContLang
55
 *
56
 * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
57
 *
58
 * @par Settings:
59
 * $wgNamespacesWithSubpages
60
 *
61
 * @par Settings only within ParserOptions:
62
 * $wgAllowExternalImages
63
 * $wgAllowSpecialInclusion
64
 * $wgInterwikiMagic
65
 * $wgMaxArticleSize
66
 *
67
 * @ingroup Parser
68
 */
69
class Parser {
70
	/**
71
	 * Update this version number when the ParserOutput format
72
	 * changes in an incompatible way, so the parser cache
73
	 * can automatically discard old data.
74
	 */
75
	const VERSION = '1.6.4';
76
77
	/**
78
	 * Update this version number when the output of serialiseHalfParsedText()
79
	 * changes in an incompatible way
80
	 */
81
	const HALF_PARSED_VERSION = 2;
82
83
	# Flags for Parser::setFunctionHook
84
	const SFH_NO_HASH = 1;
85
	const SFH_OBJECT_ARGS = 2;
86
87
	# Constants needed for external link processing
88
	# Everything except bracket, space, or control characters
89
	# \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
90
	# as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
91
	const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
92
	# Simplified expression to match an IPv4 or IPv6 address, or
93
	# at least one character of a host name (embeds EXT_LINK_URL_CLASS)
94
	const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
95
	# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
96
	// @codingStandardsIgnoreStart Generic.Files.LineLength
97
	const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
98
		\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
99
	// @codingStandardsIgnoreEnd
100
101
	# Regular expression for a non-newline space
102
	const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
103
104
	# Flags for preprocessToDom
105
	const PTD_FOR_INCLUSION = 1;
106
107
	# Allowed values for $this->mOutputType
108
	# Parameter to startExternalParse().
109
	const OT_HTML = 1; # like parse()
110
	const OT_WIKI = 2; # like preSaveTransform()
111
	const OT_PREPROCESS = 3; # like preprocess()
112
	const OT_MSG = 3;
113
	const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
114
115
	/**
116
	 * @var string Prefix and suffix for temporary replacement strings
117
	 * for the multipass parser.
118
	 *
119
	 * \x7f should never appear in input as it's disallowed in XML.
120
	 * Using it at the front also gives us a little extra robustness
121
	 * since it shouldn't match when butted up against identifier-like
122
	 * string constructs.
123
	 *
124
	 * Must not consist of all title characters, or else it will change
125
	 * the behavior of <nowiki> in a link.
126
	 *
127
	 * Must have a character that needs escaping in attributes, otherwise
128
	 * someone could put a strip marker in an attribute, to get around
129
	 * escaping quote marks, and break out of the attribute. Thus we add
130
	 * `'".
131
	 */
132
	const MARKER_SUFFIX = "-QINU`\"'\x7f";
133
	const MARKER_PREFIX = "\x7f'\"`UNIQ-";
134
135
	# Markers used for wrapping the table of contents
136
	const TOC_START = '<mw:toc>';
137
	const TOC_END = '</mw:toc>';
138
139
	# Persistent:
140
	public $mTagHooks = [];
141
	public $mTransparentTagHooks = [];
142
	public $mFunctionHooks = [];
143
	public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
144
	public $mFunctionTagHooks = [];
145
	public $mStripList = [];
146
	public $mDefaultStripList = [];
147
	public $mVarCache = [];
148
	public $mImageParams = [];
149
	public $mImageParamsMagicArray = [];
150
	public $mMarkerIndex = 0;
151
	public $mFirstCall = true;
152
153
	# Initialised by initialiseVariables()
154
155
	/**
156
	 * @var MagicWordArray
157
	 */
158
	public $mVariables;
159
160
	/**
161
	 * @var MagicWordArray
162
	 */
163
	public $mSubstWords;
164
	# Initialised in constructor
165
	public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

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

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

Loading history...
166
167
	# Initialized in getPreprocessor()
168
	/** @var Preprocessor */
169
	public $mPreprocessor;
170
171
	# Cleared with clearState():
172
	/**
173
	 * @var ParserOutput
174
	 */
175
	public $mOutput;
176
	public $mAutonumber;
177
178
	/**
179
	 * @var StripState
180
	 */
181
	public $mStripState;
182
183
	public $mIncludeCount;
184
	/**
185
	 * @var LinkHolderArray
186
	 */
187
	public $mLinkHolders;
188
189
	public $mLinkID;
190
	public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

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

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

Loading history...
191
	public $mDefaultSort;
192
	public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

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

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

Loading history...
193
	public $mExpensiveFunctionCount; # number of expensive parser function calls
194
	public $mShowToc, $mForceTocPosition;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

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

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

Loading history...
195
196
	/**
197
	 * @var User
198
	 */
199
	public $mUser; # User object; only used when doing pre-save transform
200
201
	# Temporary
202
	# These are variables reset at least once per parse regardless of $clearState
203
204
	/**
205
	 * @var ParserOptions
206
	 */
207
	public $mOptions;
208
209
	/**
210
	 * @var Title
211
	 */
212
	public $mTitle;        # Title context, used for self-link rendering and similar things
213
	public $mOutputType;   # Output type, one of the OT_xxx constants
214
	public $ot;            # Shortcut alias, see setOutputType()
215
	public $mRevisionObject; # The revision object of the specified revision ID
216
	public $mRevisionId;   # ID to display in {{REVISIONID}} tags
217
	public $mRevisionTimestamp; # The timestamp of the specified revision ID
218
	public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
219
	public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
220
	public $mRevIdForTs;   # The revision ID which was used to fetch the timestamp
221
	public $mInputSize = false; # For {{PAGESIZE}} on current page.
222
223
	/**
224
	 * @var string Deprecated accessor for the strip marker prefix.
225
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
226
	 **/
227
	public $mUniqPrefix = Parser::MARKER_PREFIX;
228
229
	/**
230
	 * @var array Array with the language name of each language link (i.e. the
231
	 * interwiki prefix) in the key, value arbitrary. Used to avoid sending
232
	 * duplicate language links to the ParserOutput.
233
	 */
234
	public $mLangLinkLanguages;
235
236
	/**
237
	 * @var MapCacheLRU|null
238
	 * @since 1.24
239
	 *
240
	 * A cache of the current revisions of titles. Keys are $title->getPrefixedDbKey()
241
	 */
242
	public $currentRevisionCache;
243
244
	/**
245
	 * @var bool Recursive call protection.
246
	 * This variable should be treated as if it were private.
247
	 */
248
	public $mInParse = false;
249
250
	/** @var SectionProfiler */
251
	protected $mProfiler;
252
253
	/**
254
	 * @var LinkRenderer
255
	 */
256
	protected $mLinkRenderer;
257
258
	/**
259
	 * @param array $conf
260
	 */
261
	public function __construct( $conf = [] ) {
262
		$this->mConf = $conf;
263
		$this->mUrlProtocols = wfUrlProtocols();
264
		$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
265
			self::EXT_LINK_ADDR .
266
			self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
267
		if ( isset( $conf['preprocessorClass'] ) ) {
268
			$this->mPreprocessorClass = $conf['preprocessorClass'];
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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

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

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

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

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

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

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

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

Loading history...
276
		} elseif ( extension_loaded( 'dom' ) ) {
277
			$this->mPreprocessorClass = 'Preprocessor_DOM';
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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

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

Loading history...
278
		} else {
279
			$this->mPreprocessorClass = 'Preprocessor_Hash';
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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

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

Loading history...
280
		}
281
		wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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

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

Loading history...
282
	}
283
284
	/**
285
	 * Reduce memory usage to reduce the impact of circular references
286
	 */
287
	public function __destruct() {
288
		if ( isset( $this->mLinkHolders ) ) {
289
			unset( $this->mLinkHolders );
290
		}
291
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Parser> is not traversable.
Loading history...
292
			unset( $this->$name );
293
		}
294
	}
295
296
	/**
297
	 * Allow extensions to clean up when the parser is cloned
298
	 */
299
	public function __clone() {
300
		$this->mInParse = false;
301
302
		// Bug 56226: When you create a reference "to" an object field, that
303
		// makes the object field itself be a reference too (until the other
304
		// reference goes out of scope). When cloning, any field that's a
305
		// reference is copied as a reference in the new object. Both of these
306
		// are defined PHP5 behaviors, as inconvenient as it is for us when old
307
		// hooks from PHP4 days are passing fields by reference.
308
		foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
309
			// Make a non-reference copy of the field, then rebind the field to
310
			// reference the new copy.
311
			$tmp = $this->$k;
312
			$this->$k =& $tmp;
313
			unset( $tmp );
314
		}
315
316
		Hooks::run( 'ParserCloned', [ $this ] );
317
	}
318
319
	/**
320
	 * Do various kinds of initialisation on the first call of the parser
321
	 */
322
	public function firstCallInit() {
323
		if ( !$this->mFirstCall ) {
324
			return;
325
		}
326
		$this->mFirstCall = false;
327
328
		CoreParserFunctions::register( $this );
329
		CoreTagHooks::register( $this );
330
		$this->initialiseVariables();
331
332
		Hooks::run( 'ParserFirstCallInit', [ &$this ] );
333
	}
334
335
	/**
336
	 * Clear Parser state
337
	 *
338
	 * @private
339
	 */
340
	public function clearState() {
341
		if ( $this->mFirstCall ) {
342
			$this->firstCallInit();
343
		}
344
		$this->mOutput = new ParserOutput;
345
		$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
346
		$this->mAutonumber = 0;
347
		$this->mIncludeCount = [];
348
		$this->mLinkHolders = new LinkHolderArray( $this );
349
		$this->mLinkID = 0;
350
		$this->mRevisionObject = $this->mRevisionTimestamp =
351
			$this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
352
		$this->mVarCache = [];
353
		$this->mUser = null;
354
		$this->mLangLinkLanguages = [];
355
		$this->currentRevisionCache = null;
356
357
		$this->mStripState = new StripState;
358
359
		# Clear these on every parse, bug 4549
360
		$this->mTplRedirCache = $this->mTplDomCache = [];
361
362
		$this->mShowToc = true;
363
		$this->mForceTocPosition = false;
364
		$this->mIncludeSizes = [
365
			'post-expand' => 0,
366
			'arg' => 0,
367
		];
368
		$this->mPPNodeCount = 0;
369
		$this->mGeneratedPPNodeCount = 0;
370
		$this->mHighestExpansionDepth = 0;
371
		$this->mDefaultSort = false;
372
		$this->mHeadings = [];
373
		$this->mDoubleUnderscores = [];
374
		$this->mExpensiveFunctionCount = 0;
375
376
		# Fix cloning
377
		if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist in Preprocessor.

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

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

Loading history...
378
			$this->mPreprocessor = null;
379
		}
380
381
		$this->mProfiler = new SectionProfiler();
382
383
		Hooks::run( 'ParserClearState', [ &$this ] );
384
	}
385
386
	/**
387
	 * Convert wikitext to HTML
388
	 * Do not call this function recursively.
389
	 *
390
	 * @param string $text Text we want to parse
391
	 * @param Title $title
392
	 * @param ParserOptions $options
393
	 * @param bool $linestart
394
	 * @param bool $clearState
395
	 * @param int $revid Number to pass in {{REVISIONID}}
396
	 * @return ParserOutput A ParserOutput
397
	 */
398
	public function parse( $text, Title $title, ParserOptions $options,
399
		$linestart = true, $clearState = true, $revid = null
400
	) {
401
		/**
402
		 * First pass--just handle <nowiki> sections, pass the rest off
403
		 * to internalParse() which does all the real work.
404
		 */
405
406
		global $wgShowHostnames;
407
408
		if ( $clearState ) {
409
			// We use U+007F DELETE to construct strip markers, so we have to make
410
			// sure that this character does not occur in the input text.
411
			$text = strtr( $text, "\x7f", "?" );
412
			$magicScopeVariable = $this->lock();
0 ignored issues
show
Unused Code introduced by
$magicScopeVariable is not used, you could remove the assignment.

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

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

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

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

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

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

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

$answer = 42;

$correct = false;

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

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

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

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

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

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

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

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

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

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

Loading history...
687
		$this->startParse( $title, $options, self::OT_PLAIN, true );
688
689
		$flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
690
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
691
		$text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
692
		$text = $this->mStripState->unstripBoth( $text );
693
		return $text;
694
	}
695
696
	/**
697
	 * Get a random string
698
	 *
699
	 * @return string
700
	 * @deprecated since 1.26; use wfRandomString() instead.
701
	 */
702
	public static function getRandomString() {
703
		wfDeprecated( __METHOD__, '1.26' );
704
		return wfRandomString( 16 );
705
	}
706
707
	/**
708
	 * Set the current user.
709
	 * Should only be used when doing pre-save transform.
710
	 *
711
	 * @param User|null $user User object or null (to reset)
712
	 */
713
	public function setUser( $user ) {
714
		$this->mUser = $user;
715
	}
716
717
	/**
718
	 * Accessor for mUniqPrefix.
719
	 *
720
	 * @return string
721
	 * @deprecated since 1.26; use Parser::MARKER_PREFIX instead.
722
	 */
723
	public function uniqPrefix() {
724
		wfDeprecated( __METHOD__, '1.26' );
725
		return self::MARKER_PREFIX;
726
	}
727
728
	/**
729
	 * Set the context title
730
	 *
731
	 * @param Title $t
732
	 */
733
	public function setTitle( $t ) {
734
		if ( !$t ) {
735
			$t = Title::newFromText( 'NO TITLE' );
736
		}
737
738
		if ( $t->hasFragment() ) {
739
			# Strip the fragment to avoid various odd effects
740
			$this->mTitle = $t->createFragmentTarget( '' );
0 ignored issues
show
Bug introduced by
It seems like $t is not always an object, but can also be of type null. Maybe add an additional type check?

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

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
741
		} else {
742
			$this->mTitle = $t;
743
		}
744
	}
745
746
	/**
747
	 * Accessor for the Title object
748
	 *
749
	 * @return Title
750
	 */
751
	public function getTitle() {
752
		return $this->mTitle;
753
	}
754
755
	/**
756
	 * Accessor/mutator for the Title object
757
	 *
758
	 * @param Title $x Title object or null to just get the current one
759
	 * @return Title
760
	 */
761
	public function Title( $x = null ) {
762
		return wfSetVar( $this->mTitle, $x );
763
	}
764
765
	/**
766
	 * Set the output type
767
	 *
768
	 * @param int $ot New value
769
	 */
770
	public function setOutputType( $ot ) {
771
		$this->mOutputType = $ot;
772
		# Shortcut alias
773
		$this->ot = [
774
			'html' => $ot == self::OT_HTML,
775
			'wiki' => $ot == self::OT_WIKI,
776
			'pre' => $ot == self::OT_PREPROCESS,
777
			'plain' => $ot == self::OT_PLAIN,
778
		];
779
	}
780
781
	/**
782
	 * Accessor/mutator for the output type
783
	 *
784
	 * @param int|null $x New value or null to just get the current one
785
	 * @return int
786
	 */
787
	public function OutputType( $x = null ) {
788
		return wfSetVar( $this->mOutputType, $x );
789
	}
790
791
	/**
792
	 * Get the ParserOutput object
793
	 *
794
	 * @return ParserOutput
795
	 */
796
	public function getOutput() {
797
		return $this->mOutput;
798
	}
799
800
	/**
801
	 * Get the ParserOptions object
802
	 *
803
	 * @return ParserOptions
804
	 */
805
	public function getOptions() {
806
		return $this->mOptions;
807
	}
808
809
	/**
810
	 * Accessor/mutator for the ParserOptions object
811
	 *
812
	 * @param ParserOptions $x New value or null to just get the current one
813
	 * @return ParserOptions Current ParserOptions object
814
	 */
815
	public function Options( $x = null ) {
816
		return wfSetVar( $this->mOptions, $x );
817
	}
818
819
	/**
820
	 * @return int
821
	 */
822
	public function nextLinkID() {
823
		return $this->mLinkID++;
824
	}
825
826
	/**
827
	 * @param int $id
828
	 */
829
	public function setLinkID( $id ) {
830
		$this->mLinkID = $id;
831
	}
832
833
	/**
834
	 * Get a language object for use in parser functions such as {{FORMATNUM:}}
835
	 * @return Language
836
	 */
837
	public function getFunctionLang() {
838
		return $this->getTargetLanguage();
839
	}
840
841
	/**
842
	 * Get the target language for the content being parsed. This is usually the
843
	 * language that the content is in.
844
	 *
845
	 * @since 1.19
846
	 *
847
	 * @throws MWException
848
	 * @return Language
849
	 */
850
	public function getTargetLanguage() {
851
		$target = $this->mOptions->getTargetLanguage();
852
853
		if ( $target !== null ) {
854
			return $target;
855
		} elseif ( $this->mOptions->getInterfaceMessage() ) {
856
			return $this->mOptions->getUserLangObj();
857
		} elseif ( is_null( $this->mTitle ) ) {
858
			throw new MWException( __METHOD__ . ': $this->mTitle is null' );
859
		}
860
861
		return $this->mTitle->getPageLanguage();
862
	}
863
864
	/**
865
	 * Get the language object for language conversion
866
	 * @return Language|null
867
	 */
868
	public function getConverterLanguage() {
869
		return $this->getTargetLanguage();
870
	}
871
872
	/**
873
	 * Get a User object either from $this->mUser, if set, or from the
874
	 * ParserOptions object otherwise
875
	 *
876
	 * @return User
877
	 */
878
	public function getUser() {
879
		if ( !is_null( $this->mUser ) ) {
880
			return $this->mUser;
881
		}
882
		return $this->mOptions->getUser();
883
	}
884
885
	/**
886
	 * Get a preprocessor object
887
	 *
888
	 * @return Preprocessor
889
	 */
890
	public function getPreprocessor() {
891
		if ( !isset( $this->mPreprocessor ) ) {
892
			$class = $this->mPreprocessorClass;
0 ignored issues
show
Bug introduced by
The property mPreprocessorClass does not seem to exist. Did you mean mPreprocessor?

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

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

Loading history...
893
			$this->mPreprocessor = new $class( $this );
894
		}
895
		return $this->mPreprocessor;
896
	}
897
898
	/**
899
	 * Get a LinkRenderer instance to make links with
900
	 *
901
	 * @since 1.28
902
	 * @return LinkRenderer
903
	 */
904
	public function getLinkRenderer() {
905
		if ( !$this->mLinkRenderer ) {
906
			$this->mLinkRenderer = MediaWikiServices::getInstance()
907
				->getLinkRendererFactory()->create();
908
			$this->mLinkRenderer->setStubThreshold(
909
				$this->getOptions()->getStubThreshold()
910
			);
911
		}
912
913
		return $this->mLinkRenderer;
914
	}
915
916
	/**
917
	 * Replaces all occurrences of HTML-style comments and the given tags
918
	 * in the text with a random marker and returns the next text. The output
919
	 * parameter $matches will be an associative array filled with data in
920
	 * the form:
921
	 *
922
	 * @code
923
	 *   'UNIQ-xxxxx' => array(
924
	 *     'element',
925
	 *     'tag content',
926
	 *     array( 'param' => 'x' ),
927
	 *     '<element param="x">tag content</element>' ) )
928
	 * @endcode
929
	 *
930
	 * @param array $elements List of element names. Comments are always extracted.
931
	 * @param string $text Source text string.
932
	 * @param array $matches Out parameter, Array: extracted tags
933
	 * @param string|null $uniq_prefix
934
	 * @return string Stripped text
935
	 * @since 1.26 The uniq_prefix argument is deprecated.
936
	 */
937
	public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
938
		if ( $uniq_prefix !== null ) {
939
			wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
940
		}
941
		static $n = 1;
942
		$stripped = '';
943
		$matches = [];
944
945
		$taglist = implode( '|', $elements );
946
		$start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
947
948
		while ( $text != '' ) {
949
			$p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
950
			$stripped .= $p[0];
951
			if ( count( $p ) < 5 ) {
952
				break;
953
			}
954
			if ( count( $p ) > 5 ) {
955
				# comment
956
				$element = $p[4];
957
				$attributes = '';
958
				$close = '';
959
				$inside = $p[5];
960
			} else {
961
				# tag
962
				$element = $p[1];
963
				$attributes = $p[2];
964
				$close = $p[3];
965
				$inside = $p[4];
966
			}
967
968
			$marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
969
			$stripped .= $marker;
970
971
			if ( $close === '/>' ) {
972
				# Empty element tag, <tag />
973
				$content = null;
974
				$text = $inside;
975
				$tail = null;
976
			} else {
977
				if ( $element === '!--' ) {
978
					$end = '/(-->)/';
979
				} else {
980
					$end = "/(<\\/$element\\s*>)/i";
981
				}
982
				$q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
983
				$content = $q[0];
984
				if ( count( $q ) < 3 ) {
985
					# No end tag -- let it run out to the end of the text.
986
					$tail = '';
987
					$text = '';
988
				} else {
989
					$tail = $q[1];
990
					$text = $q[2];
991
				}
992
			}
993
994
			$matches[$marker] = [ $element,
995
				$content,
996
				Sanitizer::decodeTagAttributes( $attributes ),
997
				"<$element$attributes$close$content$tail" ];
998
		}
999
		return $stripped;
1000
	}
1001
1002
	/**
1003
	 * Get a list of strippable XML-like elements
1004
	 *
1005
	 * @return array
1006
	 */
1007
	public function getStripList() {
1008
		return $this->mStripList;
1009
	}
1010
1011
	/**
1012
	 * Add an item to the strip state
1013
	 * Returns the unique tag which must be inserted into the stripped text
1014
	 * The tag will be replaced with the original text in unstrip()
1015
	 *
1016
	 * @param string $text
1017
	 *
1018
	 * @return string
1019
	 */
1020
	public function insertStripItem( $text ) {
1021
		$marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1022
		$this->mMarkerIndex++;
1023
		$this->mStripState->addGeneral( $marker, $text );
1024
		return $marker;
1025
	}
1026
1027
	/**
1028
	 * parse the wiki syntax used to render tables
1029
	 *
1030
	 * @private
1031
	 * @param string $text
1032
	 * @return string
1033
	 */
1034
	public function doTableStuff( $text ) {
1035
1036
		$lines = StringUtils::explode( "\n", $text );
1037
		$out = '';
1038
		$td_history = []; # Is currently a td tag open?
1039
		$last_tag_history = []; # Save history of last lag activated (td, th or caption)
1040
		$tr_history = []; # Is currently a tr tag open?
1041
		$tr_attributes = []; # history of tr attributes
1042
		$has_opened_tr = []; # Did this table open a <tr> element?
1043
		$indent_level = 0; # indent level of the table
1044
1045
		foreach ( $lines as $outLine ) {
1046
			$line = trim( $outLine );
1047
1048
			if ( $line === '' ) { # empty line, go to next line
1049
				$out .= $outLine . "\n";
1050
				continue;
1051
			}
1052
1053
			$first_character = $line[0];
1054
			$first_two = substr( $line, 0, 2 );
1055
			$matches = [];
1056
1057
			if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1058
				# First check if we are starting a new table
1059
				$indent_level = strlen( $matches[1] );
1060
1061
				$attributes = $this->mStripState->unstripBoth( $matches[2] );
1062
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1063
1064
				$outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1065
				array_push( $td_history, false );
1066
				array_push( $last_tag_history, '' );
1067
				array_push( $tr_history, false );
1068
				array_push( $tr_attributes, '' );
1069
				array_push( $has_opened_tr, false );
1070
			} elseif ( count( $td_history ) == 0 ) {
1071
				# Don't do any of the following
1072
				$out .= $outLine . "\n";
1073
				continue;
1074
			} elseif ( $first_two === '|}' ) {
1075
				# We are ending a table
1076
				$line = '</table>' . substr( $line, 2 );
1077
				$last_tag = array_pop( $last_tag_history );
1078
1079
				if ( !array_pop( $has_opened_tr ) ) {
1080
					$line = "<tr><td></td></tr>{$line}";
1081
				}
1082
1083
				if ( array_pop( $tr_history ) ) {
1084
					$line = "</tr>{$line}";
1085
				}
1086
1087
				if ( array_pop( $td_history ) ) {
1088
					$line = "</{$last_tag}>{$line}";
1089
				}
1090
				array_pop( $tr_attributes );
1091
				$outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1092
			} elseif ( $first_two === '|-' ) {
1093
				# Now we have a table row
1094
				$line = preg_replace( '#^\|-+#', '', $line );
1095
1096
				# Whats after the tag is now only attributes
1097
				$attributes = $this->mStripState->unstripBoth( $line );
1098
				$attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1099
				array_pop( $tr_attributes );
1100
				array_push( $tr_attributes, $attributes );
1101
1102
				$line = '';
1103
				$last_tag = array_pop( $last_tag_history );
1104
				array_pop( $has_opened_tr );
1105
				array_push( $has_opened_tr, true );
1106
1107
				if ( array_pop( $tr_history ) ) {
1108
					$line = '</tr>';
1109
				}
1110
1111
				if ( array_pop( $td_history ) ) {
1112
					$line = "</{$last_tag}>{$line}";
1113
				}
1114
1115
				$outLine = $line;
1116
				array_push( $tr_history, false );
1117
				array_push( $td_history, false );
1118
				array_push( $last_tag_history, '' );
1119
			} elseif ( $first_character === '|'
1120
				|| $first_character === '!'
1121
				|| $first_two === '|+'
1122
			) {
1123
				# This might be cell elements, td, th or captions
1124
				if ( $first_two === '|+' ) {
1125
					$first_character = '+';
1126
					$line = substr( $line, 2 );
1127
				} else {
1128
					$line = substr( $line, 1 );
1129
				}
1130
1131
				// Implies both are valid for table headings.
1132
				if ( $first_character === '!' ) {
1133
					$line = StringUtils::replaceMarkup( '!!', '||', $line );
1134
				}
1135
1136
				# Split up multiple cells on the same line.
1137
				# FIXME : This can result in improper nesting of tags processed
1138
				# by earlier parser steps.
1139
				$cells = explode( '||', $line );
1140
1141
				$outLine = '';
1142
1143
				# Loop through each table cell
1144
				foreach ( $cells as $cell ) {
1145
					$previous = '';
1146
					if ( $first_character !== '+' ) {
1147
						$tr_after = array_pop( $tr_attributes );
1148
						if ( !array_pop( $tr_history ) ) {
1149
							$previous = "<tr{$tr_after}>\n";
1150
						}
1151
						array_push( $tr_history, true );
1152
						array_push( $tr_attributes, '' );
1153
						array_pop( $has_opened_tr );
1154
						array_push( $has_opened_tr, true );
1155
					}
1156
1157
					$last_tag = array_pop( $last_tag_history );
1158
1159
					if ( array_pop( $td_history ) ) {
1160
						$previous = "</{$last_tag}>\n{$previous}";
1161
					}
1162
1163
					if ( $first_character === '|' ) {
1164
						$last_tag = 'td';
1165
					} elseif ( $first_character === '!' ) {
1166
						$last_tag = 'th';
1167
					} elseif ( $first_character === '+' ) {
1168
						$last_tag = 'caption';
1169
					} else {
1170
						$last_tag = '';
1171
					}
1172
1173
					array_push( $last_tag_history, $last_tag );
1174
1175
					# A cell could contain both parameters and data
1176
					$cell_data = explode( '|', $cell, 2 );
1177
1178
					# Bug 553: Note that a '|' inside an invalid link should not
1179
					# be mistaken as delimiting cell parameters
1180
					if ( strpos( $cell_data[0], '[[' ) !== false ) {
1181
						$cell = "{$previous}<{$last_tag}>{$cell}";
1182
					} elseif ( count( $cell_data ) == 1 ) {
1183
						$cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1184
					} else {
1185
						$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1186
						$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1187
						$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1188
					}
1189
1190
					$outLine .= $cell;
1191
					array_push( $td_history, true );
1192
				}
1193
			}
1194
			$out .= $outLine . "\n";
1195
		}
1196
1197
		# Closing open td, tr && table
1198
		while ( count( $td_history ) > 0 ) {
1199
			if ( array_pop( $td_history ) ) {
1200
				$out .= "</td>\n";
1201
			}
1202
			if ( array_pop( $tr_history ) ) {
1203
				$out .= "</tr>\n";
1204
			}
1205
			if ( !array_pop( $has_opened_tr ) ) {
1206
				$out .= "<tr><td></td></tr>\n";
1207
			}
1208
1209
			$out .= "</table>\n";
1210
		}
1211
1212
		# Remove trailing line-ending (b/c)
1213 View Code Duplication
		if ( substr( $out, -1 ) === "\n" ) {
1214
			$out = substr( $out, 0, -1 );
1215
		}
1216
1217
		# special case: don't return empty table
1218
		if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1219
			$out = '';
1220
		}
1221
1222
		return $out;
1223
	}
1224
1225
	/**
1226
	 * Helper function for parse() that transforms wiki markup into half-parsed
1227
	 * HTML. Only called for $mOutputType == self::OT_HTML.
1228
	 *
1229
	 * @private
1230
	 *
1231
	 * @param string $text The text to parse
1232
	 * @param bool $isMain Whether this is being called from the main parse() function
1233
	 * @param PPFrame|bool $frame A pre-processor frame
1234
	 *
1235
	 * @return string
1236
	 */
1237
	public function internalParse( $text, $isMain = true, $frame = false ) {
1238
1239
		$origText = $text;
1240
1241
		# Hook to suspend the parser in this state
1242
		if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) {
1243
			return $text;
1244
		}
1245
1246
		# if $frame is provided, then use $frame for replacing any variables
1247
		if ( $frame ) {
1248
			# use frame depth to infer how include/noinclude tags should be handled
1249
			# depth=0 means this is the top-level document; otherwise it's an included document
1250
			if ( !$frame->depth ) {
1251
				$flag = 0;
1252
			} else {
1253
				$flag = Parser::PTD_FOR_INCLUSION;
1254
			}
1255
			$dom = $this->preprocessToDom( $text, $flag );
1256
			$text = $frame->expand( $dom );
0 ignored issues
show
Bug introduced by
It seems like $frame is not always an object, but can also be of type boolean. Maybe add an additional type check?

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

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1257
		} else {
1258
			# if $frame is not provided, then use old-style replaceVariables
1259
			$text = $this->replaceVariables( $text );
1260
		}
1261
1262
		Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] );
1263
		$text = Sanitizer::removeHTMLtags(
1264
			$text,
1265
			[ &$this, 'attributeStripCallback' ],
1266
			false,
1267
			array_keys( $this->mTransparentTagHooks )
1268
		);
1269
		Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
1270
1271
		# Tables need to come after variable replacement for things to work
1272
		# properly; putting them before other transformations should keep
1273
		# exciting things like link expansions from showing up in surprising
1274
		# places.
1275
		$text = $this->doTableStuff( $text );
1276
1277
		$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1278
1279
		$text = $this->doDoubleUnderscore( $text );
1280
1281
		$text = $this->doHeadings( $text );
1282
		$text = $this->replaceInternalLinks( $text );
1283
		$text = $this->doAllQuotes( $text );
1284
		$text = $this->replaceExternalLinks( $text );
1285
1286
		# replaceInternalLinks may sometimes leave behind
1287
		# absolute URLs, which have to be masked to hide them from replaceExternalLinks
1288
		$text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1289
1290
		$text = $this->doMagicLinks( $text );
1291
		$text = $this->formatHeadings( $text, $origText, $isMain );
1292
1293
		return $text;
1294
	}
1295
1296
	/**
1297
	 * Helper function for parse() that transforms half-parsed HTML into fully
1298
	 * parsed HTML.
1299
	 *
1300
	 * @param string $text
1301
	 * @param bool $isMain
1302
	 * @param bool $linestart
1303
	 * @return string
1304
	 */
1305
	private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1306
		$text = $this->mStripState->unstripGeneral( $text );
1307
1308
		if ( $isMain ) {
1309
			Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
1310
		}
1311
1312
		# Clean up special characters, only run once, next-to-last before doBlockLevels
1313
		$fixtags = [
1314
			# french spaces, last one Guillemet-left
1315
			# only if there is something before the space
1316
			'/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1317
			# french spaces, Guillemet-right
1318
			'/(\\302\\253) /' => '\\1&#160;',
1319
			'/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1320
		];
1321
		$text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1322
1323
		$text = $this->doBlockLevels( $text, $linestart );
1324
1325
		$this->replaceLinkHolders( $text );
1326
1327
		/**
1328
		 * The input doesn't get language converted if
1329
		 * a) It's disabled
1330
		 * b) Content isn't converted
1331
		 * c) It's a conversion table
1332
		 * d) it is an interface message (which is in the user language)
1333
		 */
1334
		if ( !( $this->mOptions->getDisableContentConversion()
1335
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1336
		) {
1337
			if ( !$this->mOptions->getInterfaceMessage() ) {
1338
				# The position of the convert() call should not be changed. it
1339
				# assumes that the links are all replaced and the only thing left
1340
				# is the <nowiki> mark.
1341
				$text = $this->getConverterLanguage()->convert( $text );
1342
			}
1343
		}
1344
1345
		$text = $this->mStripState->unstripNoWiki( $text );
1346
1347
		if ( $isMain ) {
1348
			Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
1349
		}
1350
1351
		$text = $this->replaceTransparentTags( $text );
1352
		$text = $this->mStripState->unstripGeneral( $text );
1353
1354
		$text = Sanitizer::normalizeCharReferences( $text );
1355
1356
		if ( MWTidy::isEnabled() && $this->mOptions->getTidy() ) {
1357
			$text = MWTidy::tidy( $text );
1358
			$this->mOutput->addModuleStyles( MWTidy::getModuleStyles() );
1359
		} else {
1360
			# attempt to sanitize at least some nesting problems
1361
			# (bug #2702 and quite a few others)
1362
			$tidyregs = [
1363
				# ''Something [http://www.cool.com cool''] -->
1364
				# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1365
				'/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1366
				'\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1367
				# fix up an anchor inside another anchor, only
1368
				# at least for a single single nested link (bug 3695)
1369
				'/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1370
				'\\1\\2</a>\\3</a>\\1\\4</a>',
1371
				# fix div inside inline elements- doBlockLevels won't wrap a line which
1372
				# contains a div, so fix it up here; replace
1373
				# div with escaped text
1374
				'/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1375
				'\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1376
				# remove empty italic or bold tag pairs, some
1377
				# introduced by rules above
1378
				'/<([bi])><\/\\1>/' => '',
1379
			];
1380
1381
			$text = preg_replace(
1382
				array_keys( $tidyregs ),
1383
				array_values( $tidyregs ),
1384
				$text );
1385
		}
1386
1387
		if ( $isMain ) {
1388
			Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
1389
		}
1390
1391
		return $text;
1392
	}
1393
1394
	/**
1395
	 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1396
	 * magic external links.
1397
	 *
1398
	 * DML
1399
	 * @private
1400
	 *
1401
	 * @param string $text
1402
	 *
1403
	 * @return string
1404
	 */
1405
	public function doMagicLinks( $text ) {
1406
		$prots = wfUrlProtocolsWithoutProtRel();
1407
		$urlChar = self::EXT_LINK_URL_CLASS;
1408
		$addr = self::EXT_LINK_ADDR;
1409
		$space = self::SPACE_NOT_NL; #  non-newline space
1410
		$spdash = "(?:-|$space)"; # a dash or a non-newline space
1411
		$spaces = "$space++"; # possessive match of 1 or more spaces
1412
		$text = preg_replace_callback(
1413
			'!(?:                            # Start cases
1414
				(<a[ \t\r\n>].*?</a>) |      # m[1]: Skip link text
1415
				(<.*?>) |                    # m[2]: Skip stuff inside
1416
				                             #       HTML elements' . "
1417
				(\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1418
				                             # m[4]: Post-protocol path
1419
				\b(?:RFC|PMID) $spaces       # m[5]: RFC or PMID, capture number
1420
					([0-9]+)\b |
1421
				\bISBN $spaces (             # m[6]: ISBN, capture number
1422
					(?: 97[89] $spdash? )?   #  optional 13-digit ISBN prefix
1423
					(?: [0-9]  $spdash? ){9} #  9 digits with opt. delimiters
1424
					[0-9Xx]                  #  check digit
1425
				)\b
1426
			)!xu", [ &$this, 'magicLinkCallback' ], $text );
1427
		return $text;
1428
	}
1429
1430
	/**
1431
	 * @throws MWException
1432
	 * @param array $m
1433
	 * @return HTML|string
1434
	 */
1435
	public function magicLinkCallback( $m ) {
1436
		if ( isset( $m[1] ) && $m[1] !== '' ) {
1437
			# Skip anchor
1438
			return $m[0];
1439
		} elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1440
			# Skip HTML element
1441
			return $m[0];
1442
		} elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1443
			# Free external link
1444
			return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1445
		} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1446
			# RFC or PMID
1447
			if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1448
				$keyword = 'RFC';
1449
				$urlmsg = 'rfcurl';
1450
				$cssClass = 'mw-magiclink-rfc';
1451
				$id = $m[5];
1452
			} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1453
				$keyword = 'PMID';
1454
				$urlmsg = 'pubmedurl';
1455
				$cssClass = 'mw-magiclink-pmid';
1456
				$id = $m[5];
1457
			} else {
1458
				throw new MWException( __METHOD__ . ': unrecognised match type "' .
1459
					substr( $m[0], 0, 20 ) . '"' );
1460
			}
1461
			$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1462
			return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1463
		} elseif ( isset( $m[6] ) && $m[6] !== '' ) {
1464
			# ISBN
1465
			$isbn = $m[6];
1466
			$space = self::SPACE_NOT_NL; #  non-newline space
1467
			$isbn = preg_replace( "/$space/", ' ', $isbn );
1468
			$num = strtr( $isbn, [
1469
				'-' => '',
1470
				' ' => '',
1471
				'x' => 'X',
1472
			] );
1473
			$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
1474
			return '<a href="' .
1475
				htmlspecialchars( $titleObj->getLocalURL() ) .
1476
				"\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1477
		} else {
1478
			return $m[0];
1479
		}
1480
	}
1481
1482
	/**
1483
	 * Make a free external link, given a user-supplied URL
1484
	 *
1485
	 * @param string $url
1486
	 * @param int $numPostProto
1487
	 *   The number of characters after the protocol.
1488
	 * @return string HTML
1489
	 * @private
1490
	 */
1491
	public function makeFreeExternalLink( $url, $numPostProto ) {
1492
		$trail = '';
1493
1494
		# The characters '<' and '>' (which were escaped by
1495
		# removeHTMLtags()) should not be included in
1496
		# URLs, per RFC 2396.
1497
		# Make &nbsp; terminate a URL as well (bug T84937)
1498
		$m2 = [];
1499 View Code Duplication
		if ( preg_match(
1500
			'/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1501
			$url,
1502
			$m2,
1503
			PREG_OFFSET_CAPTURE
1504
		) ) {
1505
			$trail = substr( $url, $m2[0][1] ) . $trail;
1506
			$url = substr( $url, 0, $m2[0][1] );
1507
		}
1508
1509
		# Move trailing punctuation to $trail
1510
		$sep = ',;\.:!?';
1511
		# If there is no left bracket, then consider right brackets fair game too
1512
		if ( strpos( $url, '(' ) === false ) {
1513
			$sep .= ')';
1514
		}
1515
1516
		$urlRev = strrev( $url );
1517
		$numSepChars = strspn( $urlRev, $sep );
1518
		# Don't break a trailing HTML entity by moving the ; into $trail
1519
		# This is in hot code, so use substr_compare to avoid having to
1520
		# create a new string object for the comparison
1521
		if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1522
			# more optimization: instead of running preg_match with a $
1523
			# anchor, which can be slow, do the match on the reversed
1524
			# string starting at the desired offset.
1525
			# un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1526
			if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1527
				$numSepChars--;
1528
			}
1529
		}
1530
		if ( $numSepChars ) {
1531
			$trail = substr( $url, -$numSepChars ) . $trail;
1532
			$url = substr( $url, 0, -$numSepChars );
1533
		}
1534
1535
		# Verify that we still have a real URL after trail removal, and
1536
		# not just lone protocol
1537
		if ( strlen( $trail ) >= $numPostProto ) {
1538
			return $url . $trail;
1539
		}
1540
1541
		$url = Sanitizer::cleanUrl( $url );
1542
1543
		# Is this an external image?
1544
		$text = $this->maybeMakeExternalImage( $url );
1545
		if ( $text === false ) {
1546
			# Not an image, make a link
1547
			$text = Linker::makeExternalLink( $url,
1548
				$this->getConverterLanguage()->markNoConversion( $url, true ),
1549
				true, 'free',
1550
				$this->getExternalLinkAttribs( $url ), $this->mTitle );
1551
			# Register it in the output object...
1552
			# Replace unnecessary URL escape codes with their equivalent characters
1553
			$pasteurized = self::normalizeLinkUrl( $url );
1554
			$this->mOutput->addExternalLink( $pasteurized );
1555
		}
1556
		return $text . $trail;
1557
	}
1558
1559
	/**
1560
	 * Parse headers and return html
1561
	 *
1562
	 * @private
1563
	 *
1564
	 * @param string $text
1565
	 *
1566
	 * @return string
1567
	 */
1568
	public function doHeadings( $text ) {
1569
		for ( $i = 6; $i >= 1; --$i ) {
1570
			$h = str_repeat( '=', $i );
1571
			$text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1572
		}
1573
		return $text;
1574
	}
1575
1576
	/**
1577
	 * Replace single quotes with HTML markup
1578
	 * @private
1579
	 *
1580
	 * @param string $text
1581
	 *
1582
	 * @return string The altered text
1583
	 */
1584
	public function doAllQuotes( $text ) {
1585
		$outtext = '';
1586
		$lines = StringUtils::explode( "\n", $text );
1587
		foreach ( $lines as $line ) {
1588
			$outtext .= $this->doQuotes( $line ) . "\n";
1589
		}
1590
		$outtext = substr( $outtext, 0, -1 );
1591
		return $outtext;
1592
	}
1593
1594
	/**
1595
	 * Helper function for doAllQuotes()
1596
	 *
1597
	 * @param string $text
1598
	 *
1599
	 * @return string
1600
	 */
1601
	public function doQuotes( $text ) {
1602
		$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1603
		$countarr = count( $arr );
1604
		if ( $countarr == 1 ) {
1605
			return $text;
1606
		}
1607
1608
		// First, do some preliminary work. This may shift some apostrophes from
1609
		// being mark-up to being text. It also counts the number of occurrences
1610
		// of bold and italics mark-ups.
1611
		$numbold = 0;
1612
		$numitalics = 0;
1613
		for ( $i = 1; $i < $countarr; $i += 2 ) {
1614
			$thislen = strlen( $arr[$i] );
1615
			// If there are ever four apostrophes, assume the first is supposed to
1616
			// be text, and the remaining three constitute mark-up for bold text.
1617
			// (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1618
			if ( $thislen == 4 ) {
1619
				$arr[$i - 1] .= "'";
1620
				$arr[$i] = "'''";
1621
				$thislen = 3;
1622
			} elseif ( $thislen > 5 ) {
1623
				// If there are more than 5 apostrophes in a row, assume they're all
1624
				// text except for the last 5.
1625
				// (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1626
				$arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1627
				$arr[$i] = "'''''";
1628
				$thislen = 5;
1629
			}
1630
			// Count the number of occurrences of bold and italics mark-ups.
1631
			if ( $thislen == 2 ) {
1632
				$numitalics++;
1633
			} elseif ( $thislen == 3 ) {
1634
				$numbold++;
1635
			} elseif ( $thislen == 5 ) {
1636
				$numitalics++;
1637
				$numbold++;
1638
			}
1639
		}
1640
1641
		// If there is an odd number of both bold and italics, it is likely
1642
		// that one of the bold ones was meant to be an apostrophe followed
1643
		// by italics. Which one we cannot know for certain, but it is more
1644
		// likely to be one that has a single-letter word before it.
1645
		if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1646
			$firstsingleletterword = -1;
1647
			$firstmultiletterword = -1;
1648
			$firstspace = -1;
1649
			for ( $i = 1; $i < $countarr; $i += 2 ) {
1650
				if ( strlen( $arr[$i] ) == 3 ) {
1651
					$x1 = substr( $arr[$i - 1], -1 );
1652
					$x2 = substr( $arr[$i - 1], -2, 1 );
1653
					if ( $x1 === ' ' ) {
1654
						if ( $firstspace == -1 ) {
1655
							$firstspace = $i;
1656
						}
1657
					} elseif ( $x2 === ' ' ) {
1658
						$firstsingleletterword = $i;
1659
						// if $firstsingleletterword is set, we don't
1660
						// look at the other options, so we can bail early.
1661
						break;
1662
					} else {
1663
						if ( $firstmultiletterword == -1 ) {
1664
							$firstmultiletterword = $i;
1665
						}
1666
					}
1667
				}
1668
			}
1669
1670
			// If there is a single-letter word, use it!
1671
			if ( $firstsingleletterword > -1 ) {
1672
				$arr[$firstsingleletterword] = "''";
1673
				$arr[$firstsingleletterword - 1] .= "'";
1674
			} elseif ( $firstmultiletterword > -1 ) {
1675
				// If not, but there's a multi-letter word, use that one.
1676
				$arr[$firstmultiletterword] = "''";
1677
				$arr[$firstmultiletterword - 1] .= "'";
1678
			} elseif ( $firstspace > -1 ) {
1679
				// ... otherwise use the first one that has neither.
1680
				// (notice that it is possible for all three to be -1 if, for example,
1681
				// there is only one pentuple-apostrophe in the line)
1682
				$arr[$firstspace] = "''";
1683
				$arr[$firstspace - 1] .= "'";
1684
			}
1685
		}
1686
1687
		// Now let's actually convert our apostrophic mush to HTML!
1688
		$output = '';
1689
		$buffer = '';
1690
		$state = '';
1691
		$i = 0;
1692
		foreach ( $arr as $r ) {
1693
			if ( ( $i % 2 ) == 0 ) {
1694
				if ( $state === 'both' ) {
1695
					$buffer .= $r;
1696
				} else {
1697
					$output .= $r;
1698
				}
1699
			} else {
1700
				$thislen = strlen( $r );
1701
				if ( $thislen == 2 ) {
1702 View Code Duplication
					if ( $state === 'i' ) {
1703
						$output .= '</i>';
1704
						$state = '';
1705
					} elseif ( $state === 'bi' ) {
1706
						$output .= '</i>';
1707
						$state = 'b';
1708
					} elseif ( $state === 'ib' ) {
1709
						$output .= '</b></i><b>';
1710
						$state = 'b';
1711
					} elseif ( $state === 'both' ) {
1712
						$output .= '<b><i>' . $buffer . '</i>';
1713
						$state = 'b';
1714
					} else { // $state can be 'b' or ''
1715
						$output .= '<i>';
1716
						$state .= 'i';
1717
					}
1718 View Code Duplication
				} elseif ( $thislen == 3 ) {
1719
					if ( $state === 'b' ) {
1720
						$output .= '</b>';
1721
						$state = '';
1722
					} elseif ( $state === 'bi' ) {
1723
						$output .= '</i></b><i>';
1724
						$state = 'i';
1725
					} elseif ( $state === 'ib' ) {
1726
						$output .= '</b>';
1727
						$state = 'i';
1728
					} elseif ( $state === 'both' ) {
1729
						$output .= '<i><b>' . $buffer . '</b>';
1730
						$state = 'i';
1731
					} else { // $state can be 'i' or ''
1732
						$output .= '<b>';
1733
						$state .= 'b';
1734
					}
1735
				} elseif ( $thislen == 5 ) {
1736
					if ( $state === 'b' ) {
1737
						$output .= '</b><i>';
1738
						$state = 'i';
1739
					} elseif ( $state === 'i' ) {
1740
						$output .= '</i><b>';
1741
						$state = 'b';
1742
					} elseif ( $state === 'bi' ) {
1743
						$output .= '</i></b>';
1744
						$state = '';
1745
					} elseif ( $state === 'ib' ) {
1746
						$output .= '</b></i>';
1747
						$state = '';
1748
					} elseif ( $state === 'both' ) {
1749
						$output .= '<i><b>' . $buffer . '</b></i>';
1750
						$state = '';
1751
					} else { // ($state == '')
1752
						$buffer = '';
1753
						$state = 'both';
1754
					}
1755
				}
1756
			}
1757
			$i++;
1758
		}
1759
		// Now close all remaining tags.  Notice that the order is important.
1760
		if ( $state === 'b' || $state === 'ib' ) {
1761
			$output .= '</b>';
1762
		}
1763
		if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1764
			$output .= '</i>';
1765
		}
1766
		if ( $state === 'bi' ) {
1767
			$output .= '</b>';
1768
		}
1769
		// There might be lonely ''''', so make sure we have a buffer
1770
		if ( $state === 'both' && $buffer ) {
1771
			$output .= '<b><i>' . $buffer . '</i></b>';
1772
		}
1773
		return $output;
1774
	}
1775
1776
	/**
1777
	 * Replace external links (REL)
1778
	 *
1779
	 * Note: this is all very hackish and the order of execution matters a lot.
1780
	 * Make sure to run tests/parserTests.php if you change this code.
1781
	 *
1782
	 * @private
1783
	 *
1784
	 * @param string $text
1785
	 *
1786
	 * @throws MWException
1787
	 * @return string
1788
	 */
1789
	public function replaceExternalLinks( $text ) {
1790
1791
		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1792
		if ( $bits === false ) {
1793
			throw new MWException( "PCRE needs to be compiled with "
1794
				. "--enable-unicode-properties in order for MediaWiki to function" );
1795
		}
1796
		$s = array_shift( $bits );
1797
1798
		$i = 0;
1799
		while ( $i < count( $bits ) ) {
1800
			$url = $bits[$i++];
1801
			$i++; // protocol
1802
			$text = $bits[$i++];
1803
			$trail = $bits[$i++];
1804
1805
			# The characters '<' and '>' (which were escaped by
1806
			# removeHTMLtags()) should not be included in
1807
			# URLs, per RFC 2396.
1808
			$m2 = [];
1809 View Code Duplication
			if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1810
				$text = substr( $url, $m2[0][1] ) . ' ' . $text;
1811
				$url = substr( $url, 0, $m2[0][1] );
1812
			}
1813
1814
			# If the link text is an image URL, replace it with an <img> tag
1815
			# This happened by accident in the original parser, but some people used it extensively
1816
			$img = $this->maybeMakeExternalImage( $text );
1817
			if ( $img !== false ) {
1818
				$text = $img;
1819
			}
1820
1821
			$dtrail = '';
1822
1823
			# Set linktype for CSS - if URL==text, link is essentially free
1824
			$linktype = ( $text === $url ) ? 'free' : 'text';
1825
1826
			# No link text, e.g. [http://domain.tld/some.link]
1827
			if ( $text == '' ) {
1828
				# Autonumber
1829
				$langObj = $this->getTargetLanguage();
1830
				$text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1831
				$linktype = 'autonumber';
1832
			} else {
1833
				# Have link text, e.g. [http://domain.tld/some.link text]s
1834
				# Check for trail
1835
				list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1836
			}
1837
1838
			$text = $this->getConverterLanguage()->markNoConversion( $text );
1839
1840
			$url = Sanitizer::cleanUrl( $url );
1841
1842
			# Use the encoded URL
1843
			# This means that users can paste URLs directly into the text
1844
			# Funny characters like ö aren't valid in URLs anyway
1845
			# This was changed in August 2004
1846
			$s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1847
				$this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1848
1849
			# Register link in the output object.
1850
			# Replace unnecessary URL escape codes with the referenced character
1851
			# This prevents spammers from hiding links from the filters
1852
			$pasteurized = self::normalizeLinkUrl( $url );
1853
			$this->mOutput->addExternalLink( $pasteurized );
1854
		}
1855
1856
		return $s;
1857
	}
1858
1859
	/**
1860
	 * Get the rel attribute for a particular external link.
1861
	 *
1862
	 * @since 1.21
1863
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1864
	 *   nofollow if appropriate
1865
	 * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups
1866
	 * @return string|null Rel attribute for $url
1867
	 */
1868
	public static function getExternalLinkRel( $url = false, $title = null ) {
1869
		global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1870
		$ns = $title ? $title->getNamespace() : false;
1871
		if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1872
			&& !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
0 ignored issues
show
Bug introduced by
It seems like $url defined by parameter $url on line 1868 can also be of type boolean; however, wfMatchesDomainList() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2135
					$prefix = $first_prefix;
0 ignored issues
show
Bug introduced by
The variable $first_prefix does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

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

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

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

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2136
					$first_prefix = false;
2137
				}
2138
			}
2139
2140
			$might_be_img = false;
2141
2142
			if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2143
				$text = $m[2];
2144
				# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2145
				# [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2146
				# the real problem is with the $e1 regex
2147
				# See bug 1300.
2148
				# Still some problems for cases where the ] is meant to be outside punctuation,
2149
				# and no image is in sight. See bug 2095.
2150
				if ( $text !== ''
2151
					&& substr( $m[3], 0, 1 ) === ']'
2152
					&& strpos( $text, '[' ) !== false
2153
				) {
2154
					$text .= ']'; # so that replaceExternalLinks($text) works later
2155
					$m[3] = substr( $m[3], 1 );
2156
				}
2157
				# fix up urlencoded title texts
2158
				if ( strpos( $m[1], '%' ) !== false ) {
2159
					# Should anchors '#' also be rejected?
2160
					$m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2161
				}
2162
				$trail = $m[3];
2163
			} elseif ( preg_match( $e1_img, $line, $m ) ) {
2164
				# Invalid, but might be an image with a link in its caption
2165
				$might_be_img = true;
2166
				$text = $m[2];
2167
				if ( strpos( $m[1], '%' ) !== false ) {
2168
					$m[1] = rawurldecode( $m[1] );
2169
				}
2170
				$trail = "";
2171
			} else { # Invalid form; output directly
2172
				$s .= $prefix . '[[' . $line;
0 ignored issues
show
Bug introduced by
The variable $prefix does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

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

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

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

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2173
				continue;
2174
			}
2175
2176
			$origLink = $m[1];
2177
2178
			# Don't allow internal links to pages containing
2179
			# PROTO: where PROTO is a valid URL protocol; these
2180
			# should be external links.
2181
			if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2182
				$s .= $prefix . '[[' . $line;
2183
				continue;
2184
			}
2185
2186
			# Make subpage if necessary
2187
			if ( $useSubpages ) {
2188
				$link = $this->maybeDoSubpageLink( $origLink, $text );
2189
			} else {
2190
				$link = $origLink;
2191
			}
2192
2193
			$noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2194
			if ( !$noforce ) {
2195
				# Strip off leading ':'
2196
				$link = substr( $link, 1 );
2197
			}
2198
2199
			$unstrip = $this->mStripState->unstripNoWiki( $link );
2200
			$nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2201
			if ( $nt === null ) {
2202
				$s .= $prefix . '[[' . $line;
2203
				continue;
2204
			}
2205
2206
			$ns = $nt->getNamespace();
2207
			$iw = $nt->getInterwiki();
2208
2209
			if ( $might_be_img ) { # if this is actually an invalid link
2210
				if ( $ns == NS_FILE && $noforce ) { # but might be an image
2211
					$found = false;
2212
					while ( true ) {
2213
						# look at the next 'line' to see if we can close it there
2214
						$a->next();
2215
						$next_line = $a->current();
2216
						if ( $next_line === false || $next_line === null ) {
2217
							break;
2218
						}
2219
						$m = explode( ']]', $next_line, 3 );
2220
						if ( count( $m ) == 3 ) {
2221
							# the first ]] closes the inner link, the second the image
2222
							$found = true;
2223
							$text .= "[[{$m[0]}]]{$m[1]}";
2224
							$trail = $m[2];
2225
							break;
2226
						} elseif ( count( $m ) == 2 ) {
2227
							# if there's exactly one ]] that's fine, we'll keep looking
2228
							$text .= "[[{$m[0]}]]{$m[1]}";
2229
						} else {
2230
							# if $next_line is invalid too, we need look no further
2231
							$text .= '[[' . $next_line;
2232
							break;
2233
						}
2234
					}
2235
					if ( !$found ) {
2236
						# we couldn't find the end of this imageLink, so output it raw
2237
						# but don't ignore what might be perfectly normal links in the text we've examined
2238
						$holders->merge( $this->replaceInternalLinks2( $text ) );
2239
						$s .= "{$prefix}[[$link|$text";
2240
						# note: no $trail, because without an end, there *is* no trail
2241
						continue;
2242
					}
2243
				} else { # it's not an image, so output it raw
2244
					$s .= "{$prefix}[[$link|$text";
2245
					# note: no $trail, because without an end, there *is* no trail
2246
					continue;
2247
				}
2248
			}
2249
2250
			$wasblank = ( $text == '' );
2251
			if ( $wasblank ) {
2252
				$text = $link;
2253
			} else {
2254
				# Bug 4598 madness. Handle the quotes only if they come from the alternate part
2255
				# [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2256
				# [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2257
				#    -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2258
				$text = $this->doQuotes( $text );
2259
			}
2260
2261
			# Link not escaped by : , create the various objects
2262
			if ( $noforce && !$nt->wasLocalInterwiki() ) {
2263
				# Interwikis
2264
				if (
2265
					$iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2266
						Language::fetchLanguageName( $iw, null, 'mw' ) ||
2267
						in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2268
					)
2269
				) {
2270
					# Bug 24502: filter duplicates
2271
					if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2272
						$this->mLangLinkLanguages[$iw] = true;
2273
						$this->mOutput->addLanguageLink( $nt->getFullText() );
2274
					}
2275
2276
					$s = rtrim( $s . $prefix );
2277
					$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2278
					continue;
2279
				}
2280
2281
				if ( $ns == NS_FILE ) {
2282
					if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2283
						if ( $wasblank ) {
2284
							# if no parameters were passed, $text
2285
							# becomes something like "File:Foo.png",
2286
							# which we don't want to pass on to the
2287
							# image generator
2288
							$text = '';
2289
						} else {
2290
							# recursively parse links inside the image caption
2291
							# actually, this will parse them in any other parameters, too,
2292
							# but it might be hard to fix that, and it doesn't matter ATM
2293
							$text = $this->replaceExternalLinks( $text );
2294
							$holders->merge( $this->replaceInternalLinks2( $text ) );
2295
						}
2296
						# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2297
						$s .= $prefix . $this->armorLinks(
2298
							$this->makeImage( $nt, $text, $holders ) ) . $trail;
2299
					} else {
2300
						$s .= $prefix . $trail;
2301
					}
2302
					continue;
2303
				}
2304
2305
				if ( $ns == NS_CATEGORY ) {
2306
					$s = rtrim( $s . "\n" ); # bug 87
2307
2308
					if ( $wasblank ) {
2309
						$sortkey = $this->getDefaultSort();
2310
					} else {
2311
						$sortkey = $text;
2312
					}
2313
					$sortkey = Sanitizer::decodeCharReferences( $sortkey );
2314
					$sortkey = str_replace( "\n", '', $sortkey );
2315
					$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2316
					$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2317
2318
					/**
2319
					 * Strip the whitespace Category links produce, see bug 87
2320
					 */
2321
					$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2322
2323
					continue;
2324
				}
2325
			}
2326
2327
			# Self-link checking. For some languages, variants of the title are checked in
2328
			# LinkHolderArray::doVariants() to allow batching the existence checks necessary
2329
			# for linking to a different variant.
2330
			if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2331
				$s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2332
				continue;
2333
			}
2334
2335
			# NS_MEDIA is a pseudo-namespace for linking directly to a file
2336
			# @todo FIXME: Should do batch file existence checks, see comment below
2337
			if ( $ns == NS_MEDIA ) {
2338
				# Give extensions a chance to select the file revision for us
2339
				$options = [];
2340
				$descQuery = false;
2341
				Hooks::run( 'BeforeParserFetchFileAndTitle',
2342
					[ $this, $nt, &$options, &$descQuery ] );
2343
				# Fetch and register the file (file title may be different via hooks)
2344
				list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2345
				# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2346
				$s .= $prefix . $this->armorLinks(
2347
					Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2348
				continue;
2349
			}
2350
2351
			# Some titles, such as valid special pages or files in foreign repos, should
2352
			# be shown as bluelinks even though they're not included in the page table
2353
			# @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2354
			# batch file existence checks for NS_FILE and NS_MEDIA
2355
			if ( $iw == '' && $nt->isAlwaysKnown() ) {
2356
				$this->mOutput->addLink( $nt );
2357
				$s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2358
			} else {
2359
				# Links will be added to the output link list after checking
2360
				$s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2361
			}
2362
		}
2363
		return $holders;
2364
	}
2365
2366
	/**
2367
	 * Render a forced-blue link inline; protect against double expansion of
2368
	 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2369
	 * Since this little disaster has to split off the trail text to avoid
2370
	 * breaking URLs in the following text without breaking trails on the
2371
	 * wiki links, it's been made into a horrible function.
2372
	 *
2373
	 * @param Title $nt
2374
	 * @param string $text
2375
	 * @param string $trail
2376
	 * @param string $prefix
2377
	 * @return string HTML-wikitext mix oh yuck
2378
	 */
2379
	protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2380
		list( $inside, $trail ) = Linker::splitTrail( $trail );
2381
2382
		if ( $text == '' ) {
2383
			$text = htmlspecialchars( $nt->getPrefixedText() );
2384
		}
2385
2386
		$link = $this->getLinkRenderer()->makeKnownLink(
2387
			$nt, new HtmlArmor( "$prefix$text$inside" )
2388
		);
2389
2390
		return $this->armorLinks( $link ) . $trail;
2391
	}
2392
2393
	/**
2394
	 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2395
	 * going to go through further parsing steps before inline URL expansion.
2396
	 *
2397
	 * Not needed quite as much as it used to be since free links are a bit
2398
	 * more sensible these days. But bracketed links are still an issue.
2399
	 *
2400
	 * @param string $text More-or-less HTML
2401
	 * @return string Less-or-more HTML with NOPARSE bits
2402
	 */
2403
	public function armorLinks( $text ) {
2404
		return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2405
			self::MARKER_PREFIX . "NOPARSE$1", $text );
2406
	}
2407
2408
	/**
2409
	 * Return true if subpage links should be expanded on this page.
2410
	 * @return bool
2411
	 */
2412
	public function areSubpagesAllowed() {
2413
		# Some namespaces don't allow subpages
2414
		return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2415
	}
2416
2417
	/**
2418
	 * Handle link to subpage if necessary
2419
	 *
2420
	 * @param string $target The source of the link
2421
	 * @param string &$text The link text, modified as necessary
2422
	 * @return string The full name of the link
2423
	 * @private
2424
	 */
2425
	public function maybeDoSubpageLink( $target, &$text ) {
2426
		return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2427
	}
2428
2429
	/**
2430
	 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
2431
	 *
2432
	 * @param string $text
2433
	 * @param bool $linestart Whether or not this is at the start of a line.
2434
	 * @private
2435
	 * @return string The lists rendered as HTML
2436
	 */
2437
	public function doBlockLevels( $text, $linestart ) {
2438
		return BlockLevelPass::doBlockLevels( $text, $linestart );
2439
	}
2440
2441
	/**
2442
	 * Return value of a magic variable (like PAGENAME)
2443
	 *
2444
	 * @private
2445
	 *
2446
	 * @param int $index
2447
	 * @param bool|PPFrame $frame
2448
	 *
2449
	 * @throws MWException
2450
	 * @return string
2451
	 */
2452
	public function getVariableValue( $index, $frame = false ) {
2453
		global $wgContLang, $wgSitename, $wgServer, $wgServerName;
2454
		global $wgArticlePath, $wgScriptPath, $wgStylePath;
2455
2456
		if ( is_null( $this->mTitle ) ) {
2457
			// If no title set, bad things are going to happen
2458
			// later. Title should always be set since this
2459
			// should only be called in the middle of a parse
2460
			// operation (but the unit-tests do funky stuff)
2461
			throw new MWException( __METHOD__ . ' Should only be '
2462
				. ' called while parsing (no title set)' );
2463
		}
2464
2465
		/**
2466
		 * Some of these require message or data lookups and can be
2467
		 * expensive to check many times.
2468
		 */
2469
		if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
2470
			if ( isset( $this->mVarCache[$index] ) ) {
2471
				return $this->mVarCache[$index];
2472
			}
2473
		}
2474
2475
		$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2476
		Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
2477
2478
		$pageLang = $this->getFunctionLang();
2479
2480
		switch ( $index ) {
2481
			case '!':
2482
				$value = '|';
2483
				break;
2484
			case 'currentmonth':
2485
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2486
				break;
2487
			case 'currentmonth1':
2488
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2489
				break;
2490
			case 'currentmonthname':
2491
				$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2492
				break;
2493
			case 'currentmonthnamegen':
2494
				$value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2495
				break;
2496
			case 'currentmonthabbrev':
2497
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2498
				break;
2499
			case 'currentday':
2500
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2501
				break;
2502
			case 'currentday2':
2503
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2504
				break;
2505
			case 'localmonth':
2506
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2507
				break;
2508
			case 'localmonth1':
2509
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2510
				break;
2511
			case 'localmonthname':
2512
				$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2513
				break;
2514
			case 'localmonthnamegen':
2515
				$value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2516
				break;
2517
			case 'localmonthabbrev':
2518
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2519
				break;
2520
			case 'localday':
2521
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2522
				break;
2523
			case 'localday2':
2524
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2525
				break;
2526
			case 'pagename':
2527
				$value = wfEscapeWikiText( $this->mTitle->getText() );
2528
				break;
2529
			case 'pagenamee':
2530
				$value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2531
				break;
2532
			case 'fullpagename':
2533
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2534
				break;
2535
			case 'fullpagenamee':
2536
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2537
				break;
2538
			case 'subpagename':
2539
				$value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2540
				break;
2541
			case 'subpagenamee':
2542
				$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2543
				break;
2544
			case 'rootpagename':
2545
				$value = wfEscapeWikiText( $this->mTitle->getRootText() );
2546
				break;
2547 View Code Duplication
			case 'rootpagenamee':
2548
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2549
					' ',
2550
					'_',
2551
					$this->mTitle->getRootText()
2552
				) ) );
2553
				break;
2554
			case 'basepagename':
2555
				$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2556
				break;
2557 View Code Duplication
			case 'basepagenamee':
2558
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2559
					' ',
2560
					'_',
2561
					$this->mTitle->getBaseText()
2562
				) ) );
2563
				break;
2564 View Code Duplication
			case 'talkpagename':
2565
				if ( $this->mTitle->canTalk() ) {
2566
					$talkPage = $this->mTitle->getTalkPage();
2567
					$value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2568
				} else {
2569
					$value = '';
2570
				}
2571
				break;
2572 View Code Duplication
			case 'talkpagenamee':
2573
				if ( $this->mTitle->canTalk() ) {
2574
					$talkPage = $this->mTitle->getTalkPage();
2575
					$value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2576
				} else {
2577
					$value = '';
2578
				}
2579
				break;
2580
			case 'subjectpagename':
2581
				$subjPage = $this->mTitle->getSubjectPage();
2582
				$value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2583
				break;
2584
			case 'subjectpagenamee':
2585
				$subjPage = $this->mTitle->getSubjectPage();
2586
				$value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2587
				break;
2588
			case 'pageid': // requested in bug 23427
2589
				$pageid = $this->getTitle()->getArticleID();
2590
				if ( $pageid == 0 ) {
2591
					# 0 means the page doesn't exist in the database,
2592
					# which means the user is previewing a new page.
2593
					# The vary-revision flag must be set, because the magic word
2594
					# will have a different value once the page is saved.
2595
					$this->mOutput->setFlag( 'vary-revision' );
2596
					wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2597
				}
2598
				$value = $pageid ? $pageid : null;
2599
				break;
2600
			case 'revisionid':
2601
				# Let the edit saving system know we should parse the page
2602
				# *after* a revision ID has been assigned.
2603
				$this->mOutput->setFlag( 'vary-revision' );
2604
				wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
2605
				$value = $this->mRevisionId;
2606
				break;
2607 View Code Duplication
			case 'revisionday':
2608
				# Let the edit saving system know we should parse the page
2609
				# *after* a revision ID has been assigned. This is for null edits.
2610
				$this->mOutput->setFlag( 'vary-revision' );
2611
				wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2612
				$value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2613
				break;
2614 View Code Duplication
			case 'revisionday2':
2615
				# Let the edit saving system know we should parse the page
2616
				# *after* a revision ID has been assigned. This is for null edits.
2617
				$this->mOutput->setFlag( 'vary-revision' );
2618
				wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2619
				$value = substr( $this->getRevisionTimestamp(), 6, 2 );
2620
				break;
2621 View Code Duplication
			case 'revisionmonth':
2622
				# Let the edit saving system know we should parse the page
2623
				# *after* a revision ID has been assigned. This is for null edits.
2624
				$this->mOutput->setFlag( 'vary-revision' );
2625
				wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2626
				$value = substr( $this->getRevisionTimestamp(), 4, 2 );
2627
				break;
2628 View Code Duplication
			case 'revisionmonth1':
2629
				# Let the edit saving system know we should parse the page
2630
				# *after* a revision ID has been assigned. This is for null edits.
2631
				$this->mOutput->setFlag( 'vary-revision' );
2632
				wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2633
				$value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2634
				break;
2635 View Code Duplication
			case 'revisionyear':
2636
				# Let the edit saving system know we should parse the page
2637
				# *after* a revision ID has been assigned. This is for null edits.
2638
				$this->mOutput->setFlag( 'vary-revision' );
2639
				wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2640
				$value = substr( $this->getRevisionTimestamp(), 0, 4 );
2641
				break;
2642
			case 'revisiontimestamp':
2643
				# Let the edit saving system know we should parse the page
2644
				# *after* a revision ID has been assigned. This is for null edits.
2645
				$this->mOutput->setFlag( 'vary-revision' );
2646
				wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2647
				$value = $this->getRevisionTimestamp();
2648
				break;
2649
			case 'revisionuser':
2650
				# Let the edit saving system know we should parse the page
2651
				# *after* a revision ID has been assigned for null edits.
2652
				$this->mOutput->setFlag( 'vary-user' );
2653
				wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2654
				$value = $this->getRevisionUser();
2655
				break;
2656
			case 'revisionsize':
2657
				$value = $this->getRevisionSize();
2658
				break;
2659
			case 'namespace':
2660
				$value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2661
				break;
2662
			case 'namespacee':
2663
				$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2664
				break;
2665
			case 'namespacenumber':
2666
				$value = $this->mTitle->getNamespace();
2667
				break;
2668
			case 'talkspace':
2669
				$value = $this->mTitle->canTalk()
2670
					? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2671
					: '';
2672
				break;
2673
			case 'talkspacee':
2674
				$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2675
				break;
2676
			case 'subjectspace':
2677
				$value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2678
				break;
2679
			case 'subjectspacee':
2680
				$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2681
				break;
2682
			case 'currentdayname':
2683
				$value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2684
				break;
2685
			case 'currentyear':
2686
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2687
				break;
2688
			case 'currenttime':
2689
				$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...
2690
				break;
2691
			case 'currenthour':
2692
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2693
				break;
2694
			case 'currentweek':
2695
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2696
				# int to remove the padding
2697
				$value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2698
				break;
2699
			case 'currentdow':
2700
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2701
				break;
2702
			case 'localdayname':
2703
				$value = $pageLang->getWeekdayName(
2704
					(int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2705
				);
2706
				break;
2707
			case 'localyear':
2708
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2709
				break;
2710
			case 'localtime':
2711
				$value = $pageLang->time(
2712
					MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2713
					false,
2714
					false
2715
				);
2716
				break;
2717
			case 'localhour':
2718
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2719
				break;
2720
			case 'localweek':
2721
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2722
				# int to remove the padding
2723
				$value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2724
				break;
2725
			case 'localdow':
2726
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2727
				break;
2728
			case 'numberofarticles':
2729
				$value = $pageLang->formatNum( SiteStats::articles() );
2730
				break;
2731
			case 'numberoffiles':
2732
				$value = $pageLang->formatNum( SiteStats::images() );
2733
				break;
2734
			case 'numberofusers':
2735
				$value = $pageLang->formatNum( SiteStats::users() );
2736
				break;
2737
			case 'numberofactiveusers':
2738
				$value = $pageLang->formatNum( SiteStats::activeUsers() );
2739
				break;
2740
			case 'numberofpages':
2741
				$value = $pageLang->formatNum( SiteStats::pages() );
2742
				break;
2743
			case 'numberofadmins':
2744
				$value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2745
				break;
2746
			case 'numberofedits':
2747
				$value = $pageLang->formatNum( SiteStats::edits() );
2748
				break;
2749
			case 'currenttimestamp':
2750
				$value = wfTimestamp( TS_MW, $ts );
2751
				break;
2752
			case 'localtimestamp':
2753
				$value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2754
				break;
2755
			case 'currentversion':
2756
				$value = SpecialVersion::getVersion();
2757
				break;
2758
			case 'articlepath':
2759
				return $wgArticlePath;
2760
			case 'sitename':
2761
				return $wgSitename;
2762
			case 'server':
2763
				return $wgServer;
2764
			case 'servername':
2765
				return $wgServerName;
2766
			case 'scriptpath':
2767
				return $wgScriptPath;
2768
			case 'stylepath':
2769
				return $wgStylePath;
2770
			case 'directionmark':
2771
				return $pageLang->getDirMark();
2772
			case 'contentlanguage':
2773
				global $wgLanguageCode;
2774
				return $wgLanguageCode;
2775
			case 'cascadingsources':
2776
				$value = CoreParserFunctions::cascadingsources( $this );
2777
				break;
2778
			default:
2779
				$ret = null;
2780
				Hooks::run(
2781
					'ParserGetVariableValueSwitch',
2782
					[ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
2783
				);
2784
2785
				return $ret;
2786
		}
2787
2788
		if ( $index ) {
2789
			$this->mVarCache[$index] = $value;
2790
		}
2791
2792
		return $value;
2793
	}
2794
2795
	/**
2796
	 * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
2797
	 *
2798
	 * @private
2799
	 */
2800
	public function initialiseVariables() {
2801
		$variableIDs = MagicWord::getVariableIDs();
2802
		$substIDs = MagicWord::getSubstIDs();
2803
2804
		$this->mVariables = new MagicWordArray( $variableIDs );
2805
		$this->mSubstWords = new MagicWordArray( $substIDs );
2806
	}
2807
2808
	/**
2809
	 * Preprocess some wikitext and return the document tree.
2810
	 * This is the ghost of replace_variables().
2811
	 *
2812
	 * @param string $text The text to parse
2813
	 * @param int $flags Bitwise combination of:
2814
	 *   - self::PTD_FOR_INCLUSION: Handle "<noinclude>" and "<includeonly>" as if the text is being
2815
	 *     included. Default is to assume a direct page view.
2816
	 *
2817
	 * The generated DOM tree must depend only on the input text and the flags.
2818
	 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
2819
	 *
2820
	 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
2821
	 * change in the DOM tree for a given text, must be passed through the section identifier
2822
	 * in the section edit link and thus back to extractSections().
2823
	 *
2824
	 * The output of this function is currently only cached in process memory, but a persistent
2825
	 * cache may be implemented at a later date which takes further advantage of these strict
2826
	 * dependency requirements.
2827
	 *
2828
	 * @return PPNode
2829
	 */
2830
	public function preprocessToDom( $text, $flags = 0 ) {
2831
		$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2832
		return $dom;
2833
	}
2834
2835
	/**
2836
	 * Return a three-element array: leading whitespace, string contents, trailing whitespace
2837
	 *
2838
	 * @param string $s
2839
	 *
2840
	 * @return array
2841
	 */
2842
	public static function splitWhitespace( $s ) {
2843
		$ltrimmed = ltrim( $s );
2844
		$w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2845
		$trimmed = rtrim( $ltrimmed );
2846
		$diff = strlen( $ltrimmed ) - strlen( $trimmed );
2847
		if ( $diff > 0 ) {
2848
			$w2 = substr( $ltrimmed, -$diff );
2849
		} else {
2850
			$w2 = '';
2851
		}
2852
		return [ $w1, $trimmed, $w2 ];
2853
	}
2854
2855
	/**
2856
	 * Replace magic variables, templates, and template arguments
2857
	 * with the appropriate text. Templates are substituted recursively,
2858
	 * taking care to avoid infinite loops.
2859
	 *
2860
	 * Note that the substitution depends on value of $mOutputType:
2861
	 *  self::OT_WIKI: only {{subst:}} templates
2862
	 *  self::OT_PREPROCESS: templates but not extension tags
2863
	 *  self::OT_HTML: all templates and extension tags
2864
	 *
2865
	 * @param string $text The text to transform
2866
	 * @param bool|PPFrame $frame Object describing the arguments passed to the
2867
	 *   template. Arguments may also be provided as an associative array, as
2868
	 *   was the usual case before MW1.12. Providing arguments this way may be
2869
	 *   useful for extensions wishing to perform variable replacement
2870
	 *   explicitly.
2871
	 * @param bool $argsOnly Only do argument (triple-brace) expansion, not
2872
	 *   double-brace expansion.
2873
	 * @return string
2874
	 */
2875
	public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2876
		# Is there any text? Also, Prevent too big inclusions!
2877
		$textSize = strlen( $text );
2878
		if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2879
			return $text;
2880
		}
2881
2882
		if ( $frame === false ) {
2883
			$frame = $this->getPreprocessor()->newFrame();
2884
		} elseif ( !( $frame instanceof PPFrame ) ) {
2885
			wfDebug( __METHOD__ . " called using plain parameters instead of "
2886
				. "a PPFrame instance. Creating custom frame.\n" );
2887
			$frame = $this->getPreprocessor()->newCustomFrame( $frame );
2888
		}
2889
2890
		$dom = $this->preprocessToDom( $text );
2891
		$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2892
		$text = $frame->expand( $dom, $flags );
2893
2894
		return $text;
2895
	}
2896
2897
	/**
2898
	 * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
2899
	 *
2900
	 * @param array $args
2901
	 *
2902
	 * @return array
2903
	 */
2904
	public static function createAssocArgs( $args ) {
2905
		$assocArgs = [];
2906
		$index = 1;
2907
		foreach ( $args as $arg ) {
2908
			$eqpos = strpos( $arg, '=' );
2909
			if ( $eqpos === false ) {
2910
				$assocArgs[$index++] = $arg;
2911
			} else {
2912
				$name = trim( substr( $arg, 0, $eqpos ) );
2913
				$value = trim( substr( $arg, $eqpos + 1 ) );
2914
				if ( $value === false ) {
2915
					$value = '';
2916
				}
2917
				if ( $name !== false ) {
2918
					$assocArgs[$name] = $value;
2919
				}
2920
			}
2921
		}
2922
2923
		return $assocArgs;
2924
	}
2925
2926
	/**
2927
	 * Warn the user when a parser limitation is reached
2928
	 * Will warn at most once the user per limitation type
2929
	 *
2930
	 * The results are shown during preview and run through the Parser (See EditPage.php)
2931
	 *
2932
	 * @param string $limitationType Should be one of:
2933
	 *   'expensive-parserfunction' (corresponding messages:
2934
	 *       'expensive-parserfunction-warning',
2935
	 *       'expensive-parserfunction-category')
2936
	 *   'post-expand-template-argument' (corresponding messages:
2937
	 *       'post-expand-template-argument-warning',
2938
	 *       'post-expand-template-argument-category')
2939
	 *   'post-expand-template-inclusion' (corresponding messages:
2940
	 *       'post-expand-template-inclusion-warning',
2941
	 *       'post-expand-template-inclusion-category')
2942
	 *   'node-count-exceeded' (corresponding messages:
2943
	 *       'node-count-exceeded-warning',
2944
	 *       'node-count-exceeded-category')
2945
	 *   'expansion-depth-exceeded' (corresponding messages:
2946
	 *       'expansion-depth-exceeded-warning',
2947
	 *       'expansion-depth-exceeded-category')
2948
	 * @param string|int|null $current Current value
2949
	 * @param string|int|null $max Maximum allowed, when an explicit limit has been
2950
	 *	 exceeded, provide the values (optional)
2951
	 */
2952
	public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2953
		# does no harm if $current and $max are present but are unnecessary for the message
2954
		# Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2955
		# only during preview, and that would split the parser cache unnecessarily.
2956
		$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2957
			->text();
2958
		$this->mOutput->addWarning( $warning );
2959
		$this->addTrackingCategory( "$limitationType-category" );
2960
	}
2961
2962
	/**
2963
	 * Return the text of a template, after recursively
2964
	 * replacing any variables or templates within the template.
2965
	 *
2966
	 * @param array $piece The parts of the template
2967
	 *   $piece['title']: the title, i.e. the part before the |
2968
	 *   $piece['parts']: the parameter array
2969
	 *   $piece['lineStart']: whether the brace was at the start of a line
2970
	 * @param PPFrame $frame The current frame, contains template arguments
2971
	 * @throws Exception
2972
	 * @return string The text of the template
2973
	 */
2974
	public function braceSubstitution( $piece, $frame ) {
2975
2976
		// Flags
2977
2978
		// $text has been filled
2979
		$found = false;
2980
		// wiki markup in $text should be escaped
2981
		$nowiki = false;
2982
		// $text is HTML, armour it against wikitext transformation
2983
		$isHTML = false;
2984
		// Force interwiki transclusion to be done in raw mode not rendered
2985
		$forceRawInterwiki = false;
2986
		// $text is a DOM node needing expansion in a child frame
2987
		$isChildObj = false;
2988
		// $text is a DOM node needing expansion in the current frame
2989
		$isLocalObj = false;
2990
2991
		# Title object, where $text came from
2992
		$title = false;
2993
2994
		# $part1 is the bit before the first |, and must contain only title characters.
2995
		# Various prefixes will be stripped from it later.
2996
		$titleWithSpaces = $frame->expand( $piece['title'] );
2997
		$part1 = trim( $titleWithSpaces );
2998
		$titleText = false;
2999
3000
		# Original title text preserved for various purposes
3001
		$originalTitle = $part1;
3002
3003
		# $args is a list of argument nodes, starting from index 0, not including $part1
3004
		# @todo FIXME: If piece['parts'] is null then the call to getLength()
3005
		# below won't work b/c this $args isn't an object
3006
		$args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3007
3008
		$profileSection = null; // profile templates
3009
3010
		# SUBST
3011
		if ( !$found ) {
3012
			$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3013
3014
			# Possibilities for substMatch: "subst", "safesubst" or FALSE
3015
			# Decide whether to expand template or keep wikitext as-is.
3016
			if ( $this->ot['wiki'] ) {
3017
				if ( $substMatch === false ) {
3018
					$literal = true;  # literal when in PST with no prefix
3019
				} else {
3020
					$literal = false; # expand when in PST with subst: or safesubst:
3021
				}
3022
			} else {
3023
				if ( $substMatch == 'subst' ) {
3024
					$literal = true;  # literal when not in PST with plain subst:
3025
				} else {
3026
					$literal = false; # expand when not in PST with safesubst: or no prefix
3027
				}
3028
			}
3029
			if ( $literal ) {
3030
				$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3031
				$isLocalObj = true;
3032
				$found = true;
3033
			}
3034
		}
3035
3036
		# Variables
3037
		if ( !$found && $args->getLength() == 0 ) {
3038
			$id = $this->mVariables->matchStartToEnd( $part1 );
3039
			if ( $id !== false ) {
3040
				$text = $this->getVariableValue( $id, $frame );
3041
				if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3042
					$this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3043
				}
3044
				$found = true;
3045
			}
3046
		}
3047
3048
		# MSG, MSGNW and RAW
3049
		if ( !$found ) {
3050
			# Check for MSGNW:
3051
			$mwMsgnw = MagicWord::get( 'msgnw' );
3052
			if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3053
				$nowiki = true;
3054
			} else {
3055
				# Remove obsolete MSG:
3056
				$mwMsg = MagicWord::get( 'msg' );
3057
				$mwMsg->matchStartAndRemove( $part1 );
3058
			}
3059
3060
			# Check for RAW:
3061
			$mwRaw = MagicWord::get( 'raw' );
3062
			if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3063
				$forceRawInterwiki = true;
3064
			}
3065
		}
3066
3067
		# Parser functions
3068
		if ( !$found ) {
3069
			$colonPos = strpos( $part1, ':' );
3070
			if ( $colonPos !== false ) {
3071
				$func = substr( $part1, 0, $colonPos );
3072
				$funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3073
				$argsLength = $args->getLength();
3074
				for ( $i = 0; $i < $argsLength; $i++ ) {
3075
					$funcArgs[] = $args->item( $i );
3076
				}
3077
				try {
3078
					$result = $this->callParserFunction( $frame, $func, $funcArgs );
3079
				} catch ( Exception $ex ) {
3080
					throw $ex;
3081
				}
3082
3083
				# The interface for parser functions allows for extracting
3084
				# flags into the local scope. Extract any forwarded flags
3085
				# here.
3086
				extract( $result );
3087
			}
3088
		}
3089
3090
		# Finish mangling title and then check for loops.
3091
		# Set $title to a Title object and $titleText to the PDBK
3092
		if ( !$found ) {
3093
			$ns = NS_TEMPLATE;
3094
			# Split the title into page and subpage
3095
			$subpage = '';
3096
			$relative = $this->maybeDoSubpageLink( $part1, $subpage );
3097
			if ( $part1 !== $relative ) {
3098
				$part1 = $relative;
3099
				$ns = $this->mTitle->getNamespace();
3100
			}
3101
			$title = Title::newFromText( $part1, $ns );
3102
			if ( $title ) {
3103
				$titleText = $title->getPrefixedText();
3104
				# Check for language variants if the template is not found
3105
				if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3106
					$this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3107
				}
3108
				# Do recursion depth check
3109
				$limit = $this->mOptions->getMaxTemplateDepth();
3110 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...
3111
					$found = true;
3112
					$text = '<span class="error">'
3113
						. wfMessage( 'parser-template-recursion-depth-warning' )
3114
							->numParams( $limit )->inContentLanguage()->text()
3115
						. '</span>';
3116
				}
3117
			}
3118
		}
3119
3120
		# Load from database
3121
		if ( !$found && $title ) {
3122
			$profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3123
			if ( !$title->isExternal() ) {
3124
				if ( $title->isSpecialPage()
3125
					&& $this->mOptions->getAllowSpecialInclusion()
3126
					&& $this->ot['html']
3127
				) {
3128
					$specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3129
					// Pass the template arguments as URL parameters.
3130
					// "uselang" will have no effect since the Language object
3131
					// is forced to the one defined in ParserOptions.
3132
					$pageArgs = [];
3133
					$argsLength = $args->getLength();
3134
					for ( $i = 0; $i < $argsLength; $i++ ) {
3135
						$bits = $args->item( $i )->splitArg();
3136
						if ( strval( $bits['index'] ) === '' ) {
3137
							$name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3138
							$value = trim( $frame->expand( $bits['value'] ) );
3139
							$pageArgs[$name] = $value;
3140
						}
3141
					}
3142
3143
					// Create a new context to execute the special page
3144
					$context = new RequestContext;
3145
					$context->setTitle( $title );
3146
					$context->setRequest( new FauxRequest( $pageArgs ) );
3147
					if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3148
						$context->setUser( $this->getUser() );
3149
					} else {
3150
						// If this page is cached, then we better not be per user.
3151
						$context->setUser( User::newFromName( '127.0.0.1', false ) );
0 ignored issues
show
Security Bug introduced by
It seems like \User::newFromName('127.0.0.1', false) targeting User::newFromName() can also be of type false; however, RequestContext::setUser() does only seem to accept object<User>, did you maybe forget to handle an error condition?
Loading history...
3152
					}
3153
					$context->setLanguage( $this->mOptions->getUserLangObj() );
3154
					$ret = SpecialPageFactory::capturePath( $title, $context );
3155
					if ( $ret ) {
3156
						$text = $context->getOutput()->getHTML();
3157
						$this->mOutput->addOutputPageMetadata( $context->getOutput() );
3158
						$found = true;
3159
						$isHTML = true;
3160
						if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3161
							$this->mOutput->updateCacheExpiry( $specialPage->maxIncludeCacheTime() );
3162
						}
3163
					}
3164
				} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3165
					$found = false; # access denied
3166
					wfDebug( __METHOD__ . ": template inclusion denied for " .
3167
						$title->getPrefixedDBkey() . "\n" );
3168
				} else {
3169
					list( $text, $title ) = $this->getTemplateDom( $title );
3170
					if ( $text !== false ) {
3171
						$found = true;
3172
						$isChildObj = true;
3173
					}
3174
				}
3175
3176
				# If the title is valid but undisplayable, make a link to it
3177
				if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3178
					$text = "[[:$titleText]]";
3179
					$found = true;
3180
				}
3181
			} elseif ( $title->isTrans() ) {
3182
				# Interwiki transclusion
3183
				if ( $this->ot['html'] && !$forceRawInterwiki ) {
3184
					$text = $this->interwikiTransclude( $title, 'render' );
3185
					$isHTML = true;
3186
				} else {
3187
					$text = $this->interwikiTransclude( $title, 'raw' );
3188
					# Preprocess it like a template
3189
					$text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3190
					$isChildObj = true;
3191
				}
3192
				$found = true;
3193
			}
3194
3195
			# Do infinite loop check
3196
			# This has to be done after redirect resolution to avoid infinite loops via redirects
3197
			if ( !$frame->loopCheck( $title ) ) {
3198
				$found = true;
3199
				$text = '<span class="error">'
3200
					. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3201
					. '</span>';
3202
				wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3203
			}
3204
		}
3205
3206
		# If we haven't found text to substitute by now, we're done
3207
		# Recover the source wikitext and return it
3208
		if ( !$found ) {
3209
			$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3210
			if ( $profileSection ) {
3211
				$this->mProfiler->scopedProfileOut( $profileSection );
3212
			}
3213
			return [ 'object' => $text ];
3214
		}
3215
3216
		# Expand DOM-style return values in a child frame
3217
		if ( $isChildObj ) {
3218
			# Clean up argument array
3219
			$newFrame = $frame->newChild( $args, $title );
3220
3221
			if ( $nowiki ) {
3222
				$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3223
			} elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3224
				# Expansion is eligible for the empty-frame cache
3225
				$text = $newFrame->cachedExpand( $titleText, $text );
3226
			} else {
3227
				# Uncached expansion
3228
				$text = $newFrame->expand( $text );
3229
			}
3230
		}
3231
		if ( $isLocalObj && $nowiki ) {
3232
			$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3233
			$isLocalObj = false;
3234
		}
3235
3236
		if ( $profileSection ) {
3237
			$this->mProfiler->scopedProfileOut( $profileSection );
3238
		}
3239
3240
		# Replace raw HTML by a placeholder
3241
		if ( $isHTML ) {
3242
			$text = $this->insertStripItem( $text );
3243
		} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3244
			# Escape nowiki-style return values
3245
			$text = wfEscapeWikiText( $text );
3246
		} elseif ( is_string( $text )
3247
			&& !$piece['lineStart']
3248
			&& preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3249
		) {
3250
			# Bug 529: if the template begins with a table or block-level
3251
			# element, it should be treated as beginning a new line.
3252
			# This behavior is somewhat controversial.
3253
			$text = "\n" . $text;
3254
		}
3255
3256
		if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3257
			# Error, oversize inclusion
3258
			if ( $titleText !== false ) {
3259
				# Make a working, properly escaped link if possible (bug 23588)
3260
				$text = "[[:$titleText]]";
3261
			} else {
3262
				# This will probably not be a working link, but at least it may
3263
				# provide some hint of where the problem is
3264
				preg_replace( '/^:/', '', $originalTitle );
3265
				$text = "[[:$originalTitle]]";
3266
			}
3267
			$text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3268
				. 'post-expand include size too large -->' );
3269
			$this->limitationWarn( 'post-expand-template-inclusion' );
3270
		}
3271
3272
		if ( $isLocalObj ) {
3273
			$ret = [ 'object' => $text ];
3274
		} else {
3275
			$ret = [ 'text' => $text ];
3276
		}
3277
3278
		return $ret;
3279
	}
3280
3281
	/**
3282
	 * Call a parser function and return an array with text and flags.
3283
	 *
3284
	 * The returned array will always contain a boolean 'found', indicating
3285
	 * whether the parser function was found or not. It may also contain the
3286
	 * following:
3287
	 *  text: string|object, resulting wikitext or PP DOM object
3288
	 *  isHTML: bool, $text is HTML, armour it against wikitext transformation
3289
	 *  isChildObj: bool, $text is a DOM node needing expansion in a child frame
3290
	 *  isLocalObj: bool, $text is a DOM node needing expansion in the current frame
3291
	 *  nowiki: bool, wiki markup in $text should be escaped
3292
	 *
3293
	 * @since 1.21
3294
	 * @param PPFrame $frame The current frame, contains template arguments
3295
	 * @param string $function Function name
3296
	 * @param array $args Arguments to the function
3297
	 * @throws MWException
3298
	 * @return array
3299
	 */
3300
	public function callParserFunction( $frame, $function, array $args = [] ) {
3301
		global $wgContLang;
3302
3303
		# Case sensitive functions
3304
		if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3305
			$function = $this->mFunctionSynonyms[1][$function];
3306
		} else {
3307
			# Case insensitive functions
3308
			$function = $wgContLang->lc( $function );
3309
			if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3310
				$function = $this->mFunctionSynonyms[0][$function];
3311
			} else {
3312
				return [ 'found' => false ];
3313
			}
3314
		}
3315
3316
		list( $callback, $flags ) = $this->mFunctionHooks[$function];
3317
3318
		# Workaround for PHP bug 35229 and similar
3319
		if ( !is_callable( $callback ) ) {
3320
			throw new MWException( "Tag hook for $function is not callable\n" );
3321
		}
3322
3323
		$allArgs = [ &$this ];
3324
		if ( $flags & self::SFH_OBJECT_ARGS ) {
3325
			# Convert arguments to PPNodes and collect for appending to $allArgs
3326
			$funcArgs = [];
3327
			foreach ( $args as $k => $v ) {
3328
				if ( $v instanceof PPNode || $k === 0 ) {
3329
					$funcArgs[] = $v;
3330
				} else {
3331
					$funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3332
				}
3333
			}
3334
3335
			# Add a frame parameter, and pass the arguments as an array
3336
			$allArgs[] = $frame;
3337
			$allArgs[] = $funcArgs;
3338
		} else {
3339
			# Convert arguments to plain text and append to $allArgs
3340
			foreach ( $args as $k => $v ) {
3341
				if ( $v instanceof PPNode ) {
3342
					$allArgs[] = trim( $frame->expand( $v ) );
3343
				} elseif ( is_int( $k ) && $k >= 0 ) {
3344
					$allArgs[] = trim( $v );
3345
				} else {
3346
					$allArgs[] = trim( "$k=$v" );
3347
				}
3348
			}
3349
		}
3350
3351
		$result = call_user_func_array( $callback, $allArgs );
3352
3353
		# The interface for function hooks allows them to return a wikitext
3354
		# string or an array containing the string and any flags. This mungs
3355
		# things around to match what this method should return.
3356
		if ( !is_array( $result ) ) {
3357
			$result =[
3358
				'found' => true,
3359
				'text' => $result,
3360
			];
3361
		} else {
3362
			if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3363
				$result['text'] = $result[0];
3364
			}
3365
			unset( $result[0] );
3366
			$result += [
3367
				'found' => true,
3368
			];
3369
		}
3370
3371
		$noparse = true;
3372
		$preprocessFlags = 0;
3373
		if ( isset( $result['noparse'] ) ) {
3374
			$noparse = $result['noparse'];
3375
		}
3376
		if ( isset( $result['preprocessFlags'] ) ) {
3377
			$preprocessFlags = $result['preprocessFlags'];
3378
		}
3379
3380
		if ( !$noparse ) {
3381
			$result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3382
			$result['isChildObj'] = true;
3383
		}
3384
3385
		return $result;
3386
	}
3387
3388
	/**
3389
	 * Get the semi-parsed DOM representation of a template with a given title,
3390
	 * and its redirect destination title. Cached.
3391
	 *
3392
	 * @param Title $title
3393
	 *
3394
	 * @return array
3395
	 */
3396
	public function getTemplateDom( $title ) {
3397
		$cacheTitle = $title;
3398
		$titleText = $title->getPrefixedDBkey();
3399
3400
		if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3401
			list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3402
			$title = Title::makeTitle( $ns, $dbk );
3403
			$titleText = $title->getPrefixedDBkey();
3404
		}
3405
		if ( isset( $this->mTplDomCache[$titleText] ) ) {
3406
			return [ $this->mTplDomCache[$titleText], $title ];
3407
		}
3408
3409
		# Cache miss, go to the database
3410
		list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3411
3412
		if ( $text === false ) {
3413
			$this->mTplDomCache[$titleText] = false;
3414
			return [ false, $title ];
3415
		}
3416
3417
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3418
		$this->mTplDomCache[$titleText] = $dom;
3419
3420
		if ( !$title->equals( $cacheTitle ) ) {
3421
			$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3422
				[ $title->getNamespace(), $cdb = $title->getDBkey() ];
3423
		}
3424
3425
		return [ $dom, $title ];
3426
	}
3427
3428
	/**
3429
	 * Fetch the current revision of a given title. Note that the revision
3430
	 * (and even the title) may not exist in the database, so everything
3431
	 * contributing to the output of the parser should use this method
3432
	 * where possible, rather than getting the revisions themselves. This
3433
	 * method also caches its results, so using it benefits performance.
3434
	 *
3435
	 * @since 1.24
3436
	 * @param Title $title
3437
	 * @return Revision
3438
	 */
3439
	public function fetchCurrentRevisionOfTitle( $title ) {
3440
		$cacheKey = $title->getPrefixedDBkey();
3441
		if ( !$this->currentRevisionCache ) {
3442
			$this->currentRevisionCache = new MapCacheLRU( 100 );
3443
		}
3444
		if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3445
			$this->currentRevisionCache->set( $cacheKey,
3446
				// Defaults to Parser::statelessFetchRevision()
3447
				call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3448
			);
3449
		}
3450
		return $this->currentRevisionCache->get( $cacheKey );
3451
	}
3452
3453
	/**
3454
	 * Wrapper around Revision::newFromTitle to allow passing additional parameters
3455
	 * without passing them on to it.
3456
	 *
3457
	 * @since 1.24
3458
	 * @param Title $title
3459
	 * @param Parser|bool $parser
3460
	 * @return Revision
3461
	 */
3462
	public static function statelessFetchRevision( $title, $parser = false ) {
3463
		return Revision::newFromTitle( $title );
3464
	}
3465
3466
	/**
3467
	 * Fetch the unparsed text of a template and register a reference to it.
3468
	 * @param Title $title
3469
	 * @return array ( string or false, Title )
3470
	 */
3471
	public function fetchTemplateAndTitle( $title ) {
3472
		// Defaults to Parser::statelessFetchTemplate()
3473
		$templateCb = $this->mOptions->getTemplateCallback();
3474
		$stuff = call_user_func( $templateCb, $title, $this );
3475
		// We use U+007F DELETE to distinguish strip markers from regular text.
3476
		$text = $stuff['text'];
3477
		if ( is_string( $stuff['text'] ) ) {
3478
			$text = strtr( $text, "\x7f", "?" );
3479
		}
3480
		$finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3481
		if ( isset( $stuff['deps'] ) ) {
3482
			foreach ( $stuff['deps'] as $dep ) {
3483
				$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3484
				if ( $dep['title']->equals( $this->getTitle() ) ) {
3485
					// If we transclude ourselves, the final result
3486
					// will change based on the new version of the page
3487
					$this->mOutput->setFlag( 'vary-revision' );
3488
				}
3489
			}
3490
		}
3491
		return [ $text, $finalTitle ];
3492
	}
3493
3494
	/**
3495
	 * Fetch the unparsed text of a template and register a reference to it.
3496
	 * @param Title $title
3497
	 * @return string|bool
3498
	 */
3499
	public function fetchTemplate( $title ) {
3500
		return $this->fetchTemplateAndTitle( $title )[0];
3501
	}
3502
3503
	/**
3504
	 * Static function to get a template
3505
	 * Can be overridden via ParserOptions::setTemplateCallback().
3506
	 *
3507
	 * @param Title $title
3508
	 * @param bool|Parser $parser
3509
	 *
3510
	 * @return array
3511
	 */
3512
	public static function statelessFetchTemplate( $title, $parser = false ) {
3513
		$text = $skip = false;
3514
		$finalTitle = $title;
3515
		$deps = [];
3516
3517
		# Loop to fetch the article, with up to 1 redirect
3518
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3519
		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3520
			// @codingStandardsIgnoreEnd
3521
			# Give extensions a chance to select the revision instead
3522
			$id = false; # Assume current
3523
			Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3524
				[ $parser, $title, &$skip, &$id ] );
3525
3526
			if ( $skip ) {
3527
				$text = false;
3528
				$deps[] = [
3529
					'title' => $title,
3530
					'page_id' => $title->getArticleID(),
3531
					'rev_id' => null
3532
				];
3533
				break;
3534
			}
3535
			# Get the revision
3536
			if ( $id ) {
3537
				$rev = Revision::newFromId( $id );
3538
			} elseif ( $parser ) {
3539
				$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...
3540
			} else {
3541
				$rev = Revision::newFromTitle( $title );
3542
			}
3543
			$rev_id = $rev ? $rev->getId() : 0;
3544
			# If there is no current revision, there is no page
3545
			if ( $id === false && !$rev ) {
3546
				$linkCache = LinkCache::singleton();
0 ignored issues
show
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

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

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

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