MediaWiki REL1_34
Parser.php
Go to the documentation of this file.
1<?php
30use Psr\Log\NullLogger;
31use Wikimedia\ScopedCallback;
32use Psr\Log\LoggerInterface;
33
74class Parser {
80 const VERSION = '1.6.4';
81
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
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
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
146 const MAX_TTS = 900;
147
148 # Persistent:
149 public $mTagHooks = [];
151 public $mFunctionHooks = [];
152 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
154 public $mStripList = [];
156 public $mVarCache = [];
157 public $mImageParams = [];
159 public $mMarkerIndex = 0;
163 public $mFirstCall = true;
164
165 # Initialised by initializeVariables()
166
171
176
181 public $mConf;
182
183 # Initialised in constructor
184 public $mExtLinkBracketedRegex, $mUrlProtocols;
185
186 # Initialized in getPreprocessor()
189
190 # Cleared with clearState():
194 public $mOutput;
196
201
207
208 public $mLinkID;
209 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
212 public $mExpensiveFunctionCount; # number of expensive parser function calls
216
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
228 public $mOptions;
229
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
254
262
267 public $mInParse = false;
268
270 protected $mProfiler;
271
275 protected $mLinkRenderer;
276
279
281 private $contLang;
282
284 private $factory;
285
288
296 private $svcOptions;
297
300
302 private $nsInfo;
303
305 private $logger;
306
309
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
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
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
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
457 public static function getDefaultPreprocessorClass() {
458 return Preprocessor_Hash::class;
459 }
460
464 public function firstCallInit() {
465 if ( !$this->mFirstCall ) {
466 return;
467 }
468 $this->mFirstCall = false;
469
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
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
532 public function resetOutput() {
533 $this->mOutput = new ParserOutput;
534 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
535 }
536
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
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
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
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
823 public function recursiveTagParseFully( $text, $frame = false ) {
824 $text = $this->recursiveTagParse( $text, $frame );
825 $text = $this->internalParseHalfParsed( $text, false );
826 return $text;
827 }
828
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
866 public function recursivePreprocess( $text, $frame = false ) {
867 $text = $this->replaceVariables( $text, $frame );
868 $text = $this->mStripState->unstripBoth( $text );
869 return $text;
870 }
871
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
906 public function setUser( $user ) {
907 $this->mUser = $user;
908 }
909
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
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
948 public function Title( Title $x = null ) : ?Title {
949 return wfSetVar( $this->mTitle, $x );
950 }
951
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
974 public function OutputType( $x = null ) {
975 return wfSetVar( $this->mOutputType, $x );
976 }
977
983 public function getOutput() {
984 return $this->mOutput;
985 }
986
992 public function getOptions() {
993 return $this->mOptions;
994 }
995
1002 public function Options( $x = null ) {
1003 return wfSetVar( $this->mOptions, $x );
1004 }
1005
1009 public function nextLinkID() {
1010 return $this->mLinkID++;
1011 }
1012
1016 public function setLinkID( $id ) {
1017 $this->mLinkID = $id;
1018 }
1019
1024 public function getFunctionLang() {
1025 return $this->getTargetLanguage();
1026 }
1027
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
1056 public function getConverterLanguage() {
1057 wfDeprecated( __METHOD__, '1.32' );
1058 return $this->getTargetLanguage();
1059 }
1060
1067 public function getUser() {
1068 if ( !is_null( $this->mUser ) ) {
1069 return $this->mUser;
1070 }
1071 return $this->mOptions->getUser();
1072 }
1073
1079 public function getPreprocessor() {
1080 if ( !isset( $this->mPreprocessor ) ) {
1081 $class = $this->svcOptions->get( 'preprocessorClass' );
1082 $this->mPreprocessor = new $class( $this );
1083 }
1084 return $this->mPreprocessor;
1085 }
1086
1093 public function getLinkRenderer() {
1094 // XXX We make the LinkRenderer with current options and then cache it forever
1095 if ( !$this->mLinkRenderer ) {
1096 $this->mLinkRenderer = $this->linkRendererFactory->create();
1097 $this->mLinkRenderer->setStubThreshold(
1098 $this->getOptions()->getStubThreshold()
1099 );
1100 }
1101
1102 return $this->mLinkRenderer;
1103 }
1104
1111 public function getMagicWordFactory() {
1112 return $this->magicWordFactory;
1113 }
1114
1121 public function getContentLanguage() {
1122 return $this->contLang;
1123 }
1124
1144 public static function extractTagsAndParams( $elements, $text, &$matches ) {
1145 static $n = 1;
1146 $stripped = '';
1147 $matches = [];
1148
1149 $taglist = implode( '|', $elements );
1150 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1151
1152 while ( $text != '' ) {
1153 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1154 $stripped .= $p[0];
1155 if ( count( $p ) < 5 ) {
1156 break;
1157 }
1158 if ( count( $p ) > 5 ) {
1159 # comment
1160 $element = $p[4];
1161 $attributes = '';
1162 $close = '';
1163 $inside = $p[5];
1164 } else {
1165 # tag
1166 list( , $element, $attributes, $close, $inside ) = $p;
1167 }
1168
1169 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1170 $stripped .= $marker;
1171
1172 if ( $close === '/>' ) {
1173 # Empty element tag, <tag />
1174 $content = null;
1175 $text = $inside;
1176 $tail = null;
1177 } else {
1178 if ( $element === '!--' ) {
1179 $end = '/(-->)/';
1180 } else {
1181 $end = "/(<\\/$element\\s*>)/i";
1182 }
1183 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1184 $content = $q[0];
1185 if ( count( $q ) < 3 ) {
1186 # No end tag -- let it run out to the end of the text.
1187 $tail = '';
1188 $text = '';
1189 } else {
1190 list( , $tail, $text ) = $q;
1191 }
1192 }
1193
1194 $matches[$marker] = [ $element,
1195 $content,
1196 Sanitizer::decodeTagAttributes( $attributes ),
1197 "<$element$attributes$close$content$tail" ];
1198 }
1199 return $stripped;
1200 }
1201
1207 public function getStripList() {
1208 return $this->mStripList;
1209 }
1210
1216 public function getStripState() {
1217 return $this->mStripState;
1218 }
1219
1229 public function insertStripItem( $text ) {
1230 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1231 $this->mMarkerIndex++;
1232 $this->mStripState->addGeneral( $marker, $text );
1233 return $marker;
1234 }
1235
1244 public function doTableStuff( $text ) {
1245 wfDeprecated( __METHOD__, '1.34' );
1246 return $this->handleTables( $text );
1247 }
1248
1255 private function handleTables( $text ) {
1256 $lines = StringUtils::explode( "\n", $text );
1257 $out = '';
1258 $td_history = []; # Is currently a td tag open?
1259 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1260 $tr_history = []; # Is currently a tr tag open?
1261 $tr_attributes = []; # history of tr attributes
1262 $has_opened_tr = []; # Did this table open a <tr> element?
1263 $indent_level = 0; # indent level of the table
1264
1265 foreach ( $lines as $outLine ) {
1266 $line = trim( $outLine );
1267
1268 if ( $line === '' ) { # empty line, go to next line
1269 $out .= $outLine . "\n";
1270 continue;
1271 }
1272
1273 $first_character = $line[0];
1274 $first_two = substr( $line, 0, 2 );
1275 $matches = [];
1276
1277 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1278 # First check if we are starting a new table
1279 $indent_level = strlen( $matches[1] );
1280
1281 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1282 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1283
1284 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1285 array_push( $td_history, false );
1286 array_push( $last_tag_history, '' );
1287 array_push( $tr_history, false );
1288 array_push( $tr_attributes, '' );
1289 array_push( $has_opened_tr, false );
1290 } elseif ( count( $td_history ) == 0 ) {
1291 # Don't do any of the following
1292 $out .= $outLine . "\n";
1293 continue;
1294 } elseif ( $first_two === '|}' ) {
1295 # We are ending a table
1296 $line = '</table>' . substr( $line, 2 );
1297 $last_tag = array_pop( $last_tag_history );
1298
1299 if ( !array_pop( $has_opened_tr ) ) {
1300 $line = "<tr><td></td></tr>{$line}";
1301 }
1302
1303 if ( array_pop( $tr_history ) ) {
1304 $line = "</tr>{$line}";
1305 }
1306
1307 if ( array_pop( $td_history ) ) {
1308 $line = "</{$last_tag}>{$line}";
1309 }
1310 array_pop( $tr_attributes );
1311 if ( $indent_level > 0 ) {
1312 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1313 } else {
1314 $outLine = $line;
1315 }
1316 } elseif ( $first_two === '|-' ) {
1317 # Now we have a table row
1318 $line = preg_replace( '#^\|-+#', '', $line );
1319
1320 # Whats after the tag is now only attributes
1321 $attributes = $this->mStripState->unstripBoth( $line );
1322 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1323 array_pop( $tr_attributes );
1324 array_push( $tr_attributes, $attributes );
1325
1326 $line = '';
1327 $last_tag = array_pop( $last_tag_history );
1328 array_pop( $has_opened_tr );
1329 array_push( $has_opened_tr, true );
1330
1331 if ( array_pop( $tr_history ) ) {
1332 $line = '</tr>';
1333 }
1334
1335 if ( array_pop( $td_history ) ) {
1336 $line = "</{$last_tag}>{$line}";
1337 }
1338
1339 $outLine = $line;
1340 array_push( $tr_history, false );
1341 array_push( $td_history, false );
1342 array_push( $last_tag_history, '' );
1343 } elseif ( $first_character === '|'
1344 || $first_character === '!'
1345 || $first_two === '|+'
1346 ) {
1347 # This might be cell elements, td, th or captions
1348 if ( $first_two === '|+' ) {
1349 $first_character = '+';
1350 $line = substr( $line, 2 );
1351 } else {
1352 $line = substr( $line, 1 );
1353 }
1354
1355 // Implies both are valid for table headings.
1356 if ( $first_character === '!' ) {
1357 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1358 }
1359
1360 # Split up multiple cells on the same line.
1361 # FIXME : This can result in improper nesting of tags processed
1362 # by earlier parser steps.
1363 $cells = explode( '||', $line );
1364
1365 $outLine = '';
1366
1367 # Loop through each table cell
1368 foreach ( $cells as $cell ) {
1369 $previous = '';
1370 if ( $first_character !== '+' ) {
1371 $tr_after = array_pop( $tr_attributes );
1372 if ( !array_pop( $tr_history ) ) {
1373 $previous = "<tr{$tr_after}>\n";
1374 }
1375 array_push( $tr_history, true );
1376 array_push( $tr_attributes, '' );
1377 array_pop( $has_opened_tr );
1378 array_push( $has_opened_tr, true );
1379 }
1380
1381 $last_tag = array_pop( $last_tag_history );
1382
1383 if ( array_pop( $td_history ) ) {
1384 $previous = "</{$last_tag}>\n{$previous}";
1385 }
1386
1387 if ( $first_character === '|' ) {
1388 $last_tag = 'td';
1389 } elseif ( $first_character === '!' ) {
1390 $last_tag = 'th';
1391 } elseif ( $first_character === '+' ) {
1392 $last_tag = 'caption';
1393 } else {
1394 $last_tag = '';
1395 }
1396
1397 array_push( $last_tag_history, $last_tag );
1398
1399 # A cell could contain both parameters and data
1400 $cell_data = explode( '|', $cell, 2 );
1401
1402 # T2553: Note that a '|' inside an invalid link should not
1403 # be mistaken as delimiting cell parameters
1404 # Bug T153140: Neither should language converter markup.
1405 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1406 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1407 } elseif ( count( $cell_data ) == 1 ) {
1408 // Whitespace in cells is trimmed
1409 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1410 } else {
1411 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1412 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1413 // Whitespace in cells is trimmed
1414 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1415 }
1416
1417 $outLine .= $cell;
1418 array_push( $td_history, true );
1419 }
1420 }
1421 $out .= $outLine . "\n";
1422 }
1423
1424 # Closing open td, tr && table
1425 while ( count( $td_history ) > 0 ) {
1426 if ( array_pop( $td_history ) ) {
1427 $out .= "</td>\n";
1428 }
1429 if ( array_pop( $tr_history ) ) {
1430 $out .= "</tr>\n";
1431 }
1432 if ( !array_pop( $has_opened_tr ) ) {
1433 $out .= "<tr><td></td></tr>\n";
1434 }
1435
1436 $out .= "</table>\n";
1437 }
1438
1439 # Remove trailing line-ending (b/c)
1440 if ( substr( $out, -1 ) === "\n" ) {
1441 $out = substr( $out, 0, -1 );
1442 }
1443
1444 # special case: don't return empty table
1445 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1446 $out = '';
1447 }
1448
1449 return $out;
1450 }
1451
1465 public function internalParse( $text, $isMain = true, $frame = false ) {
1466 $origText = $text;
1467
1468 // Avoid PHP 7.1 warning from passing $this by reference
1469 $parser = $this;
1470
1471 # Hook to suspend the parser in this state
1472 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1473 return $text;
1474 }
1475
1476 # if $frame is provided, then use $frame for replacing any variables
1477 if ( $frame ) {
1478 # use frame depth to infer how include/noinclude tags should be handled
1479 # depth=0 means this is the top-level document; otherwise it's an included document
1480 if ( !$frame->depth ) {
1481 $flag = 0;
1482 } else {
1483 $flag = self::PTD_FOR_INCLUSION;
1484 }
1485 $dom = $this->preprocessToDom( $text, $flag );
1486 $text = $frame->expand( $dom );
1487 } else {
1488 # if $frame is not provided, then use old-style replaceVariables
1489 $text = $this->replaceVariables( $text );
1490 }
1491
1492 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1493 $text = Sanitizer::removeHTMLtags(
1494 $text,
1495 [ $this, 'attributeStripCallback' ],
1496 false,
1497 array_keys( $this->mTransparentTagHooks ),
1498 [],
1499 [ $this, 'addTrackingCategory' ]
1500 );
1501 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1502
1503 # Tables need to come after variable replacement for things to work
1504 # properly; putting them before other transformations should keep
1505 # exciting things like link expansions from showing up in surprising
1506 # places.
1507 $text = $this->handleTables( $text );
1508
1509 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1510
1511 $text = $this->handleDoubleUnderscore( $text );
1512
1513 $text = $this->handleHeadings( $text );
1514 $text = $this->handleInternalLinks( $text );
1515 $text = $this->handleAllQuotes( $text );
1516 $text = $this->handleExternalLinks( $text );
1517
1518 # handleInternalLinks may sometimes leave behind
1519 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1520 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1521
1522 $text = $this->handleMagicLinks( $text );
1523 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1524
1525 return $text;
1526 }
1527
1537 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1538 $text = $this->mStripState->unstripGeneral( $text );
1539
1540 // Avoid PHP 7.1 warning from passing $this by reference
1541 $parser = $this;
1542
1543 if ( $isMain ) {
1544 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1545 }
1546
1547 # Clean up special characters, only run once, next-to-last before doBlockLevels
1548 $text = Sanitizer::armorFrenchSpaces( $text );
1549
1550 $text = $this->doBlockLevels( $text, $linestart );
1551
1552 $this->replaceLinkHoldersPrivate( $text );
1553
1561 if ( !( $this->mOptions->getDisableContentConversion()
1562 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1563 && !$this->mOptions->getInterfaceMessage()
1564 ) {
1565 # The position of the convert() call should not be changed. it
1566 # assumes that the links are all replaced and the only thing left
1567 # is the <nowiki> mark.
1568 $text = $this->getTargetLanguage()->convert( $text );
1569 }
1570
1571 $text = $this->mStripState->unstripNoWiki( $text );
1572
1573 if ( $isMain ) {
1574 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1575 }
1576
1577 $text = $this->replaceTransparentTags( $text );
1578 $text = $this->mStripState->unstripGeneral( $text );
1579
1580 $text = Sanitizer::normalizeCharReferences( $text );
1581
1582 if ( MWTidy::isEnabled() ) {
1583 if ( $this->mOptions->getTidy() ) {
1584 $text = MWTidy::tidy( $text );
1585 }
1586 } else {
1587 # attempt to sanitize at least some nesting problems
1588 # (T4702 and quite a few others)
1589 # This code path is buggy and deprecated!
1590 wfDeprecated( 'disabling tidy', '1.33' );
1591 $tidyregs = [
1592 # ''Something [http://www.cool.com cool''] -->
1593 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1594 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1595 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1596 # fix up an anchor inside another anchor, only
1597 # at least for a single single nested link (T5695)
1598 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1599 '\\1\\2</a>\\3</a>\\1\\4</a>',
1600 # fix div inside inline elements- doBlockLevels won't wrap a line which
1601 # contains a div, so fix it up here; replace
1602 # div with escaped text
1603 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1604 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1605 # remove empty italic or bold tag pairs, some
1606 # introduced by rules above
1607 '/<([bi])><\/\\1>/' => '',
1608 ];
1609
1610 $text = preg_replace(
1611 array_keys( $tidyregs ),
1612 array_values( $tidyregs ),
1613 $text );
1614 }
1615
1616 if ( $isMain ) {
1617 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1618 }
1619
1620 return $text;
1621 }
1622
1633 public function doMagicLinks( $text ) {
1634 wfDeprecated( __METHOD__, '1.34' );
1635 return $this->handleMagicLinks( $text );
1636 }
1637
1648 private function handleMagicLinks( $text ) {
1650 $urlChar = self::EXT_LINK_URL_CLASS;
1651 $addr = self::EXT_LINK_ADDR;
1652 $space = self::SPACE_NOT_NL; # non-newline space
1653 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1654 $spaces = "$space++"; # possessive match of 1 or more spaces
1655 $text = preg_replace_callback(
1656 '!(?: # Start cases
1657 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1658 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1659 (\b # m[3]: Free external links
1660 (?i:$prots)
1661 ($addr$urlChar*) # m[4]: Post-protocol path
1662 ) |
1663 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1664 ([0-9]+)\b |
1665 \bISBN $spaces ( # m[6]: ISBN, capture number
1666 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1667 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1668 [0-9Xx] # check digit
1669 )\b
1670 )!xu", [ $this, 'magicLinkCallback' ], $text );
1671 return $text;
1672 }
1673
1679 public function magicLinkCallback( $m ) {
1680 if ( isset( $m[1] ) && $m[1] !== '' ) {
1681 # Skip anchor
1682 return $m[0];
1683 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1684 # Skip HTML element
1685 return $m[0];
1686 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1687 # Free external link
1688 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1689 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1690 # RFC or PMID
1691 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1692 if ( !$this->mOptions->getMagicRFCLinks() ) {
1693 return $m[0];
1694 }
1695 $keyword = 'RFC';
1696 $urlmsg = 'rfcurl';
1697 $cssClass = 'mw-magiclink-rfc';
1698 $trackingCat = 'magiclink-tracking-rfc';
1699 $id = $m[5];
1700 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1701 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1702 return $m[0];
1703 }
1704 $keyword = 'PMID';
1705 $urlmsg = 'pubmedurl';
1706 $cssClass = 'mw-magiclink-pmid';
1707 $trackingCat = 'magiclink-tracking-pmid';
1708 $id = $m[5];
1709 } else {
1710 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1711 substr( $m[0], 0, 20 ) . '"' );
1712 }
1713 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1714 $this->addTrackingCategory( $trackingCat );
1715 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1716 } elseif ( isset( $m[6] ) && $m[6] !== ''
1717 && $this->mOptions->getMagicISBNLinks()
1718 ) {
1719 # ISBN
1720 $isbn = $m[6];
1721 $space = self::SPACE_NOT_NL; # non-newline space
1722 $isbn = preg_replace( "/$space/", ' ', $isbn );
1723 $num = strtr( $isbn, [
1724 '-' => '',
1725 ' ' => '',
1726 'x' => 'X',
1727 ] );
1728 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1729 return $this->getLinkRenderer()->makeKnownLink(
1730 SpecialPage::getTitleFor( 'Booksources', $num ),
1731 "ISBN $isbn",
1732 [
1733 'class' => 'internal mw-magiclink-isbn',
1734 'title' => false // suppress title attribute
1735 ]
1736 );
1737 } else {
1738 return $m[0];
1739 }
1740 }
1741
1751 public function makeFreeExternalLink( $url, $numPostProto ) {
1752 $trail = '';
1753
1754 # The characters '<' and '>' (which were escaped by
1755 # removeHTMLtags()) should not be included in
1756 # URLs, per RFC 2396.
1757 # Make &nbsp; terminate a URL as well (bug T84937)
1758 $m2 = [];
1759 if ( preg_match(
1760 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1761 $url,
1762 $m2,
1763 PREG_OFFSET_CAPTURE
1764 ) ) {
1765 $trail = substr( $url, $m2[0][1] ) . $trail;
1766 $url = substr( $url, 0, $m2[0][1] );
1767 }
1768
1769 # Move trailing punctuation to $trail
1770 $sep = ',;\.:!?';
1771 # If there is no left bracket, then consider right brackets fair game too
1772 if ( strpos( $url, '(' ) === false ) {
1773 $sep .= ')';
1774 }
1775
1776 $urlRev = strrev( $url );
1777 $numSepChars = strspn( $urlRev, $sep );
1778 # Don't break a trailing HTML entity by moving the ; into $trail
1779 # This is in hot code, so use substr_compare to avoid having to
1780 # create a new string object for the comparison
1781 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1782 # more optimization: instead of running preg_match with a $
1783 # anchor, which can be slow, do the match on the reversed
1784 # string starting at the desired offset.
1785 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1786 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1787 $numSepChars--;
1788 }
1789 }
1790 if ( $numSepChars ) {
1791 $trail = substr( $url, -$numSepChars ) . $trail;
1792 $url = substr( $url, 0, -$numSepChars );
1793 }
1794
1795 # Verify that we still have a real URL after trail removal, and
1796 # not just lone protocol
1797 if ( strlen( $trail ) >= $numPostProto ) {
1798 return $url . $trail;
1799 }
1800
1801 $url = Sanitizer::cleanUrl( $url );
1802
1803 # Is this an external image?
1804 $text = $this->maybeMakeExternalImage( $url );
1805 if ( $text === false ) {
1806 # Not an image, make a link
1807 $text = Linker::makeExternalLink( $url,
1808 $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1809 true, 'free',
1810 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1811 # Register it in the output object...
1812 $this->mOutput->addExternalLink( $url );
1813 }
1814 return $text . $trail;
1815 }
1816
1825 public function doHeadings( $text ) {
1826 wfDeprecated( __METHOD__, '1.34' );
1827 return $this->handleHeadings( $text );
1828 }
1829
1836 private function handleHeadings( $text ) {
1837 for ( $i = 6; $i >= 1; --$i ) {
1838 $h = str_repeat( '=', $i );
1839 // Trim non-newline whitespace from headings
1840 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1841 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1842 }
1843 return $text;
1844 }
1845
1855 public function doAllQuotes( $text ) {
1856 wfDeprecated( __METHOD__, '1.34' );
1857 return $this->handleAllQuotes( $text );
1858 }
1859
1867 private function handleAllQuotes( $text ) {
1868 $outtext = '';
1869 $lines = StringUtils::explode( "\n", $text );
1870 foreach ( $lines as $line ) {
1871 $outtext .= $this->doQuotes( $line ) . "\n";
1872 }
1873 $outtext = substr( $outtext, 0, -1 );
1874 return $outtext;
1875 }
1876
1885 public function doQuotes( $text ) {
1886 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1887 $countarr = count( $arr );
1888 if ( $countarr == 1 ) {
1889 return $text;
1890 }
1891
1892 // First, do some preliminary work. This may shift some apostrophes from
1893 // being mark-up to being text. It also counts the number of occurrences
1894 // of bold and italics mark-ups.
1895 $numbold = 0;
1896 $numitalics = 0;
1897 for ( $i = 1; $i < $countarr; $i += 2 ) {
1898 $thislen = strlen( $arr[$i] );
1899 // If there are ever four apostrophes, assume the first is supposed to
1900 // be text, and the remaining three constitute mark-up for bold text.
1901 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1902 if ( $thislen == 4 ) {
1903 $arr[$i - 1] .= "'";
1904 $arr[$i] = "'''";
1905 $thislen = 3;
1906 } elseif ( $thislen > 5 ) {
1907 // If there are more than 5 apostrophes in a row, assume they're all
1908 // text except for the last 5.
1909 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1910 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1911 $arr[$i] = "'''''";
1912 $thislen = 5;
1913 }
1914 // Count the number of occurrences of bold and italics mark-ups.
1915 if ( $thislen == 2 ) {
1916 $numitalics++;
1917 } elseif ( $thislen == 3 ) {
1918 $numbold++;
1919 } elseif ( $thislen == 5 ) {
1920 $numitalics++;
1921 $numbold++;
1922 }
1923 }
1924
1925 // If there is an odd number of both bold and italics, it is likely
1926 // that one of the bold ones was meant to be an apostrophe followed
1927 // by italics. Which one we cannot know for certain, but it is more
1928 // likely to be one that has a single-letter word before it.
1929 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1930 $firstsingleletterword = -1;
1931 $firstmultiletterword = -1;
1932 $firstspace = -1;
1933 for ( $i = 1; $i < $countarr; $i += 2 ) {
1934 if ( strlen( $arr[$i] ) == 3 ) {
1935 $x1 = substr( $arr[$i - 1], -1 );
1936 $x2 = substr( $arr[$i - 1], -2, 1 );
1937 if ( $x1 === ' ' ) {
1938 if ( $firstspace == -1 ) {
1939 $firstspace = $i;
1940 }
1941 } elseif ( $x2 === ' ' ) {
1942 $firstsingleletterword = $i;
1943 // if $firstsingleletterword is set, we don't
1944 // look at the other options, so we can bail early.
1945 break;
1946 } elseif ( $firstmultiletterword == -1 ) {
1947 $firstmultiletterword = $i;
1948 }
1949 }
1950 }
1951
1952 // If there is a single-letter word, use it!
1953 if ( $firstsingleletterword > -1 ) {
1954 $arr[$firstsingleletterword] = "''";
1955 $arr[$firstsingleletterword - 1] .= "'";
1956 } elseif ( $firstmultiletterword > -1 ) {
1957 // If not, but there's a multi-letter word, use that one.
1958 $arr[$firstmultiletterword] = "''";
1959 $arr[$firstmultiletterword - 1] .= "'";
1960 } elseif ( $firstspace > -1 ) {
1961 // ... otherwise use the first one that has neither.
1962 // (notice that it is possible for all three to be -1 if, for example,
1963 // there is only one pentuple-apostrophe in the line)
1964 $arr[$firstspace] = "''";
1965 $arr[$firstspace - 1] .= "'";
1966 }
1967 }
1968
1969 // Now let's actually convert our apostrophic mush to HTML!
1970 $output = '';
1971 $buffer = '';
1972 $state = '';
1973 $i = 0;
1974 foreach ( $arr as $r ) {
1975 if ( ( $i % 2 ) == 0 ) {
1976 if ( $state === 'both' ) {
1977 $buffer .= $r;
1978 } else {
1979 $output .= $r;
1980 }
1981 } else {
1982 $thislen = strlen( $r );
1983 if ( $thislen == 2 ) {
1984 if ( $state === 'i' ) {
1985 $output .= '</i>';
1986 $state = '';
1987 } elseif ( $state === 'bi' ) {
1988 $output .= '</i>';
1989 $state = 'b';
1990 } elseif ( $state === 'ib' ) {
1991 $output .= '</b></i><b>';
1992 $state = 'b';
1993 } elseif ( $state === 'both' ) {
1994 $output .= '<b><i>' . $buffer . '</i>';
1995 $state = 'b';
1996 } else { // $state can be 'b' or ''
1997 $output .= '<i>';
1998 $state .= 'i';
1999 }
2000 } elseif ( $thislen == 3 ) {
2001 if ( $state === 'b' ) {
2002 $output .= '</b>';
2003 $state = '';
2004 } elseif ( $state === 'bi' ) {
2005 $output .= '</i></b><i>';
2006 $state = 'i';
2007 } elseif ( $state === 'ib' ) {
2008 $output .= '</b>';
2009 $state = 'i';
2010 } elseif ( $state === 'both' ) {
2011 $output .= '<i><b>' . $buffer . '</b>';
2012 $state = 'i';
2013 } else { // $state can be 'i' or ''
2014 $output .= '<b>';
2015 $state .= 'b';
2016 }
2017 } elseif ( $thislen == 5 ) {
2018 if ( $state === 'b' ) {
2019 $output .= '</b><i>';
2020 $state = 'i';
2021 } elseif ( $state === 'i' ) {
2022 $output .= '</i><b>';
2023 $state = 'b';
2024 } elseif ( $state === 'bi' ) {
2025 $output .= '</i></b>';
2026 $state = '';
2027 } elseif ( $state === 'ib' ) {
2028 $output .= '</b></i>';
2029 $state = '';
2030 } elseif ( $state === 'both' ) {
2031 $output .= '<i><b>' . $buffer . '</b></i>';
2032 $state = '';
2033 } else { // ($state == '')
2034 $buffer = '';
2035 $state = 'both';
2036 }
2037 }
2038 }
2039 $i++;
2040 }
2041 // Now close all remaining tags. Notice that the order is important.
2042 if ( $state === 'b' || $state === 'ib' ) {
2043 $output .= '</b>';
2044 }
2045 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2046 $output .= '</i>';
2047 }
2048 if ( $state === 'bi' ) {
2049 $output .= '</b>';
2050 }
2051 // There might be lonely ''''', so make sure we have a buffer
2052 if ( $state === 'both' && $buffer ) {
2053 $output .= '<b><i>' . $buffer . '</i></b>';
2054 }
2055 return $output;
2056 }
2057
2071 public function replaceExternalLinks( $text ) {
2072 wfDeprecated( __METHOD__, '1.34' );
2073 return $this->handleExternalLinks( $text );
2074 }
2075
2086 private function handleExternalLinks( $text ) {
2087 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2088 // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2089 if ( $bits === false ) {
2090 throw new MWException( "PCRE needs to be compiled with "
2091 . "--enable-unicode-properties in order for MediaWiki to function" );
2092 }
2093 $s = array_shift( $bits );
2094
2095 $i = 0;
2096 while ( $i < count( $bits ) ) {
2097 $url = $bits[$i++];
2098 $i++; // protocol
2099 $text = $bits[$i++];
2100 $trail = $bits[$i++];
2101
2102 # The characters '<' and '>' (which were escaped by
2103 # removeHTMLtags()) should not be included in
2104 # URLs, per RFC 2396.
2105 $m2 = [];
2106 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2107 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2108 $url = substr( $url, 0, $m2[0][1] );
2109 }
2110
2111 # If the link text is an image URL, replace it with an <img> tag
2112 # This happened by accident in the original parser, but some people used it extensively
2113 $img = $this->maybeMakeExternalImage( $text );
2114 if ( $img !== false ) {
2115 $text = $img;
2116 }
2117
2118 $dtrail = '';
2119
2120 # Set linktype for CSS
2121 $linktype = 'text';
2122
2123 # No link text, e.g. [http://domain.tld/some.link]
2124 if ( $text == '' ) {
2125 # Autonumber
2126 $langObj = $this->getTargetLanguage();
2127 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2128 $linktype = 'autonumber';
2129 } else {
2130 # Have link text, e.g. [http://domain.tld/some.link text]s
2131 # Check for trail
2132 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2133 }
2134
2135 // Excluding protocol-relative URLs may avoid many false positives.
2136 if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
2137 $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
2138 }
2139
2140 $url = Sanitizer::cleanUrl( $url );
2141
2142 # Use the encoded URL
2143 # This means that users can paste URLs directly into the text
2144 # Funny characters like ö aren't valid in URLs anyway
2145 # This was changed in August 2004
2146 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2147 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
2148
2149 # Register link in the output object.
2150 $this->mOutput->addExternalLink( $url );
2151 }
2152
2153 return $s;
2154 }
2155
2166 public static function getExternalLinkRel( $url = false, $title = null ) {
2168 $ns = $title ? $title->getNamespace() : false;
2169 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2171 ) {
2172 return 'nofollow';
2173 }
2174 return null;
2175 }
2176
2188 public function getExternalLinkAttribs( $url ) {
2189 $attribs = [];
2190 $rel = self::getExternalLinkRel( $url, $this->mTitle );
2191
2192 $target = $this->mOptions->getExternalLinkTarget();
2193 if ( $target ) {
2194 $attribs['target'] = $target;
2195 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2196 // T133507. New windows can navigate parent cross-origin.
2197 // Including noreferrer due to lacking browser
2198 // support of noopener. Eventually noreferrer should be removed.
2199 if ( $rel !== '' ) {
2200 $rel .= ' ';
2201 }
2202 $rel .= 'noreferrer noopener';
2203 }
2204 }
2205 $attribs['rel'] = $rel;
2206 return $attribs;
2207 }
2208
2219 public static function normalizeLinkUrl( $url ) {
2220 # Test for RFC 3986 IPv6 syntax
2221 $scheme = '[a-z][a-z0-9+.-]*:';
2222 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2223 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2224 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2225 IP::isValid( rawurldecode( $m[1] ) )
2226 ) {
2227 $isIPv6 = rawurldecode( $m[1] );
2228 } else {
2229 $isIPv6 = false;
2230 }
2231
2232 # Make sure unsafe characters are encoded
2233 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2234 function ( $m ) {
2235 return rawurlencode( $m[0] );
2236 },
2237 $url
2238 );
2239
2240 $ret = '';
2241 $end = strlen( $url );
2242
2243 # Fragment part - 'fragment'
2244 $start = strpos( $url, '#' );
2245 if ( $start !== false && $start < $end ) {
2246 $ret = self::normalizeUrlComponent(
2247 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2248 $end = $start;
2249 }
2250
2251 # Query part - 'query' minus &=+;
2252 $start = strpos( $url, '?' );
2253 if ( $start !== false && $start < $end ) {
2254 $ret = self::normalizeUrlComponent(
2255 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2256 $end = $start;
2257 }
2258
2259 # Scheme and path part - 'pchar'
2260 # (we assume no userinfo or encoded colons in the host)
2261 $ret = self::normalizeUrlComponent(
2262 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2263
2264 # Fix IPv6 syntax
2265 if ( $isIPv6 !== false ) {
2266 $ipv6Host = "%5B({$isIPv6})%5D";
2267 $ret = preg_replace(
2268 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2269 "$1[$2]",
2270 $ret
2271 );
2272 }
2273
2274 return $ret;
2275 }
2276
2277 private static function normalizeUrlComponent( $component, $unsafe ) {
2278 $callback = function ( $matches ) use ( $unsafe ) {
2279 $char = urldecode( $matches[0] );
2280 $ord = ord( $char );
2281 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2282 # Unescape it
2283 return $char;
2284 } else {
2285 # Leave it escaped, but use uppercase for a-f
2286 return strtoupper( $matches[0] );
2287 }
2288 };
2289 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2290 }
2291
2300 private function maybeMakeExternalImage( $url ) {
2301 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2302 $imagesexception = !empty( $imagesfrom );
2303 $text = false;
2304 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2305 if ( $imagesexception && is_array( $imagesfrom ) ) {
2306 $imagematch = false;
2307 foreach ( $imagesfrom as $match ) {
2308 if ( strpos( $url, $match ) === 0 ) {
2309 $imagematch = true;
2310 break;
2311 }
2312 }
2313 } elseif ( $imagesexception ) {
2314 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2315 } else {
2316 $imagematch = false;
2317 }
2318
2319 if ( $this->mOptions->getAllowExternalImages()
2320 || ( $imagesexception && $imagematch )
2321 ) {
2322 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2323 # Image found
2324 $text = Linker::makeExternalImage( $url );
2325 }
2326 }
2327 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2328 && preg_match( self::EXT_IMAGE_REGEX, $url )
2329 ) {
2330 $whitelist = explode(
2331 "\n",
2332 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2333 );
2334
2335 foreach ( $whitelist as $entry ) {
2336 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2337 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2338 continue;
2339 }
2340 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2341 # Image matches a whitelist entry
2342 $text = Linker::makeExternalImage( $url );
2343 break;
2344 }
2345 }
2346 }
2347 return $text;
2348 }
2349
2360 public function replaceInternalLinks( $text ) {
2361 wfDeprecated( __METHOD__, '1.34' );
2362 return $this->handleInternalLinks( $text );
2363 }
2364
2372 private function handleInternalLinks( $text ) {
2373 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2374 return $text;
2375 }
2376
2386 public function replaceInternalLinks2( &$text ) {
2387 wfDeprecated( __METHOD__, '1.34' );
2388 return $this->handleInternalLinks2( $text );
2389 }
2390
2397 private function handleInternalLinks2( &$s ) {
2398 static $tc = false, $e1, $e1_img;
2399 # the % is needed to support urlencoded titles as well
2400 if ( !$tc ) {
2401 $tc = Title::legalChars() . '#%';
2402 # Match a link having the form [[namespace:link|alternate]]trail
2403 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2404 # Match cases where there is no "]]", which might still be images
2405 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2406 }
2407
2408 $holders = new LinkHolderArray( $this );
2409
2410 # split the entire text string on occurrences of [[
2411 $a = StringUtils::explode( '[[', ' ' . $s );
2412 # get the first element (all text up to first [[), and remove the space we added
2413 $s = $a->current();
2414 $a->next();
2415 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2416 $s = substr( $s, 1 );
2417
2418 if ( is_null( $this->mTitle ) ) {
2419 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2420 }
2421 $nottalk = !$this->mTitle->isTalkPage();
2422
2423 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2424 $e2 = null;
2425 if ( $useLinkPrefixExtension ) {
2426 # Match the end of a line for a word that's not followed by whitespace,
2427 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2428 $charset = $this->contLang->linkPrefixCharset();
2429 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2430 $m = [];
2431 if ( preg_match( $e2, $s, $m ) ) {
2432 $first_prefix = $m[2];
2433 } else {
2434 $first_prefix = false;
2435 }
2436 } else {
2437 $prefix = '';
2438 }
2439
2440 # Some namespaces don't allow subpages
2441 $useSubpages = $this->nsInfo->hasSubpages(
2442 $this->mTitle->getNamespace()
2443 );
2444
2445 # Loop for each link
2446 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2447 # Check for excessive memory usage
2448 if ( $holders->isBig() ) {
2449 # Too big
2450 # Do the existence check, replace the link holders and clear the array
2451 $holders->replace( $s );
2452 $holders->clear();
2453 }
2454
2455 if ( $useLinkPrefixExtension ) {
2456 if ( preg_match( $e2, $s, $m ) ) {
2457 list( , $s, $prefix ) = $m;
2458 } else {
2459 $prefix = '';
2460 }
2461 # first link
2462 if ( $first_prefix ) {
2463 $prefix = $first_prefix;
2464 $first_prefix = false;
2465 }
2466 }
2467
2468 $might_be_img = false;
2469
2470 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2471 $text = $m[2];
2472 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2473 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2474 # the real problem is with the $e1 regex
2475 # See T1500.
2476 # Still some problems for cases where the ] is meant to be outside punctuation,
2477 # and no image is in sight. See T4095.
2478 if ( $text !== ''
2479 && substr( $m[3], 0, 1 ) === ']'
2480 && strpos( $text, '[' ) !== false
2481 ) {
2482 $text .= ']'; # so that handleExternalLinks($text) works later
2483 $m[3] = substr( $m[3], 1 );
2484 }
2485 # fix up urlencoded title texts
2486 if ( strpos( $m[1], '%' ) !== false ) {
2487 # Should anchors '#' also be rejected?
2488 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2489 }
2490 $trail = $m[3];
2491 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2492 # Invalid, but might be an image with a link in its caption
2493 $might_be_img = true;
2494 $text = $m[2];
2495 if ( strpos( $m[1], '%' ) !== false ) {
2496 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2497 }
2498 $trail = "";
2499 } else { # Invalid form; output directly
2500 $s .= $prefix . '[[' . $line;
2501 continue;
2502 }
2503
2504 $origLink = ltrim( $m[1], ' ' );
2505
2506 # Don't allow internal links to pages containing
2507 # PROTO: where PROTO is a valid URL protocol; these
2508 # should be external links.
2509 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2510 $s .= $prefix . '[[' . $line;
2511 continue;
2512 }
2513
2514 # Make subpage if necessary
2515 if ( $useSubpages ) {
2517 $this->mTitle, $origLink, $text
2518 );
2519 } else {
2520 $link = $origLink;
2521 }
2522
2523 // \x7f isn't a default legal title char, so most likely strip
2524 // markers will force us into the "invalid form" path above. But,
2525 // just in case, let's assert that xmlish tags aren't valid in
2526 // the title position.
2527 $unstrip = $this->mStripState->killMarkers( $link );
2528 $noMarkers = ( $unstrip === $link );
2529
2530 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2531 if ( $nt === null ) {
2532 $s .= $prefix . '[[' . $line;
2533 continue;
2534 }
2535
2536 $ns = $nt->getNamespace();
2537 $iw = $nt->getInterwiki();
2538
2539 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2540
2541 if ( $might_be_img ) { # if this is actually an invalid link
2542 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2543 $found = false;
2544 while ( true ) {
2545 # look at the next 'line' to see if we can close it there
2546 $a->next();
2547 $next_line = $a->current();
2548 if ( $next_line === false || $next_line === null ) {
2549 break;
2550 }
2551 $m = explode( ']]', $next_line, 3 );
2552 if ( count( $m ) == 3 ) {
2553 # the first ]] closes the inner link, the second the image
2554 $found = true;
2555 $text .= "[[{$m[0]}]]{$m[1]}";
2556 $trail = $m[2];
2557 break;
2558 } elseif ( count( $m ) == 2 ) {
2559 # if there's exactly one ]] that's fine, we'll keep looking
2560 $text .= "[[{$m[0]}]]{$m[1]}";
2561 } else {
2562 # if $next_line is invalid too, we need look no further
2563 $text .= '[[' . $next_line;
2564 break;
2565 }
2566 }
2567 if ( !$found ) {
2568 # we couldn't find the end of this imageLink, so output it raw
2569 # but don't ignore what might be perfectly normal links in the text we've examined
2570 $holders->merge( $this->handleInternalLinks2( $text ) );
2571 $s .= "{$prefix}[[$link|$text";
2572 # note: no $trail, because without an end, there *is* no trail
2573 continue;
2574 }
2575 } else { # it's not an image, so output it raw
2576 $s .= "{$prefix}[[$link|$text";
2577 # note: no $trail, because without an end, there *is* no trail
2578 continue;
2579 }
2580 }
2581
2582 $wasblank = ( $text == '' );
2583 if ( $wasblank ) {
2584 $text = $link;
2585 if ( !$noforce ) {
2586 # Strip off leading ':'
2587 $text = substr( $text, 1 );
2588 }
2589 } else {
2590 # T6598 madness. Handle the quotes only if they come from the alternate part
2591 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2592 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2593 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2594 $text = $this->doQuotes( $text );
2595 }
2596
2597 # Link not escaped by : , create the various objects
2598 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2599 # Interwikis
2600 if (
2601 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2602 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2603 in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes' ) )
2604 )
2605 ) {
2606 # T26502: filter duplicates
2607 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2608 $this->mLangLinkLanguages[$iw] = true;
2609 $this->mOutput->addLanguageLink( $nt->getFullText() );
2610 }
2611
2615 $s = rtrim( $s . $prefix ) . $trail; # T175416
2616 continue;
2617 }
2618
2619 if ( $ns == NS_FILE ) {
2620 if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->mTitle ) ) {
2621 if ( $wasblank ) {
2622 # if no parameters were passed, $text
2623 # becomes something like "File:Foo.png",
2624 # which we don't want to pass on to the
2625 # image generator
2626 $text = '';
2627 } else {
2628 # recursively parse links inside the image caption
2629 # actually, this will parse them in any other parameters, too,
2630 # but it might be hard to fix that, and it doesn't matter ATM
2631 $text = $this->handleExternalLinks( $text );
2632 $holders->merge( $this->handleInternalLinks2( $text ) );
2633 }
2634 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2635 $s .= $prefix . $this->armorLinksPrivate(
2636 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2637 continue;
2638 }
2639 } elseif ( $ns == NS_CATEGORY ) {
2643 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2644
2645 if ( $wasblank ) {
2646 $sortkey = $this->getDefaultSort();
2647 } else {
2648 $sortkey = $text;
2649 }
2650 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2651 $sortkey = str_replace( "\n", '', $sortkey );
2652 $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2653 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2654
2655 continue;
2656 }
2657 }
2658
2659 # Self-link checking. For some languages, variants of the title are checked in
2660 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2661 # for linking to a different variant.
2662 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2663 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2664 continue;
2665 }
2666
2667 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2668 # @todo FIXME: Should do batch file existence checks, see comment below
2669 if ( $ns == NS_MEDIA ) {
2670 # Give extensions a chance to select the file revision for us
2671 $options = [];
2672 $descQuery = false;
2673 Hooks::run( 'BeforeParserFetchFileAndTitle',
2674 [ $this, $nt, &$options, &$descQuery ] );
2675 # Fetch and register the file (file title may be different via hooks)
2676 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2677 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2678 $s .= $prefix . $this->armorLinksPrivate(
2679 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2680 continue;
2681 }
2682
2683 # Some titles, such as valid special pages or files in foreign repos, should
2684 # be shown as bluelinks even though they're not included in the page table
2685 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2686 # batch file existence checks for NS_FILE and NS_MEDIA
2687 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2688 $this->mOutput->addLink( $nt );
2689 $s .= $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2690 } else {
2691 # Links will be added to the output link list after checking
2692 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2693 }
2694 }
2695 return $holders;
2696 }
2697
2712 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2713 wfDeprecated( __METHOD__, '1.34' );
2714 return $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2715 }
2716
2730 private function makeKnownLinkHolderPrivate( $nt, $text = '', $trail = '', $prefix = '' ) {
2731 list( $inside, $trail ) = Linker::splitTrail( $trail );
2732
2733 if ( $text == '' ) {
2734 $text = htmlspecialchars( $nt->getPrefixedText() );
2735 }
2736
2737 $link = $this->getLinkRenderer()->makeKnownLink(
2738 $nt, new HtmlArmor( "$prefix$text$inside" )
2739 );
2740
2741 return $this->armorLinksPrivate( $link ) . $trail;
2742 }
2743
2755 public function armorLinks( $text ) {
2756 wfDeprecated( __METHOD__, '1.34' );
2757 return $this->armorLinksPrivate( $text );
2758 }
2759
2770 private function armorLinksPrivate( $text ) {
2771 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2772 self::MARKER_PREFIX . "NOPARSE$1", $text );
2773 }
2774
2780 public function areSubpagesAllowed() {
2781 # Some namespaces don't allow subpages
2782 wfDeprecated( __METHOD__, '1.34' );
2783 return $this->nsInfo->hasSubpages( $this->mTitle->getNamespace() );
2784 }
2785
2795 public function maybeDoSubpageLink( $target, &$text ) {
2796 wfDeprecated( __METHOD__, '1.34' );
2797 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2798 }
2799
2808 public function doBlockLevels( $text, $linestart ) {
2809 return BlockLevelPass::doBlockLevels( $text, $linestart );
2810 }
2811
2824 public function getVariableValue( $index, $frame = false ) {
2825 wfDeprecated( __METHOD__, '1.34' );
2826 return $this->expandMagicVariable( $index, $frame );
2827 }
2828
2838 private function expandMagicVariable( $index, $frame = false ) {
2839 // XXX This function should be moved out of Parser class for
2840 // reuse by Parsoid/etc.
2841 if ( is_null( $this->mTitle ) ) {
2842 // If no title set, bad things are going to happen
2843 // later. Title should always be set since this
2844 // should only be called in the middle of a parse
2845 // operation (but the unit-tests do funky stuff)
2846 throw new MWException( __METHOD__ . ' Should only be '
2847 . ' called while parsing (no title set)' );
2848 }
2849
2850 // Avoid PHP 7.1 warning from passing $this by reference
2851 $parser = $this;
2852
2857 if (
2858 Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) &&
2859 isset( $this->mVarCache[$index] )
2860 ) {
2861 return $this->mVarCache[$index];
2862 }
2863
2864 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2865 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2866
2867 $pageLang = $this->getFunctionLang();
2868
2869 switch ( $index ) {
2870 case '!':
2871 $value = '|';
2872 break;
2873 case 'currentmonth':
2874 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2875 break;
2876 case 'currentmonth1':
2877 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2878 break;
2879 case 'currentmonthname':
2880 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2881 break;
2882 case 'currentmonthnamegen':
2883 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2884 break;
2885 case 'currentmonthabbrev':
2886 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2887 break;
2888 case 'currentday':
2889 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2890 break;
2891 case 'currentday2':
2892 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2893 break;
2894 case 'localmonth':
2895 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2896 break;
2897 case 'localmonth1':
2898 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2899 break;
2900 case 'localmonthname':
2901 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2902 break;
2903 case 'localmonthnamegen':
2904 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2905 break;
2906 case 'localmonthabbrev':
2907 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2908 break;
2909 case 'localday':
2910 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2911 break;
2912 case 'localday2':
2913 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2914 break;
2915 case 'pagename':
2916 $value = wfEscapeWikiText( $this->mTitle->getText() );
2917 break;
2918 case 'pagenamee':
2919 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2920 break;
2921 case 'fullpagename':
2922 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2923 break;
2924 case 'fullpagenamee':
2925 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2926 break;
2927 case 'subpagename':
2928 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2929 break;
2930 case 'subpagenamee':
2931 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2932 break;
2933 case 'rootpagename':
2934 $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2935 break;
2936 case 'rootpagenamee':
2937 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2938 ' ',
2939 '_',
2940 $this->mTitle->getRootText()
2941 ) ) );
2942 break;
2943 case 'basepagename':
2944 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2945 break;
2946 case 'basepagenamee':
2947 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2948 ' ',
2949 '_',
2950 $this->mTitle->getBaseText()
2951 ) ) );
2952 break;
2953 case 'talkpagename':
2954 if ( $this->mTitle->canHaveTalkPage() ) {
2955 $talkPage = $this->mTitle->getTalkPage();
2956 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2957 } else {
2958 $value = '';
2959 }
2960 break;
2961 case 'talkpagenamee':
2962 if ( $this->mTitle->canHaveTalkPage() ) {
2963 $talkPage = $this->mTitle->getTalkPage();
2964 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2965 } else {
2966 $value = '';
2967 }
2968 break;
2969 case 'subjectpagename':
2970 $subjPage = $this->mTitle->getSubjectPage();
2971 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2972 break;
2973 case 'subjectpagenamee':
2974 $subjPage = $this->mTitle->getSubjectPage();
2975 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2976 break;
2977 case 'pageid': // requested in T25427
2978 # Inform the edit saving system that getting the canonical output
2979 # after page insertion requires a parse that used that exact page ID
2980 $this->setOutputFlag( 'vary-page-id', '{{PAGEID}} used' );
2981 $value = $this->mTitle->getArticleID();
2982 if ( !$value ) {
2983 $value = $this->mOptions->getSpeculativePageId();
2984 if ( $value ) {
2985 $this->mOutput->setSpeculativePageIdUsed( $value );
2986 }
2987 }
2988 break;
2989 case 'revisionid':
2990 if (
2991 $this->svcOptions->get( 'MiserMode' ) &&
2992 !$this->mOptions->getInterfaceMessage() &&
2993 // @TODO: disallow this word on all namespaces
2994 $this->nsInfo->isContent( $this->mTitle->getNamespace() )
2995 ) {
2996 // Use a stub result instead of the actual revision ID in order to avoid
2997 // double parses on page save but still allow preview detection (T137900)
2998 if ( $this->getRevisionId() || $this->mOptions->getSpeculativeRevId() ) {
2999 $value = '-';
3000 } else {
3001 $this->setOutputFlag( 'vary-revision-exists', '{{REVISIONID}} used' );
3002 $value = '';
3003 }
3004 } else {
3005 # Inform the edit saving system that getting the canonical output after
3006 # revision insertion requires a parse that used that exact revision ID
3007 $this->setOutputFlag( 'vary-revision-id', '{{REVISIONID}} used' );
3008 $value = $this->getRevisionId();
3009 if ( $value === 0 ) {
3010 $rev = $this->getRevisionObject();
3011 $value = $rev ? $rev->getId() : $value;
3012 }
3013 if ( !$value ) {
3014 $value = $this->mOptions->getSpeculativeRevId();
3015 if ( $value ) {
3016 $this->mOutput->setSpeculativeRevIdUsed( $value );
3017 }
3018 }
3019 }
3020 break;
3021 case 'revisionday':
3022 $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
3023 break;
3024 case 'revisionday2':
3025 $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
3026 break;
3027 case 'revisionmonth':
3028 $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
3029 break;
3030 case 'revisionmonth1':
3031 $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
3032 break;
3033 case 'revisionyear':
3034 $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
3035 break;
3036 case 'revisiontimestamp':
3037 $value = $this->getRevisionTimestampSubstring( 0, 14, self::MAX_TTS, $index );
3038 break;
3039 case 'revisionuser':
3040 # Inform the edit saving system that getting the canonical output after
3041 # revision insertion requires a parse that used the actual user ID
3042 $this->setOutputFlag( 'vary-user', '{{REVISIONUSER}} used' );
3043 $value = $this->getRevisionUser();
3044 break;
3045 case 'revisionsize':
3046 $value = $this->getRevisionSize();
3047 break;
3048 case 'namespace':
3049 $value = str_replace( '_', ' ',
3050 $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
3051 break;
3052 case 'namespacee':
3053 $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
3054 break;
3055 case 'namespacenumber':
3056 $value = $this->mTitle->getNamespace();
3057 break;
3058 case 'talkspace':
3059 $value = $this->mTitle->canHaveTalkPage()
3060 ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
3061 : '';
3062 break;
3063 case 'talkspacee':
3064 $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
3065 break;
3066 case 'subjectspace':
3067 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
3068 break;
3069 case 'subjectspacee':
3070 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3071 break;
3072 case 'currentdayname':
3073 $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
3074 break;
3075 case 'currentyear':
3076 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
3077 break;
3078 case 'currenttime':
3079 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
3080 break;
3081 case 'currenthour':
3082 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
3083 break;
3084 case 'currentweek':
3085 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
3086 # int to remove the padding
3087 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
3088 break;
3089 case 'currentdow':
3090 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
3091 break;
3092 case 'localdayname':
3093 $value = $pageLang->getWeekdayName(
3094 (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
3095 );
3096 break;
3097 case 'localyear':
3098 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
3099 break;
3100 case 'localtime':
3101 $value = $pageLang->time(
3102 MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
3103 false,
3104 false
3105 );
3106 break;
3107 case 'localhour':
3108 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
3109 break;
3110 case 'localweek':
3111 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
3112 # int to remove the padding
3113 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
3114 break;
3115 case 'localdow':
3116 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
3117 break;
3118 case 'numberofarticles':
3119 $value = $pageLang->formatNum( SiteStats::articles() );
3120 break;
3121 case 'numberoffiles':
3122 $value = $pageLang->formatNum( SiteStats::images() );
3123 break;
3124 case 'numberofusers':
3125 $value = $pageLang->formatNum( SiteStats::users() );
3126 break;
3127 case 'numberofactiveusers':
3128 $value = $pageLang->formatNum( SiteStats::activeUsers() );
3129 break;
3130 case 'numberofpages':
3131 $value = $pageLang->formatNum( SiteStats::pages() );
3132 break;
3133 case 'numberofadmins':
3134 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
3135 break;
3136 case 'numberofedits':
3137 $value = $pageLang->formatNum( SiteStats::edits() );
3138 break;
3139 case 'currenttimestamp':
3140 $value = wfTimestamp( TS_MW, $ts );
3141 break;
3142 case 'localtimestamp':
3143 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
3144 break;
3145 case 'currentversion':
3146 $value = SpecialVersion::getVersion();
3147 break;
3148 case 'articlepath':
3149 return $this->svcOptions->get( 'ArticlePath' );
3150 case 'sitename':
3151 return $this->svcOptions->get( 'Sitename' );
3152 case 'server':
3153 return $this->svcOptions->get( 'Server' );
3154 case 'servername':
3155 return $this->svcOptions->get( 'ServerName' );
3156 case 'scriptpath':
3157 return $this->svcOptions->get( 'ScriptPath' );
3158 case 'stylepath':
3159 return $this->svcOptions->get( 'StylePath' );
3160 case 'directionmark':
3161 return $pageLang->getDirMark();
3162 case 'contentlanguage':
3163 return $this->svcOptions->get( 'LanguageCode' );
3164 case 'pagelanguage':
3165 $value = $pageLang->getCode();
3166 break;
3167 case 'cascadingsources':
3168 $value = CoreParserFunctions::cascadingsources( $this );
3169 break;
3170 default:
3171 $ret = null;
3172 Hooks::run(
3173 'ParserGetVariableValueSwitch',
3174 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
3175 );
3176
3177 return $ret;
3178 }
3179
3180 if ( $index ) {
3181 $this->mVarCache[$index] = $value;
3182 }
3183
3184 return $value;
3185 }
3186
3194 private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
3195 # Get the timezone-adjusted timestamp to be used for this revision
3196 $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
3197 # Possibly set vary-revision if there is not yet an associated revision
3198 if ( !$this->getRevisionObject() ) {
3199 # Get the timezone-adjusted timestamp $mtts seconds in the future.
3200 # This future is relative to the current time and not that of the
3201 # parser options. The rendered timestamp can be compared to that
3202 # of the timestamp specified by the parser options.
3203 $resThen = substr(
3204 $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
3205 $start,
3206 $len
3207 );
3208
3209 if ( $resNow !== $resThen ) {
3210 # Inform the edit saving system that getting the canonical output after
3211 # revision insertion requires a parse that used an actual revision timestamp
3212 $this->setOutputFlag( 'vary-revision-timestamp', "$variable used" );
3213 }
3214 }
3215
3216 return $resNow;
3217 }
3218
3225 public function initialiseVariables() {
3226 wfDeprecated( __METHOD__, '1.34' );
3227 $this->initializeVariables();
3228 }
3229
3234 private function initializeVariables() {
3235 $variableIDs = $this->magicWordFactory->getVariableIDs();
3236 $substIDs = $this->magicWordFactory->getSubstIDs();
3237
3238 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
3239 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
3240 }
3241
3264 public function preprocessToDom( $text, $flags = 0 ) {
3265 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3266 return $dom;
3267 }
3268
3277 public static function splitWhitespace( $s ) {
3278 wfDeprecated( __METHOD__, '1.34' );
3279 $ltrimmed = ltrim( $s );
3280 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3281 $trimmed = rtrim( $ltrimmed );
3282 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3283 if ( $diff > 0 ) {
3284 $w2 = substr( $ltrimmed, -$diff );
3285 } else {
3286 $w2 = '';
3287 }
3288 return [ $w1, $trimmed, $w2 ];
3289 }
3290
3311 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3312 # Is there any text? Also, Prevent too big inclusions!
3313 $textSize = strlen( $text );
3314 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3315 return $text;
3316 }
3317
3318 if ( $frame === false ) {
3319 $frame = $this->getPreprocessor()->newFrame();
3320 } elseif ( !( $frame instanceof PPFrame ) ) {
3321 $this->logger->debug(
3322 __METHOD__ . " called using plain parameters instead of " .
3323 "a PPFrame instance. Creating custom frame."
3324 );
3325 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3326 }
3327
3328 $dom = $this->preprocessToDom( $text );
3329 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3330 $text = $frame->expand( $dom, $flags );
3331
3332 return $text;
3333 }
3334
3343 public static function createAssocArgs( $args ) {
3344 wfDeprecated( __METHOD__, '1.34' );
3345 $assocArgs = [];
3346 $index = 1;
3347 foreach ( $args as $arg ) {
3348 $eqpos = strpos( $arg, '=' );
3349 if ( $eqpos === false ) {
3350 $assocArgs[$index++] = $arg;
3351 } else {
3352 $name = trim( substr( $arg, 0, $eqpos ) );
3353 $value = trim( substr( $arg, $eqpos + 1 ) );
3354 if ( $value === false ) {
3355 $value = '';
3356 }
3357 if ( $name !== false ) {
3358 $assocArgs[$name] = $value;
3359 }
3360 }
3361 }
3362
3363 return $assocArgs;
3364 }
3365
3392 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3393 # does no harm if $current and $max are present but are unnecessary for the message
3394 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3395 # only during preview, and that would split the parser cache unnecessarily.
3396 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3397 ->text();
3398 $this->mOutput->addWarning( $warning );
3399 $this->addTrackingCategory( "$limitationType-category" );
3400 }
3401
3415 public function braceSubstitution( $piece, $frame ) {
3416 // Flags
3417
3418 // $text has been filled
3419 $found = false;
3420 // wiki markup in $text should be escaped
3421 $nowiki = false;
3422 // $text is HTML, armour it against wikitext transformation
3423 $isHTML = false;
3424 // Force interwiki transclusion to be done in raw mode not rendered
3425 $forceRawInterwiki = false;
3426 // $text is a DOM node needing expansion in a child frame
3427 $isChildObj = false;
3428 // $text is a DOM node needing expansion in the current frame
3429 $isLocalObj = false;
3430
3431 # Title object, where $text came from
3432 $title = false;
3433
3434 # $part1 is the bit before the first |, and must contain only title characters.
3435 # Various prefixes will be stripped from it later.
3436 $titleWithSpaces = $frame->expand( $piece['title'] );
3437 $part1 = trim( $titleWithSpaces );
3438 $titleText = false;
3439
3440 # Original title text preserved for various purposes
3441 $originalTitle = $part1;
3442
3443 # $args is a list of argument nodes, starting from index 0, not including $part1
3444 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3445 # below won't work b/c this $args isn't an object
3446 $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
3447
3448 $profileSection = null; // profile templates
3449
3450 # SUBST
3451 if ( !$found ) {
3452 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3453
3454 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3455 # Decide whether to expand template or keep wikitext as-is.
3456 if ( $this->ot['wiki'] ) {
3457 if ( $substMatch === false ) {
3458 $literal = true; # literal when in PST with no prefix
3459 } else {
3460 $literal = false; # expand when in PST with subst: or safesubst:
3461 }
3462 } else {
3463 if ( $substMatch == 'subst' ) {
3464 $literal = true; # literal when not in PST with plain subst:
3465 } else {
3466 $literal = false; # expand when not in PST with safesubst: or no prefix
3467 }
3468 }
3469 if ( $literal ) {
3470 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3471 $isLocalObj = true;
3472 $found = true;
3473 }
3474 }
3475
3476 # Variables
3477 if ( !$found && $args->getLength() == 0 ) {
3478 $id = $this->mVariables->matchStartToEnd( $part1 );
3479 if ( $id !== false ) {
3480 $text = $this->expandMagicVariable( $id, $frame );
3481 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3482 $this->mOutput->updateCacheExpiry(
3483 $this->magicWordFactory->getCacheTTL( $id ) );
3484 }
3485 $found = true;
3486 }
3487 }
3488
3489 # MSG, MSGNW and RAW
3490 if ( !$found ) {
3491 # Check for MSGNW:
3492 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3493 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3494 $nowiki = true;
3495 } else {
3496 # Remove obsolete MSG:
3497 $mwMsg = $this->magicWordFactory->get( 'msg' );
3498 $mwMsg->matchStartAndRemove( $part1 );
3499 }
3500
3501 # Check for RAW:
3502 $mwRaw = $this->magicWordFactory->get( 'raw' );
3503 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3504 $forceRawInterwiki = true;
3505 }
3506 }
3507
3508 # Parser functions
3509 if ( !$found ) {
3510 $colonPos = strpos( $part1, ':' );
3511 if ( $colonPos !== false ) {
3512 $func = substr( $part1, 0, $colonPos );
3513 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3514 $argsLength = $args->getLength();
3515 for ( $i = 0; $i < $argsLength; $i++ ) {
3516 $funcArgs[] = $args->item( $i );
3517 }
3518
3519 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3520
3521 // Extract any forwarded flags
3522 if ( isset( $result['title'] ) ) {
3523 $title = $result['title'];
3524 }
3525 if ( isset( $result['found'] ) ) {
3526 $found = $result['found'];
3527 }
3528 if ( array_key_exists( 'text', $result ) ) {
3529 // a string or null
3530 $text = $result['text'];
3531 }
3532 if ( isset( $result['nowiki'] ) ) {
3533 $nowiki = $result['nowiki'];
3534 }
3535 if ( isset( $result['isHTML'] ) ) {
3536 $isHTML = $result['isHTML'];
3537 }
3538 if ( isset( $result['forceRawInterwiki'] ) ) {
3539 $forceRawInterwiki = $result['forceRawInterwiki'];
3540 }
3541 if ( isset( $result['isChildObj'] ) ) {
3542 $isChildObj = $result['isChildObj'];
3543 }
3544 if ( isset( $result['isLocalObj'] ) ) {
3545 $isLocalObj = $result['isLocalObj'];
3546 }
3547 }
3548 }
3549
3550 # Finish mangling title and then check for loops.
3551 # Set $title to a Title object and $titleText to the PDBK
3552 if ( !$found ) {
3553 $ns = NS_TEMPLATE;
3554 # Split the title into page and subpage
3555 $subpage = '';
3556 $relative = Linker::normalizeSubpageLink(
3557 $this->mTitle, $part1, $subpage
3558 );
3559 if ( $part1 !== $relative ) {
3560 $part1 = $relative;
3561 $ns = $this->mTitle->getNamespace();
3562 }
3563 $title = Title::newFromText( $part1, $ns );
3564 if ( $title ) {
3565 $titleText = $title->getPrefixedText();
3566 # Check for language variants if the template is not found
3567 if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3568 $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
3569 }
3570 # Do recursion depth check
3571 $limit = $this->mOptions->getMaxTemplateDepth();
3572 if ( $frame->depth >= $limit ) {
3573 $found = true;
3574 $text = '<span class="error">'
3575 . wfMessage( 'parser-template-recursion-depth-warning' )
3576 ->numParams( $limit )->inContentLanguage()->text()
3577 . '</span>';
3578 }
3579 }
3580 }
3581
3582 # Load from database
3583 if ( !$found && $title ) {
3584 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3585 if ( !$title->isExternal() ) {
3586 if ( $title->isSpecialPage()
3587 && $this->mOptions->getAllowSpecialInclusion()
3588 && $this->ot['html']
3589 ) {
3590 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3591 // Pass the template arguments as URL parameters.
3592 // "uselang" will have no effect since the Language object
3593 // is forced to the one defined in ParserOptions.
3594 $pageArgs = [];
3595 $argsLength = $args->getLength();
3596 for ( $i = 0; $i < $argsLength; $i++ ) {
3597 $bits = $args->item( $i )->splitArg();
3598 if ( strval( $bits['index'] ) === '' ) {
3599 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3600 $value = trim( $frame->expand( $bits['value'] ) );
3601 $pageArgs[$name] = $value;
3602 }
3603 }
3604
3605 // Create a new context to execute the special page
3608 $context->setRequest( new FauxRequest( $pageArgs ) );
3609 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3610 $context->setUser( $this->getUser() );
3611 } else {
3612 // If this page is cached, then we better not be per user.
3613 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3614 }
3615 $context->setLanguage( $this->mOptions->getUserLangObj() );
3616 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3617 if ( $ret ) {
3618 $text = $context->getOutput()->getHTML();
3619 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3620 $found = true;
3621 $isHTML = true;
3622 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3623 $this->mOutput->updateRuntimeAdaptiveExpiry(
3624 $specialPage->maxIncludeCacheTime()
3625 );
3626 }
3627 }
3628 } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3629 $found = false; # access denied
3630 $this->logger->debug(
3631 __METHOD__ .
3632 ": template inclusion denied for " . $title->getPrefixedDBkey()
3633 );
3634 } else {
3635 list( $text, $title ) = $this->getTemplateDom( $title );
3636 if ( $text !== false ) {
3637 $found = true;
3638 $isChildObj = true;
3639 }
3640 }
3641
3642 # If the title is valid but undisplayable, make a link to it
3643 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3644 $text = "[[:$titleText]]";
3645 $found = true;
3646 }
3647 } elseif ( $title->isTrans() ) {
3648 # Interwiki transclusion
3649 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3650 $text = $this->interwikiTransclude( $title, 'render' );
3651 $isHTML = true;
3652 } else {
3653 $text = $this->interwikiTransclude( $title, 'raw' );
3654 # Preprocess it like a template
3655 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3656 $isChildObj = true;
3657 }
3658 $found = true;
3659 }
3660
3661 # Do infinite loop check
3662 # This has to be done after redirect resolution to avoid infinite loops via redirects
3663 if ( !$frame->loopCheck( $title ) ) {
3664 $found = true;
3665 $text = '<span class="error">'
3666 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3667 . '</span>';
3668 $this->addTrackingCategory( 'template-loop-category' );
3669 $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3670 wfEscapeWikiText( $titleText ) )->text() );
3671 $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3672 }
3673 }
3674
3675 # If we haven't found text to substitute by now, we're done
3676 # Recover the source wikitext and return it
3677 if ( !$found ) {
3678 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3679 if ( $profileSection ) {
3680 $this->mProfiler->scopedProfileOut( $profileSection );
3681 }
3682 return [ 'object' => $text ];
3683 }
3684
3685 # Expand DOM-style return values in a child frame
3686 if ( $isChildObj ) {
3687 # Clean up argument array
3688 $newFrame = $frame->newChild( $args, $title );
3689
3690 if ( $nowiki ) {
3691 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3692 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3693 # Expansion is eligible for the empty-frame cache
3694 $text = $newFrame->cachedExpand( $titleText, $text );
3695 } else {
3696 # Uncached expansion
3697 $text = $newFrame->expand( $text );
3698 }
3699 }
3700 if ( $isLocalObj && $nowiki ) {
3701 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3702 $isLocalObj = false;
3703 }
3704
3705 if ( $profileSection ) {
3706 $this->mProfiler->scopedProfileOut( $profileSection );
3707 }
3708
3709 # Replace raw HTML by a placeholder
3710 if ( $isHTML ) {
3711 $text = $this->insertStripItem( $text );
3712 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3713 # Escape nowiki-style return values
3714 $text = wfEscapeWikiText( $text );
3715 } elseif ( is_string( $text )
3716 && !$piece['lineStart']
3717 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3718 ) {
3719 # T2529: if the template begins with a table or block-level
3720 # element, it should be treated as beginning a new line.
3721 # This behavior is somewhat controversial.
3722 $text = "\n" . $text;
3723 }
3724
3725 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3726 # Error, oversize inclusion
3727 if ( $titleText !== false ) {
3728 # Make a working, properly escaped link if possible (T25588)
3729 $text = "[[:$titleText]]";
3730 } else {
3731 # This will probably not be a working link, but at least it may
3732 # provide some hint of where the problem is
3733 preg_replace( '/^:/', '', $originalTitle );
3734 $text = "[[:$originalTitle]]";
3735 }
3736 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3737 . 'post-expand include size too large -->' );
3738 $this->limitationWarn( 'post-expand-template-inclusion' );
3739 }
3740
3741 if ( $isLocalObj ) {
3742 $ret = [ 'object' => $text ];
3743 } else {
3744 $ret = [ 'text' => $text ];
3745 }
3746
3747 return $ret;
3748 }
3749
3769 public function callParserFunction( $frame, $function, array $args = [] ) {
3770 # Case sensitive functions
3771 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3772 $function = $this->mFunctionSynonyms[1][$function];
3773 } else {
3774 # Case insensitive functions
3775 $function = $this->contLang->lc( $function );
3776 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3777 $function = $this->mFunctionSynonyms[0][$function];
3778 } else {
3779 return [ 'found' => false ];
3780 }
3781 }
3782
3783 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3784
3785 // Avoid PHP 7.1 warning from passing $this by reference
3786 $parser = $this;
3787
3788 $allArgs = [ &$parser ];
3789 if ( $flags & self::SFH_OBJECT_ARGS ) {
3790 # Convert arguments to PPNodes and collect for appending to $allArgs
3791 $funcArgs = [];
3792 foreach ( $args as $k => $v ) {
3793 if ( $v instanceof PPNode || $k === 0 ) {
3794 $funcArgs[] = $v;
3795 } else {
3796 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3797 }
3798 }
3799
3800 # Add a frame parameter, and pass the arguments as an array
3801 $allArgs[] = $frame;
3802 $allArgs[] = $funcArgs;
3803 } else {
3804 # Convert arguments to plain text and append to $allArgs
3805 foreach ( $args as $k => $v ) {
3806 if ( $v instanceof PPNode ) {
3807 $allArgs[] = trim( $frame->expand( $v ) );
3808 } elseif ( is_int( $k ) && $k >= 0 ) {
3809 $allArgs[] = trim( $v );
3810 } else {
3811 $allArgs[] = trim( "$k=$v" );
3812 }
3813 }
3814 }
3815
3816 $result = $callback( ...$allArgs );
3817
3818 # The interface for function hooks allows them to return a wikitext
3819 # string or an array containing the string and any flags. This mungs
3820 # things around to match what this method should return.
3821 if ( !is_array( $result ) ) {
3822 $result = [
3823 'found' => true,
3824 'text' => $result,
3825 ];
3826 } else {
3827 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3828 $result['text'] = $result[0];
3829 }
3830 unset( $result[0] );
3831 $result += [
3832 'found' => true,
3833 ];
3834 }
3835
3836 $noparse = true;
3837 $preprocessFlags = 0;
3838 if ( isset( $result['noparse'] ) ) {
3839 $noparse = $result['noparse'];
3840 }
3841 if ( isset( $result['preprocessFlags'] ) ) {
3842 $preprocessFlags = $result['preprocessFlags'];
3843 }
3844
3845 if ( !$noparse ) {
3846 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3847 $result['isChildObj'] = true;
3848 }
3849
3850 return $result;
3851 }
3852
3861 public function getTemplateDom( $title ) {
3862 $cacheTitle = $title;
3863 $titleText = $title->getPrefixedDBkey();
3864
3865 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3866 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3867 $title = Title::makeTitle( $ns, $dbk );
3868 $titleText = $title->getPrefixedDBkey();
3869 }
3870 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3871 return [ $this->mTplDomCache[$titleText], $title ];
3872 }
3873
3874 # Cache miss, go to the database
3875 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3876
3877 if ( $text === false ) {
3878 $this->mTplDomCache[$titleText] = false;
3879 return [ false, $title ];
3880 }
3881
3882 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3883 $this->mTplDomCache[$titleText] = $dom;
3884
3885 if ( !$title->equals( $cacheTitle ) ) {
3886 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3887 [ $title->getNamespace(), $title->getDBkey() ];
3888 }
3889
3890 return [ $dom, $title ];
3891 }
3892
3905 $cacheKey = $title->getPrefixedDBkey();
3906 if ( !$this->currentRevisionCache ) {
3907 $this->currentRevisionCache = new MapCacheLRU( 100 );
3908 }
3909 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3910 $this->currentRevisionCache->set( $cacheKey,
3911 // Defaults to Parser::statelessFetchRevision()
3912 call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3913 );
3914 }
3915 return $this->currentRevisionCache->get( $cacheKey );
3916 }
3917
3924 return (
3925 $this->currentRevisionCache &&
3926 $this->currentRevisionCache->has( $title->getPrefixedText() )
3927 );
3928 }
3929
3939 public static function statelessFetchRevision( Title $title, $parser = false ) {
3940 $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
3941
3942 return $rev;
3943 }
3944
3950 public function fetchTemplateAndTitle( $title ) {
3951 // Defaults to Parser::statelessFetchTemplate()
3952 $templateCb = $this->mOptions->getTemplateCallback();
3953 $stuff = call_user_func( $templateCb, $title, $this );
3954 $rev = $stuff['revision'] ?? null;
3955 $text = $stuff['text'];
3956 if ( is_string( $stuff['text'] ) ) {
3957 // We use U+007F DELETE to distinguish strip markers from regular text
3958 $text = strtr( $text, "\x7f", "?" );
3959 }
3960 $finalTitle = $stuff['finalTitle'] ?? $title;
3961 foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3962 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3963 if ( $dep['title']->equals( $this->getTitle() ) && $rev instanceof Revision ) {
3964 // Self-transclusion; final result may change based on the new page version
3965 $this->setOutputFlag( 'vary-revision-sha1', 'Self transclusion' );
3966 $this->getOutput()->setRevisionUsedSha1Base36( $rev->getSha1() );
3967 }
3968 }
3969
3970 return [ $text, $finalTitle ];
3971 }
3972
3978 public function fetchTemplate( $title ) {
3979 return $this->fetchTemplateAndTitle( $title )[0];
3980 }
3981
3991 public static function statelessFetchTemplate( $title, $parser = false ) {
3992 $text = $skip = false;
3993 $finalTitle = $title;
3994 $deps = [];
3995 $rev = null;
3996
3997 # Loop to fetch the article, with up to 1 redirect
3998 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3999 # Give extensions a chance to select the revision instead
4000 $id = false; # Assume current
4001 Hooks::run( 'BeforeParserFetchTemplateAndtitle',
4002 [ $parser, $title, &$skip, &$id ] );
4003
4004 if ( $skip ) {
4005 $text = false;
4006 $deps[] = [
4007 'title' => $title,
4008 'page_id' => $title->getArticleID(),
4009 'rev_id' => null
4010 ];
4011 break;
4012 }
4013 # Get the revision
4014 if ( $id ) {
4015 $rev = Revision::newFromId( $id );
4016 } elseif ( $parser ) {
4017 $rev = $parser->fetchCurrentRevisionOfTitle( $title );
4018 } else {
4019 $rev = Revision::newFromTitle( $title );
4020 }
4021 $rev_id = $rev ? $rev->getId() : 0;
4022 # If there is no current revision, there is no page
4023 if ( $id === false && !$rev ) {
4024 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
4025 $linkCache->addBadLinkObj( $title );
4026 }
4027
4028 $deps[] = [
4029 'title' => $title,
4030 'page_id' => $title->getArticleID(),
4031 'rev_id' => $rev_id
4032 ];
4033 if ( $rev && !$title->equals( $rev->getTitle() ) ) {
4034 # We fetched a rev from a different title; register it too...
4035 $deps[] = [
4036 'title' => $rev->getTitle(),
4037 'page_id' => $rev->getPage(),
4038 'rev_id' => $rev_id
4039 ];
4040 }
4041
4042 if ( $rev ) {
4043 $content = $rev->getContent();
4044 $text = $content ? $content->getWikitextForTransclusion() : null;
4045
4046 Hooks::run( 'ParserFetchTemplate',
4047 [ $parser, $title, $rev, &$text, &$deps ] );
4048
4049 if ( $text === false || $text === null ) {
4050 $text = false;
4051 break;
4052 }
4053 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
4054 $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
4055 lcfirst( $title->getText() ) )->inContentLanguage();
4056 if ( !$message->exists() ) {
4057 $text = false;
4058 break;
4059 }
4060 $content = $message->content();
4061 $text = $message->plain();
4062 } else {
4063 break;
4064 }
4065 if ( !$content ) {
4066 break;
4067 }
4068 # Redirect?
4069 $finalTitle = $title;
4070 $title = $content->getRedirectTarget();
4071 }
4072 return [
4073 'revision' => $rev,
4074 'text' => $text,
4075 'finalTitle' => $finalTitle,
4076 'deps' => $deps
4077 ];
4078 }
4079
4087 public function fetchFileAndTitle( $title, $options = [] ) {
4088 $file = $this->fetchFileNoRegister( $title, $options );
4089
4090 $time = $file ? $file->getTimestamp() : false;
4091 $sha1 = $file ? $file->getSha1() : false;
4092 # Register the file as a dependency...
4093 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4094 if ( $file && !$title->equals( $file->getTitle() ) ) {
4095 # Update fetched file title
4096 $title = $file->getTitle();
4097 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4098 }
4099 return [ $file, $title ];
4100 }
4101
4112 protected function fetchFileNoRegister( $title, $options = [] ) {
4113 if ( isset( $options['broken'] ) ) {
4114 $file = false; // broken thumbnail forced by hook
4115 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
4116 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
4117 } else { // get by (name,timestamp)
4118 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title, $options );
4119 }
4120 return $file;
4121 }
4122
4131 public function interwikiTransclude( $title, $action ) {
4132 if ( !$this->svcOptions->get( 'EnableScaryTranscluding' ) ) {
4133 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
4134 }
4135
4136 $url = $title->getFullURL( [ 'action' => $action ] );
4137 if ( strlen( $url ) > 1024 ) {
4138 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
4139 }
4140
4141 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
4142
4143 $fname = __METHOD__;
4144 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
4145
4146 $data = $cache->getWithSetCallback(
4147 $cache->makeGlobalKey(
4148 'interwiki-transclude',
4149 ( $wikiId !== false ) ? $wikiId : 'external',
4150 sha1( $url )
4151 ),
4152 $this->svcOptions->get( 'TranscludeCacheExpiry' ),
4153 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
4154 $req = MWHttpRequest::factory( $url, [], $fname );
4155
4156 $status = $req->execute(); // Status object
4157 if ( !$status->isOK() ) {
4158 $ttl = $cache::TTL_UNCACHEABLE;
4159 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
4160 $ttl = min( $cache::TTL_LAGGED, $ttl );
4161 }
4162
4163 return [
4164 'text' => $status->isOK() ? $req->getContent() : null,
4165 'code' => $req->getStatus()
4166 ];
4167 },
4168 [
4169 'checkKeys' => ( $wikiId !== false )
4170 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
4171 : [],
4172 'pcGroup' => 'interwiki-transclude:5',
4173 'pcTTL' => $cache::TTL_PROC_LONG
4174 ]
4175 );
4176
4177 if ( is_string( $data['text'] ) ) {
4178 $text = $data['text'];
4179 } elseif ( $data['code'] != 200 ) {
4180 // Though we failed to fetch the content, this status is useless.
4181 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
4182 ->params( $url, $data['code'] )->inContentLanguage()->text();
4183 } else {
4184 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
4185 }
4186
4187 return $text;
4188 }
4189
4199 public function argSubstitution( $piece, $frame ) {
4200 $error = false;
4201 $parts = $piece['parts'];
4202 $nameWithSpaces = $frame->expand( $piece['title'] );
4203 $argName = trim( $nameWithSpaces );
4204 $object = false;
4205 $text = $frame->getArgument( $argName );
4206 if ( $text === false && $parts->getLength() > 0
4207 && ( $this->ot['html']
4208 || $this->ot['pre']
4209 || ( $this->ot['wiki'] && $frame->isTemplate() )
4210 )
4211 ) {
4212 # No match in frame, use the supplied default
4213 $object = $parts->item( 0 )->getChildren();
4214 }
4215 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
4216 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
4217 $this->limitationWarn( 'post-expand-template-argument' );
4218 }
4219
4220 if ( $text === false && $object === false ) {
4221 # No match anywhere
4222 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
4223 }
4224 if ( $error !== false ) {
4225 $text .= $error;
4226 }
4227 if ( $object !== false ) {
4228 $ret = [ 'object' => $object ];
4229 } else {
4230 $ret = [ 'text' => $text ];
4231 }
4232
4233 return $ret;
4234 }
4235
4252 public function extensionSubstitution( $params, $frame ) {
4253 static $errorStr = '<span class="error">';
4254 static $errorLen = 20;
4255
4256 $name = $frame->expand( $params['name'] );
4257 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
4258 // Probably expansion depth or node count exceeded. Just punt the
4259 // error up.
4260 return $name;
4261 }
4262
4263 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
4264 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
4265 // See above
4266 return $attrText;
4267 }
4268
4269 // We can't safely check if the expansion for $content resulted in an
4270 // error, because the content could happen to be the error string
4271 // (T149622).
4272 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4273
4274 $marker = self::MARKER_PREFIX . "-$name-"
4275 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4276
4277 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4278 ( $this->ot['html'] || $this->ot['pre'] );
4279 if ( $isFunctionTag ) {
4280 $markerType = 'none';
4281 } else {
4282 $markerType = 'general';
4283 }
4284 if ( $this->ot['html'] || $isFunctionTag ) {
4285 $name = strtolower( $name );
4286 $attributes = Sanitizer::decodeTagAttributes( $attrText );
4287 if ( isset( $params['attributes'] ) ) {
4288 $attributes += $params['attributes'];
4289 }
4290
4291 if ( isset( $this->mTagHooks[$name] ) ) {
4292 $output = call_user_func_array( $this->mTagHooks[$name],
4293 [ $content, $attributes, $this, $frame ] );
4294 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4295 list( $callback, ) = $this->mFunctionTagHooks[$name];
4296
4297 // Avoid PHP 7.1 warning from passing $this by reference
4298 $parser = $this;
4299 $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
4300 } else {
4301 $output = '<span class="error">Invalid tag extension name: ' .
4302 htmlspecialchars( $name ) . '</span>';
4303 }
4304
4305 if ( is_array( $output ) ) {
4306 // Extract flags
4307 $flags = $output;
4308 $output = $flags[0];
4309 if ( isset( $flags['markerType'] ) ) {
4310 $markerType = $flags['markerType'];
4311 }
4312 }
4313 } else {
4314 if ( is_null( $attrText ) ) {
4315 $attrText = '';
4316 }
4317 if ( isset( $params['attributes'] ) ) {
4318 foreach ( $params['attributes'] as $attrName => $attrValue ) {
4319 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4320 htmlspecialchars( $attrValue ) . '"';
4321 }
4322 }
4323 if ( $content === null ) {
4324 $output = "<$name$attrText/>";
4325 } else {
4326 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4327 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4328 // See above
4329 return $close;
4330 }
4331 $output = "<$name$attrText>$content$close";
4332 }
4333 }
4334
4335 if ( $markerType === 'none' ) {
4336 return $output;
4337 } elseif ( $markerType === 'nowiki' ) {
4338 $this->mStripState->addNoWiki( $marker, $output );
4339 } elseif ( $markerType === 'general' ) {
4340 $this->mStripState->addGeneral( $marker, $output );
4341 } else {
4342 throw new MWException( __METHOD__ . ': invalid marker type' );
4343 }
4344 return $marker;
4345 }
4346
4354 public function incrementIncludeSize( $type, $size ) {
4355 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4356 return false;
4357 } else {
4358 $this->mIncludeSizes[$type] += $size;
4359 return true;
4360 }
4361 }
4362
4369 $this->mExpensiveFunctionCount++;
4370 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4371 }
4372
4381 public function doDoubleUnderscore( $text ) {
4382 wfDeprecated( __METHOD__, '1.34' );
4383 return $this->handleDoubleUnderscore( $text );
4384 }
4385
4393 private function handleDoubleUnderscore( $text ) {
4394 # The position of __TOC__ needs to be recorded
4395 $mw = $this->magicWordFactory->get( 'toc' );
4396 if ( $mw->match( $text ) ) {
4397 $this->mShowToc = true;
4398 $this->mForceTocPosition = true;
4399
4400 # Set a placeholder. At the end we'll fill it in with the TOC.
4401 $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4402
4403 # Only keep the first one.
4404 $text = $mw->replace( '', $text );
4405 }
4406
4407 # Now match and remove the rest of them
4408 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4409 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4410
4411 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4412 $this->mOutput->mNoGallery = true;
4413 }
4414 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4415 $this->mShowToc = false;
4416 }
4417 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4418 && $this->mTitle->getNamespace() == NS_CATEGORY
4419 ) {
4420 $this->addTrackingCategory( 'hidden-category-category' );
4421 }
4422 # (T10068) Allow control over whether robots index a page.
4423 # __INDEX__ always overrides __NOINDEX__, see T16899
4424 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4425 $this->mOutput->setIndexPolicy( 'noindex' );
4426 $this->addTrackingCategory( 'noindex-category' );
4427 }
4428 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4429 $this->mOutput->setIndexPolicy( 'index' );
4430 $this->addTrackingCategory( 'index-category' );
4431 }
4432
4433 # Cache all double underscores in the database
4434 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4435 $this->mOutput->setProperty( $key, '' );
4436 }
4437
4438 return $text;
4439 }
4440
4446 public function addTrackingCategory( $msg ) {
4447 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4448 }
4449
4467 public function formatHeadings( $text, $origText, $isMain = true ) {
4468 wfDeprecated( __METHOD__, '1.34' );
4469 return $this->finalizeHeadings( $text, $origText, $isMain );
4470 }
4471
4487 private function finalizeHeadings( $text, $origText, $isMain = true ) {
4488 # Inhibit editsection links if requested in the page
4489 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4490 $maybeShowEditLink = false;
4491 } else {
4492 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4493 }
4494
4495 # Get all headlines for numbering them and adding funky stuff like [edit]
4496 # links - this is for later, but we need the number of headlines right now
4497 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4498 # be trimmed here since whitespace in HTML headings is significant.
4499 $matches = [];
4500 $numMatches = preg_match_all(
4501 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4502 $text,
4503 $matches
4504 );
4505
4506 # if there are fewer than 4 headlines in the article, do not show TOC
4507 # unless it's been explicitly enabled.
4508 $enoughToc = $this->mShowToc &&
4509 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4510
4511 # Allow user to stipulate that a page should have a "new section"
4512 # link added via __NEWSECTIONLINK__
4513 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4514 $this->mOutput->setNewSection( true );
4515 }
4516
4517 # Allow user to remove the "new section"
4518 # link via __NONEWSECTIONLINK__
4519 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4520 $this->mOutput->hideNewSection( true );
4521 }
4522
4523 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4524 # override above conditions and always show TOC above first header
4525 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4526 $this->mShowToc = true;
4527 $enoughToc = true;
4528 }
4529
4530 # headline counter
4531 $headlineCount = 0;
4532 $numVisible = 0;
4533
4534 # Ugh .. the TOC should have neat indentation levels which can be
4535 # passed to the skin functions. These are determined here
4536 $toc = '';
4537 $full = '';
4538 $head = [];
4539 $sublevelCount = [];
4540 $levelCount = [];
4541 $level = 0;
4542 $prevlevel = 0;
4543 $toclevel = 0;
4544 $prevtoclevel = 0;
4545 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4546 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4547 $oldType = $this->mOutputType;
4548 $this->setOutputType( self::OT_WIKI );
4549 $frame = $this->getPreprocessor()->newFrame();
4550 $root = $this->preprocessToDom( $origText );
4551 $node = $root->getFirstChild();
4552 $byteOffset = 0;
4553 $tocraw = [];
4554 $refers = [];
4555
4556 $headlines = $numMatches !== false ? $matches[3] : [];
4557
4558 $maxTocLevel = $this->svcOptions->get( 'MaxTocLevel' );
4559 foreach ( $headlines as $headline ) {
4560 $isTemplate = false;
4561 $titleText = false;
4562 $sectionIndex = false;
4563 $numbering = '';
4564 $markerMatches = [];
4565 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4566 $serial = $markerMatches[1];
4567 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4568 $isTemplate = ( $titleText != $baseTitleText );
4569 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4570 }
4571
4572 if ( $toclevel ) {
4573 $prevlevel = $level;
4574 }
4575 $level = $matches[1][$headlineCount];
4576
4577 if ( $level > $prevlevel ) {
4578 # Increase TOC level
4579 $toclevel++;
4580 $sublevelCount[$toclevel] = 0;
4581 if ( $toclevel < $maxTocLevel ) {
4582 $prevtoclevel = $toclevel;
4583 $toc .= Linker::tocIndent();
4584 $numVisible++;
4585 }
4586 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4587 # Decrease TOC level, find level to jump to
4588
4589 for ( $i = $toclevel; $i > 0; $i-- ) {
4590 if ( $levelCount[$i] == $level ) {
4591 # Found last matching level
4592 $toclevel = $i;
4593 break;
4594 } elseif ( $levelCount[$i] < $level ) {
4595 # Found first matching level below current level
4596 $toclevel = $i + 1;
4597 break;
4598 }
4599 }
4600 if ( $i == 0 ) {
4601 $toclevel = 1;
4602 }
4603 if ( $toclevel < $maxTocLevel ) {
4604 if ( $prevtoclevel < $maxTocLevel ) {
4605 # Unindent only if the previous toc level was shown :p
4606 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4607 $prevtoclevel = $toclevel;
4608 } else {
4609 $toc .= Linker::tocLineEnd();
4610 }
4611 }
4612 } else {
4613 # No change in level, end TOC line
4614 if ( $toclevel < $maxTocLevel ) {
4615 $toc .= Linker::tocLineEnd();
4616 }
4617 }
4618
4619 $levelCount[$toclevel] = $level;
4620
4621 # count number of headlines for each level
4622 $sublevelCount[$toclevel]++;
4623 $dot = 0;
4624 for ( $i = 1; $i <= $toclevel; $i++ ) {
4625 if ( !empty( $sublevelCount[$i] ) ) {
4626 if ( $dot ) {
4627 $numbering .= '.';
4628 }
4629 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4630 $dot = 1;
4631 }
4632 }
4633
4634 # The safe header is a version of the header text safe to use for links
4635
4636 # Remove link placeholders by the link text.
4637 # <!--LINK number-->
4638 # turns into
4639 # link text with suffix
4640 # Do this before unstrip since link text can contain strip markers
4641 $safeHeadline = $this->replaceLinkHoldersTextPrivate( $headline );
4642
4643 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4644 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4645
4646 # Remove any <style> or <script> tags (T198618)
4647 $safeHeadline = preg_replace(
4648 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4649 '',
4650 $safeHeadline
4651 );
4652
4653 # Strip out HTML (first regex removes any tag not allowed)
4654 # Allowed tags are:
4655 # * <sup> and <sub> (T10393)
4656 # * <i> (T28375)
4657 # * <b> (r105284)
4658 # * <bdi> (T74884)
4659 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4660 # * <s> and <strike> (T35715)
4661 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4662 # to allow setting directionality in toc items.
4663 $tocline = preg_replace(
4664 [
4665 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4666 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4667 ],
4668 [ '', '<$1>' ],
4669 $safeHeadline
4670 );
4671
4672 # Strip '<span></span>', which is the result from the above if
4673 # <span id="foo"></span> is used to produce an additional anchor
4674 # for a section.
4675 $tocline = str_replace( '<span></span>', '', $tocline );
4676
4677 $tocline = trim( $tocline );
4678
4679 # For the anchor, strip out HTML-y stuff period
4680 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4681 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4682
4683 # Save headline for section edit hint before it's escaped
4684 $headlineHint = $safeHeadline;
4685
4686 # Decode HTML entities
4687 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4688
4689 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4690
4691 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4692 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4693 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4694 if ( $fallbackHeadline === $safeHeadline ) {
4695 # No reason to have both (in fact, we can't)
4696 $fallbackHeadline = false;
4697 }
4698
4699 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4700 # @todo FIXME: We may be changing them depending on the current locale.
4701 $arrayKey = strtolower( $safeHeadline );
4702 if ( $fallbackHeadline === false ) {
4703 $fallbackArrayKey = false;
4704 } else {
4705 $fallbackArrayKey = strtolower( $fallbackHeadline );
4706 }
4707
4708 # Create the anchor for linking from the TOC to the section
4709 $anchor = $safeHeadline;
4710 $fallbackAnchor = $fallbackHeadline;
4711 if ( isset( $refers[$arrayKey] ) ) {
4712 // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4713 for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4714 $anchor .= "_$i";
4715 $linkAnchor .= "_$i";
4716 $refers["${arrayKey}_$i"] = true;
4717 } else {
4718 $refers[$arrayKey] = true;
4719 }
4720 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4721 // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4722 for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4723 $fallbackAnchor .= "_$i";
4724 $refers["${fallbackArrayKey}_$i"] = true;
4725 } else {
4726 $refers[$fallbackArrayKey] = true;
4727 }
4728
4729 # Don't number the heading if it is the only one (looks silly)
4730 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4731 # the two are different if the line contains a link
4732 $headline = Html::element(
4733 'span',
4734 [ 'class' => 'mw-headline-number' ],
4735 $numbering
4736 ) . ' ' . $headline;
4737 }
4738
4739 if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4740 $toc .= Linker::tocLine( $linkAnchor, $tocline,
4741 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4742 }
4743
4744 # Add the section to the section tree
4745 # Find the DOM node for this header
4746 $noOffset = ( $isTemplate || $sectionIndex === false );
4747 while ( $node && !$noOffset ) {
4748 if ( $node->getName() === 'h' ) {
4749 $bits = $node->splitHeading();
4750 if ( $bits['i'] == $sectionIndex ) {
4751 break;
4752 }
4753 }
4754 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4755 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4756 $node = $node->getNextSibling();
4757 }
4758 $tocraw[] = [
4759 'toclevel' => $toclevel,
4760 'level' => $level,
4761 'line' => $tocline,
4762 'number' => $numbering,
4763 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4764 'fromtitle' => $titleText,
4765 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4766 'anchor' => $anchor,
4767 ];
4768
4769 # give headline the correct <h#> tag
4770 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4771 // Output edit section links as markers with styles that can be customized by skins
4772 if ( $isTemplate ) {
4773 # Put a T flag in the section identifier, to indicate to extractSections()
4774 # that sections inside <includeonly> should be counted.
4775 $editsectionPage = $titleText;
4776 $editsectionSection = "T-$sectionIndex";
4777 $editsectionContent = null;
4778 } else {
4779 $editsectionPage = $this->mTitle->getPrefixedText();
4780 $editsectionSection = $sectionIndex;
4781 $editsectionContent = $headlineHint;
4782 }
4783 // We use a bit of pesudo-xml for editsection markers. The
4784 // language converter is run later on. Using a UNIQ style marker
4785 // leads to the converter screwing up the tokens when it
4786 // converts stuff. And trying to insert strip tags fails too. At
4787 // this point all real inputted tags have already been escaped,
4788 // so we don't have to worry about a user trying to input one of
4789 // these markers directly. We use a page and section attribute
4790 // to stop the language converter from converting these
4791 // important bits of data, but put the headline hint inside a
4792 // content block because the language converter is supposed to
4793 // be able to convert that piece of data.
4794 // Gets replaced with html in ParserOutput::getText
4795 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4796 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4797 if ( $editsectionContent !== null ) {
4798 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4799 } else {
4800 $editlink .= '/>';
4801 }
4802 } else {
4803 $editlink = '';
4804 }
4805 $head[$headlineCount] = Linker::makeHeadline( $level,
4806 $matches['attrib'][$headlineCount], $anchor, $headline,
4807 $editlink, $fallbackAnchor );
4808
4809 $headlineCount++;
4810 }
4811
4812 $this->setOutputType( $oldType );
4813
4814 # Never ever show TOC if no headers
4815 if ( $numVisible < 1 ) {
4816 $enoughToc = false;
4817 }
4818
4819 if ( $enoughToc ) {
4820 if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4821 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4822 }
4823 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4824 $this->mOutput->setTOCHTML( $toc );
4825 $toc = self::TOC_START . $toc . self::TOC_END;
4826 }
4827
4828 if ( $isMain ) {
4829 $this->mOutput->setSections( $tocraw );
4830 }
4831
4832 # split up and insert constructed headlines
4833 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4834 $i = 0;
4835
4836 // build an array of document sections
4837 $sections = [];
4838 foreach ( $blocks as $block ) {
4839 // $head is zero-based, sections aren't.
4840 if ( empty( $head[$i - 1] ) ) {
4841 $sections[$i] = $block;
4842 } else {
4843 $sections[$i] = $head[$i - 1] . $block;
4844 }
4845
4856 Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4857
4858 $i++;
4859 }
4860
4861 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4862 // append the TOC at the beginning
4863 // Top anchor now in skin
4864 $sections[0] .= $toc . "\n";
4865 }
4866
4867 $full .= implode( '', $sections );
4868
4869 if ( $this->mForceTocPosition ) {
4870 return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4871 } else {
4872 return $full;
4873 }
4874 }
4875
4887 public function preSaveTransform( $text, Title $title, User $user,
4888 ParserOptions $options, $clearState = true
4889 ) {
4890 if ( $clearState ) {
4891 $magicScopeVariable = $this->lock();
4892 }
4893 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4894 $this->setUser( $user );
4895
4896 // Strip U+0000 NULL (T159174)
4897 $text = str_replace( "\000", '', $text );
4898
4899 // We still normalize line endings for backwards-compatibility
4900 // with other code that just calls PST, but this should already
4901 // be handled in TextContent subclasses
4902 $text = TextContent::normalizeLineEndings( $text );
4903
4904 if ( $options->getPreSaveTransform() ) {
4905 $text = $this->pstPass2( $text, $user );
4906 }
4907 $text = $this->mStripState->unstripBoth( $text );
4908
4909 $this->setUser( null ); # Reset
4910
4911 return $text;
4912 }
4913
4922 private function pstPass2( $text, $user ) {
4923 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4924 # $this->contLang here in order to give everyone the same signature and use the default one
4925 # rather than the one selected in each user's preferences. (see also T14815)
4926 $ts = $this->mOptions->getTimestamp();
4927 $timestamp = MWTimestamp::getLocalInstance( $ts );
4928 $ts = $timestamp->format( 'YmdHis' );
4929 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4930
4931 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4932
4933 # Variable replacement
4934 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4935 $text = $this->replaceVariables( $text );
4936
4937 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4938 # which may corrupt this parser instance via its wfMessage()->text() call-
4939
4940 # Signatures
4941 if ( strpos( $text, '~~~' ) !== false ) {
4942 $sigText = $this->getUserSig( $user );
4943 $text = strtr( $text, [
4944 '~~~~~' => $d,
4945 '~~~~' => "$sigText $d",
4946 '~~~' => $sigText
4947 ] );
4948 # The main two signature forms used above are time-sensitive
4949 $this->setOutputFlag( 'user-signature', 'User signature detected' );
4950 }
4951
4952 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4953 $tc = '[' . Title::legalChars() . ']';
4954 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4955
4956 // [[ns:page (context)|]]
4957 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4958 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4959 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4960 // [[ns:page (context), context|]] (using either single or double-width comma)
4961 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4962 // [[|page]] (reverse pipe trick: add context from page title)
4963 $p2 = "/\[\[\\|($tc+)]]/";
4964
4965 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4966 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4967 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4968 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4969
4970 $t = $this->mTitle->getText();
4971 $m = [];
4972 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4973 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4974 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4975 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4976 } else {
4977 # if there's no context, don't bother duplicating the title
4978 $text = preg_replace( $p2, '[[\\1]]', $text );
4979 }
4980
4981 return $text;
4982 }
4983
4998 public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4999 $username = $user->getName();
5000
5001 # If not given, retrieve from the user object.
5002 if ( $nickname === false ) {
5003 $nickname = $user->getOption( 'nickname' );
5004 }
5005
5006 if ( is_null( $fancySig ) ) {
5007 $fancySig = $user->getBoolOption( 'fancysig' );
5008 }
5009
5010 $nickname = $nickname == null ? $username : $nickname;
5011
5012 if ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) {
5013 $nickname = $username;
5014 $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
5015 } elseif ( $fancySig !== false ) {
5016 # Sig. might contain markup; validate this
5017 if ( $this->validateSig( $nickname ) !== false ) {
5018 # Validated; clean up (if needed) and return it
5019 return $this->cleanSig( $nickname, true );
5020 } else {
5021 # Failed to validate; fall back to the default
5022 $nickname = $username;
5023 $this->logger->debug( __METHOD__ . ": $username has bad XML tags in signature." );
5024 }
5025 }
5026
5027 # Make sure nickname doesnt get a sig in a sig
5028 $nickname = self::cleanSigInSig( $nickname );
5029
5030 # If we're still here, make it a link to the user page
5031 $userText = wfEscapeWikiText( $username );
5032 $nickText = wfEscapeWikiText( $nickname );
5033 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
5034
5035 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
5036 ->title( $this->getTitle() )->text();
5037 }
5038
5045 public function validateSig( $text ) {
5046 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
5047 }
5048
5059 public function cleanSig( $text, $parsing = false ) {
5060 if ( !$parsing ) {
5061 global $wgTitle;
5062 $magicScopeVariable = $this->lock();
5063 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
5064 }
5065
5066 # Option to disable this feature
5067 if ( !$this->mOptions->getCleanSignatures() ) {
5068 return $text;
5069 }
5070
5071 # @todo FIXME: Regex doesn't respect extension tags or nowiki
5072 # => Move this logic to braceSubstitution()
5073 $substWord = $this->magicWordFactory->get( 'subst' );
5074 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
5075 $substText = '{{' . $substWord->getSynonym( 0 );
5076
5077 $text = preg_replace( $substRegex, $substText, $text );
5078 $text = self::cleanSigInSig( $text );
5079 $dom = $this->preprocessToDom( $text );
5080 $frame = $this->getPreprocessor()->newFrame();
5081 $text = $frame->expand( $dom );
5082
5083 if ( !$parsing ) {
5084 $text = $this->mStripState->unstripBoth( $text );
5085 }
5086
5087 return $text;
5088 }
5089
5096 public static function cleanSigInSig( $text ) {
5097 $text = preg_replace( '/~{3,5}/', '', $text );
5098 return $text;
5099 }
5100
5111 public function startExternalParse( Title $title = null, ParserOptions $options,
5112 $outputType, $clearState = true, $revId = null
5113 ) {
5114 $this->startParse( $title, $options, $outputType, $clearState );
5115 if ( $revId !== null ) {
5116 $this->mRevisionId = $revId;
5117 }
5118 }
5119
5126 private function startParse( Title $title = null, ParserOptions $options,
5127 $outputType, $clearState = true
5128 ) {
5129 $this->setTitle( $title );
5130 $this->mOptions = $options;
5131 $this->setOutputType( $outputType );
5132 if ( $clearState ) {
5133 $this->clearState();
5134 }
5135 }
5136
5145 public function transformMsg( $text, $options, $title = null ) {
5146 static $executing = false;
5147
5148 # Guard against infinite recursion
5149 if ( $executing ) {
5150 return $text;
5151 }
5152 $executing = true;
5153
5154 if ( !$title ) {
5155 global $wgTitle;
5156 $title = $wgTitle;
5157 }
5158
5159 $text = $this->preprocess( $text, $title, $options );
5160
5161 $executing = false;
5162 return $text;
5163 }
5164
5189 public function setHook( $tag, callable $callback ) {
5190 $tag = strtolower( $tag );
5191 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5192 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5193 }
5194 $oldVal = $this->mTagHooks[$tag] ?? null;
5195 $this->mTagHooks[$tag] = $callback;
5196 if ( !in_array( $tag, $this->mStripList ) ) {
5197 $this->mStripList[] = $tag;
5198 }
5199
5200 return $oldVal;
5201 }
5202
5220 public function setTransparentTagHook( $tag, callable $callback ) {
5221 $tag = strtolower( $tag );
5222 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5223 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5224 }
5225 $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
5226 $this->mTransparentTagHooks[$tag] = $callback;
5227
5228 return $oldVal;
5229 }
5230
5234 public function clearTagHooks() {
5235 $this->mTagHooks = [];
5236 $this->mFunctionTagHooks = [];
5237 $this->mStripList = $this->mDefaultStripList;
5238 }
5239
5283 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
5284 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5285 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5286
5287 # Add to function cache
5288 $mw = $this->magicWordFactory->get( $id );
5289 if ( !$mw ) {
5290 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5291 }
5292
5293 $synonyms = $mw->getSynonyms();
5294 $sensitive = intval( $mw->isCaseSensitive() );
5295
5296 foreach ( $synonyms as $syn ) {
5297 # Case
5298 if ( !$sensitive ) {
5299 $syn = $this->contLang->lc( $syn );
5300 }
5301 # Add leading hash
5302 if ( !( $flags & self::SFH_NO_HASH ) ) {
5303 $syn = '#' . $syn;
5304 }
5305 # Remove trailing colon
5306 if ( substr( $syn, -1, 1 ) === ':' ) {
5307 $syn = substr( $syn, 0, -1 );
5308 }
5309 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5310 }
5311 return $oldVal;
5312 }
5313
5319 public function getFunctionHooks() {
5320 $this->firstCallInit();
5321 return array_keys( $this->mFunctionHooks );
5322 }
5323
5334 public function setFunctionTagHook( $tag, callable $callback, $flags ) {
5335 $tag = strtolower( $tag );
5336 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5337 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5338 }
5339 $old = $this->mFunctionTagHooks[$tag] ?? null;
5340 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5341
5342 if ( !in_array( $tag, $this->mStripList ) ) {
5343 $this->mStripList[] = $tag;
5344 }
5345
5346 return $old;
5347 }
5348
5357 public function replaceLinkHolders( &$text, $options = 0 ) {
5358 $this->replaceLinkHoldersPrivate( $text, $options );
5359 }
5360
5368 private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
5369 $this->mLinkHolders->replace( $text );
5370 }
5371
5380 public function replaceLinkHoldersText( $text ) {
5381 wfDeprecated( __METHOD__, '1.34' );
5382 return $this->replaceLinkHoldersTextPrivate( $text );
5383 }
5384
5392 private function replaceLinkHoldersTextPrivate( $text ) {
5393 return $this->mLinkHolders->replaceText( $text );
5394 }
5395
5409 public function renderImageGallery( $text, $params ) {
5410 $mode = false;
5411 if ( isset( $params['mode'] ) ) {
5412 $mode = $params['mode'];
5413 }
5414
5415 try {
5416 $ig = ImageGalleryBase::factory( $mode );
5417 } catch ( Exception $e ) {
5418 // If invalid type set, fallback to default.
5419 $ig = ImageGalleryBase::factory( false );
5420 }
5421
5422 $ig->setContextTitle( $this->mTitle );
5423 $ig->setShowBytes( false );
5424 $ig->setShowDimensions( false );
5425 $ig->setShowFilename( false );
5426 $ig->setParser( $this );
5427 $ig->setHideBadImages();
5428 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5429
5430 if ( isset( $params['showfilename'] ) ) {
5431 $ig->setShowFilename( true );
5432 } else {
5433 $ig->setShowFilename( false );
5434 }
5435 if ( isset( $params['caption'] ) ) {
5436 // NOTE: We aren't passing a frame here or below. Frame info
5437 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5438 // See T107332#4030581
5439 $caption = $this->recursiveTagParse( $params['caption'] );
5440 $ig->setCaptionHtml( $caption );
5441 }
5442 if ( isset( $params['perrow'] ) ) {
5443 $ig->setPerRow( $params['perrow'] );
5444 }
5445 if ( isset( $params['widths'] ) ) {
5446 $ig->setWidths( $params['widths'] );
5447 }
5448 if ( isset( $params['heights'] ) ) {
5449 $ig->setHeights( $params['heights'] );
5450 }
5451 $ig->setAdditionalOptions( $params );
5452
5453 // Avoid PHP 7.1 warning from passing $this by reference
5454 $parser = $this;
5455 Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5456
5457 $lines = StringUtils::explode( "\n", $text );
5458 foreach ( $lines as $line ) {
5459 # match lines like these:
5460 # Image:someimage.jpg|This is some image
5461 $matches = [];
5462 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5463 # Skip empty lines
5464 if ( count( $matches ) == 0 ) {
5465 continue;
5466 }
5467
5468 if ( strpos( $matches[0], '%' ) !== false ) {
5469 $matches[1] = rawurldecode( $matches[1] );
5470 }
5471 $title = Title::newFromText( $matches[1], NS_FILE );
5472 if ( is_null( $title ) ) {
5473 # Bogus title. Ignore these so we don't bomb out later.
5474 continue;
5475 }
5476
5477 # We need to get what handler the file uses, to figure out parameters.
5478 # Note, a hook can overide the file name, and chose an entirely different
5479 # file (which potentially could be of a different type and have different handler).
5480 $options = [];
5481 $descQuery = false;
5482 Hooks::run( 'BeforeParserFetchFileAndTitle',
5483 [ $this, $title, &$options, &$descQuery ] );
5484 # Don't register it now, as TraditionalImageGallery does that later.
5485 $file = $this->fetchFileNoRegister( $title, $options );
5486 $handler = $file ? $file->getHandler() : false;
5487
5488 $paramMap = [
5489 'img_alt' => 'gallery-internal-alt',
5490 'img_link' => 'gallery-internal-link',
5491 ];
5492 if ( $handler ) {
5493 $paramMap += $handler->getParamMap();
5494 // We don't want people to specify per-image widths.
5495 // Additionally the width parameter would need special casing anyhow.
5496 unset( $paramMap['img_width'] );
5497 }
5498
5499 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5500
5501 $label = '';
5502 $alt = '';
5503 $link = '';
5504 $handlerOptions = [];
5505 if ( isset( $matches[3] ) ) {
5506 // look for an |alt= definition while trying not to break existing
5507 // captions with multiple pipes (|) in it, until a more sensible grammar
5508 // is defined for images in galleries
5509
5510 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5511 // splitting on '|' is a bit odd, and different from makeImage.
5512 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5513 // Protect LanguageConverter markup
5514 $parameterMatches = StringUtils::delimiterExplode(
5515 '-{', '}-', '|', $matches[3], true /* nested */
5516 );
5517
5518 foreach ( $parameterMatches as $parameterMatch ) {
5519 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5520 if ( $magicName ) {
5521 $paramName = $paramMap[$magicName];
5522
5523 switch ( $paramName ) {
5524 case 'gallery-internal-alt':
5525 $alt = $this->stripAltTextPrivate( $match, false );
5526 break;
5527 case 'gallery-internal-link':
5528 $linkValue = $this->stripAltTextPrivate( $match, false );
5529 if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5530 // Result of LanguageConverter::markNoConversion
5531 // invoked on an external link.
5532 $linkValue = substr( $linkValue, 4, -2 );
5533 }
5534 list( $type, $target ) = $this->parseLinkParameterPrivate( $linkValue );
5535 if ( $type === 'link-url' ) {
5536 $link = $target;
5537 $this->mOutput->addExternalLink( $target );
5538 } elseif ( $type === 'link-title' ) {
5539 $link = $target->getLinkURL();
5540 $this->mOutput->addLink( $target );
5541 }
5542 break;
5543 default:
5544 // Must be a handler specific parameter.
5545 if ( $handler->validateParam( $paramName, $match ) ) {
5546 $handlerOptions[$paramName] = $match;
5547 } else {
5548 // Guess not, consider it as caption.
5549 $this->logger->debug(
5550 "$parameterMatch failed parameter validation" );
5551 $label = $parameterMatch;
5552 }
5553 }
5554
5555 } else {
5556 // Last pipe wins.
5557 $label = $parameterMatch;
5558 }
5559 }
5560 }
5561
5562 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5563 }
5564 $html = $ig->toHTML();
5565 Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5566 return $html;
5567 }
5568
5574 public function getImageParams( $handler ) {
5575 wfDeprecated( __METHOD__, '1.34' );
5576 return $this->getImageParamsPrivate( $handler );
5577 }
5578
5583 private function getImageParamsPrivate( $handler ) {
5584 if ( $handler ) {
5585 $handlerClass = get_class( $handler );
5586 } else {
5587 $handlerClass = '';
5588 }
5589 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5590 # Initialise static lists
5591 static $internalParamNames = [
5592 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5593 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5594 'bottom', 'text-bottom' ],
5595 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5596 'upright', 'border', 'link', 'alt', 'class' ],
5597 ];
5598 static $internalParamMap;
5599 if ( !$internalParamMap ) {
5600 $internalParamMap = [];
5601 foreach ( $internalParamNames as $type => $names ) {
5602 foreach ( $names as $name ) {
5603 // For grep: img_left, img_right, img_center, img_none,
5604 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5605 // img_bottom, img_text_bottom,
5606 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5607 // img_border, img_link, img_alt, img_class
5608 $magicName = str_replace( '-', '_', "img_$name" );
5609 $internalParamMap[$magicName] = [ $type, $name ];
5610 }
5611 }
5612 }
5613
5614 # Add handler params
5615 $paramMap = $internalParamMap;
5616 if ( $handler ) {
5617 $handlerParamMap = $handler->getParamMap();
5618 foreach ( $handlerParamMap as $magic => $paramName ) {
5619 $paramMap[$magic] = [ 'handler', $paramName ];
5620 }
5621 }
5622 $this->mImageParams[$handlerClass] = $paramMap;
5623 $this->mImageParamsMagicArray[$handlerClass] =
5624 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5625 }
5626 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5627 }
5628
5637 public function makeImage( $title, $options, $holders = false ) {
5638 # Check if the options text is of the form "options|alt text"
5639 # Options are:
5640 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5641 # * left no resizing, just left align. label is used for alt= only
5642 # * right same, but right aligned
5643 # * none same, but not aligned
5644 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5645 # * center center the image
5646 # * frame Keep original image size, no magnify-button.
5647 # * framed Same as "frame"
5648 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5649 # * upright reduce width for upright images, rounded to full __0 px
5650 # * border draw a 1px border around the image
5651 # * alt Text for HTML alt attribute (defaults to empty)
5652 # * class Set a class for img node
5653 # * link Set the target of the image link. Can be external, interwiki, or local
5654 # vertical-align values (no % or length right now):
5655 # * baseline
5656 # * sub
5657 # * super
5658 # * top
5659 # * text-top
5660 # * middle
5661 # * bottom
5662 # * text-bottom
5663
5664 # Protect LanguageConverter markup when splitting into parts
5665 $parts = StringUtils::delimiterExplode(
5666 '-{', '}-', '|', $options, true /* allow nesting */
5667 );
5668
5669 # Give extensions a chance to select the file revision for us
5670 $options = [];
5671 $descQuery = false;
5672 Hooks::run( 'BeforeParserFetchFileAndTitle',
5673 [ $this, $title, &$options, &$descQuery ] );
5674 # Fetch and register the file (file title may be different via hooks)
5675 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5676
5677 # Get parameter map
5678 $handler = $file ? $file->getHandler() : false;
5679
5680 list( $paramMap, $mwArray ) = $this->getImageParamsPrivate( $handler );
5681
5682 if ( !$file ) {
5683 $this->addTrackingCategory( 'broken-file-category' );
5684 }
5685
5686 # Process the input parameters
5687 $caption = '';
5688 $params = [ 'frame' => [], 'handler' => [],
5689 'horizAlign' => [], 'vertAlign' => [] ];
5690 $seenformat = false;
5691 foreach ( $parts as $part ) {
5692 $part = trim( $part );
5693 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5694 $validated = false;
5695 if ( isset( $paramMap[$magicName] ) ) {
5696 list( $type, $paramName ) = $paramMap[$magicName];
5697
5698 # Special case; width and height come in one variable together
5699 if ( $type === 'handler' && $paramName === 'width' ) {
5700 $parsedWidthParam = self::parseWidthParam( $value );
5701 if ( isset( $parsedWidthParam['width'] ) ) {
5702 $width = $parsedWidthParam['width'];
5703 if ( $handler->validateParam( 'width', $width ) ) {
5704 $params[$type]['width'] = $width;
5705 $validated = true;
5706 }
5707 }
5708 if ( isset( $parsedWidthParam['height'] ) ) {
5709 $height = $parsedWidthParam['height'];
5710 if ( $handler->validateParam( 'height', $height ) ) {
5711 $params[$type]['height'] = $height;
5712 $validated = true;
5713 }
5714 }
5715 # else no validation -- T15436
5716 } else {
5717 if ( $type === 'handler' ) {
5718 # Validate handler parameter
5719 $validated = $handler->validateParam( $paramName, $value );
5720 } else {
5721 # Validate internal parameters
5722 switch ( $paramName ) {
5723 case 'manualthumb':
5724 case 'alt':
5725 case 'class':
5726 # @todo FIXME: Possibly check validity here for
5727 # manualthumb? downstream behavior seems odd with
5728 # missing manual thumbs.
5729 $validated = true;
5730 $value = $this->stripAltTextPrivate( $value, $holders );
5731 break;
5732 case 'link':
5733 list( $paramName, $value ) =
5734 $this->parseLinkParameterPrivate(
5735 $this->stripAltTextPrivate( $value, $holders )
5736 );
5737 if ( $paramName ) {
5738 $validated = true;
5739 if ( $paramName === 'no-link' ) {
5740 $value = true;
5741 }
5742 if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5743 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5744 }
5745 }
5746 break;
5747 case 'frameless':
5748 case 'framed':
5749 case 'thumbnail':
5750 // use first appearing option, discard others.
5751 $validated = !$seenformat;
5752 $seenformat = true;
5753 break;
5754 default:
5755 # Most other things appear to be empty or numeric...
5756 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5757 }
5758 }
5759
5760 if ( $validated ) {
5761 $params[$type][$paramName] = $value;
5762 }
5763 }
5764 }
5765 if ( !$validated ) {
5766 $caption = $part;
5767 }
5768 }
5769
5770 # Process alignment parameters
5771 if ( $params['horizAlign'] ) {
5772 $params['frame']['align'] = key( $params['horizAlign'] );
5773 }
5774 if ( $params['vertAlign'] ) {
5775 $params['frame']['valign'] = key( $params['vertAlign'] );
5776 }
5777
5778 $params['frame']['caption'] = $caption;
5779
5780 # Will the image be presented in a frame, with the caption below?
5781 $imageIsFramed = isset( $params['frame']['frame'] )
5782 || isset( $params['frame']['framed'] )
5783 || isset( $params['frame']['thumbnail'] )
5784 || isset( $params['frame']['manualthumb'] );
5785
5786 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5787 # came to also set the caption, ordinary text after the image -- which
5788 # makes no sense, because that just repeats the text multiple times in
5789 # screen readers. It *also* came to set the title attribute.
5790 # Now that we have an alt attribute, we should not set the alt text to
5791 # equal the caption: that's worse than useless, it just repeats the
5792 # text. This is the framed/thumbnail case. If there's no caption, we
5793 # use the unnamed parameter for alt text as well, just for the time be-
5794 # ing, if the unnamed param is set and the alt param is not.
5795 # For the future, we need to figure out if we want to tweak this more,
5796 # e.g., introducing a title= parameter for the title; ignoring the un-
5797 # named parameter entirely for images without a caption; adding an ex-
5798 # plicit caption= parameter and preserving the old magic unnamed para-
5799 # meter for BC; ...
5800 if ( $imageIsFramed ) { # Framed image
5801 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5802 # No caption or alt text, add the filename as the alt text so
5803 # that screen readers at least get some description of the image
5804 $params['frame']['alt'] = $title->getText();
5805 }
5806 # Do not set $params['frame']['title'] because tooltips don't make sense
5807 # for framed images
5808 } else { # Inline image
5809 if ( !isset( $params['frame']['alt'] ) ) {
5810 # No alt text, use the "caption" for the alt text
5811 if ( $caption !== '' ) {
5812 $params['frame']['alt'] = $this->stripAltTextPrivate( $caption, $holders );
5813 } else {
5814 # No caption, fall back to using the filename for the
5815 # alt text
5816 $params['frame']['alt'] = $title->getText();
5817 }
5818 }
5819 # Use the "caption" for the tooltip text
5820 $params['frame']['title'] = $this->stripAltTextPrivate( $caption, $holders );
5821 }
5822 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5823
5824 Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5825
5826 # Linker does the rest
5827 $time = $options['time'] ?? false;
5828 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5829 $time, $descQuery, $this->mOptions->getThumbSize() );
5830
5831 # Give the handler a chance to modify the parser object
5832 if ( $handler ) {
5833 $handler->parserTransformHook( $this, $file );
5834 }
5835
5836 return $ret;
5837 }
5838
5858 public function parseLinkParameter( $value ) {
5859 wfDeprecated( __METHOD__, '1.34' );
5860 return $this->parseLinkParameterPrivate( $value );
5861 }
5862
5881 private function parseLinkParameterPrivate( $value ) {
5882 $chars = self::EXT_LINK_URL_CLASS;
5883 $addr = self::EXT_LINK_ADDR;
5884 $prots = $this->mUrlProtocols;
5885 $type = null;
5886 $target = false;
5887 if ( $value === '' ) {
5888 $type = 'no-link';
5889 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5890 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5891 $this->mOutput->addExternalLink( $value );
5892 $type = 'link-url';
5893 $target = $value;
5894 }
5895 } else {
5896 $linkTitle = Title::newFromText( $value );
5897 if ( $linkTitle ) {
5898 $this->mOutput->addLink( $linkTitle );
5899 $type = 'link-title';
5900 $target = $linkTitle;
5901 }
5902 }
5903 return [ $type, $target ];
5904 }
5905
5912 protected function stripAltText( $caption, $holders ) {
5913 wfDeprecated( __METHOD__, '1.34' );
5914 return $this->stripAltTextPrivate( $caption, $holders );
5915 }
5916
5922 private function stripAltTextPrivate( $caption, $holders ) {
5923 # Strip bad stuff out of the title (tooltip). We can't just use
5924 # replaceLinkHoldersText() here, because if this function is called
5925 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5926 if ( $holders ) {
5927 $tooltip = $holders->replaceText( $caption );
5928 } else {
5929 $tooltip = $this->replaceLinkHoldersTextPrivate( $caption );
5930 }
5931
5932 # make sure there are no placeholders in thumbnail attributes
5933 # that are later expanded to html- so expand them now and
5934 # remove the tags
5935 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5936 # Compatibility hack! In HTML certain entity references not terminated
5937 # by a semicolon are decoded (but not if we're in an attribute; that's
5938 # how link URLs get away without properly escaping & in queries).
5939 # But wikitext has always required semicolon-termination of entities,
5940 # so encode & where needed to avoid decode of semicolon-less entities.
5941 # See T209236 and
5942 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5943 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5944 $tooltip = preg_replace( "/
5945 & # 1. entity prefix
5946 (?= # 2. followed by:
5947 (?: # a. one of the legacy semicolon-less named entities
5948 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5949 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5950 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5951 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5952 U(?:acute|circ|grave|uml)|Yacute|
5953 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5954 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5955 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5956 frac(?:1(?:2|4)|34)|
5957 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5958 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5959 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5960 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5961 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5962 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5963 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5964 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5965 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5966 )
5967 (?:[^;]|$)) # b. and not followed by a semicolon
5968 # S = study, for efficiency
5969 /Sx", '&amp;', $tooltip );
5970 $tooltip = Sanitizer::stripAllTags( $tooltip );
5971
5972 return $tooltip;
5973 }
5974
5980 public function disableCache() {
5981 wfDeprecated( __METHOD__, '1.28' );
5982 $this->logger->debug( "Parser output marked as uncacheable." );
5983 if ( !$this->mOutput ) {
5984 throw new MWException( __METHOD__ .
5985 " can only be called when actually parsing something" );
5986 }
5987 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5988 }
5989
5998 public function attributeStripCallback( &$text, $frame = false ) {
5999 $text = $this->replaceVariables( $text, $frame );
6000 $text = $this->mStripState->unstripBoth( $text );
6001 return $text;
6002 }
6003
6009 public function getTags() {
6010 $this->firstCallInit();
6011 return array_merge(
6012 array_keys( $this->mTransparentTagHooks ),
6013 array_keys( $this->mTagHooks ),
6014 array_keys( $this->mFunctionTagHooks )
6015 );
6016 }
6017
6022 public function getFunctionSynonyms() {
6023 $this->firstCallInit();
6024 return $this->mFunctionSynonyms;
6025 }
6026
6031 public function getUrlProtocols() {
6032 return $this->mUrlProtocols;
6033 }
6034
6045 public function replaceTransparentTags( $text ) {
6046 $matches = [];
6047 $elements = array_keys( $this->mTransparentTagHooks );
6048 $text = self::extractTagsAndParams( $elements, $text, $matches );
6049 $replacements = [];
6050
6051 foreach ( $matches as $marker => $data ) {
6052 list( $element, $content, $params, $tag ) = $data;
6053 $tagName = strtolower( $element );
6054 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
6055 $output = call_user_func_array(
6056 $this->mTransparentTagHooks[$tagName],
6057 [ $content, $params, $this ]
6058 );
6059 } else {
6060 $output = $tag;
6061 }
6062 $replacements[$marker] = $output;
6063 }
6064 return strtr( $text, $replacements );
6065 }
6066
6096 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
6097 global $wgTitle; # not generally used but removes an ugly failure mode
6098
6099 $magicScopeVariable = $this->lock();
6100 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
6101 $outText = '';
6102 $frame = $this->getPreprocessor()->newFrame();
6103
6104 # Process section extraction flags
6105 $flags = 0;
6106 $sectionParts = explode( '-', $sectionId );
6107 $sectionIndex = array_pop( $sectionParts );
6108 foreach ( $sectionParts as $part ) {
6109 if ( $part === 'T' ) {
6110 $flags |= self::PTD_FOR_INCLUSION;
6111 }
6112 }
6113
6114 # Check for empty input
6115 if ( strval( $text ) === '' ) {
6116 # Only sections 0 and T-0 exist in an empty document
6117 if ( $sectionIndex == 0 ) {
6118 if ( $mode === 'get' ) {
6119 return '';
6120 }
6121
6122 return $newText;
6123 } else {
6124 if ( $mode === 'get' ) {
6125 return $newText;
6126 }
6127
6128 return $text;
6129 }
6130 }
6131
6132 # Preprocess the text
6133 $root = $this->preprocessToDom( $text, $flags );
6134
6135 # <h> nodes indicate section breaks
6136 # They can only occur at the top level, so we can find them by iterating the root's children
6137 $node = $root->getFirstChild();
6138
6139 # Find the target section
6140 if ( $sectionIndex == 0 ) {
6141 # Section zero doesn't nest, level=big
6142 $targetLevel = 1000;
6143 } else {
6144 while ( $node ) {
6145 if ( $node->getName() === 'h' ) {
6146 $bits = $node->splitHeading();
6147 if ( $bits['i'] == $sectionIndex ) {
6148 $targetLevel = $bits['level'];
6149 break;
6150 }
6151 }
6152 if ( $mode === 'replace' ) {
6153 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6154 }
6155 $node = $node->getNextSibling();
6156 }
6157 }
6158
6159 if ( !$node ) {
6160 # Not found
6161 if ( $mode === 'get' ) {
6162 return $newText;
6163 } else {
6164 return $text;
6165 }
6166 }
6167
6168 # Find the end of the section, including nested sections
6169 do {
6170 if ( $node->getName() === 'h' ) {
6171 $bits = $node->splitHeading();
6172 $curLevel = $bits['level'];
6173 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
6174 break;
6175 }
6176 }
6177 if ( $mode === 'get' ) {
6178 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6179 }
6180 $node = $node->getNextSibling();
6181 } while ( $node );
6182
6183 # Write out the remainder (in replace mode only)
6184 if ( $mode === 'replace' ) {
6185 # Output the replacement text
6186 # Add two newlines on -- trailing whitespace in $newText is conventionally
6187 # stripped by the editor, so we need both newlines to restore the paragraph gap
6188 # Only add trailing whitespace if there is newText
6189 if ( $newText != "" ) {
6190 $outText .= $newText . "\n\n";
6191 }
6192
6193 while ( $node ) {
6194 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6195 $node = $node->getNextSibling();
6196 }
6197 }
6198
6199 if ( is_string( $outText ) ) {
6200 # Re-insert stripped tags
6201 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
6202 }
6203
6204 return $outText;
6205 }
6206
6221 public function getSection( $text, $sectionId, $defaultText = '' ) {
6222 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
6223 }
6224
6237 public function replaceSection( $oldText, $sectionId, $newText ) {
6238 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
6239 }
6240
6251 public function getRevisionId() {
6252 return $this->mRevisionId;
6253 }
6254
6261 public function getRevisionObject() {
6262 if ( $this->mRevisionObject ) {
6263 return $this->mRevisionObject;
6264 }
6265
6266 // NOTE: try to get the RevisionObject even if mRevisionId is null.
6267 // This is useful when parsing a revision that has not yet been saved.
6268 // However, if we get back a saved revision even though we are in
6269 // preview mode, we'll have to ignore it, see below.
6270 // NOTE: This callback may be used to inject an OLD revision that was
6271 // already loaded, so "current" is a bit of a misnomer. We can't just
6272 // skip it if mRevisionId is set.
6273 $rev = call_user_func(
6274 $this->mOptions->getCurrentRevisionCallback(),
6275 $this->getTitle(),
6276 $this
6277 );
6278
6279 if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
6280 // We are in preview mode (mRevisionId is null), and the current revision callback
6281 // returned an existing revision. Ignore it and return null, it's probably the page's
6282 // current revision, which is not what we want here. Note that we do want to call the
6283 // callback to allow the unsaved revision to be injected here, e.g. for
6284 // self-transclusion previews.
6285 return null;
6286 }
6287
6288 // If the parse is for a new revision, then the callback should have
6289 // already been set to force the object and should match mRevisionId.
6290 // If not, try to fetch by mRevisionId for sanity.
6291 if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
6292 $rev = Revision::newFromId( $this->mRevisionId );
6293 }
6294
6295 $this->mRevisionObject = $rev;
6296
6297 return $this->mRevisionObject;
6298 }
6299
6305 public function getRevisionTimestamp() {
6306 if ( $this->mRevisionTimestamp !== null ) {
6307 return $this->mRevisionTimestamp;
6308 }
6309
6310 # Use specified revision timestamp, falling back to the current timestamp
6311 $revObject = $this->getRevisionObject();
6312 $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
6313 $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6314
6315 # The cryptic '' timezone parameter tells to use the site-default
6316 # timezone offset instead of the user settings.
6317 # Since this value will be saved into the parser cache, served
6318 # to other users, and potentially even used inside links and such,
6319 # it needs to be consistent for all visitors.
6320 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6321
6322 return $this->mRevisionTimestamp;
6323 }
6324
6330 public function getRevisionUser() {
6331 if ( is_null( $this->mRevisionUser ) ) {
6332 $revObject = $this->getRevisionObject();
6333
6334 # if this template is subst: the revision id will be blank,
6335 # so just use the current user's name
6336 if ( $revObject ) {
6337 $this->mRevisionUser = $revObject->getUserText();
6338 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6339 $this->mRevisionUser = $this->getUser()->getName();
6340 }
6341 }
6342 return $this->mRevisionUser;
6343 }
6344
6350 public function getRevisionSize() {
6351 if ( is_null( $this->mRevisionSize ) ) {
6352 $revObject = $this->getRevisionObject();
6353
6354 # if this variable is subst: the revision id will be blank,
6355 # so just use the parser input size, because the own substituation
6356 # will change the size.
6357 if ( $revObject ) {
6358 $this->mRevisionSize = $revObject->getSize();
6359 } else {
6360 $this->mRevisionSize = $this->mInputSize;
6361 }
6362 }
6363 return $this->mRevisionSize;
6364 }
6365
6371 public function setDefaultSort( $sort ) {
6372 $this->mDefaultSort = $sort;
6373 $this->mOutput->setProperty( 'defaultsort', $sort );
6374 }
6375
6386 public function getDefaultSort() {
6387 if ( $this->mDefaultSort !== false ) {
6388 return $this->mDefaultSort;
6389 } else {
6390 return '';
6391 }
6392 }
6393
6400 public function getCustomDefaultSort() {
6401 return $this->mDefaultSort;
6402 }
6403
6404 private static function getSectionNameFromStrippedText( $text ) {
6405 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6406 $text = Sanitizer::decodeCharReferences( $text );
6407 $text = self::normalizeSectionName( $text );
6408 return $text;
6409 }
6410
6411 private static function makeAnchor( $sectionName ) {
6412 return '#' . Sanitizer::escapeIdForLink( $sectionName );
6413 }
6414
6415 private function makeLegacyAnchor( $sectionName ) {
6416 $fragmentMode = $this->svcOptions->get( 'FragmentMode' );
6417 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6418 // ForAttribute() and ForLink() are the same for legacy encoding
6419 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6420 } else {
6421 $id = Sanitizer::escapeIdForLink( $sectionName );
6422 }
6423
6424 return "#$id";
6425 }
6426
6435 public function guessSectionNameFromWikiText( $text ) {
6436 # Strip out wikitext links(they break the anchor)
6437 $text = $this->stripSectionName( $text );
6438 $sectionName = self::getSectionNameFromStrippedText( $text );
6439 return self::makeAnchor( $sectionName );
6440 }
6441
6451 public function guessLegacySectionNameFromWikiText( $text ) {
6452 # Strip out wikitext links(they break the anchor)
6453 $text = $this->stripSectionName( $text );
6454 $sectionName = self::getSectionNameFromStrippedText( $text );
6455 return $this->makeLegacyAnchor( $sectionName );
6456 }
6457
6463 public static function guessSectionNameFromStrippedText( $text ) {
6464 $sectionName = self::getSectionNameFromStrippedText( $text );
6465 return self::makeAnchor( $sectionName );
6466 }
6467
6474 private static function normalizeSectionName( $text ) {
6475 # T90902: ensure the same normalization is applied for IDs as to links
6477 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6478 '@phan-var MediaWikiTitleCodec $titleParser';
6479 try {
6480
6481 $parts = $titleParser->splitTitleString( "#$text" );
6482 } catch ( MalformedTitleException $ex ) {
6483 return $text;
6484 }
6485 return $parts['fragment'];
6486 }
6487
6502 public function stripSectionName( $text ) {
6503 # Strip internal link markup
6504 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6505 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6506
6507 # Strip external link markup
6508 # @todo FIXME: Not tolerant to blank link text
6509 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6510 # on how many empty links there are on the page - need to figure that out.
6511 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6512
6513 # Parse wikitext quotes (italics & bold)
6514 $text = $this->doQuotes( $text );
6515
6516 # Strip HTML tags
6517 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6518 return $text;
6519 }
6520
6532 public function testSrvus( $text, Title $title, ParserOptions $options,
6533 $outputType = self::OT_HTML
6534 ) {
6535 wfDeprecated( __METHOD__, '1.34' );
6536 return $this->fuzzTestSrvus( $text, $title, $options, $outputType );
6537 }
6538
6549 private function fuzzTestSrvus( $text, Title $title, ParserOptions $options,
6550 $outputType = self::OT_HTML
6551 ) {
6552 $magicScopeVariable = $this->lock();
6553 $this->startParse( $title, $options, $outputType, true );
6554
6555 $text = $this->replaceVariables( $text );
6556 $text = $this->mStripState->unstripBoth( $text );
6557 $text = Sanitizer::removeHTMLtags( $text );
6558 return $text;
6559 }
6560
6568 public function testPst( $text, Title $title, ParserOptions $options ) {
6569 wfDeprecated( __METHOD__, '1.34' );
6570 return $this->fuzzTestPst( $text, $title, $options );
6571 }
6572
6579 private function fuzzTestPst( $text, Title $title, ParserOptions $options ) {
6580 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6581 }
6582
6590 public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6591 wfDeprecated( __METHOD__, '1.34' );
6592 return $this->fuzzTestPreprocess( $text, $title, $options );
6593 }
6594
6601 private function fuzzTestPreprocess( $text, Title $title, ParserOptions $options ) {
6602 return $this->fuzzTestSrvus( $text, $title, $options, self::OT_PREPROCESS );
6603 }
6604
6621 public function markerSkipCallback( $s, $callback ) {
6622 $i = 0;
6623 $out = '';
6624 while ( $i < strlen( $s ) ) {
6625 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6626 if ( $markerStart === false ) {
6627 $out .= call_user_func( $callback, substr( $s, $i ) );
6628 break;
6629 } else {
6630 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6631 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6632 if ( $markerEnd === false ) {
6633 $out .= substr( $s, $markerStart );
6634 break;
6635 } else {
6636 $markerEnd += strlen( self::MARKER_SUFFIX );
6637 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6638 $i = $markerEnd;
6639 }
6640 }
6641 }
6642 return $out;
6643 }
6644
6651 public function killMarkers( $text ) {
6652 return $this->mStripState->killMarkers( $text );
6653 }
6654
6672 public function serializeHalfParsedText( $text ) {
6673 wfDeprecated( __METHOD__, '1.31' );
6674 $data = [
6675 'text' => $text,
6676 'version' => self::HALF_PARSED_VERSION,
6677 'stripState' => $this->mStripState->getSubState( $text ),
6678 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6679 ];
6680 return $data;
6681 }
6682
6699 public function unserializeHalfParsedText( $data ) {
6700 wfDeprecated( __METHOD__, '1.31' );
6701 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6702 throw new MWException( __METHOD__ . ': invalid version' );
6703 }
6704
6705 # First, extract the strip state.
6706 $texts = [ $data['text'] ];
6707 $texts = $this->mStripState->merge( $data['stripState'], $texts );
6708
6709 # Now renumber links
6710 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6711
6712 # Should be good to go.
6713 return $texts[0];
6714 }
6715
6726 public function isValidHalfParsedText( $data ) {
6727 wfDeprecated( __METHOD__, '1.31' );
6728 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6729 }
6730
6740 public static function parseWidthParam( $value, $parseHeight = true ) {
6741 $parsedWidthParam = [];
6742 if ( $value === '' ) {
6743 return $parsedWidthParam;
6744 }
6745 $m = [];
6746 # (T15500) In both cases (width/height and width only),
6747 # permit trailing "px" for backward compatibility.
6748 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6749 $width = intval( $m[1] );
6750 $height = intval( $m[2] );
6751 $parsedWidthParam['width'] = $width;
6752 $parsedWidthParam['height'] = $height;
6753 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6754 $width = intval( $value );
6755 $parsedWidthParam['width'] = $width;
6756 }
6757 return $parsedWidthParam;
6758 }
6759
6769 protected function lock() {
6770 if ( $this->mInParse ) {
6771 throw new MWException( "Parser state cleared while parsing. "
6772 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6773 }
6774
6775 // Save the backtrace when locking, so that if some code tries locking again,
6776 // we can print the lock owner's backtrace for easier debugging
6777 $e = new Exception;
6778 $this->mInParse = $e->getTraceAsString();
6779
6780 $recursiveCheck = new ScopedCallback( function () {
6781 $this->mInParse = false;
6782 } );
6783
6784 return $recursiveCheck;
6785 }
6786
6797 public static function stripOuterParagraph( $html ) {
6798 $m = [];
6799 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6800 $html = $m[1];
6801 }
6802
6803 return $html;
6804 }
6805
6816 public function getFreshParser() {
6817 if ( $this->mInParse ) {
6818 return $this->factory->create();
6819 } else {
6820 return $this;
6821 }
6822 }
6823
6830 public function enableOOUI() {
6831 OutputPage::setupOOUI();
6832 $this->mOutput->setEnableOOUI( true );
6833 }
6834
6839 protected function setOutputFlag( $flag, $reason ) {
6840 $this->mOutput->setFlag( $flag );
6841 $name = $this->mTitle->getPrefixedText();
6842 $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6843 }
6844}
getUser()
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition api.php:58
$line
Definition cdb.php:59
if( $line===false) $args
Definition cdb.php:64
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
static register( $parser)
static register( $parser)
WebRequest clone which takes values from a provided array.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition Hooks.php:200
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
Internationalisation code.
Definition Language.php:37
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:781
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1643
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:247
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1455
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:163
static tocIndent()
Add another level to the Table of Contents.
Definition Linker.php:1617
static makeImageLink(Parser $parser, LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
Definition Linker.php:303
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition Linker.php:1775
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:848
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1754
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1628
static tocList( $toc, Language $lang=null)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition Linker.php:1679
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1667
MediaWiki exception.
Class for handling an array of magic words.
A factory that stores information about MagicWords, and creates them on demand with caching.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Handles a simple LRU key/value map with a maximum number of entries.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Factory to create LinkRender objects.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Factory for handling the special page list and generating SpecialPage objects.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Set options of the Parser.
getPreSaveTransform()
Transform wiki markup when saving the page?
getUser()
Current user.
getDisableTitleConversion()
Whether title conversion should be disabled.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:74
addTrackingCategory( $msg)
Definition Parser.php:4446
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1037
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:4393
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6305
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:2277
array $mTplDomCache
Definition Parser.php:215
bool string $mInParse
Recursive call protection.
Definition Parser.php:267
handleInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2397
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:4252
const TOC_END
Definition Parser.php:143
$mDefaultStripList
Definition Parser.php:155
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:6371
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6797
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2219
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:885
MagicWordFactory $magicWordFactory
Definition Parser.php:278
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition Parser.php:2780
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:430
ParserOutput $mOutput
Definition Parser.php:194
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2300
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:5096
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:4112
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1111
LinkRenderer $mLinkRenderer
Definition Parser.php:275
preprocess( $text, Title $title=null, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition Parser.php:840
$mHighestExpansionDepth
Definition Parser.php:209
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5409
__construct( $svcOptions=null, MagicWordFactory $magicWordFactory=null, Language $contLang=null, ParserFactory $factory=null, $urlProtocols=null, SpecialPageFactory $spFactory=null, $linkRendererFactory=null, $nsInfo=null, $logger=null, BadFileLookup $badFileLookup=null)
Constructing parsers directly is deprecated! Use a ParserFactory.
Definition Parser.php:352
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:6400
static getSectionNameFromStrippedText( $text)
Definition Parser.php:6404
stripAltText( $caption, $holders)
Definition Parser.php:5912
getOptions()
Get the ParserOptions object.
Definition Parser.php:992
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:5059
getStripState()
Get the StripState.
Definition Parser.php:1216
preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition Parser.php:4887
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:5126
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6330
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3939
replaceExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:2071
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:3311
getFunctionSynonyms()
Definition Parser.php:6022
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
Definition Parser.php:86
makeKnownLinkHolder( $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
Definition Parser.php:2712
armorLinks( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition Parser.php:2755
Language $contLang
Definition Parser.php:281
$mHeadings
Definition Parser.php:211
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:5334
const PTD_FOR_INCLUSION
Definition Parser.php:111
setTitle(Title $t=null)
Set the context title.
Definition Parser.php:915
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:6463
$mGeneratedPPNodeCount
Definition Parser.php:209
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1633
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
Definition Parser.php:6699
limitationWarn( $limitationType, $current='', $max='')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
Definition Parser.php:3392
LinkHolderArray $mLinkHolders
Definition Parser.php:206
$mFunctionTagHooks
Definition Parser.php:153
$mRevisionId
Definition Parser.php:241
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5637
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
Definition Parser.php:5980
pstPass2( $text, $user)
Pre-save transform helper function.
Definition Parser.php:4922
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Definition Parser.php:5145
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated.
Definition Parser.php:237
getRevisionSize()
Get the size of the revision.
Definition Parser.php:6350
const TOC_START
Definition Parser.php:142
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition Parser.php:6096
BadFileLookup $badFileLookup
Definition Parser.php:308
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6502
$mDefaultSort
Definition Parser.php:210
$mTagHooks
Definition Parser.php:149
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition Parser.php:6474
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition Parser.php:1751
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:866
$mFunctionHooks
Definition Parser.php:151
$mShowToc
Definition Parser.php:213
getRevisionTimestampSubstring( $start, $len, $mtts, $variable)
Definition Parser.php:3194
getImageParams( $handler)
Definition Parser.php:5574
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition Parser.php:6672
formatHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition Parser.php:4467
replaceInternalLinks2(&$text)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2386
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1537
getUrlProtocols()
Definition Parser.php:6031
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5283
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3861
$mRevisionSize
Definition Parser.php:244
bool $mFirstCall
Whether firstCallInit still needs to be called.
Definition Parser.php:163
doTableStuff( $text)
Parse the wiki syntax used to render tables.
Definition Parser.php:1244
getTitle()
Accessor for the Title object.
Definition Parser.php:935
handleHeadings( $text)
Parse headers and return html.
Definition Parser.php:1836
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:2166
ParserOptions $mOptions
Definition Parser.php:228
startExternalParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true, $revId=null)
Set up some variables which are usually set up in parse() so that an external function can call some ...
Definition Parser.php:5111
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition Parser.php:6532
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:80
MagicWordArray $mSubstWords
Definition Parser.php:175
$mUrlProtocols
Definition Parser.php:184
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
Definition Parser.php:6045
MapCacheLRU null $currentRevisionCache
Definition Parser.php:261
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1093
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition Parser.php:3277
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2188
setOutputType( $ot)
Set the output type.
Definition Parser.php:957
array $mConf
Definition Parser.php:181
makeLegacyAnchor( $sectionName)
Definition Parser.php:6415
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:1024
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2824
StripState $mStripState
Definition Parser.php:200
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition Parser.php:661
ServiceOptions $svcOptions
This is called $svcOptions instead of $options like elsewhere to avoid confusion with $mOptions,...
Definition Parser.php:296
$mInputSize
Definition Parser.php:246
Title(Title $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:948
MagicWordArray $mVariables
Definition Parser.php:170
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1207
$mOutputType
Definition Parser.php:238
$mImageParamsMagicArray
Definition Parser.php:158
setUser( $user)
Set the current user.
Definition Parser.php:906
setOutputFlag( $flag, $reason)
Definition Parser.php:6839
$mAutonumber
Definition Parser.php:195
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6621
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:5189
expandMagicVariable( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2838
getTags()
Accessor.
Definition Parser.php:6009
static getDefaultPreprocessorClass()
Which class should we use for the preprocessor if not otherwise specified?
Definition Parser.php:457
replaceLinkHoldersTextPrivate( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:5392
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:4381
getImageParamsPrivate( $handler)
Definition Parser.php:5583
fuzzTestPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:6601
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:4199
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:253
LinkRendererFactory $linkRendererFactory
Definition Parser.php:299
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:795
$mRevisionUser
Definition Parser.php:243
handleInternalLinks( $text)
Process [[ ]] wikilinks.
Definition Parser.php:2372
$mTplRedirCache
Definition Parser.php:211
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:4087
$mFunctionSynonyms
Definition Parser.php:152
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:6221
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1465
$mRevisionTimestamp
Definition Parser.php:242
static extractTagsAndParams( $elements, $text, &$matches)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition Parser.php:1144
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:6237
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:5368
interwikiTransclude( $title, $action)
Transclude an interwiki link.
Definition Parser.php:4131
armorLinksPrivate( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition Parser.php:2770
makeKnownLinkHolderPrivate( $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
Definition Parser.php:2730
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:4354
User $mUser
Definition Parser.php:220
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
Definition Parser.php:2795
handleExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:2086
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:6261
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2808
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3950
$mPPNodeCount
Definition Parser.php:209
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:6386
const MARKER_PREFIX
Definition Parser.php:139
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:5380
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:5319
doAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1855
$mMarkerIndex
Definition Parser.php:159
const EXT_LINK_ADDR
Definition Parser.php:101
handleTables( $text)
Parse the wiki syntax used to render tables.
Definition Parser.php:1255
$mExpensiveFunctionCount
Definition Parser.php:212
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1121
Preprocessor $mPreprocessor
Definition Parser.php:188
$mDoubleUnderscores
Definition Parser.php:211
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Definition Parser.php:3904
getOutput()
Get the ParserOutput object.
Definition Parser.php:983
$mVarCache
Definition Parser.php:156
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:1002
$mImageParams
Definition Parser.php:157
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:823
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6435
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:5234
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6251
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6740
$mIncludeCount
Definition Parser.php:202
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:5045
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:1079
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition Parser.php:5858
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:6451
const EXT_LINK_URL_CLASS
Definition Parser.php:98
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:974
LoggerInterface $logger
Definition Parser.php:305
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
Definition Parser.php:3234
magicLinkCallback( $m)
Definition Parser.php:1679
$mIncludeSizes
Definition Parser.php:209
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:416
resetOutput()
Reset the ParserOutput.
Definition Parser.php:532
setLinkID( $id)
Definition Parser.php:1016
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:1067
doQuotes( $text)
Helper function for doAllQuotes()
Definition Parser.php:1885
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3978
doHeadings( $text)
Parse headers and return html.
Definition Parser.php:1825
$mExtLinkBracketedRegex
Definition Parser.php:184
finalizeHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition Parser.php:4487
const EXT_IMAGE_REGEX
Definition Parser.php:104
$mRevIdForTs
Definition Parser.php:245
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:5357
insertStripItem( $text)
Add an item to the strip state Returns the unique tag which must be inserted into the stripped text T...
Definition Parser.php:1229
handleMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1648
NamespaceInfo $nsInfo
Definition Parser.php:302
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
Definition Parser.php:5220
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:4368
testPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:6590
static makeAnchor( $sectionName)
Definition Parser.php:6411
const SPACE_NOT_NL
Definition Parser.php:108
handleAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1867
SpecialPageFactory $specialPageFactory
Definition Parser.php:287
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:464
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:3264
$mForceTocPosition
Definition Parser.php:213
clearState()
Clear Parser state.
Definition Parser.php:484
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6651
ParserFactory $factory
Definition Parser.php:284
static array $constructorOptions
TODO Make this a const when HHVM support is dropped (T192166)
Definition Parser.php:316
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6830
nextLinkID()
Definition Parser.php:1009
getUserSig(&$user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext.
Definition Parser.php:4998
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3415
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5998
fuzzTestPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:6579
parseLinkParameterPrivate( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition Parser.php:5881
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
Definition Parser.php:3225
fuzzTestSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition Parser.php:6549
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
Definition Parser.php:6726
replaceInternalLinks( $text)
Process [[ ]] wikilinks.
Definition Parser.php:2360
lock()
Lock the current instance of the parser.
Definition Parser.php:6769
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition Parser.php:3343
isCurrentRevisionOfTitleCached( $title)
Definition Parser.php:3923
parse( $text, Title $title, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition Parser.php:554
stripAltTextPrivate( $caption, $holders)
Definition Parser.php:5922
$mStripList
Definition Parser.php:154
testPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:6568
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6816
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3769
SectionProfiler $mProfiler
Definition Parser.php:270
$mTransparentTagHooks
Definition Parser.php:150
getConverterLanguage()
Get the language object for language conversion.
Definition Parser.php:1056
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3991
Variant of the Message class.
Group all the pieces relevant to the context of a request into one instance.
setTitle(Title $title=null)
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static articles()
static images()
static edits()
Definition SiteStats.php:94
static users()
static pages()
static numberingroup( $group)
Find the number of users in a given user group.
static activeUsers()
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
return[ 'OATHAuth'=> function(MediaWikiServices $services) { return new OATHAuth($services->getMainConfig(), $services->getDBLoadBalancerFactory());}, 'OATHUserRepository'=> function(MediaWikiServices $services) { global $wgOATHAuthDatabase;$auth=$services->getService( 'OATHAuth');return new OATHUserRepository($services->getDBLoadBalancerFactory() ->getMainLB( $wgOATHAuthDatabase), new \HashBagOStuff(['maxKey'=> 5]), $auth);}]
const OT_WIKI
Definition Defines.php:174
const SFH_NO_HASH
Definition Defines.php:186
const SFH_OBJECT_ARGS
Definition Defines.php:187
const NS_FILE
Definition Defines.php:75
const NS_MEDIAWIKI
Definition Defines.php:77
const NS_TEMPLATE
Definition Defines.php:79
const NS_SPECIAL
Definition Defines.php:58
const OT_PLAIN
Definition Defines.php:177
const OT_PREPROCESS
Definition Defines.php:175
const OT_HTML
Definition Defines.php:173
const NS_MEDIA
Definition Defines.php:57
const NS_CATEGORY
Definition Defines.php:83
const OT_MSG
Definition Defines.php:176
if($IP===false)
There are three types of nodes:
Definition PPNode.php:35
$context
Definition load.php:45
if(PHP_SAPI !=='cli' &&PHP_SAPI !=='phpdbg' $chars)
$cache
Definition mcc.php:33
$sort
const DB_REPLICA
Definition defines.php:25
$lines
Definition router.php:61
$content
Definition router.php:78
return true
Definition router.php:94
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42