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