Completed
Branch master (af7ffa)
by
unknown
24:08
created

Parser::internalParse()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 60
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 32
nc 4
nop 3
dl 0
loc 60
rs 8.9618
c 1
b 0
f 1

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHP parser that converts wiki markup to HTML.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Parser
22
 */
23
use MediaWiki\Linker\LinkRenderer;
24
use MediaWiki\MediaWikiServices;
25
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
			[ &$this, 'addTrackingCategory' ]
1270
		);
1271
		Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
1272
1273
		# Tables need to come after variable replacement for things to work
1274
		# properly; putting them before other transformations should keep
1275
		# exciting things like link expansions from showing up in surprising
1276
		# places.
1277
		$text = $this->doTableStuff( $text );
1278
1279
		$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1280
1281
		$text = $this->doDoubleUnderscore( $text );
1282
1283
		$text = $this->doHeadings( $text );
1284
		$text = $this->replaceInternalLinks( $text );
1285
		$text = $this->doAllQuotes( $text );
1286
		$text = $this->replaceExternalLinks( $text );
1287
1288
		# replaceInternalLinks may sometimes leave behind
1289
		# absolute URLs, which have to be masked to hide them from replaceExternalLinks
1290
		$text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1291
1292
		$text = $this->doMagicLinks( $text );
1293
		$text = $this->formatHeadings( $text, $origText, $isMain );
1294
1295
		return $text;
1296
	}
1297
1298
	/**
1299
	 * Helper function for parse() that transforms half-parsed HTML into fully
1300
	 * parsed HTML.
1301
	 *
1302
	 * @param string $text
1303
	 * @param bool $isMain
1304
	 * @param bool $linestart
1305
	 * @return string
1306
	 */
1307
	private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1308
		$text = $this->mStripState->unstripGeneral( $text );
1309
1310
		if ( $isMain ) {
1311
			Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
1312
		}
1313
1314
		# Clean up special characters, only run once, next-to-last before doBlockLevels
1315
		$fixtags = [
1316
			# french spaces, last one Guillemet-left
1317
			# only if there is something before the space
1318
			'/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1319
			# french spaces, Guillemet-right
1320
			'/(\\302\\253) /' => '\\1&#160;',
1321
			'/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1322
		];
1323
		$text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1324
1325
		$text = $this->doBlockLevels( $text, $linestart );
1326
1327
		$this->replaceLinkHolders( $text );
1328
1329
		/**
1330
		 * The input doesn't get language converted if
1331
		 * a) It's disabled
1332
		 * b) Content isn't converted
1333
		 * c) It's a conversion table
1334
		 * d) it is an interface message (which is in the user language)
1335
		 */
1336
		if ( !( $this->mOptions->getDisableContentConversion()
1337
			|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1338
		) {
1339
			if ( !$this->mOptions->getInterfaceMessage() ) {
1340
				# The position of the convert() call should not be changed. it
1341
				# assumes that the links are all replaced and the only thing left
1342
				# is the <nowiki> mark.
1343
				$text = $this->getConverterLanguage()->convert( $text );
1344
			}
1345
		}
1346
1347
		$text = $this->mStripState->unstripNoWiki( $text );
1348
1349
		if ( $isMain ) {
1350
			Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
1351
		}
1352
1353
		$text = $this->replaceTransparentTags( $text );
1354
		$text = $this->mStripState->unstripGeneral( $text );
1355
1356
		$text = Sanitizer::normalizeCharReferences( $text );
1357
1358
		if ( MWTidy::isEnabled() && $this->mOptions->getTidy() ) {
1359
			$text = MWTidy::tidy( $text );
1360
			$this->mOutput->addModuleStyles( MWTidy::getModuleStyles() );
1361
		} else {
1362
			# attempt to sanitize at least some nesting problems
1363
			# (bug #2702 and quite a few others)
1364
			$tidyregs = [
1365
				# ''Something [http://www.cool.com cool''] -->
1366
				# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1367
				'/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1368
				'\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1369
				# fix up an anchor inside another anchor, only
1370
				# at least for a single single nested link (bug 3695)
1371
				'/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1372
				'\\1\\2</a>\\3</a>\\1\\4</a>',
1373
				# fix div inside inline elements- doBlockLevels won't wrap a line which
1374
				# contains a div, so fix it up here; replace
1375
				# div with escaped text
1376
				'/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1377
				'\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1378
				# remove empty italic or bold tag pairs, some
1379
				# introduced by rules above
1380
				'/<([bi])><\/\\1>/' => '',
1381
			];
1382
1383
			$text = preg_replace(
1384
				array_keys( $tidyregs ),
1385
				array_values( $tidyregs ),
1386
				$text );
1387
		}
1388
1389
		if ( $isMain ) {
1390
			Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
1391
		}
1392
1393
		return $text;
1394
	}
1395
1396
	/**
1397
	 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1398
	 * magic external links.
1399
	 *
1400
	 * DML
1401
	 * @private
1402
	 *
1403
	 * @param string $text
1404
	 *
1405
	 * @return string
1406
	 */
1407
	public function doMagicLinks( $text ) {
1408
		$prots = wfUrlProtocolsWithoutProtRel();
1409
		$urlChar = self::EXT_LINK_URL_CLASS;
1410
		$addr = self::EXT_LINK_ADDR;
1411
		$space = self::SPACE_NOT_NL; #  non-newline space
1412
		$spdash = "(?:-|$space)"; # a dash or a non-newline space
1413
		$spaces = "$space++"; # possessive match of 1 or more spaces
1414
		$text = preg_replace_callback(
1415
			'!(?:                            # Start cases
1416
				(<a[ \t\r\n>].*?</a>) |      # m[1]: Skip link text
1417
				(<.*?>) |                    # m[2]: Skip stuff inside
1418
				                             #       HTML elements' . "
1419
				(\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1420
				                             # m[4]: Post-protocol path
1421
				\b(?:RFC|PMID) $spaces       # m[5]: RFC or PMID, capture number
1422
					([0-9]+)\b |
1423
				\bISBN $spaces (             # m[6]: ISBN, capture number
1424
					(?: 97[89] $spdash? )?   #  optional 13-digit ISBN prefix
1425
					(?: [0-9]  $spdash? ){9} #  9 digits with opt. delimiters
1426
					[0-9Xx]                  #  check digit
1427
				)\b
1428
			)!xu", [ &$this, 'magicLinkCallback' ], $text );
1429
		return $text;
1430
	}
1431
1432
	/**
1433
	 * @throws MWException
1434
	 * @param array $m
1435
	 * @return HTML|string
1436
	 */
1437
	public function magicLinkCallback( $m ) {
1438
		if ( isset( $m[1] ) && $m[1] !== '' ) {
1439
			# Skip anchor
1440
			return $m[0];
1441
		} elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1442
			# Skip HTML element
1443
			return $m[0];
1444
		} elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1445
			# Free external link
1446
			return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1447
		} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1448
			# RFC or PMID
1449
			if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1450
				$keyword = 'RFC';
1451
				$urlmsg = 'rfcurl';
1452
				$cssClass = 'mw-magiclink-rfc';
1453
				$id = $m[5];
1454
			} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1455
				$keyword = 'PMID';
1456
				$urlmsg = 'pubmedurl';
1457
				$cssClass = 'mw-magiclink-pmid';
1458
				$id = $m[5];
1459
			} else {
1460
				throw new MWException( __METHOD__ . ': unrecognised match type "' .
1461
					substr( $m[0], 0, 20 ) . '"' );
1462
			}
1463
			$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1464
			return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1465
		} elseif ( isset( $m[6] ) && $m[6] !== '' ) {
1466
			# ISBN
1467
			$isbn = $m[6];
1468
			$space = self::SPACE_NOT_NL; #  non-newline space
1469
			$isbn = preg_replace( "/$space/", ' ', $isbn );
1470
			$num = strtr( $isbn, [
1471
				'-' => '',
1472
				' ' => '',
1473
				'x' => 'X',
1474
			] );
1475
			return $this->getLinkRenderer()->makeKnownLink(
1476
				SpecialPage::getTitleFor( 'Booksources', $num ),
1477
				"ISBN $isbn",
1478
				[
1479
					'class' => 'internal mw-magiclink-isbn',
1480
					'title' => false // suppress title attribute
1481
				]
1482
			);
1483
		} else {
1484
			return $m[0];
1485
		}
1486
	}
1487
1488
	/**
1489
	 * Make a free external link, given a user-supplied URL
1490
	 *
1491
	 * @param string $url
1492
	 * @param int $numPostProto
1493
	 *   The number of characters after the protocol.
1494
	 * @return string HTML
1495
	 * @private
1496
	 */
1497
	public function makeFreeExternalLink( $url, $numPostProto ) {
1498
		$trail = '';
1499
1500
		# The characters '<' and '>' (which were escaped by
1501
		# removeHTMLtags()) should not be included in
1502
		# URLs, per RFC 2396.
1503
		# Make &nbsp; terminate a URL as well (bug T84937)
1504
		$m2 = [];
1505 View Code Duplication
		if ( preg_match(
1506
			'/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1507
			$url,
1508
			$m2,
1509
			PREG_OFFSET_CAPTURE
1510
		) ) {
1511
			$trail = substr( $url, $m2[0][1] ) . $trail;
1512
			$url = substr( $url, 0, $m2[0][1] );
1513
		}
1514
1515
		# Move trailing punctuation to $trail
1516
		$sep = ',;\.:!?';
1517
		# If there is no left bracket, then consider right brackets fair game too
1518
		if ( strpos( $url, '(' ) === false ) {
1519
			$sep .= ')';
1520
		}
1521
1522
		$urlRev = strrev( $url );
1523
		$numSepChars = strspn( $urlRev, $sep );
1524
		# Don't break a trailing HTML entity by moving the ; into $trail
1525
		# This is in hot code, so use substr_compare to avoid having to
1526
		# create a new string object for the comparison
1527
		if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1528
			# more optimization: instead of running preg_match with a $
1529
			# anchor, which can be slow, do the match on the reversed
1530
			# string starting at the desired offset.
1531
			# un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1532
			if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1533
				$numSepChars--;
1534
			}
1535
		}
1536
		if ( $numSepChars ) {
1537
			$trail = substr( $url, -$numSepChars ) . $trail;
1538
			$url = substr( $url, 0, -$numSepChars );
1539
		}
1540
1541
		# Verify that we still have a real URL after trail removal, and
1542
		# not just lone protocol
1543
		if ( strlen( $trail ) >= $numPostProto ) {
1544
			return $url . $trail;
1545
		}
1546
1547
		$url = Sanitizer::cleanUrl( $url );
1548
1549
		# Is this an external image?
1550
		$text = $this->maybeMakeExternalImage( $url );
1551
		if ( $text === false ) {
1552
			# Not an image, make a link
1553
			$text = Linker::makeExternalLink( $url,
1554
				$this->getConverterLanguage()->markNoConversion( $url, true ),
1555
				true, 'free',
1556
				$this->getExternalLinkAttribs( $url ), $this->mTitle );
1557
			# Register it in the output object...
1558
			# Replace unnecessary URL escape codes with their equivalent characters
1559
			$pasteurized = self::normalizeLinkUrl( $url );
1560
			$this->mOutput->addExternalLink( $pasteurized );
1561
		}
1562
		return $text . $trail;
1563
	}
1564
1565
	/**
1566
	 * Parse headers and return html
1567
	 *
1568
	 * @private
1569
	 *
1570
	 * @param string $text
1571
	 *
1572
	 * @return string
1573
	 */
1574
	public function doHeadings( $text ) {
1575
		for ( $i = 6; $i >= 1; --$i ) {
1576
			$h = str_repeat( '=', $i );
1577
			$text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1578
		}
1579
		return $text;
1580
	}
1581
1582
	/**
1583
	 * Replace single quotes with HTML markup
1584
	 * @private
1585
	 *
1586
	 * @param string $text
1587
	 *
1588
	 * @return string The altered text
1589
	 */
1590
	public function doAllQuotes( $text ) {
1591
		$outtext = '';
1592
		$lines = StringUtils::explode( "\n", $text );
1593
		foreach ( $lines as $line ) {
1594
			$outtext .= $this->doQuotes( $line ) . "\n";
1595
		}
1596
		$outtext = substr( $outtext, 0, -1 );
1597
		return $outtext;
1598
	}
1599
1600
	/**
1601
	 * Helper function for doAllQuotes()
1602
	 *
1603
	 * @param string $text
1604
	 *
1605
	 * @return string
1606
	 */
1607
	public function doQuotes( $text ) {
1608
		$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1609
		$countarr = count( $arr );
1610
		if ( $countarr == 1 ) {
1611
			return $text;
1612
		}
1613
1614
		// First, do some preliminary work. This may shift some apostrophes from
1615
		// being mark-up to being text. It also counts the number of occurrences
1616
		// of bold and italics mark-ups.
1617
		$numbold = 0;
1618
		$numitalics = 0;
1619
		for ( $i = 1; $i < $countarr; $i += 2 ) {
1620
			$thislen = strlen( $arr[$i] );
1621
			// If there are ever four apostrophes, assume the first is supposed to
1622
			// be text, and the remaining three constitute mark-up for bold text.
1623
			// (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1624
			if ( $thislen == 4 ) {
1625
				$arr[$i - 1] .= "'";
1626
				$arr[$i] = "'''";
1627
				$thislen = 3;
1628
			} elseif ( $thislen > 5 ) {
1629
				// If there are more than 5 apostrophes in a row, assume they're all
1630
				// text except for the last 5.
1631
				// (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1632
				$arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1633
				$arr[$i] = "'''''";
1634
				$thislen = 5;
1635
			}
1636
			// Count the number of occurrences of bold and italics mark-ups.
1637
			if ( $thislen == 2 ) {
1638
				$numitalics++;
1639
			} elseif ( $thislen == 3 ) {
1640
				$numbold++;
1641
			} elseif ( $thislen == 5 ) {
1642
				$numitalics++;
1643
				$numbold++;
1644
			}
1645
		}
1646
1647
		// If there is an odd number of both bold and italics, it is likely
1648
		// that one of the bold ones was meant to be an apostrophe followed
1649
		// by italics. Which one we cannot know for certain, but it is more
1650
		// likely to be one that has a single-letter word before it.
1651
		if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1652
			$firstsingleletterword = -1;
1653
			$firstmultiletterword = -1;
1654
			$firstspace = -1;
1655
			for ( $i = 1; $i < $countarr; $i += 2 ) {
1656
				if ( strlen( $arr[$i] ) == 3 ) {
1657
					$x1 = substr( $arr[$i - 1], -1 );
1658
					$x2 = substr( $arr[$i - 1], -2, 1 );
1659
					if ( $x1 === ' ' ) {
1660
						if ( $firstspace == -1 ) {
1661
							$firstspace = $i;
1662
						}
1663
					} elseif ( $x2 === ' ' ) {
1664
						$firstsingleletterword = $i;
1665
						// if $firstsingleletterword is set, we don't
1666
						// look at the other options, so we can bail early.
1667
						break;
1668
					} else {
1669
						if ( $firstmultiletterword == -1 ) {
1670
							$firstmultiletterword = $i;
1671
						}
1672
					}
1673
				}
1674
			}
1675
1676
			// If there is a single-letter word, use it!
1677
			if ( $firstsingleletterword > -1 ) {
1678
				$arr[$firstsingleletterword] = "''";
1679
				$arr[$firstsingleletterword - 1] .= "'";
1680
			} elseif ( $firstmultiletterword > -1 ) {
1681
				// If not, but there's a multi-letter word, use that one.
1682
				$arr[$firstmultiletterword] = "''";
1683
				$arr[$firstmultiletterword - 1] .= "'";
1684
			} elseif ( $firstspace > -1 ) {
1685
				// ... otherwise use the first one that has neither.
1686
				// (notice that it is possible for all three to be -1 if, for example,
1687
				// there is only one pentuple-apostrophe in the line)
1688
				$arr[$firstspace] = "''";
1689
				$arr[$firstspace - 1] .= "'";
1690
			}
1691
		}
1692
1693
		// Now let's actually convert our apostrophic mush to HTML!
1694
		$output = '';
1695
		$buffer = '';
1696
		$state = '';
1697
		$i = 0;
1698
		foreach ( $arr as $r ) {
1699
			if ( ( $i % 2 ) == 0 ) {
1700
				if ( $state === 'both' ) {
1701
					$buffer .= $r;
1702
				} else {
1703
					$output .= $r;
1704
				}
1705
			} else {
1706
				$thislen = strlen( $r );
1707
				if ( $thislen == 2 ) {
1708 View Code Duplication
					if ( $state === 'i' ) {
1709
						$output .= '</i>';
1710
						$state = '';
1711
					} elseif ( $state === 'bi' ) {
1712
						$output .= '</i>';
1713
						$state = 'b';
1714
					} elseif ( $state === 'ib' ) {
1715
						$output .= '</b></i><b>';
1716
						$state = 'b';
1717
					} elseif ( $state === 'both' ) {
1718
						$output .= '<b><i>' . $buffer . '</i>';
1719
						$state = 'b';
1720
					} else { // $state can be 'b' or ''
1721
						$output .= '<i>';
1722
						$state .= 'i';
1723
					}
1724 View Code Duplication
				} elseif ( $thislen == 3 ) {
1725
					if ( $state === 'b' ) {
1726
						$output .= '</b>';
1727
						$state = '';
1728
					} elseif ( $state === 'bi' ) {
1729
						$output .= '</i></b><i>';
1730
						$state = 'i';
1731
					} elseif ( $state === 'ib' ) {
1732
						$output .= '</b>';
1733
						$state = 'i';
1734
					} elseif ( $state === 'both' ) {
1735
						$output .= '<i><b>' . $buffer . '</b>';
1736
						$state = 'i';
1737
					} else { // $state can be 'i' or ''
1738
						$output .= '<b>';
1739
						$state .= 'b';
1740
					}
1741
				} elseif ( $thislen == 5 ) {
1742
					if ( $state === 'b' ) {
1743
						$output .= '</b><i>';
1744
						$state = 'i';
1745
					} elseif ( $state === 'i' ) {
1746
						$output .= '</i><b>';
1747
						$state = 'b';
1748
					} elseif ( $state === 'bi' ) {
1749
						$output .= '</i></b>';
1750
						$state = '';
1751
					} elseif ( $state === 'ib' ) {
1752
						$output .= '</b></i>';
1753
						$state = '';
1754
					} elseif ( $state === 'both' ) {
1755
						$output .= '<i><b>' . $buffer . '</b></i>';
1756
						$state = '';
1757
					} else { // ($state == '')
1758
						$buffer = '';
1759
						$state = 'both';
1760
					}
1761
				}
1762
			}
1763
			$i++;
1764
		}
1765
		// Now close all remaining tags.  Notice that the order is important.
1766
		if ( $state === 'b' || $state === 'ib' ) {
1767
			$output .= '</b>';
1768
		}
1769
		if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1770
			$output .= '</i>';
1771
		}
1772
		if ( $state === 'bi' ) {
1773
			$output .= '</b>';
1774
		}
1775
		// There might be lonely ''''', so make sure we have a buffer
1776
		if ( $state === 'both' && $buffer ) {
1777
			$output .= '<b><i>' . $buffer . '</i></b>';
1778
		}
1779
		return $output;
1780
	}
1781
1782
	/**
1783
	 * Replace external links (REL)
1784
	 *
1785
	 * Note: this is all very hackish and the order of execution matters a lot.
1786
	 * Make sure to run tests/parserTests.php if you change this code.
1787
	 *
1788
	 * @private
1789
	 *
1790
	 * @param string $text
1791
	 *
1792
	 * @throws MWException
1793
	 * @return string
1794
	 */
1795
	public function replaceExternalLinks( $text ) {
1796
1797
		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1798
		if ( $bits === false ) {
1799
			throw new MWException( "PCRE needs to be compiled with "
1800
				. "--enable-unicode-properties in order for MediaWiki to function" );
1801
		}
1802
		$s = array_shift( $bits );
1803
1804
		$i = 0;
1805
		while ( $i < count( $bits ) ) {
1806
			$url = $bits[$i++];
1807
			$i++; // protocol
1808
			$text = $bits[$i++];
1809
			$trail = $bits[$i++];
1810
1811
			# The characters '<' and '>' (which were escaped by
1812
			# removeHTMLtags()) should not be included in
1813
			# URLs, per RFC 2396.
1814
			$m2 = [];
1815 View Code Duplication
			if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1816
				$text = substr( $url, $m2[0][1] ) . ' ' . $text;
1817
				$url = substr( $url, 0, $m2[0][1] );
1818
			}
1819
1820
			# If the link text is an image URL, replace it with an <img> tag
1821
			# This happened by accident in the original parser, but some people used it extensively
1822
			$img = $this->maybeMakeExternalImage( $text );
1823
			if ( $img !== false ) {
1824
				$text = $img;
1825
			}
1826
1827
			$dtrail = '';
1828
1829
			# Set linktype for CSS - if URL==text, link is essentially free
1830
			$linktype = ( $text === $url ) ? 'free' : 'text';
1831
1832
			# No link text, e.g. [http://domain.tld/some.link]
1833
			if ( $text == '' ) {
1834
				# Autonumber
1835
				$langObj = $this->getTargetLanguage();
1836
				$text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1837
				$linktype = 'autonumber';
1838
			} else {
1839
				# Have link text, e.g. [http://domain.tld/some.link text]s
1840
				# Check for trail
1841
				list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1842
			}
1843
1844
			$text = $this->getConverterLanguage()->markNoConversion( $text );
1845
1846
			$url = Sanitizer::cleanUrl( $url );
1847
1848
			# Use the encoded URL
1849
			# This means that users can paste URLs directly into the text
1850
			# Funny characters like ö aren't valid in URLs anyway
1851
			# This was changed in August 2004
1852
			$s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1853
				$this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1854
1855
			# Register link in the output object.
1856
			# Replace unnecessary URL escape codes with the referenced character
1857
			# This prevents spammers from hiding links from the filters
1858
			$pasteurized = self::normalizeLinkUrl( $url );
1859
			$this->mOutput->addExternalLink( $pasteurized );
1860
		}
1861
1862
		return $s;
1863
	}
1864
1865
	/**
1866
	 * Get the rel attribute for a particular external link.
1867
	 *
1868
	 * @since 1.21
1869
	 * @param string|bool $url Optional URL, to extract the domain from for rel =>
1870
	 *   nofollow if appropriate
1871
	 * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups
1872
	 * @return string|null Rel attribute for $url
1873
	 */
1874
	public static function getExternalLinkRel( $url = false, $title = null ) {
1875
		global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1876
		$ns = $title ? $title->getNamespace() : false;
1877
		if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1878
			&& !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
0 ignored issues
show
Bug introduced by
It seems like $url defined by parameter $url on line 1874 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...
1879
		) {
1880
			return 'nofollow';
1881
		}
1882
		return null;
1883
	}
1884
1885
	/**
1886
	 * Get an associative array of additional HTML attributes appropriate for a
1887
	 * particular external link.  This currently may include rel => nofollow
1888
	 * (depending on configuration, namespace, and the URL's domain) and/or a
1889
	 * target attribute (depending on configuration).
1890
	 *
1891
	 * @param string $url URL to extract the domain from for rel =>
1892
	 *   nofollow if appropriate
1893
	 * @return array Associative array of HTML attributes
1894
	 */
1895
	public function getExternalLinkAttribs( $url ) {
1896
		$attribs = [];
1897
		$rel = self::getExternalLinkRel( $url, $this->mTitle );
1898
1899
		$target = $this->mOptions->getExternalLinkTarget();
1900
		if ( $target ) {
1901
			$attribs['target'] = $target;
1902
			if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1903
				// T133507. New windows can navigate parent cross-origin.
1904
				// Including noreferrer due to lacking browser
1905
				// support of noopener. Eventually noreferrer should be removed.
1906
				if ( $rel !== '' ) {
1907
					$rel .= ' ';
1908
				}
1909
				$rel .= 'noreferrer noopener';
1910
			}
1911
		}
1912
		$attribs['rel'] = $rel;
1913
		return $attribs;
1914
	}
1915
1916
	/**
1917
	 * Replace unusual escape codes in a URL with their equivalent characters
1918
	 *
1919
	 * @deprecated since 1.24, use normalizeLinkUrl
1920
	 * @param string $url
1921
	 * @return string
1922
	 */
1923
	public static function replaceUnusualEscapes( $url ) {
1924
		wfDeprecated( __METHOD__, '1.24' );
1925
		return self::normalizeLinkUrl( $url );
1926
	}
1927
1928
	/**
1929
	 * Replace unusual escape codes in a URL with their equivalent characters
1930
	 *
1931
	 * This generally follows the syntax defined in RFC 3986, with special
1932
	 * consideration for HTTP query strings.
1933
	 *
1934
	 * @param string $url
1935
	 * @return string
1936
	 */
1937
	public static function normalizeLinkUrl( $url ) {
1938
		# First, make sure unsafe characters are encoded
1939
		$url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1940
			function ( $m ) {
1941
				return rawurlencode( $m[0] );
1942
			},
1943
			$url
1944
		);
1945
1946
		$ret = '';
1947
		$end = strlen( $url );
1948
1949
		# Fragment part - 'fragment'
1950
		$start = strpos( $url, '#' );
1951 View Code Duplication
		if ( $start !== false && $start < $end ) {
1952
			$ret = self::normalizeUrlComponent(
1953
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1954
			$end = $start;
1955
		}
1956
1957
		# Query part - 'query' minus &=+;
1958
		$start = strpos( $url, '?' );
1959 View Code Duplication
		if ( $start !== false && $start < $end ) {
1960
			$ret = self::normalizeUrlComponent(
1961
				substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1962
			$end = $start;
1963
		}
1964
1965
		# Scheme and path part - 'pchar'
1966
		# (we assume no userinfo or encoded colons in the host)
1967
		$ret = self::normalizeUrlComponent(
1968
			substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1969
1970
		return $ret;
1971
	}
1972
1973
	private static function normalizeUrlComponent( $component, $unsafe ) {
1974
		$callback = function ( $matches ) use ( $unsafe ) {
1975
			$char = urldecode( $matches[0] );
1976
			$ord = ord( $char );
1977
			if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1978
				# Unescape it
1979
				return $char;
1980
			} else {
1981
				# Leave it escaped, but use uppercase for a-f
1982
				return strtoupper( $matches[0] );
1983
			}
1984
		};
1985
		return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
1986
	}
1987
1988
	/**
1989
	 * make an image if it's allowed, either through the global
1990
	 * option, through the exception, or through the on-wiki whitelist
1991
	 *
1992
	 * @param string $url
1993
	 *
1994
	 * @return string
1995
	 */
1996
	private function maybeMakeExternalImage( $url ) {
1997
		$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1998
		$imagesexception = !empty( $imagesfrom );
1999
		$text = false;
2000
		# $imagesfrom could be either a single string or an array of strings, parse out the latter
2001
		if ( $imagesexception && is_array( $imagesfrom ) ) {
2002
			$imagematch = false;
2003
			foreach ( $imagesfrom as $match ) {
2004
				if ( strpos( $url, $match ) === 0 ) {
2005
					$imagematch = true;
2006
					break;
2007
				}
2008
			}
2009
		} elseif ( $imagesexception ) {
2010
			$imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2011
		} else {
2012
			$imagematch = false;
2013
		}
2014
2015
		if ( $this->mOptions->getAllowExternalImages()
2016
			|| ( $imagesexception && $imagematch )
2017
		) {
2018
			if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2019
				# Image found
2020
				$text = Linker::makeExternalImage( $url );
2021
			}
2022
		}
2023
		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...
2024
			&& preg_match( self::EXT_IMAGE_REGEX, $url )
2025
		) {
2026
			$whitelist = explode(
2027
				"\n",
2028
				wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2029
			);
2030
2031
			foreach ( $whitelist as $entry ) {
2032
				# Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2033
				if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2034
					continue;
2035
				}
2036
				if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2037
					# Image matches a whitelist entry
2038
					$text = Linker::makeExternalImage( $url );
2039
					break;
2040
				}
2041
			}
2042
		}
2043
		return $text;
2044
	}
2045
2046
	/**
2047
	 * Process [[ ]] wikilinks
2048
	 *
2049
	 * @param string $s
2050
	 *
2051
	 * @return string Processed text
2052
	 *
2053
	 * @private
2054
	 */
2055
	public function replaceInternalLinks( $s ) {
2056
		$this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2057
		return $s;
2058
	}
2059
2060
	/**
2061
	 * Process [[ ]] wikilinks (RIL)
2062
	 * @param string $s
2063
	 * @throws MWException
2064
	 * @return LinkHolderArray
2065
	 *
2066
	 * @private
2067
	 */
2068
	public function replaceInternalLinks2( &$s ) {
2069
		global $wgExtraInterlanguageLinkPrefixes;
2070
2071
		static $tc = false, $e1, $e1_img;
2072
		# the % is needed to support urlencoded titles as well
2073
		if ( !$tc ) {
2074
			$tc = Title::legalChars() . '#%';
2075
			# Match a link having the form [[namespace:link|alternate]]trail
2076
			$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2077
			# Match cases where there is no "]]", which might still be images
2078
			$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2079
		}
2080
2081
		$holders = new LinkHolderArray( $this );
2082
2083
		# split the entire text string on occurrences of [[
2084
		$a = StringUtils::explode( '[[', ' ' . $s );
2085
		# get the first element (all text up to first [[), and remove the space we added
2086
		$s = $a->current();
2087
		$a->next();
2088
		$line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2089
		$s = substr( $s, 1 );
2090
2091
		$useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2092
		$e2 = null;
2093
		if ( $useLinkPrefixExtension ) {
2094
			# Match the end of a line for a word that's not followed by whitespace,
2095
			# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2096
			global $wgContLang;
2097
			$charset = $wgContLang->linkPrefixCharset();
2098
			$e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2099
		}
2100
2101
		if ( is_null( $this->mTitle ) ) {
2102
			throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2103
		}
2104
		$nottalk = !$this->mTitle->isTalkPage();
2105
2106 View Code Duplication
		if ( $useLinkPrefixExtension ) {
2107
			$m = [];
2108
			if ( preg_match( $e2, $s, $m ) ) {
2109
				$first_prefix = $m[2];
2110
			} else {
2111
				$first_prefix = false;
2112
			}
2113
		} else {
2114
			$prefix = '';
2115
		}
2116
2117
		$useSubpages = $this->areSubpagesAllowed();
2118
2119
		// @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2120
		# Loop for each link
2121
		for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2122
			// @codingStandardsIgnoreEnd
2123
2124
			# Check for excessive memory usage
2125
			if ( $holders->isBig() ) {
2126
				# Too big
2127
				# Do the existence check, replace the link holders and clear the array
2128
				$holders->replace( $s );
2129
				$holders->clear();
2130
			}
2131
2132
			if ( $useLinkPrefixExtension ) {
2133 View Code Duplication
				if ( preg_match( $e2, $s, $m ) ) {
2134
					$prefix = $m[2];
2135
					$s = $m[1];
2136
				} else {
2137
					$prefix = '';
2138
				}
2139
				# first link
2140
				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...
2141
					$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...
2142
					$first_prefix = false;
2143
				}
2144
			}
2145
2146
			$might_be_img = false;
2147
2148
			if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2149
				$text = $m[2];
2150
				# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2151
				# [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2152
				# the real problem is with the $e1 regex
2153
				# See bug 1300.
2154
				# Still some problems for cases where the ] is meant to be outside punctuation,
2155
				# and no image is in sight. See bug 2095.
2156
				if ( $text !== ''
2157
					&& substr( $m[3], 0, 1 ) === ']'
2158
					&& strpos( $text, '[' ) !== false
2159
				) {
2160
					$text .= ']'; # so that replaceExternalLinks($text) works later
2161
					$m[3] = substr( $m[3], 1 );
2162
				}
2163
				# fix up urlencoded title texts
2164
				if ( strpos( $m[1], '%' ) !== false ) {
2165
					# Should anchors '#' also be rejected?
2166
					$m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2167
				}
2168
				$trail = $m[3];
2169
			} elseif ( preg_match( $e1_img, $line, $m ) ) {
2170
				# Invalid, but might be an image with a link in its caption
2171
				$might_be_img = true;
2172
				$text = $m[2];
2173
				if ( strpos( $m[1], '%' ) !== false ) {
2174
					$m[1] = rawurldecode( $m[1] );
2175
				}
2176
				$trail = "";
2177
			} else { # Invalid form; output directly
2178
				$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...
2179
				continue;
2180
			}
2181
2182
			$origLink = $m[1];
2183
2184
			# Don't allow internal links to pages containing
2185
			# PROTO: where PROTO is a valid URL protocol; these
2186
			# should be external links.
2187
			if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2188
				$s .= $prefix . '[[' . $line;
2189
				continue;
2190
			}
2191
2192
			# Make subpage if necessary
2193
			if ( $useSubpages ) {
2194
				$link = $this->maybeDoSubpageLink( $origLink, $text );
2195
			} else {
2196
				$link = $origLink;
2197
			}
2198
2199
			$noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2200
			if ( !$noforce ) {
2201
				# Strip off leading ':'
2202
				$link = substr( $link, 1 );
2203
			}
2204
2205
			$unstrip = $this->mStripState->unstripNoWiki( $link );
2206
			$nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2207
			if ( $nt === null ) {
2208
				$s .= $prefix . '[[' . $line;
2209
				continue;
2210
			}
2211
2212
			$ns = $nt->getNamespace();
2213
			$iw = $nt->getInterwiki();
2214
2215
			if ( $might_be_img ) { # if this is actually an invalid link
2216
				if ( $ns == NS_FILE && $noforce ) { # but might be an image
2217
					$found = false;
2218
					while ( true ) {
2219
						# look at the next 'line' to see if we can close it there
2220
						$a->next();
2221
						$next_line = $a->current();
2222
						if ( $next_line === false || $next_line === null ) {
2223
							break;
2224
						}
2225
						$m = explode( ']]', $next_line, 3 );
2226
						if ( count( $m ) == 3 ) {
2227
							# the first ]] closes the inner link, the second the image
2228
							$found = true;
2229
							$text .= "[[{$m[0]}]]{$m[1]}";
2230
							$trail = $m[2];
2231
							break;
2232
						} elseif ( count( $m ) == 2 ) {
2233
							# if there's exactly one ]] that's fine, we'll keep looking
2234
							$text .= "[[{$m[0]}]]{$m[1]}";
2235
						} else {
2236
							# if $next_line is invalid too, we need look no further
2237
							$text .= '[[' . $next_line;
2238
							break;
2239
						}
2240
					}
2241
					if ( !$found ) {
2242
						# we couldn't find the end of this imageLink, so output it raw
2243
						# but don't ignore what might be perfectly normal links in the text we've examined
2244
						$holders->merge( $this->replaceInternalLinks2( $text ) );
2245
						$s .= "{$prefix}[[$link|$text";
2246
						# note: no $trail, because without an end, there *is* no trail
2247
						continue;
2248
					}
2249
				} else { # it's not an image, so output it raw
2250
					$s .= "{$prefix}[[$link|$text";
2251
					# note: no $trail, because without an end, there *is* no trail
2252
					continue;
2253
				}
2254
			}
2255
2256
			$wasblank = ( $text == '' );
2257
			if ( $wasblank ) {
2258
				$text = $link;
2259
			} else {
2260
				# Bug 4598 madness. Handle the quotes only if they come from the alternate part
2261
				# [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2262
				# [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2263
				#    -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2264
				$text = $this->doQuotes( $text );
2265
			}
2266
2267
			# Link not escaped by : , create the various objects
2268
			if ( $noforce && !$nt->wasLocalInterwiki() ) {
2269
				# Interwikis
2270
				if (
2271
					$iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2272
						Language::fetchLanguageName( $iw, null, 'mw' ) ||
2273
						in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2274
					)
2275
				) {
2276
					# Bug 24502: filter duplicates
2277
					if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2278
						$this->mLangLinkLanguages[$iw] = true;
2279
						$this->mOutput->addLanguageLink( $nt->getFullText() );
2280
					}
2281
2282
					$s = rtrim( $s . $prefix );
2283
					$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2284
					continue;
2285
				}
2286
2287
				if ( $ns == NS_FILE ) {
2288
					if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2289
						if ( $wasblank ) {
2290
							# if no parameters were passed, $text
2291
							# becomes something like "File:Foo.png",
2292
							# which we don't want to pass on to the
2293
							# image generator
2294
							$text = '';
2295
						} else {
2296
							# recursively parse links inside the image caption
2297
							# actually, this will parse them in any other parameters, too,
2298
							# but it might be hard to fix that, and it doesn't matter ATM
2299
							$text = $this->replaceExternalLinks( $text );
2300
							$holders->merge( $this->replaceInternalLinks2( $text ) );
2301
						}
2302
						# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2303
						$s .= $prefix . $this->armorLinks(
2304
							$this->makeImage( $nt, $text, $holders ) ) . $trail;
2305
						continue;
2306
					}
2307
				} elseif ( $ns == NS_CATEGORY ) {
2308
					$s = rtrim( $s . "\n" ); # bug 87
2309
2310
					if ( $wasblank ) {
2311
						$sortkey = $this->getDefaultSort();
2312
					} else {
2313
						$sortkey = $text;
2314
					}
2315
					$sortkey = Sanitizer::decodeCharReferences( $sortkey );
2316
					$sortkey = str_replace( "\n", '', $sortkey );
2317
					$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2318
					$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2319
2320
					/**
2321
					 * Strip the whitespace Category links produce, see bug 87
2322
					 */
2323
					$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2324
2325
					continue;
2326
				}
2327
			}
2328
2329
			# Self-link checking. For some languages, variants of the title are checked in
2330
			# LinkHolderArray::doVariants() to allow batching the existence checks necessary
2331
			# for linking to a different variant.
2332
			if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2333
				$s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2334
				continue;
2335
			}
2336
2337
			# NS_MEDIA is a pseudo-namespace for linking directly to a file
2338
			# @todo FIXME: Should do batch file existence checks, see comment below
2339
			if ( $ns == NS_MEDIA ) {
2340
				# Give extensions a chance to select the file revision for us
2341
				$options = [];
2342
				$descQuery = false;
2343
				Hooks::run( 'BeforeParserFetchFileAndTitle',
2344
					[ $this, $nt, &$options, &$descQuery ] );
2345
				# Fetch and register the file (file title may be different via hooks)
2346
				list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2347
				# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2348
				$s .= $prefix . $this->armorLinks(
2349
					Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2350
				continue;
2351
			}
2352
2353
			# Some titles, such as valid special pages or files in foreign repos, should
2354
			# be shown as bluelinks even though they're not included in the page table
2355
			# @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2356
			# batch file existence checks for NS_FILE and NS_MEDIA
2357
			if ( $iw == '' && $nt->isAlwaysKnown() ) {
2358
				$this->mOutput->addLink( $nt );
2359
				$s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2360
			} else {
2361
				# Links will be added to the output link list after checking
2362
				$s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2363
			}
2364
		}
2365
		return $holders;
2366
	}
2367
2368
	/**
2369
	 * Render a forced-blue link inline; protect against double expansion of
2370
	 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2371
	 * Since this little disaster has to split off the trail text to avoid
2372
	 * breaking URLs in the following text without breaking trails on the
2373
	 * wiki links, it's been made into a horrible function.
2374
	 *
2375
	 * @param Title $nt
2376
	 * @param string $text
2377
	 * @param string $trail
2378
	 * @param string $prefix
2379
	 * @return string HTML-wikitext mix oh yuck
2380
	 */
2381
	protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2382
		list( $inside, $trail ) = Linker::splitTrail( $trail );
2383
2384
		if ( $text == '' ) {
2385
			$text = htmlspecialchars( $nt->getPrefixedText() );
2386
		}
2387
2388
		$link = $this->getLinkRenderer()->makeKnownLink(
2389
			$nt, new HtmlArmor( "$prefix$text$inside" )
2390
		);
2391
2392
		return $this->armorLinks( $link ) . $trail;
2393
	}
2394
2395
	/**
2396
	 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2397
	 * going to go through further parsing steps before inline URL expansion.
2398
	 *
2399
	 * Not needed quite as much as it used to be since free links are a bit
2400
	 * more sensible these days. But bracketed links are still an issue.
2401
	 *
2402
	 * @param string $text More-or-less HTML
2403
	 * @return string Less-or-more HTML with NOPARSE bits
2404
	 */
2405
	public function armorLinks( $text ) {
2406
		return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2407
			self::MARKER_PREFIX . "NOPARSE$1", $text );
2408
	}
2409
2410
	/**
2411
	 * Return true if subpage links should be expanded on this page.
2412
	 * @return bool
2413
	 */
2414
	public function areSubpagesAllowed() {
2415
		# Some namespaces don't allow subpages
2416
		return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2417
	}
2418
2419
	/**
2420
	 * Handle link to subpage if necessary
2421
	 *
2422
	 * @param string $target The source of the link
2423
	 * @param string &$text The link text, modified as necessary
2424
	 * @return string The full name of the link
2425
	 * @private
2426
	 */
2427
	public function maybeDoSubpageLink( $target, &$text ) {
2428
		return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2429
	}
2430
2431
	/**
2432
	 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
2433
	 *
2434
	 * @param string $text
2435
	 * @param bool $linestart Whether or not this is at the start of a line.
2436
	 * @private
2437
	 * @return string The lists rendered as HTML
2438
	 */
2439
	public function doBlockLevels( $text, $linestart ) {
2440
		return BlockLevelPass::doBlockLevels( $text, $linestart );
2441
	}
2442
2443
	/**
2444
	 * Return value of a magic variable (like PAGENAME)
2445
	 *
2446
	 * @private
2447
	 *
2448
	 * @param int $index
2449
	 * @param bool|PPFrame $frame
2450
	 *
2451
	 * @throws MWException
2452
	 * @return string
2453
	 */
2454
	public function getVariableValue( $index, $frame = false ) {
2455
		global $wgContLang, $wgSitename, $wgServer, $wgServerName;
2456
		global $wgArticlePath, $wgScriptPath, $wgStylePath;
2457
2458
		if ( is_null( $this->mTitle ) ) {
2459
			// If no title set, bad things are going to happen
2460
			// later. Title should always be set since this
2461
			// should only be called in the middle of a parse
2462
			// operation (but the unit-tests do funky stuff)
2463
			throw new MWException( __METHOD__ . ' Should only be '
2464
				. ' called while parsing (no title set)' );
2465
		}
2466
2467
		/**
2468
		 * Some of these require message or data lookups and can be
2469
		 * expensive to check many times.
2470
		 */
2471
		if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
2472
			if ( isset( $this->mVarCache[$index] ) ) {
2473
				return $this->mVarCache[$index];
2474
			}
2475
		}
2476
2477
		$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2478
		Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
2479
2480
		$pageLang = $this->getFunctionLang();
2481
2482
		switch ( $index ) {
2483
			case '!':
2484
				$value = '|';
2485
				break;
2486
			case 'currentmonth':
2487
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2488
				break;
2489
			case 'currentmonth1':
2490
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2491
				break;
2492
			case 'currentmonthname':
2493
				$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2494
				break;
2495
			case 'currentmonthnamegen':
2496
				$value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2497
				break;
2498
			case 'currentmonthabbrev':
2499
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2500
				break;
2501
			case 'currentday':
2502
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2503
				break;
2504
			case 'currentday2':
2505
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2506
				break;
2507
			case 'localmonth':
2508
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2509
				break;
2510
			case 'localmonth1':
2511
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2512
				break;
2513
			case 'localmonthname':
2514
				$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2515
				break;
2516
			case 'localmonthnamegen':
2517
				$value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2518
				break;
2519
			case 'localmonthabbrev':
2520
				$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2521
				break;
2522
			case 'localday':
2523
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2524
				break;
2525
			case 'localday2':
2526
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2527
				break;
2528
			case 'pagename':
2529
				$value = wfEscapeWikiText( $this->mTitle->getText() );
2530
				break;
2531
			case 'pagenamee':
2532
				$value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2533
				break;
2534
			case 'fullpagename':
2535
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2536
				break;
2537
			case 'fullpagenamee':
2538
				$value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2539
				break;
2540
			case 'subpagename':
2541
				$value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2542
				break;
2543
			case 'subpagenamee':
2544
				$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2545
				break;
2546
			case 'rootpagename':
2547
				$value = wfEscapeWikiText( $this->mTitle->getRootText() );
2548
				break;
2549 View Code Duplication
			case 'rootpagenamee':
2550
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2551
					' ',
2552
					'_',
2553
					$this->mTitle->getRootText()
2554
				) ) );
2555
				break;
2556
			case 'basepagename':
2557
				$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2558
				break;
2559 View Code Duplication
			case 'basepagenamee':
2560
				$value = wfEscapeWikiText( wfUrlencode( str_replace(
2561
					' ',
2562
					'_',
2563
					$this->mTitle->getBaseText()
2564
				) ) );
2565
				break;
2566 View Code Duplication
			case 'talkpagename':
2567
				if ( $this->mTitle->canTalk() ) {
2568
					$talkPage = $this->mTitle->getTalkPage();
2569
					$value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2570
				} else {
2571
					$value = '';
2572
				}
2573
				break;
2574 View Code Duplication
			case 'talkpagenamee':
2575
				if ( $this->mTitle->canTalk() ) {
2576
					$talkPage = $this->mTitle->getTalkPage();
2577
					$value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2578
				} else {
2579
					$value = '';
2580
				}
2581
				break;
2582
			case 'subjectpagename':
2583
				$subjPage = $this->mTitle->getSubjectPage();
2584
				$value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2585
				break;
2586
			case 'subjectpagenamee':
2587
				$subjPage = $this->mTitle->getSubjectPage();
2588
				$value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2589
				break;
2590
			case 'pageid': // requested in bug 23427
2591
				$pageid = $this->getTitle()->getArticleID();
2592
				if ( $pageid == 0 ) {
2593
					# 0 means the page doesn't exist in the database,
2594
					# which means the user is previewing a new page.
2595
					# The vary-revision flag must be set, because the magic word
2596
					# will have a different value once the page is saved.
2597
					$this->mOutput->setFlag( 'vary-revision' );
2598
					wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2599
				}
2600
				$value = $pageid ? $pageid : null;
2601
				break;
2602
			case 'revisionid':
2603
				# Let the edit saving system know we should parse the page
2604
				# *after* a revision ID has been assigned.
2605
				$this->mOutput->setFlag( 'vary-revision-id' );
2606
				wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2607
				$value = $this->mRevisionId;
2608
				if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2609
					$value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
2610
					$this->mOutput->setSpeculativeRevIdUsed( $value );
2611
				}
2612
				break;
2613 View Code Duplication
			case 'revisionday':
2614
				# Let the edit saving system know we should parse the page
2615
				# *after* a revision ID has been assigned. This is for null edits.
2616
				$this->mOutput->setFlag( 'vary-revision' );
2617
				wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2618
				$value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2619
				break;
2620 View Code Duplication
			case 'revisionday2':
2621
				# Let the edit saving system know we should parse the page
2622
				# *after* a revision ID has been assigned. This is for null edits.
2623
				$this->mOutput->setFlag( 'vary-revision' );
2624
				wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2625
				$value = substr( $this->getRevisionTimestamp(), 6, 2 );
2626
				break;
2627 View Code Duplication
			case 'revisionmonth':
2628
				# Let the edit saving system know we should parse the page
2629
				# *after* a revision ID has been assigned. This is for null edits.
2630
				$this->mOutput->setFlag( 'vary-revision' );
2631
				wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2632
				$value = substr( $this->getRevisionTimestamp(), 4, 2 );
2633
				break;
2634 View Code Duplication
			case 'revisionmonth1':
2635
				# Let the edit saving system know we should parse the page
2636
				# *after* a revision ID has been assigned. This is for null edits.
2637
				$this->mOutput->setFlag( 'vary-revision' );
2638
				wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2639
				$value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2640
				break;
2641 View Code Duplication
			case 'revisionyear':
2642
				# Let the edit saving system know we should parse the page
2643
				# *after* a revision ID has been assigned. This is for null edits.
2644
				$this->mOutput->setFlag( 'vary-revision' );
2645
				wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2646
				$value = substr( $this->getRevisionTimestamp(), 0, 4 );
2647
				break;
2648
			case 'revisiontimestamp':
2649
				# Let the edit saving system know we should parse the page
2650
				# *after* a revision ID has been assigned. This is for null edits.
2651
				$this->mOutput->setFlag( 'vary-revision' );
2652
				wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2653
				$value = $this->getRevisionTimestamp();
2654
				break;
2655
			case 'revisionuser':
2656
				# Let the edit saving system know we should parse the page
2657
				# *after* a revision ID has been assigned for null edits.
2658
				$this->mOutput->setFlag( 'vary-user' );
2659
				wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2660
				$value = $this->getRevisionUser();
2661
				break;
2662
			case 'revisionsize':
2663
				$value = $this->getRevisionSize();
2664
				break;
2665
			case 'namespace':
2666
				$value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2667
				break;
2668
			case 'namespacee':
2669
				$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2670
				break;
2671
			case 'namespacenumber':
2672
				$value = $this->mTitle->getNamespace();
2673
				break;
2674
			case 'talkspace':
2675
				$value = $this->mTitle->canTalk()
2676
					? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2677
					: '';
2678
				break;
2679
			case 'talkspacee':
2680
				$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2681
				break;
2682
			case 'subjectspace':
2683
				$value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2684
				break;
2685
			case 'subjectspacee':
2686
				$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2687
				break;
2688
			case 'currentdayname':
2689
				$value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2690
				break;
2691
			case 'currentyear':
2692
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2693
				break;
2694
			case 'currenttime':
2695
				$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...
2696
				break;
2697
			case 'currenthour':
2698
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2699
				break;
2700
			case 'currentweek':
2701
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2702
				# int to remove the padding
2703
				$value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2704
				break;
2705
			case 'currentdow':
2706
				$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2707
				break;
2708
			case 'localdayname':
2709
				$value = $pageLang->getWeekdayName(
2710
					(int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2711
				);
2712
				break;
2713
			case 'localyear':
2714
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2715
				break;
2716
			case 'localtime':
2717
				$value = $pageLang->time(
2718
					MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2719
					false,
2720
					false
2721
				);
2722
				break;
2723
			case 'localhour':
2724
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2725
				break;
2726
			case 'localweek':
2727
				# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2728
				# int to remove the padding
2729
				$value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2730
				break;
2731
			case 'localdow':
2732
				$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2733
				break;
2734
			case 'numberofarticles':
2735
				$value = $pageLang->formatNum( SiteStats::articles() );
2736
				break;
2737
			case 'numberoffiles':
2738
				$value = $pageLang->formatNum( SiteStats::images() );
2739
				break;
2740
			case 'numberofusers':
2741
				$value = $pageLang->formatNum( SiteStats::users() );
2742
				break;
2743
			case 'numberofactiveusers':
2744
				$value = $pageLang->formatNum( SiteStats::activeUsers() );
2745
				break;
2746
			case 'numberofpages':
2747
				$value = $pageLang->formatNum( SiteStats::pages() );
2748
				break;
2749
			case 'numberofadmins':
2750
				$value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2751
				break;
2752
			case 'numberofedits':
2753
				$value = $pageLang->formatNum( SiteStats::edits() );
2754
				break;
2755
			case 'currenttimestamp':
2756
				$value = wfTimestamp( TS_MW, $ts );
2757
				break;
2758
			case 'localtimestamp':
2759
				$value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2760
				break;
2761
			case 'currentversion':
2762
				$value = SpecialVersion::getVersion();
2763
				break;
2764
			case 'articlepath':
2765
				return $wgArticlePath;
2766
			case 'sitename':
2767
				return $wgSitename;
2768
			case 'server':
2769
				return $wgServer;
2770
			case 'servername':
2771
				return $wgServerName;
2772
			case 'scriptpath':
2773
				return $wgScriptPath;
2774
			case 'stylepath':
2775
				return $wgStylePath;
2776
			case 'directionmark':
2777
				return $pageLang->getDirMark();
2778
			case 'contentlanguage':
2779
				global $wgLanguageCode;
2780
				return $wgLanguageCode;
2781
			case 'cascadingsources':
2782
				$value = CoreParserFunctions::cascadingsources( $this );
2783
				break;
2784
			default:
2785
				$ret = null;
2786
				Hooks::run(
2787
					'ParserGetVariableValueSwitch',
2788
					[ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
2789
				);
2790
2791
				return $ret;
2792
		}
2793
2794
		if ( $index ) {
2795
			$this->mVarCache[$index] = $value;
2796
		}
2797
2798
		return $value;
2799
	}
2800
2801
	/**
2802
	 * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
2803
	 *
2804
	 * @private
2805
	 */
2806
	public function initialiseVariables() {
2807
		$variableIDs = MagicWord::getVariableIDs();
2808
		$substIDs = MagicWord::getSubstIDs();
2809
2810
		$this->mVariables = new MagicWordArray( $variableIDs );
2811
		$this->mSubstWords = new MagicWordArray( $substIDs );
2812
	}
2813
2814
	/**
2815
	 * Preprocess some wikitext and return the document tree.
2816
	 * This is the ghost of replace_variables().
2817
	 *
2818
	 * @param string $text The text to parse
2819
	 * @param int $flags Bitwise combination of:
2820
	 *   - self::PTD_FOR_INCLUSION: Handle "<noinclude>" and "<includeonly>" as if the text is being
2821
	 *     included. Default is to assume a direct page view.
2822
	 *
2823
	 * The generated DOM tree must depend only on the input text and the flags.
2824
	 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
2825
	 *
2826
	 * Any flag added to the $flags parameter here, or any other parameter liable to cause a
2827
	 * change in the DOM tree for a given text, must be passed through the section identifier
2828
	 * in the section edit link and thus back to extractSections().
2829
	 *
2830
	 * The output of this function is currently only cached in process memory, but a persistent
2831
	 * cache may be implemented at a later date which takes further advantage of these strict
2832
	 * dependency requirements.
2833
	 *
2834
	 * @return PPNode
2835
	 */
2836
	public function preprocessToDom( $text, $flags = 0 ) {
2837
		$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2838
		return $dom;
2839
	}
2840
2841
	/**
2842
	 * Return a three-element array: leading whitespace, string contents, trailing whitespace
2843
	 *
2844
	 * @param string $s
2845
	 *
2846
	 * @return array
2847
	 */
2848
	public static function splitWhitespace( $s ) {
2849
		$ltrimmed = ltrim( $s );
2850
		$w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2851
		$trimmed = rtrim( $ltrimmed );
2852
		$diff = strlen( $ltrimmed ) - strlen( $trimmed );
2853
		if ( $diff > 0 ) {
2854
			$w2 = substr( $ltrimmed, -$diff );
2855
		} else {
2856
			$w2 = '';
2857
		}
2858
		return [ $w1, $trimmed, $w2 ];
2859
	}
2860
2861
	/**
2862
	 * Replace magic variables, templates, and template arguments
2863
	 * with the appropriate text. Templates are substituted recursively,
2864
	 * taking care to avoid infinite loops.
2865
	 *
2866
	 * Note that the substitution depends on value of $mOutputType:
2867
	 *  self::OT_WIKI: only {{subst:}} templates
2868
	 *  self::OT_PREPROCESS: templates but not extension tags
2869
	 *  self::OT_HTML: all templates and extension tags
2870
	 *
2871
	 * @param string $text The text to transform
2872
	 * @param bool|PPFrame $frame Object describing the arguments passed to the
2873
	 *   template. Arguments may also be provided as an associative array, as
2874
	 *   was the usual case before MW1.12. Providing arguments this way may be
2875
	 *   useful for extensions wishing to perform variable replacement
2876
	 *   explicitly.
2877
	 * @param bool $argsOnly Only do argument (triple-brace) expansion, not
2878
	 *   double-brace expansion.
2879
	 * @return string
2880
	 */
2881
	public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2882
		# Is there any text? Also, Prevent too big inclusions!
2883
		$textSize = strlen( $text );
2884
		if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2885
			return $text;
2886
		}
2887
2888
		if ( $frame === false ) {
2889
			$frame = $this->getPreprocessor()->newFrame();
2890
		} elseif ( !( $frame instanceof PPFrame ) ) {
2891
			wfDebug( __METHOD__ . " called using plain parameters instead of "
2892
				. "a PPFrame instance. Creating custom frame.\n" );
2893
			$frame = $this->getPreprocessor()->newCustomFrame( $frame );
2894
		}
2895
2896
		$dom = $this->preprocessToDom( $text );
2897
		$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2898
		$text = $frame->expand( $dom, $flags );
2899
2900
		return $text;
2901
	}
2902
2903
	/**
2904
	 * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
2905
	 *
2906
	 * @param array $args
2907
	 *
2908
	 * @return array
2909
	 */
2910
	public static function createAssocArgs( $args ) {
2911
		$assocArgs = [];
2912
		$index = 1;
2913
		foreach ( $args as $arg ) {
2914
			$eqpos = strpos( $arg, '=' );
2915
			if ( $eqpos === false ) {
2916
				$assocArgs[$index++] = $arg;
2917
			} else {
2918
				$name = trim( substr( $arg, 0, $eqpos ) );
2919
				$value = trim( substr( $arg, $eqpos + 1 ) );
2920
				if ( $value === false ) {
2921
					$value = '';
2922
				}
2923
				if ( $name !== false ) {
2924
					$assocArgs[$name] = $value;
2925
				}
2926
			}
2927
		}
2928
2929
		return $assocArgs;
2930
	}
2931
2932
	/**
2933
	 * Warn the user when a parser limitation is reached
2934
	 * Will warn at most once the user per limitation type
2935
	 *
2936
	 * The results are shown during preview and run through the Parser (See EditPage.php)
2937
	 *
2938
	 * @param string $limitationType Should be one of:
2939
	 *   'expensive-parserfunction' (corresponding messages:
2940
	 *       'expensive-parserfunction-warning',
2941
	 *       'expensive-parserfunction-category')
2942
	 *   'post-expand-template-argument' (corresponding messages:
2943
	 *       'post-expand-template-argument-warning',
2944
	 *       'post-expand-template-argument-category')
2945
	 *   'post-expand-template-inclusion' (corresponding messages:
2946
	 *       'post-expand-template-inclusion-warning',
2947
	 *       'post-expand-template-inclusion-category')
2948
	 *   'node-count-exceeded' (corresponding messages:
2949
	 *       'node-count-exceeded-warning',
2950
	 *       'node-count-exceeded-category')
2951
	 *   'expansion-depth-exceeded' (corresponding messages:
2952
	 *       'expansion-depth-exceeded-warning',
2953
	 *       'expansion-depth-exceeded-category')
2954
	 * @param string|int|null $current Current value
2955
	 * @param string|int|null $max Maximum allowed, when an explicit limit has been
2956
	 *	 exceeded, provide the values (optional)
2957
	 */
2958
	public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2959
		# does no harm if $current and $max are present but are unnecessary for the message
2960
		# Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2961
		# only during preview, and that would split the parser cache unnecessarily.
2962
		$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2963
			->text();
2964
		$this->mOutput->addWarning( $warning );
2965
		$this->addTrackingCategory( "$limitationType-category" );
2966
	}
2967
2968
	/**
2969
	 * Return the text of a template, after recursively
2970
	 * replacing any variables or templates within the template.
2971
	 *
2972
	 * @param array $piece The parts of the template
2973
	 *   $piece['title']: the title, i.e. the part before the |
2974
	 *   $piece['parts']: the parameter array
2975
	 *   $piece['lineStart']: whether the brace was at the start of a line
2976
	 * @param PPFrame $frame The current frame, contains template arguments
2977
	 * @throws Exception
2978
	 * @return string The text of the template
2979
	 */
2980
	public function braceSubstitution( $piece, $frame ) {
2981
2982
		// Flags
2983
2984
		// $text has been filled
2985
		$found = false;
2986
		// wiki markup in $text should be escaped
2987
		$nowiki = false;
2988
		// $text is HTML, armour it against wikitext transformation
2989
		$isHTML = false;
2990
		// Force interwiki transclusion to be done in raw mode not rendered
2991
		$forceRawInterwiki = false;
2992
		// $text is a DOM node needing expansion in a child frame
2993
		$isChildObj = false;
2994
		// $text is a DOM node needing expansion in the current frame
2995
		$isLocalObj = false;
2996
2997
		# Title object, where $text came from
2998
		$title = false;
2999
3000
		# $part1 is the bit before the first |, and must contain only title characters.
3001
		# Various prefixes will be stripped from it later.
3002
		$titleWithSpaces = $frame->expand( $piece['title'] );
3003
		$part1 = trim( $titleWithSpaces );
3004
		$titleText = false;
3005
3006
		# Original title text preserved for various purposes
3007
		$originalTitle = $part1;
3008
3009
		# $args is a list of argument nodes, starting from index 0, not including $part1
3010
		# @todo FIXME: If piece['parts'] is null then the call to getLength()
3011
		# below won't work b/c this $args isn't an object
3012
		$args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3013
3014
		$profileSection = null; // profile templates
3015
3016
		# SUBST
3017
		if ( !$found ) {
3018
			$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3019
3020
			# Possibilities for substMatch: "subst", "safesubst" or FALSE
3021
			# Decide whether to expand template or keep wikitext as-is.
3022
			if ( $this->ot['wiki'] ) {
3023
				if ( $substMatch === false ) {
3024
					$literal = true;  # literal when in PST with no prefix
3025
				} else {
3026
					$literal = false; # expand when in PST with subst: or safesubst:
3027
				}
3028
			} else {
3029
				if ( $substMatch == 'subst' ) {
3030
					$literal = true;  # literal when not in PST with plain subst:
3031
				} else {
3032
					$literal = false; # expand when not in PST with safesubst: or no prefix
3033
				}
3034
			}
3035
			if ( $literal ) {
3036
				$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3037
				$isLocalObj = true;
3038
				$found = true;
3039
			}
3040
		}
3041
3042
		# Variables
3043
		if ( !$found && $args->getLength() == 0 ) {
3044
			$id = $this->mVariables->matchStartToEnd( $part1 );
3045
			if ( $id !== false ) {
3046
				$text = $this->getVariableValue( $id, $frame );
3047
				if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3048
					$this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3049
				}
3050
				$found = true;
3051
			}
3052
		}
3053
3054
		# MSG, MSGNW and RAW
3055
		if ( !$found ) {
3056
			# Check for MSGNW:
3057
			$mwMsgnw = MagicWord::get( 'msgnw' );
3058
			if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3059
				$nowiki = true;
3060
			} else {
3061
				# Remove obsolete MSG:
3062
				$mwMsg = MagicWord::get( 'msg' );
3063
				$mwMsg->matchStartAndRemove( $part1 );
3064
			}
3065
3066
			# Check for RAW:
3067
			$mwRaw = MagicWord::get( 'raw' );
3068
			if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3069
				$forceRawInterwiki = true;
3070
			}
3071
		}
3072
3073
		# Parser functions
3074
		if ( !$found ) {
3075
			$colonPos = strpos( $part1, ':' );
3076
			if ( $colonPos !== false ) {
3077
				$func = substr( $part1, 0, $colonPos );
3078
				$funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3079
				$argsLength = $args->getLength();
3080
				for ( $i = 0; $i < $argsLength; $i++ ) {
3081
					$funcArgs[] = $args->item( $i );
3082
				}
3083
				try {
3084
					$result = $this->callParserFunction( $frame, $func, $funcArgs );
3085
				} catch ( Exception $ex ) {
3086
					throw $ex;
3087
				}
3088
3089
				# The interface for parser functions allows for extracting
3090
				# flags into the local scope. Extract any forwarded flags
3091
				# here.
3092
				extract( $result );
3093
			}
3094
		}
3095
3096
		# Finish mangling title and then check for loops.
3097
		# Set $title to a Title object and $titleText to the PDBK
3098
		if ( !$found ) {
3099
			$ns = NS_TEMPLATE;
3100
			# Split the title into page and subpage
3101
			$subpage = '';
3102
			$relative = $this->maybeDoSubpageLink( $part1, $subpage );
3103
			if ( $part1 !== $relative ) {
3104
				$part1 = $relative;
3105
				$ns = $this->mTitle->getNamespace();
3106
			}
3107
			$title = Title::newFromText( $part1, $ns );
3108
			if ( $title ) {
3109
				$titleText = $title->getPrefixedText();
3110
				# Check for language variants if the template is not found
3111
				if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3112
					$this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3113
				}
3114
				# Do recursion depth check
3115
				$limit = $this->mOptions->getMaxTemplateDepth();
3116 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...
3117
					$found = true;
3118
					$text = '<span class="error">'
3119
						. wfMessage( 'parser-template-recursion-depth-warning' )
3120
							->numParams( $limit )->inContentLanguage()->text()
3121
						. '</span>';
3122
				}
3123
			}
3124
		}
3125
3126
		# Load from database
3127
		if ( !$found && $title ) {
3128
			$profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3129
			if ( !$title->isExternal() ) {
3130
				if ( $title->isSpecialPage()
3131
					&& $this->mOptions->getAllowSpecialInclusion()
3132
					&& $this->ot['html']
3133
				) {
3134
					$specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3135
					// Pass the template arguments as URL parameters.
3136
					// "uselang" will have no effect since the Language object
3137
					// is forced to the one defined in ParserOptions.
3138
					$pageArgs = [];
3139
					$argsLength = $args->getLength();
3140
					for ( $i = 0; $i < $argsLength; $i++ ) {
3141
						$bits = $args->item( $i )->splitArg();
3142
						if ( strval( $bits['index'] ) === '' ) {
3143
							$name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3144
							$value = trim( $frame->expand( $bits['value'] ) );
3145
							$pageArgs[$name] = $value;
3146
						}
3147
					}
3148
3149
					// Create a new context to execute the special page
3150
					$context = new RequestContext;
3151
					$context->setTitle( $title );
3152
					$context->setRequest( new FauxRequest( $pageArgs ) );
3153
					if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3154
						$context->setUser( $this->getUser() );
3155
					} else {
3156
						// If this page is cached, then we better not be per user.
3157
						$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...
3158
					}
3159
					$context->setLanguage( $this->mOptions->getUserLangObj() );
3160
					$ret = SpecialPageFactory::capturePath( $title, $context, $this->getLinkRenderer() );
3161
					if ( $ret ) {
3162
						$text = $context->getOutput()->getHTML();
3163
						$this->mOutput->addOutputPageMetadata( $context->getOutput() );
3164
						$found = true;
3165
						$isHTML = true;
3166
						if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3167
							$this->mOutput->updateCacheExpiry( $specialPage->maxIncludeCacheTime() );
3168
						}
3169
					}
3170
				} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3171
					$found = false; # access denied
3172
					wfDebug( __METHOD__ . ": template inclusion denied for " .
3173
						$title->getPrefixedDBkey() . "\n" );
3174
				} else {
3175
					list( $text, $title ) = $this->getTemplateDom( $title );
3176
					if ( $text !== false ) {
3177
						$found = true;
3178
						$isChildObj = true;
3179
					}
3180
				}
3181
3182
				# If the title is valid but undisplayable, make a link to it
3183
				if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3184
					$text = "[[:$titleText]]";
3185
					$found = true;
3186
				}
3187
			} elseif ( $title->isTrans() ) {
3188
				# Interwiki transclusion
3189
				if ( $this->ot['html'] && !$forceRawInterwiki ) {
3190
					$text = $this->interwikiTransclude( $title, 'render' );
3191
					$isHTML = true;
3192
				} else {
3193
					$text = $this->interwikiTransclude( $title, 'raw' );
3194
					# Preprocess it like a template
3195
					$text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3196
					$isChildObj = true;
3197
				}
3198
				$found = true;
3199
			}
3200
3201
			# Do infinite loop check
3202
			# This has to be done after redirect resolution to avoid infinite loops via redirects
3203
			if ( !$frame->loopCheck( $title ) ) {
3204
				$found = true;
3205
				$text = '<span class="error">'
3206
					. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3207
					. '</span>';
3208
				wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3209
			}
3210
		}
3211
3212
		# If we haven't found text to substitute by now, we're done
3213
		# Recover the source wikitext and return it
3214
		if ( !$found ) {
3215
			$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3216
			if ( $profileSection ) {
3217
				$this->mProfiler->scopedProfileOut( $profileSection );
3218
			}
3219
			return [ 'object' => $text ];
3220
		}
3221
3222
		# Expand DOM-style return values in a child frame
3223
		if ( $isChildObj ) {
3224
			# Clean up argument array
3225
			$newFrame = $frame->newChild( $args, $title );
3226
3227
			if ( $nowiki ) {
3228
				$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3229
			} elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3230
				# Expansion is eligible for the empty-frame cache
3231
				$text = $newFrame->cachedExpand( $titleText, $text );
3232
			} else {
3233
				# Uncached expansion
3234
				$text = $newFrame->expand( $text );
3235
			}
3236
		}
3237
		if ( $isLocalObj && $nowiki ) {
3238
			$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3239
			$isLocalObj = false;
3240
		}
3241
3242
		if ( $profileSection ) {
3243
			$this->mProfiler->scopedProfileOut( $profileSection );
3244
		}
3245
3246
		# Replace raw HTML by a placeholder
3247
		if ( $isHTML ) {
3248
			$text = $this->insertStripItem( $text );
3249
		} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3250
			# Escape nowiki-style return values
3251
			$text = wfEscapeWikiText( $text );
3252
		} elseif ( is_string( $text )
3253
			&& !$piece['lineStart']
3254
			&& preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3255
		) {
3256
			# Bug 529: if the template begins with a table or block-level
3257
			# element, it should be treated as beginning a new line.
3258
			# This behavior is somewhat controversial.
3259
			$text = "\n" . $text;
3260
		}
3261
3262
		if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3263
			# Error, oversize inclusion
3264
			if ( $titleText !== false ) {
3265
				# Make a working, properly escaped link if possible (bug 23588)
3266
				$text = "[[:$titleText]]";
3267
			} else {
3268
				# This will probably not be a working link, but at least it may
3269
				# provide some hint of where the problem is
3270
				preg_replace( '/^:/', '', $originalTitle );
3271
				$text = "[[:$originalTitle]]";
3272
			}
3273
			$text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3274
				. 'post-expand include size too large -->' );
3275
			$this->limitationWarn( 'post-expand-template-inclusion' );
3276
		}
3277
3278
		if ( $isLocalObj ) {
3279
			$ret = [ 'object' => $text ];
3280
		} else {
3281
			$ret = [ 'text' => $text ];
3282
		}
3283
3284
		return $ret;
3285
	}
3286
3287
	/**
3288
	 * Call a parser function and return an array with text and flags.
3289
	 *
3290
	 * The returned array will always contain a boolean 'found', indicating
3291
	 * whether the parser function was found or not. It may also contain the
3292
	 * following:
3293
	 *  text: string|object, resulting wikitext or PP DOM object
3294
	 *  isHTML: bool, $text is HTML, armour it against wikitext transformation
3295
	 *  isChildObj: bool, $text is a DOM node needing expansion in a child frame
3296
	 *  isLocalObj: bool, $text is a DOM node needing expansion in the current frame
3297
	 *  nowiki: bool, wiki markup in $text should be escaped
3298
	 *
3299
	 * @since 1.21
3300
	 * @param PPFrame $frame The current frame, contains template arguments
3301
	 * @param string $function Function name
3302
	 * @param array $args Arguments to the function
3303
	 * @throws MWException
3304
	 * @return array
3305
	 */
3306
	public function callParserFunction( $frame, $function, array $args = [] ) {
3307
		global $wgContLang;
3308
3309
		# Case sensitive functions
3310
		if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3311
			$function = $this->mFunctionSynonyms[1][$function];
3312
		} else {
3313
			# Case insensitive functions
3314
			$function = $wgContLang->lc( $function );
3315
			if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3316
				$function = $this->mFunctionSynonyms[0][$function];
3317
			} else {
3318
				return [ 'found' => false ];
3319
			}
3320
		}
3321
3322
		list( $callback, $flags ) = $this->mFunctionHooks[$function];
3323
3324
		# Workaround for PHP bug 35229 and similar
3325
		if ( !is_callable( $callback ) ) {
3326
			throw new MWException( "Tag hook for $function is not callable\n" );
3327
		}
3328
3329
		$allArgs = [ &$this ];
3330
		if ( $flags & self::SFH_OBJECT_ARGS ) {
3331
			# Convert arguments to PPNodes and collect for appending to $allArgs
3332
			$funcArgs = [];
3333
			foreach ( $args as $k => $v ) {
3334
				if ( $v instanceof PPNode || $k === 0 ) {
3335
					$funcArgs[] = $v;
3336
				} else {
3337
					$funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3338
				}
3339
			}
3340
3341
			# Add a frame parameter, and pass the arguments as an array
3342
			$allArgs[] = $frame;
3343
			$allArgs[] = $funcArgs;
3344
		} else {
3345
			# Convert arguments to plain text and append to $allArgs
3346
			foreach ( $args as $k => $v ) {
3347
				if ( $v instanceof PPNode ) {
3348
					$allArgs[] = trim( $frame->expand( $v ) );
3349
				} elseif ( is_int( $k ) && $k >= 0 ) {
3350
					$allArgs[] = trim( $v );
3351
				} else {
3352
					$allArgs[] = trim( "$k=$v" );
3353
				}
3354
			}
3355
		}
3356
3357
		$result = call_user_func_array( $callback, $allArgs );
3358
3359
		# The interface for function hooks allows them to return a wikitext
3360
		# string or an array containing the string and any flags. This mungs
3361
		# things around to match what this method should return.
3362
		if ( !is_array( $result ) ) {
3363
			$result =[
3364
				'found' => true,
3365
				'text' => $result,
3366
			];
3367
		} else {
3368
			if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3369
				$result['text'] = $result[0];
3370
			}
3371
			unset( $result[0] );
3372
			$result += [
3373
				'found' => true,
3374
			];
3375
		}
3376
3377
		$noparse = true;
3378
		$preprocessFlags = 0;
3379
		if ( isset( $result['noparse'] ) ) {
3380
			$noparse = $result['noparse'];
3381
		}
3382
		if ( isset( $result['preprocessFlags'] ) ) {
3383
			$preprocessFlags = $result['preprocessFlags'];
3384
		}
3385
3386
		if ( !$noparse ) {
3387
			$result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3388
			$result['isChildObj'] = true;
3389
		}
3390
3391
		return $result;
3392
	}
3393
3394
	/**
3395
	 * Get the semi-parsed DOM representation of a template with a given title,
3396
	 * and its redirect destination title. Cached.
3397
	 *
3398
	 * @param Title $title
3399
	 *
3400
	 * @return array
3401
	 */
3402
	public function getTemplateDom( $title ) {
3403
		$cacheTitle = $title;
3404
		$titleText = $title->getPrefixedDBkey();
3405
3406
		if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3407
			list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3408
			$title = Title::makeTitle( $ns, $dbk );
3409
			$titleText = $title->getPrefixedDBkey();
3410
		}
3411
		if ( isset( $this->mTplDomCache[$titleText] ) ) {
3412
			return [ $this->mTplDomCache[$titleText], $title ];
3413
		}
3414
3415
		# Cache miss, go to the database
3416
		list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3417
3418
		if ( $text === false ) {
3419
			$this->mTplDomCache[$titleText] = false;
3420
			return [ false, $title ];
3421
		}
3422
3423
		$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3424
		$this->mTplDomCache[$titleText] = $dom;
3425
3426
		if ( !$title->equals( $cacheTitle ) ) {
3427
			$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3428
				[ $title->getNamespace(), $cdb = $title->getDBkey() ];
3429
		}
3430
3431
		return [ $dom, $title ];
3432
	}
3433
3434
	/**
3435
	 * Fetch the current revision of a given title. Note that the revision
3436
	 * (and even the title) may not exist in the database, so everything
3437
	 * contributing to the output of the parser should use this method
3438
	 * where possible, rather than getting the revisions themselves. This
3439
	 * method also caches its results, so using it benefits performance.
3440
	 *
3441
	 * @since 1.24
3442
	 * @param Title $title
3443
	 * @return Revision
3444
	 */
3445
	public function fetchCurrentRevisionOfTitle( $title ) {
3446
		$cacheKey = $title->getPrefixedDBkey();
3447
		if ( !$this->currentRevisionCache ) {
3448
			$this->currentRevisionCache = new MapCacheLRU( 100 );
3449
		}
3450
		if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3451
			$this->currentRevisionCache->set( $cacheKey,
3452
				// Defaults to Parser::statelessFetchRevision()
3453
				call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3454
			);
3455
		}
3456
		return $this->currentRevisionCache->get( $cacheKey );
3457
	}
3458
3459
	/**
3460
	 * Wrapper around Revision::newFromTitle to allow passing additional parameters
3461
	 * without passing them on to it.
3462
	 *
3463
	 * @since 1.24
3464
	 * @param Title $title
3465
	 * @param Parser|bool $parser
3466
	 * @return Revision
3467
	 */
3468
	public static function statelessFetchRevision( $title, $parser = false ) {
3469
		return Revision::newFromTitle( $title );
3470
	}
3471
3472
	/**
3473
	 * Fetch the unparsed text of a template and register a reference to it.
3474
	 * @param Title $title
3475
	 * @return array ( string or false, Title )
3476
	 */
3477
	public function fetchTemplateAndTitle( $title ) {
3478
		// Defaults to Parser::statelessFetchTemplate()
3479
		$templateCb = $this->mOptions->getTemplateCallback();
3480
		$stuff = call_user_func( $templateCb, $title, $this );
3481
		// We use U+007F DELETE to distinguish strip markers from regular text.
3482
		$text = $stuff['text'];
3483
		if ( is_string( $stuff['text'] ) ) {
3484
			$text = strtr( $text, "\x7f", "?" );
3485
		}
3486
		$finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3487
		if ( isset( $stuff['deps'] ) ) {
3488
			foreach ( $stuff['deps'] as $dep ) {
3489
				$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3490
				if ( $dep['title']->equals( $this->getTitle() ) ) {
3491
					// If we transclude ourselves, the final result
3492
					// will change based on the new version of the page
3493
					$this->mOutput->setFlag( 'vary-revision' );
3494
				}
3495
			}
3496
		}
3497
		return [ $text, $finalTitle ];
3498
	}
3499
3500
	/**
3501
	 * Fetch the unparsed text of a template and register a reference to it.
3502
	 * @param Title $title
3503
	 * @return string|bool
3504
	 */
3505
	public function fetchTemplate( $title ) {
3506
		return $this->fetchTemplateAndTitle( $title )[0];
3507
	}
3508
3509
	/**
3510
	 * Static function to get a template
3511
	 * Can be overridden via ParserOptions::setTemplateCallback().
3512
	 *
3513
	 * @param Title $title
3514
	 * @param bool|Parser $parser
3515
	 *
3516
	 * @return array
3517
	 */
3518
	public static function statelessFetchTemplate( $title, $parser = false ) {
3519
		$text = $skip = false;
3520
		$finalTitle = $title;
3521
		$deps = [];
3522
3523
		# Loop to fetch the article, with up to 1 redirect
3524
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3525
		for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3526
			// @codingStandardsIgnoreEnd
3527
			# Give extensions a chance to select the revision instead
3528
			$id = false; # Assume current
3529
			Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3530
				[ $parser, $title, &$skip, &$id ] );
3531
3532
			if ( $skip ) {
3533
				$text = false;
3534
				$deps[] = [
3535
					'title' => $title,
3536
					'page_id' => $title->getArticleID(),
3537
					'rev_id' => null
3538
				];
3539
				break;
3540
			}
3541
			# Get the revision
3542
			if ( $id ) {
3543
				$rev = Revision::newFromId( $id );
3544
			} elseif ( $parser ) {
3545
				$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...
3546
			} else {
3547
				$rev = Revision::newFromTitle( $title );
3548
			}
3549
			$rev_id = $rev ? $rev->getId() : 0;
3550
			# If there is no current revision, there is no page
3551
			if ( $id === false && !$rev ) {
3552
				$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...
3553
				$linkCache->addBadLinkObj( $title );
3554
			}
3555
3556
			$deps[] = [
3557
				'title' => $title,
3558
				'page_id' => $title->getArticleID(),
3559
				'rev_id' => $rev_id ];
3560
			if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3561
				# We fetched a rev from a different title; register it too...
3562
				$deps[] = [
3563
					'title' => $rev->getTitle(),
3564
					'page_id' => $rev->getPage(),
3565
					'rev_id' => $rev_id ];
3566
			}
3567
3568
			if ( $rev ) {
3569
				$content = $rev->getContent();
3570
				$text = $content ? $content->getWikitextForTransclusion() : null;
3571
3572
				if ( $text === false || $text === null ) {
3573
					$text = false;
3574
					break;
3575
				}
3576
			} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3577
				global $wgContLang;
3578
				$message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3579
				if ( !$message->exists() ) {
3580
					$text = false;
3581
					break;
3582
				}
3583
				$content = $message->content();
3584
				$text = $message->plain();
3585
			} else {
3586
				break;
3587
			}
3588
			if ( !$content ) {
3589
				break;
3590
			}
3591
			# Redirect?
3592
			$finalTitle = $title;
3593
			$title = $content->getRedirectTarget();
3594
		}
3595
		return [
3596
			'text' => $text,
3597
			'finalTitle' => $finalTitle,
3598
			'deps' => $deps ];
3599
	}
3600
3601
	/**
3602
	 * Fetch a file and its title and register a reference to it.
3603
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3604
	 * @param Title $title
3605
	 * @param array $options Array of options to RepoGroup::findFile
3606
	 * @return File|bool
3607
	 */
3608
	public function fetchFile( $title, $options = [] ) {
3609
		return $this->fetchFileAndTitle( $title, $options )[0];
3610
	}
3611
3612
	/**
3613
	 * Fetch a file and its title and register a reference to it.
3614
	 * If 'broken' is a key in $options then the file will appear as a broken thumbnail.
3615
	 * @param Title $title
3616
	 * @param array $options Array of options to RepoGroup::findFile
3617
	 * @return array ( File or false, Title of file )
3618
	 */
3619
	public function fetchFileAndTitle( $title, $options = [] ) {
3620
		$file = $this->fetchFileNoRegister( $title, $options );
3621
3622
		$time = $file ? $file->getTimestamp() : false;
3623
		$sha1 = $file ? $file->getSha1() : false;
3624
		# Register the file as a dependency...
3625
		$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3626
		if ( $file && !$title->equals( $file->getTitle() ) ) {
3627
			# Update fetched file title
3628
			$title = $file->getTitle();
3629
			$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3630
		}
3631
		return [ $file, $title ];
3632
	}
3633
3634
	/**
3635
	 * Helper function for fetchFileAndTitle.
3636
	 *
3637
	 * Also useful if you need to fetch a file but not use it yet,
3638
	 * for example to get the file's handler.
3639
	 *
3640
	 * @param Title $title
3641
	 * @param array $options Array of options to RepoGroup::findFile
3642
	 * @return File|bool
3643
	 */
3644
	protected function fetchFileNoRegister( $title, $options = [] ) {
3645
		if ( isset( $options['broken'] ) ) {
3646
			$file = false; // broken thumbnail forced by hook
3647
		} elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3648
			$file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3649
		} else { // get by (name,timestamp)
3650
			$file = wfFindFile( $title, $options );
3651
		}
3652
		return $file;
3653
	}
3654
3655
	/**
3656
	 * Transclude an interwiki link.
3657
	 *
3658
	 * @param Title $title
3659
	 * @param string $action
3660
	 *
3661
	 * @return string
3662
	 */
3663
	public function interwikiTransclude( $title, $action ) {
3664
		global $wgEnableScaryTranscluding;
3665
3666
		if ( !$wgEnableScaryTranscluding ) {
3667
			return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3668
		}
3669
3670
		$url = $title->getFullURL( [ 'action' => $action ] );
3671
3672
		if ( strlen( $url ) > 255 ) {
3673
			return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3674
		}
3675
		return $this->fetchScaryTemplateMaybeFromCache( $url );
3676
	}
3677
3678
	/**
3679
	 * @param string $url
3680
	 * @return mixed|string
3681
	 */
3682
	public function fetchScaryTemplateMaybeFromCache( $url ) {
3683
		global $wgTranscludeCacheExpiry;
3684
		$dbr = wfGetDB( DB_SLAVE );
3685
		$tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3686
		$obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3687
				[ '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 3685 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...
3688
		if ( $obj ) {
3689
			return $obj->tc_contents;
3690
		}
3691
3692
		$req = MWHttpRequest::factory( $url, [], __METHOD__ );
3693
		$status = $req->execute(); // Status object
3694
		if ( $status->isOK() ) {
3695
			$text = $req->getContent();
3696
		} elseif ( $req->getStatus() != 200 ) {
3697
			// Though we failed to fetch the content, this status is useless.
3698
			return wfMessage( 'scarytranscludefailed-httpstatus' )
3699
				->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3700
		} else {
3701
			return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3702
		}
3703
3704
		$dbw = wfGetDB( DB_MASTER );
3705
		$dbw->replace( 'transcache', [ 'tc_url' ], [
3706
			'tc_url' => $url,
3707
			'tc_time' => $dbw->timestamp( time() ),
3708
			'tc_contents' => $text
3709
		] );
3710
		return $text;
3711
	}
3712
3713
	/**
3714
	 * Triple brace replacement -- used for template arguments
3715
	 * @private
3716
	 *
3717
	 * @param array $piece
3718
	 * @param PPFrame $frame
3719
	 *
3720
	 * @return array
3721
	 */
3722
	public function argSubstitution( $piece, $frame ) {
3723
3724
		$error = false;
3725
		$parts = $piece['parts'];
3726
		$nameWithSpaces = $frame->expand( $piece['title'] );
3727
		$argName = trim( $nameWithSpaces );
3728
		$object = false;
3729
		$text = $frame->getArgument( $argName );
3730
		if ( $text === false && $parts->getLength() > 0
3731
			&& ( $this->ot['html']
3732
				|| $this->ot['pre']
3733
				|| ( $this->ot['wiki'] && $frame->isTemplate() )
3734
			)
3735
		) {
3736
			# No match in frame, use the supplied default
3737
			$object = $parts->item( 0 )->getChildren();
3738
		}
3739
		if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3740
			$error = '<!-- WARNING: argument omitted, expansion size too large -->';
3741
			$this->limitationWarn( 'post-expand-template-argument' );
3742
		}
3743
3744
		if ( $text === false && $object === false ) {
3745
			# No match anywhere
3746
			$object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3747
		}
3748
		if ( $error !== false ) {
3749
			$text .= $error;
3750
		}
3751
		if ( $object !== false ) {
3752
			$ret = [ 'object' => $object ];
3753
		} else {
3754
			$ret = [ 'text' => $text ];
3755
		}
3756
3757
		return $ret;
3758
	}
3759
3760
	/**
3761
	 * Return the text to be used for a given extension tag.
3762
	 * This is the ghost of strip().
3763
	 *
3764
	 * @param array $params Associative array of parameters:
3765
	 *     name       PPNode for the tag name
3766
	 *     attr       PPNode for unparsed text where tag attributes are thought to be
3767
	 *     attributes Optional associative array of parsed attributes
3768
	 *     inner      Contents of extension element
3769
	 *     noClose    Original text did not have a close tag
3770
	 * @param PPFrame $frame
3771
	 *
3772
	 * @throws MWException
3773
	 * @return string
3774
	 */
3775
	public function extensionSubstitution( $params, $frame ) {
3776
		$name = $frame->expand( $params['name'] );
3777
		$attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3778
		$content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3779
		$marker = self::MARKER_PREFIX . "-$name-"
3780
			. sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3781
3782
		$isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3783
			( $this->ot['html'] || $this->ot['pre'] );
3784
		if ( $isFunctionTag ) {
3785
			$markerType = 'none';
3786
		} else {
3787
			$markerType = 'general';
3788
		}
3789
		if ( $this->ot['html'] || $isFunctionTag ) {
3790
			$name = strtolower( $name );
3791
			$attributes = Sanitizer::decodeTagAttributes( $attrText );
3792
			if ( isset( $params['attributes'] ) ) {
3793
				$attributes = $attributes + $params['attributes'];
3794
			}
3795
3796
			if ( isset( $this->mTagHooks[$name] ) ) {
3797
				# Workaround for PHP bug 35229 and similar
3798
				if ( !is_callable( $this->mTagHooks[$name] ) ) {
3799
					throw new MWException( "Tag hook for $name is not callable\n" );
3800
				}
3801
				$output = call_user_func_array( $this->mTagHooks[$name],
3802
					[ $content, $attributes, $this, $frame ] );
3803
			} elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3804
				list( $callback, ) = $this->mFunctionTagHooks[$name];
3805
				if ( !is_callable( $callback ) ) {
3806
					throw new MWException( "Tag hook for $name is not callable\n" );
3807
				}
3808
3809
				$output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] );
3810
			} else {
3811
				$output = '<span class="error">Invalid tag extension name: ' .
3812
					htmlspecialchars( $name ) . '</span>';
3813
			}
3814
3815
			if ( is_array( $output ) ) {
3816
				# Extract flags to local scope (to override $markerType)
3817
				$flags = $output;
3818
				$output = $flags[0];
3819
				unset( $flags[0] );
3820
				extract( $flags );
3821
			}
3822
		} else {
3823
			if ( is_null( $attrText ) ) {
3824
				$attrText = '';
3825
			}
3826
			if ( isset( $params['attributes'] ) ) {
3827
				foreach ( $params['attributes'] as $attrName => $attrValue ) {
3828
					$attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3829
						htmlspecialchars( $attrValue ) . '"';
3830
				}
3831
			}
3832
			if ( $content === null ) {
3833
				$output = "<$name$attrText/>";
3834
			} else {
3835
				$close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3836
				$output = "<$name$attrText>$content$close";
3837
			}
3838
		}
3839
3840
		if ( $markerType === 'none' ) {
3841
			return $output;
3842
		} elseif ( $markerType === 'nowiki' ) {
3843
			$this->mStripState->addNoWiki( $marker, $output );
3844
		} elseif ( $markerType === 'general' ) {
3845
			$this->mStripState->addGeneral( $marker, $output );
3846
		} else {
3847
			throw new MWException( __METHOD__ . ': invalid marker type' );
3848
		}
3849
		return $marker;
3850
	}
3851
3852
	/**
3853
	 * Increment an include size counter
3854
	 *
3855
	 * @param string $type The type of expansion
3856
	 * @param int $size The size of the text
3857
	 * @return bool False if this inclusion would take it over the maximum, true otherwise
3858
	 */
3859
	public function incrementIncludeSize( $type, $size ) {
3860
		if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3861
			return false;
3862
		} else {
3863
			$this->mIncludeSizes[$type] += $size;
3864
			return true;
3865
		}
3866
	}
3867
3868
	/**
3869
	 * Increment the expensive function count
3870
	 *
3871
	 * @return bool False if the limit has been exceeded
3872
	 */
3873
	public function incrementExpensiveFunctionCount() {
3874
		$this->mExpensiveFunctionCount++;
3875
		return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3876
	}
3877
3878
	/**
3879
	 * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
3880
	 * Fills $this->mDoubleUnderscores, returns the modified text
3881
	 *
3882
	 * @param string $text
3883
	 *
3884
	 * @return string
3885
	 */
3886
	public function doDoubleUnderscore( $text ) {
3887
3888
		# The position of __TOC__ needs to be recorded
3889
		$mw = MagicWord::get( 'toc' );
3890
		if ( $mw->match( $text ) ) {
3891
			$this->mShowToc = true;
3892
			$this->mForceTocPosition = true;
3893
3894
			# Set a placeholder. At the end we'll fill it in with the TOC.
3895
			$text = $mw->replace( '<!--MWTOC-->', $text, 1 );
3896
3897
			# Only keep the first one.
3898
			$text = $mw->replace( '', $text );
3899
		}
3900
3901
		# Now match and remove the rest of them
3902
		$mwa = MagicWord::getDoubleUnderscoreArray();
3903
		$this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3904
3905
		if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3906
			$this->mOutput->mNoGallery = true;
3907
		}
3908
		if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3909
			$this->mShowToc = false;
3910
		}
3911
		if ( isset( $this->mDoubleUnderscores['hiddencat'] )
3912
			&& $this->mTitle->getNamespace() == NS_CATEGORY
3913
		) {
3914
			$this->addTrackingCategory( 'hidden-category-category' );
3915
		}
3916
		# (bug 8068) Allow control over whether robots index a page.
3917
		# @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here!  This
3918
		# is not desirable, the last one on the page should win.
3919 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
3920
			$this->mOutput->setIndexPolicy( 'noindex' );
3921
			$this->addTrackingCategory( 'noindex-category' );
3922
		}
3923 View Code Duplication
		if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
3924
			$this->mOutput->setIndexPolicy( 'index' );
3925
			$this->addTrackingCategory( 'index-category' );
3926
		}
3927
3928
		# Cache all double underscores in the database
3929
		foreach ( $this->mDoubleUnderscores as $key => $val ) {
3930
			$this->mOutput->setProperty( $key, '' );
3931
		}
3932
3933
		return $text;
3934
	}
3935
3936
	/**
3937
	 * @see ParserOutput::addTrackingCategory()
3938
	 * @param string $msg Message key
3939
	 * @return bool Whether the addition was successful
3940
	 */
3941
	public function addTrackingCategory( $msg ) {
3942
		return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
3943
	}
3944
3945
	/**
3946
	 * This function accomplishes several tasks:
3947
	 * 1) Auto-number headings if that option is enabled
3948
	 * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
3949
	 * 3) Add a Table of contents on the top for users who have enabled the option
3950
	 * 4) Auto-anchor headings
3951
	 *
3952
	 * It loops through all headlines, collects the necessary data, then splits up the
3953
	 * string and re-inserts the newly formatted headlines.
3954
	 *
3955
	 * @param string $text
3956
	 * @param string $origText Original, untouched wikitext
3957
	 * @param bool $isMain
3958
	 * @return mixed|string
3959
	 * @private
3960
	 */
3961
	public function formatHeadings( $text, $origText, $isMain = true ) {
3962
		global $wgMaxTocLevel, $wgExperimentalHtmlIds;
3963
3964
		# Inhibit editsection links if requested in the page
3965
		if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
3966
			$maybeShowEditLink = $showEditLink = false;
3967
		} else {
3968
			$maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
3969
			$showEditLink = $this->mOptions->getEditSection();
3970
		}
3971
		if ( $showEditLink ) {
3972
			$this->mOutput->setEditSectionTokens( true );
3973
		}
3974
3975
		# Get all headlines for numbering them and adding funky stuff like [edit]
3976
		# links - this is for later, but we need the number of headlines right now
3977
		$matches = [];
3978
		$numMatches = preg_match_all(
3979
			'/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
3980
			$text,
3981
			$matches
3982
		);
3983
3984
		# if there are fewer than 4 headlines in the article, do not show TOC
3985
		# unless it's been explicitly enabled.
3986
		$enoughToc = $this->mShowToc &&
3987
			( ( $numMatches >= 4 ) || $this->mForceTocPosition );
3988
3989
		# Allow user to stipulate that a page should have a "new section"
3990
		# link added via __NEWSECTIONLINK__
3991
		if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
3992
			$this->mOutput->setNewSection( true );
3993
		}
3994
3995
		# Allow user to remove the "new section"
3996
		# link via __NONEWSECTIONLINK__
3997
		if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
3998
			$this->mOutput->hideNewSection( true );
3999
		}
4000
4001
		# if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4002
		# override above conditions and always show TOC above first header
4003
		if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4004
			$this->mShowToc = true;
4005
			$enoughToc = true;
4006
		}
4007
4008
		# headline counter
4009
		$headlineCount = 0;
4010
		$numVisible = 0;
4011
4012
		# Ugh .. the TOC should have neat indentation levels which can be
4013
		# passed to the skin functions. These are determined here
4014
		$toc = '';
4015
		$full = '';
4016
		$head = [];
4017
		$sublevelCount = [];
4018
		$levelCount = [];
4019
		$level = 0;
4020
		$prevlevel = 0;
4021
		$toclevel = 0;
4022
		$prevtoclevel = 0;
4023
		$markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4024
		$baseTitleText = $this->mTitle->getPrefixedDBkey();
4025
		$oldType = $this->mOutputType;
4026
		$this->setOutputType( self::OT_WIKI );
4027
		$frame = $this->getPreprocessor()->newFrame();
4028
		$root = $this->preprocessToDom( $origText );
4029
		$node = $root->getFirstChild();
4030
		$byteOffset = 0;
4031
		$tocraw = [];
4032
		$refers = [];
4033
4034
		$headlines = $numMatches !== false ? $matches[3] : [];
4035
4036
		foreach ( $headlines as $headline ) {
4037
			$isTemplate = false;
4038
			$titleText = false;
4039
			$sectionIndex = false;
4040
			$numbering = '';
4041
			$markerMatches = [];
4042
			if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4043
				$serial = $markerMatches[1];
4044
				list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4045
				$isTemplate = ( $titleText != $baseTitleText );
4046
				$headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4047
			}
4048
4049
			if ( $toclevel ) {
4050
				$prevlevel = $level;
4051
			}
4052
			$level = $matches[1][$headlineCount];
4053
4054
			if ( $level > $prevlevel ) {
4055
				# Increase TOC level
4056
				$toclevel++;
4057
				$sublevelCount[$toclevel] = 0;
4058
				if ( $toclevel < $wgMaxTocLevel ) {
4059
					$prevtoclevel = $toclevel;
4060
					$toc .= Linker::tocIndent();
4061
					$numVisible++;
4062
				}
4063
			} elseif ( $level < $prevlevel && $toclevel > 1 ) {
4064
				# Decrease TOC level, find level to jump to
4065
4066
				for ( $i = $toclevel; $i > 0; $i-- ) {
4067
					if ( $levelCount[$i] == $level ) {
4068
						# Found last matching level
4069
						$toclevel = $i;
4070
						break;
4071
					} elseif ( $levelCount[$i] < $level ) {
4072
						# Found first matching level below current level
4073
						$toclevel = $i + 1;
4074
						break;
4075
					}
4076
				}
4077
				if ( $i == 0 ) {
4078
					$toclevel = 1;
4079
				}
4080
				if ( $toclevel < $wgMaxTocLevel ) {
4081
					if ( $prevtoclevel < $wgMaxTocLevel ) {
4082
						# Unindent only if the previous toc level was shown :p
4083
						$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4084
						$prevtoclevel = $toclevel;
4085
					} else {
4086
						$toc .= Linker::tocLineEnd();
4087
					}
4088
				}
4089
			} else {
4090
				# No change in level, end TOC line
4091
				if ( $toclevel < $wgMaxTocLevel ) {
4092
					$toc .= Linker::tocLineEnd();
4093
				}
4094
			}
4095
4096
			$levelCount[$toclevel] = $level;
4097
4098
			# count number of headlines for each level
4099
			$sublevelCount[$toclevel]++;
4100
			$dot = 0;
4101
			for ( $i = 1; $i <= $toclevel; $i++ ) {
4102
				if ( !empty( $sublevelCount[$i] ) ) {
4103
					if ( $dot ) {
4104
						$numbering .= '.';
4105
					}
4106
					$numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4107
					$dot = 1;
4108
				}
4109
			}
4110
4111
			# The safe header is a version of the header text safe to use for links
4112
4113
			# Remove link placeholders by the link text.
4114
			#     <!--LINK number-->
4115
			# turns into
4116
			#     link text with suffix
4117
			# Do this before unstrip since link text can contain strip markers
4118
			$safeHeadline = $this->replaceLinkHoldersText( $headline );
4119
4120
			# Avoid insertion of weird stuff like <math> by expanding the relevant sections
4121
			$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4122
4123
			# Strip out HTML (first regex removes any tag not allowed)
4124
			# Allowed tags are:
4125
			# * <sup> and <sub> (bug 8393)
4126
			# * <i> (bug 26375)
4127
			# * <b> (r105284)
4128
			# * <bdi> (bug 72884)
4129
			# * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4130
			# We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4131
			# to allow setting directionality in toc items.
4132
			$tocline = preg_replace(
4133
				[
4134
					'#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4135
					'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4136
				],
4137
				[ '', '<$1>' ],
4138
				$safeHeadline
4139
			);
4140
4141
			# Strip '<span></span>', which is the result from the above if
4142
			# <span id="foo"></span> is used to produce an additional anchor
4143
			# for a section.
4144
			$tocline = str_replace( '<span></span>', '', $tocline );
4145
4146
			$tocline = trim( $tocline );
4147
4148
			# For the anchor, strip out HTML-y stuff period
4149
			$safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4150
			$safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4151
4152
			# Save headline for section edit hint before it's escaped
4153
			$headlineHint = $safeHeadline;
4154
4155
			if ( $wgExperimentalHtmlIds ) {
4156
				# For reverse compatibility, provide an id that's
4157
				# HTML4-compatible, like we used to.
4158
				# It may be worth noting, academically, that it's possible for
4159
				# the legacy anchor to conflict with a non-legacy headline
4160
				# anchor on the page.  In this case likely the "correct" thing
4161
				# would be to either drop the legacy anchors or make sure
4162
				# they're numbered first.  However, this would require people
4163
				# to type in section names like "abc_.D7.93.D7.90.D7.A4"
4164
				# manually, so let's not bother worrying about it.
4165
				$legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4166
					[ 'noninitial', 'legacy' ] );
4167
				$safeHeadline = Sanitizer::escapeId( $safeHeadline );
4168
4169
				if ( $legacyHeadline == $safeHeadline ) {
4170
					# No reason to have both (in fact, we can't)
4171
					$legacyHeadline = false;
4172
				}
4173
			} else {
4174
				$legacyHeadline = false;
4175
				$safeHeadline = Sanitizer::escapeId( $safeHeadline,
4176
					'noninitial' );
4177
			}
4178
4179
			# HTML names must be case-insensitively unique (bug 10721).
4180
			# This does not apply to Unicode characters per
4181
			# http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4182
			# @todo FIXME: We may be changing them depending on the current locale.
4183
			$arrayKey = strtolower( $safeHeadline );
4184
			if ( $legacyHeadline === false ) {
4185
				$legacyArrayKey = false;
4186
			} else {
4187
				$legacyArrayKey = strtolower( $legacyHeadline );
4188
			}
4189
4190
			# Create the anchor for linking from the TOC to the section
4191
			$anchor = $safeHeadline;
4192
			$legacyAnchor = $legacyHeadline;
4193 View Code Duplication
			if ( isset( $refers[$arrayKey] ) ) {
4194
				// @codingStandardsIgnoreStart
4195
				for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4196
				// @codingStandardsIgnoreEnd
4197
				$anchor .= "_$i";
4198
				$refers["${arrayKey}_$i"] = true;
4199
			} else {
4200
				$refers[$arrayKey] = true;
4201
			}
4202 View Code Duplication
			if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4203
				// @codingStandardsIgnoreStart
4204
				for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4205
				// @codingStandardsIgnoreEnd
4206
				$legacyAnchor .= "_$i";
4207
				$refers["${legacyArrayKey}_$i"] = true;
4208
			} else {
4209
				$refers[$legacyArrayKey] = true;
4210
			}
4211
4212
			# Don't number the heading if it is the only one (looks silly)
4213
			if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4214
				# the two are different if the line contains a link
4215
				$headline = Html::element(
4216
					'span',
4217
					[ 'class' => 'mw-headline-number' ],
4218
					$numbering
4219
				) . ' ' . $headline;
4220
			}
4221
4222
			if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4223
				$toc .= Linker::tocLine( $anchor, $tocline,
4224
					$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4225
			}
4226
4227
			# Add the section to the section tree
4228
			# Find the DOM node for this header
4229
			$noOffset = ( $isTemplate || $sectionIndex === false );
4230
			while ( $node && !$noOffset ) {
4231
				if ( $node->getName() === 'h' ) {
4232
					$bits = $node->splitHeading();
4233
					if ( $bits['i'] == $sectionIndex ) {
4234
						break;
4235
					}
4236
				}
4237
				$byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4238
					$frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4239
				$node = $node->getNextSibling();
4240
			}
4241
			$tocraw[] = [
4242
				'toclevel' => $toclevel,
4243
				'level' => $level,
4244
				'line' => $tocline,
4245
				'number' => $numbering,
4246
				'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4247
				'fromtitle' => $titleText,
4248
				'byteoffset' => ( $noOffset ? null : $byteOffset ),
4249
				'anchor' => $anchor,
4250
			];
4251
4252
			# give headline the correct <h#> tag
4253
			if ( $maybeShowEditLink && $sectionIndex !== false ) {
4254
				// Output edit section links as markers with styles that can be customized by skins
4255
				if ( $isTemplate ) {
4256
					# Put a T flag in the section identifier, to indicate to extractSections()
4257
					# that sections inside <includeonly> should be counted.
4258
					$editsectionPage = $titleText;
4259
					$editsectionSection = "T-$sectionIndex";
4260
					$editsectionContent = null;
4261
				} else {
4262
					$editsectionPage = $this->mTitle->getPrefixedText();
4263
					$editsectionSection = $sectionIndex;
4264
					$editsectionContent = $headlineHint;
4265
				}
4266
				// We use a bit of pesudo-xml for editsection markers. The
4267
				// language converter is run later on. Using a UNIQ style marker
4268
				// leads to the converter screwing up the tokens when it
4269
				// converts stuff. And trying to insert strip tags fails too. At
4270
				// this point all real inputted tags have already been escaped,
4271
				// so we don't have to worry about a user trying to input one of
4272
				// these markers directly. We use a page and section attribute
4273
				// to stop the language converter from converting these
4274
				// important bits of data, but put the headline hint inside a
4275
				// content block because the language converter is supposed to
4276
				// be able to convert that piece of data.
4277
				// Gets replaced with html in ParserOutput::getText
4278
				$editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4279
				$editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4280
				if ( $editsectionContent !== null ) {
4281
					$editlink .= '>' . $editsectionContent . '</mw:editsection>';
4282
				} else {
4283
					$editlink .= '/>';
4284
				}
4285
			} else {
4286
				$editlink = '';
4287
			}
4288
			$head[$headlineCount] = Linker::makeHeadline( $level,
4289
				$matches['attrib'][$headlineCount], $anchor, $headline,
4290
				$editlink, $legacyAnchor );
4291
4292
			$headlineCount++;
4293
		}
4294
4295
		$this->setOutputType( $oldType );
4296
4297
		# Never ever show TOC if no headers
4298
		if ( $numVisible < 1 ) {
4299
			$enoughToc = false;
4300
		}
4301
4302
		if ( $enoughToc ) {
4303
			if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4304
				$toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4305
			}
4306
			$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4307
			$this->mOutput->setTOCHTML( $toc );
4308
			$toc = self::TOC_START . $toc . self::TOC_END;
4309
			$this->mOutput->addModules( 'mediawiki.toc' );
4310
		}
4311
4312
		if ( $isMain ) {
4313
			$this->mOutput->setSections( $tocraw );
4314
		}
4315
4316
		# split up and insert constructed headlines
4317
		$blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4318
		$i = 0;
4319
4320
		// build an array of document sections
4321
		$sections = [];
4322
		foreach ( $blocks as $block ) {
4323
			// $head is zero-based, sections aren't.
4324
			if ( empty( $head[$i - 1] ) ) {
4325
				$sections[$i] = $block;
4326
			} else {
4327
				$sections[$i] = $head[$i - 1] . $block;
4328
			}
4329
4330
			/**
4331
			 * Send a hook, one per section.
4332
			 * The idea here is to be able to make section-level DIVs, but to do so in a
4333
			 * lower-impact, more correct way than r50769
4334
			 *
4335
			 * $this : caller
4336
			 * $section : the section number
4337
			 * &$sectionContent : ref to the content of the section
4338
			 * $showEditLinks : boolean describing whether this section has an edit link
4339
			 */
4340
			Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4341
4342
			$i++;
4343
		}
4344
4345
		if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4346
			// append the TOC at the beginning
4347
			// Top anchor now in skin
4348
			$sections[0] = $sections[0] . $toc . "\n";
4349
		}
4350
4351
		$full .= implode( '', $sections );
4352
4353
		if ( $this->mForceTocPosition ) {
4354
			return str_replace( '<!--MWTOC-->', $toc, $full );
4355
		} else {
4356
			return $full;
4357
		}
4358
	}
4359
4360
	/**
4361
	 * Transform wiki markup when saving a page by doing "\r\n" -> "\n"
4362
	 * conversion, substituting signatures, {{subst:}} templates, etc.
4363
	 *
4364
	 * @param string $text The text to transform
4365
	 * @param Title $title The Title object for the current article
4366
	 * @param User $user The User object describing the current user
4367
	 * @param ParserOptions $options Parsing options
4368
	 * @param bool $clearState Whether to clear the parser state first
4369
	 * @return string The altered wiki markup
4370
	 */
4371
	public function preSaveTransform( $text, Title $title, User $user,
4372
		ParserOptions $options, $clearState = true
4373
	) {
4374
		if ( $clearState ) {
4375
			$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...
4376
		}
4377
		$this->startParse( $title, $options, self::OT_WIKI, $clearState );
4378
		$this->setUser( $user );
4379
4380
		$pairs = [
4381
			"\r\n" => "\n",
4382
			"\r" => "\n",
4383
		];
4384
		$text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4385
		if ( $options->getPreSaveTransform() ) {
4386
			$text = $this->pstPass2( $text, $user );
4387
		}
4388
		$text = $this->mStripState->unstripBoth( $text );
4389
4390
		$this->setUser( null ); # Reset
4391
4392
		return $text;
4393
	}
4394
4395
	/**
4396
	 * Pre-save transform helper function
4397
	 *
4398
	 * @param string $text
4399
	 * @param User $user
4400
	 *
4401
	 * @return string
4402
	 */
4403
	private function pstPass2( $text, $user ) {
4404
		global $wgContLang;
4405
4406
		# Note: This is the timestamp saved as hardcoded wikitext to
4407
		# the database, we use $wgContLang here in order to give
4408
		# everyone the same signature and use the default one rather
4409
		# than the one selected in each user's preferences.
4410
		# (see also bug 12815)
4411
		$ts = $this->mOptions->getTimestamp();
4412
		$timestamp = MWTimestamp::getLocalInstance( $ts );
4413
		$ts = $timestamp->format( 'YmdHis' );
4414
		$tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4415
4416
		$d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4417
4418
		# Variable replacement
4419
		# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4420
		$text = $this->replaceVariables( $text );
4421
4422
		# This works almost by chance, as the replaceVariables are done before the getUserSig(),
4423
		# which may corrupt this parser instance via its wfMessage()->text() call-
4424
4425
		# Signatures
4426
		$sigText = $this->getUserSig( $user );
4427
		$text = strtr( $text, [
4428
			'~~~~~' => $d,
4429
			'~~~~' => "$sigText $d",
4430
			'~~~' => $sigText
4431
		] );
4432
4433
		# Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4434
		$tc = '[' . Title::legalChars() . ']';
4435
		$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4436
4437
		// [[ns:page (context)|]]
4438
		$p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4439
		// [[ns:page(context)|]] (double-width brackets, added in r40257)
4440
		$p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4441
		// [[ns:page (context), context|]] (using either single or double-width comma)
4442
		$p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4443
		// [[|page]] (reverse pipe trick: add context from page title)
4444
		$p2 = "/\[\[\\|($tc+)]]/";
4445
4446
		# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4447
		$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4448
		$text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4449
		$text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4450
4451
		$t = $this->mTitle->getText();
4452
		$m = [];
4453
		if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4454
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4455
		} elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4456
			$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4457
		} else {
4458
			# if there's no context, don't bother duplicating the title
4459
			$text = preg_replace( $p2, '[[\\1]]', $text );
4460
		}
4461
4462
		# Trim trailing whitespace
4463
		$text = rtrim( $text );
4464
4465
		return $text;
4466
	}
4467
4468
	/**
4469
	 * Fetch the user's signature text, if any, and normalize to
4470
	 * validated, ready-to-insert wikitext.
4471
	 * If you have pre-fetched the nickname or the fancySig option, you can
4472
	 * specify them here to save a database query.
4473
	 * Do not reuse this parser instance after calling getUserSig(),
4474
	 * as it may have changed if it's the $wgParser.
4475
	 *
4476
	 * @param User $user
4477
	 * @param string|bool $nickname Nickname to use or false to use user's default nickname
4478
	 * @param bool|null $fancySig whether the nicknname is the complete signature
4479
	 *    or null to use default value
4480
	 * @return string
4481
	 */
4482
	public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4483
		global $wgMaxSigChars;
4484
4485
		$username = $user->getName();
4486
4487
		# If not given, retrieve from the user object.
4488
		if ( $nickname === false ) {
4489
			$nickname = $user->getOption( 'nickname' );
4490
		}
4491
4492
		if ( is_null( $fancySig ) ) {
4493
			$fancySig = $user->getBoolOption( 'fancysig' );
4494
		}
4495
4496
		$nickname = $nickname == null ? $username : $nickname;
4497
4498
		if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4499
			$nickname = $username;
4500
			wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4501
		} elseif ( $fancySig !== false ) {
4502
			# Sig. might contain markup; validate this
4503
			if ( $this->validateSig( $nickname ) !== false ) {
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4496 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...
4504
				# Validated; clean up (if needed) and return it
4505
				return $this->cleanSig( $nickname, true );
0 ignored issues
show
Bug introduced by
It seems like $nickname defined by $nickname == null ? $username : $nickname on line 4496 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...
4506
			} else {
4507
				# Failed to validate; fall back to the default
4508
				$nickname = $username;
4509
				wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4510
			}
4511
		}
4512
4513
		# Make sure nickname doesnt get a sig in a sig
4514
		$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...
4515
4516
		# If we're still here, make it a link to the user page
4517
		$userText = wfEscapeWikiText( $username );
4518
		$nickText = wfEscapeWikiText( $nickname );
4519
		$msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4520
4521
		return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4522
			->title( $this->getTitle() )->text();
4523
	}
4524
4525
	/**
4526
	 * Check that the user's signature contains no bad XML
4527
	 *
4528
	 * @param string $text
4529
	 * @return string|bool An expanded string, or false if invalid.
4530
	 */
4531
	public function validateSig( $text ) {
4532
		return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4533
	}
4534
4535
	/**
4536
	 * Clean up signature text
4537
	 *
4538
	 * 1) Strip 3, 4 or 5 tildes out of signatures @see cleanSigInSig
4539
	 * 2) Substitute all transclusions
4540
	 *
4541
	 * @param string $text
4542
	 * @param bool $parsing Whether we're cleaning (preferences save) or parsing
4543
	 * @return string Signature text
4544
	 */
4545
	public function cleanSig( $text, $parsing = false ) {
4546
		if ( !$parsing ) {
4547
			global $wgTitle;
4548
			$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...
4549
			$this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4550
		}
4551
4552
		# Option to disable this feature
4553
		if ( !$this->mOptions->getCleanSignatures() ) {
4554
			return $text;
4555
		}
4556
4557
		# @todo FIXME: Regex doesn't respect extension tags or nowiki
4558
		#  => Move this logic to braceSubstitution()
4559
		$substWord = MagicWord::get( 'subst' );
4560
		$substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4561
		$substText = '{{' . $substWord->getSynonym( 0 );
4562
4563
		$text = preg_replace( $substRegex, $substText, $text );
4564
		$text = self::cleanSigInSig( $text );
4565
		$dom = $this->preprocessToDom( $text );
4566
		$frame = $this->getPreprocessor()->newFrame();
4567
		$text = $frame->expand( $dom );
4568
4569
		if ( !$parsing ) {
4570
			$text = $this->mStripState->unstripBoth( $text );
4571
		}
4572
4573
		return $text;
4574
	}
4575
4576
	/**
4577
	 * Strip 3, 4 or 5 tildes out of signatures.
4578
	 *
4579
	 * @param string $text
4580
	 * @return string Signature text with /~{3,5}/ removed
4581
	 */
4582
	public static function cleanSigInSig( $text ) {
4583
		$text = preg_replace( '/~{3,5}/', '', $text );
4584
		return $text;
4585
	}
4586
4587
	/**
4588
	 * Set up some variables which are usually set up in parse()
4589
	 * so that an external function can call some class members with confidence
4590
	 *
4591
	 * @param Title|null $title
4592
	 * @param ParserOptions $options
4593
	 * @param int $outputType
4594
	 * @param bool $clearState
4595
	 */
4596
	public function startExternalParse( Title $title = null, ParserOptions $options,
4597
		$outputType, $clearState = true
4598
	) {
4599
		$this->startParse( $title, $options, $outputType, $clearState );
4600
	}
4601
4602
	/**
4603
	 * @param Title|null $title
4604
	 * @param ParserOptions $options
4605
	 * @param int $outputType
4606
	 * @param bool $clearState
4607
	 */
4608
	private function startParse( Title $title = null, ParserOptions $options,
4609
		$outputType, $clearState = true
4610
	) {
4611
		$this->setTitle( $title );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 4608 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...
4612
		$this->mOptions = $options;
4613
		$this->setOutputType( $outputType );
4614
		if ( $clearState ) {
4615
			$this->clearState();
4616
		}
4617
	}
4618
4619
	/**
4620
	 * Wrapper for preprocess()
4621
	 *
4622
	 * @param string $text The text to preprocess
4623
	 * @param ParserOptions $options Options
4624
	 * @param Title|null $title Title object or null to use $wgTitle
4625
	 * @return string
4626
	 */
4627
	public function transformMsg( $text, $options, $title = null ) {
4628
		static $executing = false;
4629
4630
		# Guard against infinite recursion
4631
		if ( $executing ) {
4632
			return $text;
4633
		}
4634
		$executing = true;
4635
4636
		if ( !$title ) {
4637
			global $wgTitle;
4638
			$title = $wgTitle;
4639
		}
4640
4641
		$text = $this->preprocess( $text, $title, $options );
4642
4643
		$executing = false;
4644
		return $text;
4645
	}
4646
4647
	/**
4648
	 * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
4649
	 * The callback should have the following form:
4650
	 *    function myParserHook( $text, $params, $parser, $frame ) { ... }
4651
	 *
4652
	 * Transform and return $text. Use $parser for any required context, e.g. use
4653
	 * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
4654
	 *
4655
	 * Hooks may return extended information by returning an array, of which the
4656
	 * first numbered element (index 0) must be the return string, and all other
4657
	 * entries are extracted into local variables within an internal function
4658
	 * in the Parser class.
4659
	 *
4660
	 * This interface (introduced r61913) appears to be undocumented, but
4661
	 * 'markerType' is used by some core tag hooks to override which strip
4662
	 * array their results are placed in. **Use great caution if attempting
4663
	 * this interface, as it is not documented and injudicious use could smash
4664
	 * private variables.**
4665
	 *
4666
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4667
	 * @param callable $callback The callback function (and object) to use for the tag
4668
	 * @throws MWException
4669
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4670
	 */
4671 View Code Duplication
	public function setHook( $tag, $callback ) {
4672
		$tag = strtolower( $tag );
4673
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4674
			throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4675
		}
4676
		$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4677
		$this->mTagHooks[$tag] = $callback;
4678
		if ( !in_array( $tag, $this->mStripList ) ) {
4679
			$this->mStripList[] = $tag;
4680
		}
4681
4682
		return $oldVal;
4683
	}
4684
4685
	/**
4686
	 * As setHook(), but letting the contents be parsed.
4687
	 *
4688
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
4689
	 * operate late in the transformation sequence, on HTML instead of wikitext.
4690
	 *
4691
	 * This is probably obsoleted by things dealing with parser frames?
4692
	 * The only extension currently using it is geoserver.
4693
	 *
4694
	 * @since 1.10
4695
	 * @todo better document or deprecate this
4696
	 *
4697
	 * @param string $tag The tag to use, e.g. 'hook' for "<hook>"
4698
	 * @param callable $callback The callback function (and object) to use for the tag
4699
	 * @throws MWException
4700
	 * @return callable|null The old value of the mTagHooks array associated with the hook
4701
	 */
4702
	public function setTransparentTagHook( $tag, $callback ) {
4703
		$tag = strtolower( $tag );
4704
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4705
			throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4706
		}
4707
		$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4708
		$this->mTransparentTagHooks[$tag] = $callback;
4709
4710
		return $oldVal;
4711
	}
4712
4713
	/**
4714
	 * Remove all tag hooks
4715
	 */
4716
	public function clearTagHooks() {
4717
		$this->mTagHooks = [];
4718
		$this->mFunctionTagHooks = [];
4719
		$this->mStripList = $this->mDefaultStripList;
4720
	}
4721
4722
	/**
4723
	 * Create a function, e.g. {{sum:1|2|3}}
4724
	 * The callback function should have the form:
4725
	 *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
4726
	 *
4727
	 * Or with Parser::SFH_OBJECT_ARGS:
4728
	 *    function myParserFunction( $parser, $frame, $args ) { ... }
4729
	 *
4730
	 * The callback may either return the text result of the function, or an array with the text
4731
	 * in element 0, and a number of flags in the other elements. The names of the flags are
4732
	 * specified in the keys. Valid flags are:
4733
	 *   found                     The text returned is valid, stop processing the template. This
4734
	 *                             is on by default.
4735
	 *   nowiki                    Wiki markup in the return value should be escaped
4736
	 *   isHTML                    The returned text is HTML, armour it against wikitext transformation
4737
	 *
4738
	 * @param string $id The magic word ID
4739
	 * @param callable $callback The callback function (and object) to use
4740
	 * @param int $flags A combination of the following flags:
4741
	 *     Parser::SFH_NO_HASH      No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
4742
	 *
4743
	 *     Parser::SFH_OBJECT_ARGS  Pass the template arguments as PPNode objects instead of text.
4744
	 *     This allows for conditional expansion of the parse tree, allowing you to eliminate dead
4745
	 *     branches and thus speed up parsing. It is also possible to analyse the parse tree of
4746
	 *     the arguments, and to control the way they are expanded.
4747
	 *
4748
	 *     The $frame parameter is a PPFrame. This can be used to produce expanded text from the
4749
	 *     arguments, for instance:
4750
	 *         $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
4751
	 *
4752
	 *     For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
4753
	 *     future versions. Please call $frame->expand() on it anyway so that your code keeps
4754
	 *     working if/when this is changed.
4755
	 *
4756
	 *     If you want whitespace to be trimmed from $args, you need to do it yourself, post-
4757
	 *     expansion.
4758
	 *
4759
	 *     Please read the documentation in includes/parser/Preprocessor.php for more information
4760
	 *     about the methods available in PPFrame and PPNode.
4761
	 *
4762
	 * @throws MWException
4763
	 * @return string|callable The old callback function for this name, if any
4764
	 */
4765
	public function setFunctionHook( $id, $callback, $flags = 0 ) {
4766
		global $wgContLang;
4767
4768
		$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4769
		$this->mFunctionHooks[$id] = [ $callback, $flags ];
4770
4771
		# Add to function cache
4772
		$mw = MagicWord::get( $id );
4773
		if ( !$mw ) {
4774
			throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4775
		}
4776
4777
		$synonyms = $mw->getSynonyms();
4778
		$sensitive = intval( $mw->isCaseSensitive() );
4779
4780
		foreach ( $synonyms as $syn ) {
4781
			# Case
4782
			if ( !$sensitive ) {
4783
				$syn = $wgContLang->lc( $syn );
4784
			}
4785
			# Add leading hash
4786
			if ( !( $flags & self::SFH_NO_HASH ) ) {
4787
				$syn = '#' . $syn;
4788
			}
4789
			# Remove trailing colon
4790
			if ( substr( $syn, -1, 1 ) === ':' ) {
4791
				$syn = substr( $syn, 0, -1 );
4792
			}
4793
			$this->mFunctionSynonyms[$sensitive][$syn] = $id;
4794
		}
4795
		return $oldVal;
4796
	}
4797
4798
	/**
4799
	 * Get all registered function hook identifiers
4800
	 *
4801
	 * @return array
4802
	 */
4803
	public function getFunctionHooks() {
4804
		return array_keys( $this->mFunctionHooks );
4805
	}
4806
4807
	/**
4808
	 * Create a tag function, e.g. "<test>some stuff</test>".
4809
	 * Unlike tag hooks, tag functions are parsed at preprocessor level.
4810
	 * Unlike parser functions, their content is not preprocessed.
4811
	 * @param string $tag
4812
	 * @param callable $callback
4813
	 * @param int $flags
4814
	 * @throws MWException
4815
	 * @return null
4816
	 */
4817 View Code Duplication
	public function setFunctionTagHook( $tag, $callback, $flags ) {
4818
		$tag = strtolower( $tag );
4819
		if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4820
			throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4821
		}
4822
		$old = isset( $this->mFunctionTagHooks[$tag] ) ?
4823
			$this->mFunctionTagHooks[$tag] : null;
4824
		$this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4825
4826
		if ( !in_array( $tag, $this->mStripList ) ) {
4827
			$this->mStripList[] = $tag;
4828
		}
4829
4830
		return $old;
4831
	}
4832
4833
	/**
4834
	 * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
4835
	 * Placeholders created in Linker::link()
4836
	 *
4837
	 * @param string $text
4838
	 * @param int $options
4839
	 */
4840
	public function replaceLinkHolders( &$text, $options = 0 ) {
4841
		$this->mLinkHolders->replace( $text );
4842
	}
4843
4844
	/**
4845
	 * Replace "<!--LINK-->" link placeholders with plain text of links
4846
	 * (not HTML-formatted).
4847
	 *
4848
	 * @param string $text
4849
	 * @return string
4850
	 */
4851
	public function replaceLinkHoldersText( $text ) {
4852
		return $this->mLinkHolders->replaceText( $text );
4853
	}
4854
4855
	/**
4856
	 * Renders an image gallery from a text with one line per image.
4857
	 * text labels may be given by using |-style alternative text. E.g.
4858
	 *   Image:one.jpg|The number "1"
4859
	 *   Image:tree.jpg|A tree
4860
	 * given as text will return the HTML of a gallery with two images,
4861
	 * labeled 'The number "1"' and
4862
	 * 'A tree'.
4863
	 *
4864
	 * @param string $text
4865
	 * @param array $params
4866
	 * @return string HTML
4867
	 */
4868
	public function renderImageGallery( $text, $params ) {
4869
4870
		$mode = false;
4871
		if ( isset( $params['mode'] ) ) {
4872
			$mode = $params['mode'];
4873
		}
4874
4875
		try {
4876
			$ig = ImageGalleryBase::factory( $mode );
4877
		} catch ( Exception $e ) {
4878
			// If invalid type set, fallback to default.
4879
			$ig = ImageGalleryBase::factory( false );
4880
		}
4881
4882
		$ig->setContextTitle( $this->mTitle );
4883
		$ig->setShowBytes( false );
4884
		$ig->setShowFilename( false );
4885
		$ig->setParser( $this );
4886
		$ig->setHideBadImages();
4887
		$ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
4888
4889
		if ( isset( $params['showfilename'] ) ) {
4890
			$ig->setShowFilename( true );
4891
		} else {
4892
			$ig->setShowFilename( false );
4893
		}
4894
		if ( isset( $params['caption'] ) ) {
4895
			$caption = $params['caption'];
4896
			$caption = htmlspecialchars( $caption );
4897
			$caption = $this->replaceInternalLinks( $caption );
4898
			$ig->setCaptionHtml( $caption );
4899
		}
4900
		if ( isset( $params['perrow'] ) ) {
4901
			$ig->setPerRow( $params['perrow'] );
4902
		}
4903
		if ( isset( $params['widths'] ) ) {
4904
			$ig->setWidths( $params['widths'] );
4905
		}
4906
		if ( isset( $params['heights'] ) ) {
4907
			$ig->setHeights( $params['heights'] );
4908
		}
4909
		$ig->setAdditionalOptions( $params );
4910
4911
		Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] );
4912
4913
		$lines = StringUtils::explode( "\n", $text );
4914
		foreach ( $lines as $line ) {
4915
			# match lines like these:
4916
			# Image:someimage.jpg|This is some image
4917
			$matches = [];
4918
			preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4919
			# Skip empty lines
4920
			if ( count( $matches ) == 0 ) {
4921
				continue;
4922
			}
4923
4924
			if ( strpos( $matches[0], '%' ) !== false ) {
4925
				$matches[1] = rawurldecode( $matches[1] );
4926
			}
4927
			$title = Title::newFromText( $matches[1], NS_FILE );
4928
			if ( is_null( $title ) ) {
4929
				# Bogus title. Ignore these so we don't bomb out later.
4930
				continue;
4931
			}
4932
4933
			# We need to get what handler the file uses, to figure out parameters.
4934
			# Note, a hook can overide the file name, and chose an entirely different
4935
			# file (which potentially could be of a different type and have different handler).
4936
			$options = [];
4937
			$descQuery = false;
4938
			Hooks::run( 'BeforeParserFetchFileAndTitle',
4939
				[ $this, $title, &$options, &$descQuery ] );
4940
			# Don't register it now, as ImageGallery does that later.
4941
			$file = $this->fetchFileNoRegister( $title, $options );
4942
			$handler = $file ? $file->getHandler() : false;
4943
4944
			$paramMap = [
4945
				'img_alt' => 'gallery-internal-alt',
4946
				'img_link' => 'gallery-internal-link',
4947
			];
4948
			if ( $handler ) {
4949
				$paramMap = $paramMap + $handler->getParamMap();
4950
				// We don't want people to specify per-image widths.
4951
				// Additionally the width parameter would need special casing anyhow.
4952
				unset( $paramMap['img_width'] );
4953
			}
4954
4955
			$mwArray = new MagicWordArray( array_keys( $paramMap ) );
4956
4957
			$label = '';
4958
			$alt = '';
4959
			$link = '';
4960
			$handlerOptions = [];
4961
			if ( isset( $matches[3] ) ) {
4962
				// look for an |alt= definition while trying not to break existing
4963
				// captions with multiple pipes (|) in it, until a more sensible grammar
4964
				// is defined for images in galleries
4965
4966
				// FIXME: Doing recursiveTagParse at this stage, and the trim before
4967
				// splitting on '|' is a bit odd, and different from makeImage.
4968
				$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
4969
				$parameterMatches = StringUtils::explode( '|', $matches[3] );
4970
4971
				foreach ( $parameterMatches as $parameterMatch ) {
4972
					list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
4973
					if ( $magicName ) {
4974
						$paramName = $paramMap[$magicName];
4975
4976
						switch ( $paramName ) {
4977
						case 'gallery-internal-alt':
4978
							$alt = $this->stripAltText( $match, false );
4979
							break;
4980
						case 'gallery-internal-link':
4981
							$linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
4982
							$chars = self::EXT_LINK_URL_CLASS;
4983
							$addr = self::EXT_LINK_ADDR;
4984
							$prots = $this->mUrlProtocols;
4985
							// check to see if link matches an absolute url, if not then it must be a wiki link.
4986
							if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
4987
								$link = $linkValue;
4988
							} else {
4989
								$localLinkTitle = Title::newFromText( $linkValue );
4990
								if ( $localLinkTitle !== null ) {
4991
									$link = $localLinkTitle->getLinkURL();
4992
								}
4993
							}
4994
							break;
4995
						default:
4996
							// Must be a handler specific parameter.
4997
							if ( $handler->validateParam( $paramName, $match ) ) {
4998
								$handlerOptions[$paramName] = $match;
4999
							} else {
5000
								// Guess not, consider it as caption.
5001
								wfDebug( "$parameterMatch failed parameter validation\n" );
5002
								$label = '|' . $parameterMatch;
5003
							}
5004
						}
5005
5006
					} else {
5007
						// Last pipe wins.
5008
						$label = '|' . $parameterMatch;
5009
					}
5010
				}
5011
				// Remove the pipe.
5012
				$label = substr( $label, 1 );
5013
			}
5014
5015
			$ig->add( $title, $label, $alt, $link, $handlerOptions );
5016
		}
5017
		$html = $ig->toHTML();
5018
		Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5019
		return $html;
5020
	}
5021
5022
	/**
5023
	 * @param MediaHandler $handler
5024
	 * @return array
5025
	 */
5026
	public function getImageParams( $handler ) {
5027
		if ( $handler ) {
5028
			$handlerClass = get_class( $handler );
5029
		} else {
5030
			$handlerClass = '';
5031
		}
5032
		if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5033
			# Initialise static lists
5034
			static $internalParamNames = [
5035
				'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5036
				'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5037
					'bottom', 'text-bottom' ],
5038
				'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5039
					'upright', 'border', 'link', 'alt', 'class' ],
5040
			];
5041
			static $internalParamMap;
5042
			if ( !$internalParamMap ) {
5043
				$internalParamMap = [];
5044
				foreach ( $internalParamNames as $type => $names ) {
5045
					foreach ( $names as $name ) {
5046
						$magicName = str_replace( '-', '_', "img_$name" );
5047
						$internalParamMap[$magicName] = [ $type, $name ];
5048
					}
5049
				}
5050
			}
5051
5052
			# Add handler params
5053
			$paramMap = $internalParamMap;
5054
			if ( $handler ) {
5055
				$handlerParamMap = $handler->getParamMap();
5056
				foreach ( $handlerParamMap as $magic => $paramName ) {
5057
					$paramMap[$magic] = [ 'handler', $paramName ];
5058
				}
5059
			}
5060
			$this->mImageParams[$handlerClass] = $paramMap;
5061
			$this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5062
		}
5063
		return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5064
	}
5065
5066
	/**
5067
	 * Parse image options text and use it to make an image
5068
	 *
5069
	 * @param Title $title
5070
	 * @param string $options
5071
	 * @param LinkHolderArray|bool $holders
5072
	 * @return string HTML
5073
	 */
5074
	public function makeImage( $title, $options, $holders = false ) {
5075
		# Check if the options text is of the form "options|alt text"
5076
		# Options are:
5077
		#  * thumbnail  make a thumbnail with enlarge-icon and caption, alignment depends on lang
5078
		#  * left       no resizing, just left align. label is used for alt= only
5079
		#  * right      same, but right aligned
5080
		#  * none       same, but not aligned
5081
		#  * ___px      scale to ___ pixels width, no aligning. e.g. use in taxobox
5082
		#  * center     center the image
5083
		#  * frame      Keep original image size, no magnify-button.
5084
		#  * framed     Same as "frame"
5085
		#  * frameless  like 'thumb' but without a frame. Keeps user preferences for width
5086
		#  * upright    reduce width for upright images, rounded to full __0 px
5087
		#  * border     draw a 1px border around the image
5088
		#  * alt        Text for HTML alt attribute (defaults to empty)
5089
		#  * class      Set a class for img node
5090
		#  * link       Set the target of the image link. Can be external, interwiki, or local
5091
		# vertical-align values (no % or length right now):
5092
		#  * baseline
5093
		#  * sub
5094
		#  * super
5095
		#  * top
5096
		#  * text-top
5097
		#  * middle
5098
		#  * bottom
5099
		#  * text-bottom
5100
5101
		$parts = StringUtils::explode( "|", $options );
5102
5103
		# Give extensions a chance to select the file revision for us
5104
		$options = [];
5105
		$descQuery = false;
5106
		Hooks::run( 'BeforeParserFetchFileAndTitle',
5107
			[ $this, $title, &$options, &$descQuery ] );
5108
		# Fetch and register the file (file title may be different via hooks)
5109
		list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5110
5111
		# Get parameter map
5112
		$handler = $file ? $file->getHandler() : false;
5113
5114
		list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5115
5116
		if ( !$file ) {
5117
			$this->addTrackingCategory( 'broken-file-category' );
5118
		}
5119
5120
		# Process the input parameters
5121
		$caption = '';
5122
		$params = [ 'frame' => [], 'handler' => [],
5123
			'horizAlign' => [], 'vertAlign' => [] ];
5124
		$seenformat = false;
5125
		foreach ( $parts as $part ) {
5126
			$part = trim( $part );
5127
			list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5128
			$validated = false;
5129
			if ( isset( $paramMap[$magicName] ) ) {
5130
				list( $type, $paramName ) = $paramMap[$magicName];
5131
5132
				# Special case; width and height come in one variable together
5133
				if ( $type === 'handler' && $paramName === 'width' ) {
5134
					$parsedWidthParam = $this->parseWidthParam( $value );
5135 View Code Duplication
					if ( isset( $parsedWidthParam['width'] ) ) {
5136
						$width = $parsedWidthParam['width'];
5137
						if ( $handler->validateParam( 'width', $width ) ) {
5138
							$params[$type]['width'] = $width;
5139
							$validated = true;
5140
						}
5141
					}
5142 View Code Duplication
					if ( isset( $parsedWidthParam['height'] ) ) {
5143
						$height = $parsedWidthParam['height'];
5144
						if ( $handler->validateParam( 'height', $height ) ) {
5145
							$params[$type]['height'] = $height;
5146
							$validated = true;
5147
						}
5148
					}
5149
					# else no validation -- bug 13436
5150
				} else {
5151
					if ( $type === 'handler' ) {
5152
						# Validate handler parameter
5153
						$validated = $handler->validateParam( $paramName, $value );
5154
					} else {
5155
						# Validate internal parameters
5156
						switch ( $paramName ) {
5157
						case 'manualthumb':
5158
						case 'alt':
5159
						case 'class':
5160
							# @todo FIXME: Possibly check validity here for
5161
							# manualthumb? downstream behavior seems odd with
5162
							# missing manual thumbs.
5163
							$validated = true;
5164
							$value = $this->stripAltText( $value, $holders );
5165
							break;
5166
						case 'link':
5167
							$chars = self::EXT_LINK_URL_CLASS;
5168
							$addr = self::EXT_LINK_ADDR;
5169
							$prots = $this->mUrlProtocols;
5170
							if ( $value === '' ) {
5171
								$paramName = 'no-link';
5172
								$value = true;
5173
								$validated = true;
5174
							} elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5175
								if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5176
									$paramName = 'link-url';
5177
									$this->mOutput->addExternalLink( $value );
5178
									if ( $this->mOptions->getExternalLinkTarget() ) {
5179
										$params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5180
									}
5181
									$validated = true;
5182
								}
5183
							} else {
5184
								$linkTitle = Title::newFromText( $value );
5185
								if ( $linkTitle ) {
5186
									$paramName = 'link-title';
5187
									$value = $linkTitle;
5188
									$this->mOutput->addLink( $linkTitle );
5189
									$validated = true;
5190
								}
5191
							}
5192
							break;
5193
						case 'frameless':
5194
						case 'framed':
5195
						case 'thumbnail':
5196
							// use first appearing option, discard others.
5197
							$validated = ! $seenformat;
5198
							$seenformat = true;
5199
							break;
5200
						default:
5201
							# Most other things appear to be empty or numeric...
5202
							$validated = ( $value === false || is_numeric( trim( $value ) ) );
5203
						}
5204
					}
5205
5206
					if ( $validated ) {
5207
						$params[$type][$paramName] = $value;
5208
					}
5209
				}
5210
			}
5211
			if ( !$validated ) {
5212
				$caption = $part;
5213
			}
5214
		}
5215
5216
		# Process alignment parameters
5217
		if ( $params['horizAlign'] ) {
5218
			$params['frame']['align'] = key( $params['horizAlign'] );
5219
		}
5220
		if ( $params['vertAlign'] ) {
5221
			$params['frame']['valign'] = key( $params['vertAlign'] );
5222
		}
5223
5224
		$params['frame']['caption'] = $caption;
5225
5226
		# Will the image be presented in a frame, with the caption below?
5227
		$imageIsFramed = isset( $params['frame']['frame'] )
5228
			|| isset( $params['frame']['framed'] )
5229
			|| isset( $params['frame']['thumbnail'] )
5230
			|| isset( $params['frame']['manualthumb'] );
5231
5232
		# In the old days, [[Image:Foo|text...]] would set alt text.  Later it
5233
		# came to also set the caption, ordinary text after the image -- which
5234
		# makes no sense, because that just repeats the text multiple times in
5235
		# screen readers.  It *also* came to set the title attribute.
5236
		# Now that we have an alt attribute, we should not set the alt text to
5237
		# equal the caption: that's worse than useless, it just repeats the
5238
		# text.  This is the framed/thumbnail case.  If there's no caption, we
5239
		# use the unnamed parameter for alt text as well, just for the time be-
5240
		# ing, if the unnamed param is set and the alt param is not.
5241
		# For the future, we need to figure out if we want to tweak this more,
5242
		# e.g., introducing a title= parameter for the title; ignoring the un-
5243
		# named parameter entirely for images without a caption; adding an ex-
5244
		# plicit caption= parameter and preserving the old magic unnamed para-
5245
		# meter for BC; ...
5246
		if ( $imageIsFramed ) { # Framed image
5247
			if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5248
				# No caption or alt text, add the filename as the alt text so
5249
				# that screen readers at least get some description of the image
5250
				$params['frame']['alt'] = $title->getText();
5251
			}
5252
			# Do not set $params['frame']['title'] because tooltips don't make sense
5253
			# for framed images
5254
		} else { # Inline image
5255
			if ( !isset( $params['frame']['alt'] ) ) {
5256
				# No alt text, use the "caption" for the alt text
5257
				if ( $caption !== '' ) {
5258
					$params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5259
				} else {
5260
					# No caption, fall back to using the filename for the
5261
					# alt text
5262
					$params['frame']['alt'] = $title->getText();
5263
				}
5264
			}
5265
			# Use the "caption" for the tooltip text
5266
			$params['frame']['title'] = $this->stripAltText( $caption, $holders );
5267
		}
5268
5269
		Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5270
5271
		# Linker does the rest
5272
		$time = isset( $options['time'] ) ? $options['time'] : false;
5273
		$ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5274
			$time, $descQuery, $this->mOptions->getThumbSize() );
5275
5276
		# Give the handler a chance to modify the parser object
5277
		if ( $handler ) {
5278
			$handler->parserTransformHook( $this, $file );
5279
		}
5280
5281
		return $ret;
5282
	}
5283
5284
	/**
5285
	 * @param string $caption
5286
	 * @param LinkHolderArray|bool $holders
5287
	 * @return mixed|string
5288
	 */
5289
	protected function stripAltText( $caption, $holders ) {
5290
		# Strip bad stuff out of the title (tooltip).  We can't just use
5291
		# replaceLinkHoldersText() here, because if this function is called
5292
		# from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5293
		if ( $holders ) {
5294
			$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...
5295
		} else {
5296
			$tooltip = $this->replaceLinkHoldersText( $caption );
5297
		}
5298
5299
		# make sure there are no placeholders in thumbnail attributes
5300
		# that are later expanded to html- so expand them now and
5301
		# remove the tags
5302
		$tooltip = $this->mStripState->unstripBoth( $tooltip );
5303
		$tooltip = Sanitizer::stripAllTags( $tooltip );
5304
5305
		return $tooltip;
5306
	}
5307
5308
	/**
5309
	 * Set a flag in the output object indicating that the content is dynamic and
5310
	 * shouldn't be cached.
5311
	 * @deprecated since 1.28; use getOutput()->updateCacheExpiry()
5312
	 */
5313
	public function disableCache() {
5314
		wfDebug( "Parser output marked as uncacheable.\n" );
5315
		if ( !$this->mOutput ) {
5316
			throw new MWException( __METHOD__ .
5317
				" can only be called when actually parsing something" );
5318
		}
5319
		$this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5320
	}
5321
5322
	/**
5323
	 * Callback from the Sanitizer for expanding items found in HTML attribute
5324
	 * values, so they can be safely tested and escaped.
5325
	 *
5326
	 * @param string $text
5327
	 * @param bool|PPFrame $frame
5328
	 * @return string
5329
	 */
5330
	public function attributeStripCallback( &$text, $frame = false ) {
5331
		$text = $this->replaceVariables( $text, $frame );
5332
		$text = $this->mStripState->unstripBoth( $text );
5333
		return $text;
5334
	}
5335
5336
	/**
5337
	 * Accessor
5338
	 *
5339
	 * @return array
5340
	 */
5341
	public function getTags() {
5342
		return array_merge(
5343
			array_keys( $this->mTransparentTagHooks ),
5344
			array_keys( $this->mTagHooks ),
5345
			array_keys( $this->mFunctionTagHooks )
5346
		);
5347
	}
5348
5349
	/**
5350
	 * Replace transparent tags in $text with the values given by the callbacks.
5351
	 *
5352
	 * Transparent tag hooks are like regular XML-style tag hooks, except they
5353
	 * operate late in the transformation sequence, on HTML instead of wikitext.
5354
	 *
5355
	 * @param string $text
5356
	 *
5357
	 * @return string
5358
	 */
5359
	public function replaceTransparentTags( $text ) {
5360
		$matches = [];
5361
		$elements = array_keys( $this->mTransparentTagHooks );
5362
		$text = self::extractTagsAndParams( $elements, $text, $matches );
5363
		$replacements = [];
5364
5365
		foreach ( $matches as $marker => $data ) {
5366
			list( $element, $content, $params, $tag ) = $data;
5367
			$tagName = strtolower( $element );
5368
			if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5369
				$output = call_user_func_array(
5370
					$this->mTransparentTagHooks[$tagName],
5371
					[ $content, $params, $this ]
5372
				);
5373
			} else {
5374
				$output = $tag;
5375
			}
5376
			$replacements[$marker] = $output;
5377
		}
5378
		return strtr( $text, $replacements );
5379
	}
5380
5381
	/**
5382
	 * Break wikitext input into sections, and either pull or replace
5383
	 * some particular section's text.
5384
	 *
5385
	 * External callers should use the getSection and replaceSection methods.
5386
	 *
5387
	 * @param string $text Page wikitext
5388
	 * @param string|number $sectionId A section identifier string of the form:
5389
	 *   "<flag1> - <flag2> - ... - <section number>"
5390
	 *
5391
	 * Currently the only recognised flag is "T", which means the target section number
5392
	 * was derived during a template inclusion parse, in other words this is a template
5393
	 * section edit link. If no flags are given, it was an ordinary section edit link.
5394
	 * This flag is required to avoid a section numbering mismatch when a section is
5395
	 * enclosed by "<includeonly>" (bug 6563).
5396
	 *
5397
	 * The section number 0 pulls the text before the first heading; other numbers will
5398
	 * pull the given section along with its lower-level subsections. If the section is
5399
	 * not found, $mode=get will return $newtext, and $mode=replace will return $text.
5400
	 *
5401
	 * Section 0 is always considered to exist, even if it only contains the empty
5402
	 * string. If $text is the empty string and section 0 is replaced, $newText is
5403
	 * returned.
5404
	 *
5405
	 * @param string $mode One of "get" or "replace"
5406
	 * @param string $newText Replacement text for section data.
5407
	 * @return string For "get", the extracted section text.
5408
	 *   for "replace", the whole page with the section replaced.
5409
	 */
5410
	private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5411
		global $wgTitle; # not generally used but removes an ugly failure mode
5412
5413
		$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...
5414
		$this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5415
		$outText = '';
5416
		$frame = $this->getPreprocessor()->newFrame();
5417
5418
		# Process section extraction flags
5419
		$flags = 0;
5420
		$sectionParts = explode( '-', $sectionId );
5421
		$sectionIndex = array_pop( $sectionParts );
5422
		foreach ( $sectionParts as $part ) {
5423
			if ( $part === 'T' ) {
5424
				$flags |= self::PTD_FOR_INCLUSION;
5425
			}
5426
		}
5427
5428
		# Check for empty input
5429
		if ( strval( $text ) === '' ) {
5430
			# Only sections 0 and T-0 exist in an empty document
5431
			if ( $sectionIndex == 0 ) {
5432
				if ( $mode === 'get' ) {
5433
					return '';
5434
				} else {
5435
					return $newText;
5436
				}
5437
			} else {
5438
				if ( $mode === 'get' ) {
5439
					return $newText;
5440
				} else {
5441
					return $text;
5442
				}
5443
			}
5444
		}
5445
5446
		# Preprocess the text
5447
		$root = $this->preprocessToDom( $text, $flags );
5448
5449
		# <h> nodes indicate section breaks
5450
		# They can only occur at the top level, so we can find them by iterating the root's children
5451
		$node = $root->getFirstChild();
5452
5453
		# Find the target section
5454
		if ( $sectionIndex == 0 ) {
5455
			# Section zero doesn't nest, level=big
5456
			$targetLevel = 1000;
5457
		} else {
5458
			while ( $node ) {
5459 View Code Duplication
				if ( $node->getName() === 'h' ) {
5460
					$bits = $node->splitHeading();
5461
					if ( $bits['i'] == $sectionIndex ) {
5462
						$targetLevel = $bits['level'];
5463
						break;
5464
					}
5465
				}
5466
				if ( $mode === 'replace' ) {
5467
					$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5468
				}
5469
				$node = $node->getNextSibling();
5470
			}
5471
		}
5472
5473
		if ( !$node ) {
5474
			# Not found
5475
			if ( $mode === 'get' ) {
5476
				return $newText;
5477
			} else {
5478
				return $text;
5479
			}
5480
		}
5481
5482
		# Find the end of the section, including nested sections
5483
		do {
5484 View Code Duplication
			if ( $node->getName() === 'h' ) {
5485
				$bits = $node->splitHeading();
5486
				$curLevel = $bits['level'];
5487
				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...
5488
					break;
5489
				}
5490
			}
5491
			if ( $mode === 'get' ) {
5492
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5493
			}
5494
			$node = $node->getNextSibling();
5495
		} while ( $node );
5496
5497
		# Write out the remainder (in replace mode only)
5498
		if ( $mode === 'replace' ) {
5499
			# Output the replacement text
5500
			# Add two newlines on -- trailing whitespace in $newText is conventionally
5501
			# stripped by the editor, so we need both newlines to restore the paragraph gap
5502
			# Only add trailing whitespace if there is newText
5503
			if ( $newText != "" ) {
5504
				$outText .= $newText . "\n\n";
5505
			}
5506
5507
			while ( $node ) {
5508
				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5509
				$node = $node->getNextSibling();
5510
			}
5511
		}
5512
5513
		if ( is_string( $outText ) ) {
5514
			# Re-insert stripped tags
5515
			$outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5516
		}
5517
5518
		return $outText;
5519
	}
5520
5521
	/**
5522
	 * This function returns the text of a section, specified by a number ($section).
5523
	 * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
5524
	 * the first section before any such heading (section 0).
5525
	 *
5526
	 * If a section contains subsections, these are also returned.
5527
	 *
5528
	 * @param string $text Text to look in
5529
	 * @param string|number $sectionId Section identifier as a number or string
5530
	 * (e.g. 0, 1 or 'T-1').
5531
	 * @param string $defaultText Default to return if section is not found
5532
	 *
5533
	 * @return string Text of the requested section
5534
	 */
5535
	public function getSection( $text, $sectionId, $defaultText = '' ) {
5536
		return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5537
	}
5538
5539
	/**
5540
	 * This function returns $oldtext after the content of the section
5541
	 * specified by $section has been replaced with $text. If the target
5542
	 * section does not exist, $oldtext is returned unchanged.
5543
	 *
5544
	 * @param string $oldText Former text of the article
5545
	 * @param string|number $sectionId Section identifier as a number or string
5546
	 * (e.g. 0, 1 or 'T-1').
5547
	 * @param string $newText Replacing text
5548
	 *
5549
	 * @return string Modified text
5550
	 */
5551
	public function replaceSection( $oldText, $sectionId, $newText ) {
5552
		return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5553
	}
5554
5555
	/**
5556
	 * Get the ID of the revision we are parsing
5557
	 *
5558
	 * @return int|null
5559
	 */
5560
	public function getRevisionId() {
5561
		return $this->mRevisionId;
5562
	}
5563
5564
	/**
5565
	 * Get the revision object for $this->mRevisionId
5566
	 *
5567
	 * @return Revision|null Either a Revision object or null
5568
	 * @since 1.23 (public since 1.23)
5569
	 */
5570
	public function getRevisionObject() {
5571
		if ( !is_null( $this->mRevisionObject ) ) {
5572
			return $this->mRevisionObject;
5573
		}
5574
		if ( is_null( $this->mRevisionId ) ) {
5575
			return null;
5576
		}
5577
5578
		$rev = call_user_func(
5579
			$this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5580
		);
5581
5582
		# If the parse is for a new revision, then the callback should have
5583
		# already been set to force the object and should match mRevisionId.
5584
		# If not, try to fetch by mRevisionId for sanity.
5585
		if ( $rev && $rev->getId() != $this->mRevisionId ) {
5586
			$rev = Revision::newFromId( $this->mRevisionId );
5587
		}
5588
5589
		$this->mRevisionObject = $rev;
5590
5591
		return $this->mRevisionObject;
5592
	}
5593
5594
	/**
5595
	 * Get the timestamp associated with the current revision, adjusted for
5596
	 * the default server-local timestamp
5597
	 * @return string
5598
	 */
5599
	public function getRevisionTimestamp() {
5600
		if ( is_null( $this->mRevisionTimestamp ) ) {
5601
			global $wgContLang;
5602
5603
			$revObject = $this->getRevisionObject();
5604
			$timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5605
5606
			# The cryptic '' timezone parameter tells to use the site-default
5607
			# timezone offset instead of the user settings.
5608
			# Since this value will be saved into the parser cache, served
5609
			# to other users, and potentially even used inside links and such,
5610
			# it needs to be consistent for all visitors.
5611
			$this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5612
5613
		}
5614
		return $this->mRevisionTimestamp;
5615
	}
5616
5617
	/**
5618
	 * Get the name of the user that edited the last revision
5619
	 *
5620
	 * @return string User name
5621
	 */
5622
	public function getRevisionUser() {
5623
		if ( is_null( $this->mRevisionUser ) ) {
5624
			$revObject = $this->getRevisionObject();
5625
5626
			# if this template is subst: the revision id will be blank,
5627
			# so just use the current user's name
5628
			if ( $revObject ) {
5629
				$this->mRevisionUser = $revObject->getUserText();
5630
			} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5631
				$this->mRevisionUser = $this->getUser()->getName();
5632
			}
5633
		}
5634
		return $this->mRevisionUser;
5635
	}
5636
5637
	/**
5638
	 * Get the size of the revision
5639
	 *
5640
	 * @return int|null Revision size
5641
	 */
5642
	public function getRevisionSize() {
5643
		if ( is_null( $this->mRevisionSize ) ) {
5644
			$revObject = $this->getRevisionObject();
5645
5646
			# if this variable is subst: the revision id will be blank,
5647
			# so just use the parser input size, because the own substituation
5648
			# will change the size.
5649
			if ( $revObject ) {
5650
				$this->mRevisionSize = $revObject->getSize();
5651
			} else {
5652
				$this->mRevisionSize = $this->mInputSize;
5653
			}
5654
		}
5655
		return $this->mRevisionSize;
5656
	}
5657
5658
	/**
5659
	 * Mutator for $mDefaultSort
5660
	 *
5661
	 * @param string $sort New value
5662
	 */
5663
	public function setDefaultSort( $sort ) {
5664
		$this->mDefaultSort = $sort;
5665
		$this->mOutput->setProperty( 'defaultsort', $sort );
5666
	}
5667
5668
	/**
5669
	 * Accessor for $mDefaultSort
5670
	 * Will use the empty string if none is set.
5671
	 *
5672
	 * This value is treated as a prefix, so the
5673
	 * empty string is equivalent to sorting by
5674
	 * page name.
5675
	 *
5676
	 * @return string
5677
	 */
5678
	public function getDefaultSort() {
5679
		if ( $this->mDefaultSort !== false ) {
5680
			return $this->mDefaultSort;
5681
		} else {
5682
			return '';
5683
		}
5684
	}
5685
5686
	/**
5687
	 * Accessor for $mDefaultSort
5688
	 * Unlike getDefaultSort(), will return false if none is set
5689
	 *
5690
	 * @return string|bool
5691
	 */
5692
	public function getCustomDefaultSort() {
5693
		return $this->mDefaultSort;
5694
	}
5695
5696
	/**
5697
	 * Try to guess the section anchor name based on a wikitext fragment
5698
	 * presumably extracted from a heading, for example "Header" from
5699
	 * "== Header ==".
5700
	 *
5701
	 * @param string $text
5702
	 *
5703
	 * @return string
5704
	 */
5705
	public function guessSectionNameFromWikiText( $text ) {
5706
		# Strip out wikitext links(they break the anchor)
5707
		$text = $this->stripSectionName( $text );
5708
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5709
		return '#' . Sanitizer::escapeId( $text, 'noninitial' );
5710
	}
5711
5712
	/**
5713
	 * Same as guessSectionNameFromWikiText(), but produces legacy anchors
5714
	 * instead.  For use in redirects, since IE6 interprets Redirect: headers
5715
	 * as something other than UTF-8 (apparently?), resulting in breakage.
5716
	 *
5717
	 * @param string $text The section name
5718
	 * @return string An anchor
5719
	 */
5720
	public function guessLegacySectionNameFromWikiText( $text ) {
5721
		# Strip out wikitext links(they break the anchor)
5722
		$text = $this->stripSectionName( $text );
5723
		$text = Sanitizer::normalizeSectionNameWhitespace( $text );
5724
		return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
5725
	}
5726
5727
	/**
5728
	 * Strips a text string of wikitext for use in a section anchor
5729
	 *
5730
	 * Accepts a text string and then removes all wikitext from the
5731
	 * string and leaves only the resultant text (i.e. the result of
5732
	 * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
5733
	 * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
5734
	 * to create valid section anchors by mimicing the output of the
5735
	 * parser when headings are parsed.
5736
	 *
5737
	 * @param string $text Text string to be stripped of wikitext
5738
	 * for use in a Section anchor
5739
	 * @return string Filtered text string
5740
	 */
5741
	public function stripSectionName( $text ) {
5742
		# Strip internal link markup
5743
		$text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5744
		$text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5745
5746
		# Strip external link markup
5747
		# @todo FIXME: Not tolerant to blank link text
5748
		# I.E. [https://www.mediawiki.org] will render as [1] or something depending
5749
		# on how many empty links there are on the page - need to figure that out.
5750
		$text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5751
5752
		# Parse wikitext quotes (italics & bold)
5753
		$text = $this->doQuotes( $text );
5754
5755
		# Strip HTML tags
5756
		$text = StringUtils::delimiterReplace( '<', '>', '', $text );
5757
		return $text;
5758
	}
5759
5760
	/**
5761
	 * strip/replaceVariables/unstrip for preprocessor regression testing
5762
	 *
5763
	 * @param string $text
5764
	 * @param Title $title
5765
	 * @param ParserOptions $options
5766
	 * @param int $outputType
5767
	 *
5768
	 * @return string
5769
	 */
5770
	public function testSrvus( $text, Title $title, ParserOptions $options,
5771
		$outputType = self::OT_HTML
5772
	) {
5773
		$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...
5774
		$this->startParse( $title, $options, $outputType, true );
5775
5776
		$text = $this->replaceVariables( $text );
5777
		$text = $this->mStripState->unstripBoth( $text );
5778
		$text = Sanitizer::removeHTMLtags( $text );
5779
		return $text;
5780
	}
5781
5782
	/**
5783
	 * @param string $text
5784
	 * @param Title $title
5785
	 * @param ParserOptions $options
5786
	 * @return string
5787
	 */
5788
	public function testPst( $text, Title $title, ParserOptions $options ) {
5789
		return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5790
	}
5791
5792
	/**
5793
	 * @param string $text
5794
	 * @param Title $title
5795
	 * @param ParserOptions $options
5796
	 * @return string
5797
	 */
5798
	public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5799
		return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5800
	}
5801
5802
	/**
5803
	 * Call a callback function on all regions of the given text that are not
5804
	 * inside strip markers, and replace those regions with the return value
5805
	 * of the callback. For example, with input:
5806
	 *
5807
	 *  aaa<MARKER>bbb
5808
	 *
5809
	 * This will call the callback function twice, with 'aaa' and 'bbb'. Those
5810
	 * two strings will be replaced with the value returned by the callback in
5811
	 * each case.
5812
	 *
5813
	 * @param string $s
5814
	 * @param callable $callback
5815
	 *
5816
	 * @return string
5817
	 */
5818
	public function markerSkipCallback( $s, $callback ) {
5819
		$i = 0;
5820
		$out = '';
5821
		while ( $i < strlen( $s ) ) {
5822
			$markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5823
			if ( $markerStart === false ) {
5824
				$out .= call_user_func( $callback, substr( $s, $i ) );
5825
				break;
5826
			} else {
5827
				$out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5828
				$markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5829
				if ( $markerEnd === false ) {
5830
					$out .= substr( $s, $markerStart );
5831
					break;
5832
				} else {
5833
					$markerEnd += strlen( self::MARKER_SUFFIX );
5834
					$out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5835
					$i = $markerEnd;
5836
				}
5837
			}
5838
		}
5839
		return $out;
5840
	}
5841
5842
	/**
5843
	 * Remove any strip markers found in the given text.
5844
	 *
5845
	 * @param string $text Input string
5846
	 * @return string
5847
	 */
5848
	public function killMarkers( $text ) {
5849
		return $this->mStripState->killMarkers( $text );
5850
	}
5851
5852
	/**
5853
	 * Save the parser state required to convert the given half-parsed text to
5854
	 * HTML. "Half-parsed" in this context means the output of
5855
	 * recursiveTagParse() or internalParse(). This output has strip markers
5856
	 * from replaceVariables (extensionSubstitution() etc.), and link
5857
	 * placeholders from replaceLinkHolders().
5858
	 *
5859
	 * Returns an array which can be serialized and stored persistently. This
5860
	 * array can later be loaded into another parser instance with
5861
	 * unserializeHalfParsedText(). The text can then be safely incorporated into
5862
	 * the return value of a parser hook.
5863
	 *
5864
	 * @param string $text
5865
	 *
5866
	 * @return array
5867
	 */
5868
	public function serializeHalfParsedText( $text ) {
5869
		$data = [
5870
			'text' => $text,
5871
			'version' => self::HALF_PARSED_VERSION,
5872
			'stripState' => $this->mStripState->getSubState( $text ),
5873
			'linkHolders' => $this->mLinkHolders->getSubArray( $text )
5874
		];
5875
		return $data;
5876
	}
5877
5878
	/**
5879
	 * Load the parser state given in the $data array, which is assumed to
5880
	 * have been generated by serializeHalfParsedText(). The text contents is
5881
	 * extracted from the array, and its markers are transformed into markers
5882
	 * appropriate for the current Parser instance. This transformed text is
5883
	 * returned, and can be safely included in the return value of a parser
5884
	 * hook.
5885
	 *
5886
	 * If the $data array has been stored persistently, the caller should first
5887
	 * check whether it is still valid, by calling isValidHalfParsedText().
5888
	 *
5889
	 * @param array $data Serialized data
5890
	 * @throws MWException
5891
	 * @return string
5892
	 */
5893
	public function unserializeHalfParsedText( $data ) {
5894 View Code Duplication
		if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
5895
			throw new MWException( __METHOD__ . ': invalid version' );
5896
		}
5897
5898
		# First, extract the strip state.
5899
		$texts = [ $data['text'] ];
5900
		$texts = $this->mStripState->merge( $data['stripState'], $texts );
5901
5902
		# Now renumber links
5903
		$texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
5904
5905
		# Should be good to go.
5906
		return $texts[0];
5907
	}
5908
5909
	/**
5910
	 * Returns true if the given array, presumed to be generated by
5911
	 * serializeHalfParsedText(), is compatible with the current version of the
5912
	 * parser.
5913
	 *
5914
	 * @param array $data
5915
	 *
5916
	 * @return bool
5917
	 */
5918
	public function isValidHalfParsedText( $data ) {
5919
		return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
5920
	}
5921
5922
	/**
5923
	 * Parsed a width param of imagelink like 300px or 200x300px
5924
	 *
5925
	 * @param string $value
5926
	 *
5927
	 * @return array
5928
	 * @since 1.20
5929
	 */
5930
	public function parseWidthParam( $value ) {
5931
		$parsedWidthParam = [];
5932
		if ( $value === '' ) {
5933
			return $parsedWidthParam;
5934
		}
5935
		$m = [];
5936
		# (bug 13500) In both cases (width/height and width only),
5937
		# permit trailing "px" for backward compatibility.
5938
		if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
5939
			$width = intval( $m[1] );
5940
			$height = intval( $m[2] );
5941
			$parsedWidthParam['width'] = $width;
5942
			$parsedWidthParam['height'] = $height;
5943
		} elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
5944
			$width = intval( $value );
5945
			$parsedWidthParam['width'] = $width;
5946
		}
5947
		return $parsedWidthParam;
5948
	}
5949
5950
	/**
5951
	 * Lock the current instance of the parser.
5952
	 *
5953
	 * This is meant to stop someone from calling the parser
5954
	 * recursively and messing up all the strip state.
5955
	 *
5956
	 * @throws MWException If parser is in a parse
5957
	 * @return ScopedCallback The lock will be released once the return value goes out of scope.
5958
	 */
5959
	protected function lock() {
5960
		if ( $this->mInParse ) {
5961
			throw new MWException( "Parser state cleared while parsing. "
5962
				. "Did you call Parser::parse recursively?" );
5963
		}
5964
		$this->mInParse = true;
5965
5966
		$recursiveCheck = new ScopedCallback( function() {
5967
			$this->mInParse = false;
5968
		} );
5969
5970
		return $recursiveCheck;
5971
	}
5972
5973
	/**
5974
	 * Strip outer <p></p> tag from the HTML source of a single paragraph.
5975
	 *
5976
	 * Returns original HTML if the <p/> tag has any attributes, if there's no wrapping <p/> tag,
5977
	 * or if there is more than one <p/> tag in the input HTML.
5978
	 *
5979
	 * @param string $html
5980
	 * @return string
5981
	 * @since 1.24
5982
	 */
5983
	public static function stripOuterParagraph( $html ) {
5984
		$m = [];
5985
		if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
5986
			if ( strpos( $m[1], '</p>' ) === false ) {
5987
				$html = $m[1];
5988
			}
5989
		}
5990
5991
		return $html;
5992
	}
5993
5994
	/**
5995
	 * Return this parser if it is not doing anything, otherwise
5996
	 * get a fresh parser. You can use this method by doing
5997
	 * $myParser = $wgParser->getFreshParser(), or more simply
5998
	 * $wgParser->getFreshParser()->parse( ... );
5999
	 * if you're unsure if $wgParser is safe to use.
6000
	 *
6001
	 * @since 1.24
6002
	 * @return Parser A parser object that is not parsing anything
6003
	 */
6004
	public function getFreshParser() {
6005
		global $wgParserConf;
6006
		if ( $this->mInParse ) {
6007
			return new $wgParserConf['class']( $wgParserConf );
6008
		} else {
6009
			return $this;
6010
		}
6011
	}
6012
6013
	/**
6014
	 * Set's up the PHP implementation of OOUI for use in this request
6015
	 * and instructs OutputPage to enable OOUI for itself.
6016
	 *
6017
	 * @since 1.26
6018
	 */
6019
	public function enableOOUI() {
6020
		OutputPage::setupOOUI();
6021
		$this->mOutput->setEnableOOUI( true );
6022
	}
6023
}
6024