MediaWiki REL1_32
Parser.php
Go to the documentation of this file.
1<?php
26use Wikimedia\ScopedCallback;
27
68class Parser {
74 const VERSION = '1.6.4';
75
81
82 # Flags for Parser::setFunctionHook
83 const SFH_NO_HASH = 1;
84 const SFH_OBJECT_ARGS = 2;
85
86 # Constants needed for external link processing
87 # Everything except bracket, space, or control characters
88 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
89 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
90 # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
91 # uses to replace invalid HTML characters.
92 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
93 # Simplified expression to match an IPv4 or IPv6 address, or
94 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
95 const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
96 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
97 // phpcs:ignore Generic.Files.LineLength
98 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
99 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
100
101 # Regular expression for a non-newline space
102 const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
103
104 # Flags for preprocessToDom
106
107 # Allowed values for $this->mOutputType
108 # Parameter to startExternalParse().
109 const OT_HTML = 1; # like parse()
110 const OT_WIKI = 2; # like preSaveTransform()
111 const OT_PREPROCESS = 3; # like preprocess()
112 const OT_MSG = 3;
113 const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
114
132 const MARKER_SUFFIX = "-QINU`\"'\x7f";
133 const MARKER_PREFIX = "\x7f'\"`UNIQ-";
134
135 # Markers used for wrapping the table of contents
136 const TOC_START = '<mw:toc>';
137 const TOC_END = '</mw:toc>';
138
140 const MAX_TTS = 900;
141
142 # Persistent:
143 public $mTagHooks = [];
145 public $mFunctionHooks = [];
146 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
148 public $mStripList = [];
150 public $mVarCache = [];
151 public $mImageParams = [];
153 public $mMarkerIndex = 0;
157 public $mFirstCall = true;
158
159 # Initialised by initialiseVariables()
160
165
170 # Initialised in constructor
171 public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
172
173 # Initialized in getPreprocessor()
176
177 # Cleared with clearState():
181 public $mOutput;
183
188
194
195 public $mLinkID;
196 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
198 public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
199 public $mExpensiveFunctionCount; # number of expensive parser function calls
201
205 public $mUser; # User object; only used when doing pre-save transform
206
207 # Temporary
208 # These are variables reset at least once per parse regardless of $clearState
209
213 public $mOptions;
214
218 public $mTitle; # Title context, used for self-link rendering and similar things
219 public $mOutputType; # Output type, one of the OT_xxx constants
220 public $ot; # Shortcut alias, see setOutputType()
221 public $mRevisionObject; # The revision object of the specified revision ID
222 public $mRevisionId; # ID to display in {{REVISIONID}} tags
223 public $mRevisionTimestamp; # The timestamp of the specified revision ID
224 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
225 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
226 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
227 public $mInputSize = false; # For {{PAGESIZE}} on current page.
228
233 public $mUniqPrefix = self::MARKER_PREFIX;
234
241
249
254 public $mInParse = false;
255
257 protected $mProfiler;
258
262 protected $mLinkRenderer;
263
266
268 private $contLang;
269
271 private $factory;
272
275
284 public function __construct(
285 array $conf = [], MagicWordFactory $magicWordFactory = null, Language $contLang = null,
286 ParserFactory $factory = null, $urlProtocols = null, SpecialPageFactory $spFactory = null
287 ) {
288 $this->mConf = $conf;
289 $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
290 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
291 self::EXT_LINK_ADDR .
292 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
293 if ( isset( $conf['preprocessorClass'] ) ) {
294 $this->mPreprocessorClass = $conf['preprocessorClass'];
295 } elseif ( wfIsHHVM() ) {
296 # Under HHVM Preprocessor_Hash is much faster than Preprocessor_DOM
297 $this->mPreprocessorClass = Preprocessor_Hash::class;
298 } elseif ( extension_loaded( 'domxml' ) ) {
299 # PECL extension that conflicts with the core DOM extension (T15770)
300 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
301 $this->mPreprocessorClass = Preprocessor_Hash::class;
302 } elseif ( extension_loaded( 'dom' ) ) {
303 $this->mPreprocessorClass = Preprocessor_DOM::class;
304 } else {
305 $this->mPreprocessorClass = Preprocessor_Hash::class;
306 }
307 wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
308
309 $services = MediaWikiServices::getInstance();
310 $this->magicWordFactory = $magicWordFactory ??
311 $services->getMagicWordFactory();
312
313 $this->contLang = $contLang ?? $services->getContentLanguage();
314
315 $this->factory = $factory ?? $services->getParserFactory();
316 $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
317 }
318
322 public function __destruct() {
323 if ( isset( $this->mLinkHolders ) ) {
324 unset( $this->mLinkHolders );
325 }
326 foreach ( $this as $name => $value ) {
327 unset( $this->$name );
328 }
329 }
330
334 public function __clone() {
335 $this->mInParse = false;
336
337 // T58226: When you create a reference "to" an object field, that
338 // makes the object field itself be a reference too (until the other
339 // reference goes out of scope). When cloning, any field that's a
340 // reference is copied as a reference in the new object. Both of these
341 // are defined PHP5 behaviors, as inconvenient as it is for us when old
342 // hooks from PHP4 days are passing fields by reference.
343 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
344 // Make a non-reference copy of the field, then rebind the field to
345 // reference the new copy.
346 $tmp = $this->$k;
347 $this->$k =& $tmp;
348 unset( $tmp );
349 }
350
351 Hooks::run( 'ParserCloned', [ $this ] );
352 }
353
357 public function firstCallInit() {
358 if ( !$this->mFirstCall ) {
359 return;
360 }
361 $this->mFirstCall = false;
362
364 CoreTagHooks::register( $this );
365 $this->initialiseVariables();
366
367 // Avoid PHP 7.1 warning from passing $this by reference
368 $parser = $this;
369 Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
370 }
371
377 public function clearState() {
378 $this->firstCallInit();
379 $this->mOutput = new ParserOutput;
380 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
381 $this->mAutonumber = 0;
382 $this->mIncludeCount = [];
383 $this->mLinkHolders = new LinkHolderArray( $this );
384 $this->mLinkID = 0;
385 $this->mRevisionObject = $this->mRevisionTimestamp =
386 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
387 $this->mVarCache = [];
388 $this->mUser = null;
389 $this->mLangLinkLanguages = [];
390 $this->currentRevisionCache = null;
391
392 $this->mStripState = new StripState( $this );
393
394 # Clear these on every parse, T6549
395 $this->mTplRedirCache = $this->mTplDomCache = [];
396
397 $this->mShowToc = true;
398 $this->mForceTocPosition = false;
399 $this->mIncludeSizes = [
400 'post-expand' => 0,
401 'arg' => 0,
402 ];
403 $this->mPPNodeCount = 0;
404 $this->mGeneratedPPNodeCount = 0;
405 $this->mHighestExpansionDepth = 0;
406 $this->mDefaultSort = false;
407 $this->mHeadings = [];
408 $this->mDoubleUnderscores = [];
409 $this->mExpensiveFunctionCount = 0;
410
411 # Fix cloning
412 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
413 $this->mPreprocessor = null;
414 }
415
416 $this->mProfiler = new SectionProfiler();
417
418 // Avoid PHP 7.1 warning from passing $this by reference
419 $parser = $this;
420 Hooks::run( 'ParserClearState', [ &$parser ] );
421 }
422
437 public function parse(
438 $text, Title $title, ParserOptions $options,
439 $linestart = true, $clearState = true, $revid = null
440 ) {
441 if ( $clearState ) {
442 // We use U+007F DELETE to construct strip markers, so we have to make
443 // sure that this character does not occur in the input text.
444 $text = strtr( $text, "\x7f", "?" );
445 $magicScopeVariable = $this->lock();
446 }
447 // Strip U+0000 NULL (T159174)
448 $text = str_replace( "\000", '', $text );
449
450 $this->startParse( $title, $options, self::OT_HTML, $clearState );
451
452 $this->currentRevisionCache = null;
453 $this->mInputSize = strlen( $text );
454 if ( $this->mOptions->getEnableLimitReport() ) {
455 $this->mOutput->resetParseStartTime();
456 }
457
458 $oldRevisionId = $this->mRevisionId;
459 $oldRevisionObject = $this->mRevisionObject;
460 $oldRevisionTimestamp = $this->mRevisionTimestamp;
461 $oldRevisionUser = $this->mRevisionUser;
462 $oldRevisionSize = $this->mRevisionSize;
463 if ( $revid !== null ) {
464 $this->mRevisionId = $revid;
465 $this->mRevisionObject = null;
466 $this->mRevisionTimestamp = null;
467 $this->mRevisionUser = null;
468 $this->mRevisionSize = null;
469 }
470
471 // Avoid PHP 7.1 warning from passing $this by reference
472 $parser = $this;
473 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
474 # No more strip!
475 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
476 $text = $this->internalParse( $text );
477 Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
478
479 $text = $this->internalParseHalfParsed( $text, true, $linestart );
480
488 if ( !( $options->getDisableTitleConversion()
489 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
490 || isset( $this->mDoubleUnderscores['notitleconvert'] )
491 || $this->mOutput->getDisplayTitle() !== false )
492 ) {
493 $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
494 if ( $convruletitle ) {
495 $this->mOutput->setTitleText( $convruletitle );
496 } else {
497 $titleText = $this->getTargetLanguage()->convertTitle( $title );
498 $this->mOutput->setTitleText( $titleText );
499 }
500 }
501
502 # Compute runtime adaptive expiry if set
503 $this->mOutput->finalizeAdaptiveCacheExpiry();
504
505 # Warn if too many heavyweight parser functions were used
506 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
507 $this->limitationWarn( 'expensive-parserfunction',
508 $this->mExpensiveFunctionCount,
509 $this->mOptions->getExpensiveParserFunctionLimit()
510 );
511 }
512
513 # Information on limits, for the benefit of users who try to skirt them
514 if ( $this->mOptions->getEnableLimitReport() ) {
515 $text .= $this->makeLimitReport();
516 }
517
518 # Wrap non-interface parser output in a <div> so it can be targeted
519 # with CSS (T37247)
520 $class = $this->mOptions->getWrapOutputClass();
521 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
522 $this->mOutput->addWrapperDivClass( $class );
523 }
524
525 $this->mOutput->setText( $text );
526
527 $this->mRevisionId = $oldRevisionId;
528 $this->mRevisionObject = $oldRevisionObject;
529 $this->mRevisionTimestamp = $oldRevisionTimestamp;
530 $this->mRevisionUser = $oldRevisionUser;
531 $this->mRevisionSize = $oldRevisionSize;
532 $this->mInputSize = false;
533 $this->currentRevisionCache = null;
534
535 return $this->mOutput;
536 }
537
544 protected function makeLimitReport() {
545 global $wgShowHostnames;
546
547 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
548
549 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
550 if ( $cpuTime !== null ) {
551 $this->mOutput->setLimitReportData( 'limitreport-cputime',
552 sprintf( "%.3f", $cpuTime )
553 );
554 }
555
556 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
557 $this->mOutput->setLimitReportData( 'limitreport-walltime',
558 sprintf( "%.3f", $wallTime )
559 );
560
561 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
562 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
563 );
564 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
565 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
566 );
567 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
568 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
569 );
570 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
571 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
572 );
573 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
574 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
575 );
576 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
577 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
578 );
579
580 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
581 $this->mOutput->setLimitReportData( $key, $value );
582 }
583
584 Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
585
586 $limitReport = "NewPP limit report\n";
587 if ( $wgShowHostnames ) {
588 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
589 }
590 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
591 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
592 $limitReport .= 'Dynamic content: ' .
593 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
594 "\n";
595
596 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
597 if ( Hooks::run( 'ParserLimitReportFormat',
598 [ $key, &$value, &$limitReport, false, false ]
599 ) ) {
600 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
601 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
602 ->inLanguage( 'en' )->useDatabase( false );
603 if ( !$valueMsg->exists() ) {
604 $valueMsg = new RawMessage( '$1' );
605 }
606 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
607 $valueMsg->params( $value );
608 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
609 }
610 }
611 }
612 // Since we're not really outputting HTML, decode the entities and
613 // then re-encode the things that need hiding inside HTML comments.
614 $limitReport = htmlspecialchars_decode( $limitReport );
615
616 // Sanitize for comment. Note '‐' in the replacement is U+2010,
617 // which looks much like the problematic '-'.
618 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
619 $text = "\n<!-- \n$limitReport-->\n";
620
621 // Add on template profiling data in human/machine readable way
622 $dataByFunc = $this->mProfiler->getFunctionStats();
623 uasort( $dataByFunc, function ( $a, $b ) {
624 return $b['real'] <=> $a['real']; // descending order
625 } );
626 $profileReport = [];
627 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
628 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
629 $item['%real'], $item['real'], $item['calls'],
630 htmlspecialchars( $item['name'] ) );
631 }
632 $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
633 $text .= implode( "\n", $profileReport ) . "\n-->\n";
634
635 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
636
637 // Add other cache related metadata
638 if ( $wgShowHostnames ) {
639 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
640 }
641 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
642 $this->mOutput->getCacheTime() );
643 $this->mOutput->setLimitReportData( 'cachereport-ttl',
644 $this->mOutput->getCacheExpiry() );
645 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
646 $this->mOutput->hasDynamicContent() );
647
648 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
649 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
650 $this->mTitle->getPrefixedDBkey() );
651 }
652 return $text;
653 }
654
679 public function recursiveTagParse( $text, $frame = false ) {
680 // Avoid PHP 7.1 warning from passing $this by reference
681 $parser = $this;
682 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
683 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
684 $text = $this->internalParse( $text, false, $frame );
685 return $text;
686 }
687
707 public function recursiveTagParseFully( $text, $frame = false ) {
708 $text = $this->recursiveTagParse( $text, $frame );
709 $text = $this->internalParseHalfParsed( $text, false );
710 return $text;
711 }
712
724 public function preprocess( $text, Title $title = null,
725 ParserOptions $options, $revid = null, $frame = false
726 ) {
727 $magicScopeVariable = $this->lock();
728 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
729 if ( $revid !== null ) {
730 $this->mRevisionId = $revid;
731 }
732 // Avoid PHP 7.1 warning from passing $this by reference
733 $parser = $this;
734 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
735 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
736 $text = $this->replaceVariables( $text, $frame );
737 $text = $this->mStripState->unstripBoth( $text );
738 return $text;
739 }
740
750 public function recursivePreprocess( $text, $frame = false ) {
751 $text = $this->replaceVariables( $text, $frame );
752 $text = $this->mStripState->unstripBoth( $text );
753 return $text;
754 }
755
769 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
770 $msg = new RawMessage( $text );
771 $text = $msg->params( $params )->plain();
772
773 # Parser (re)initialisation
774 $magicScopeVariable = $this->lock();
775 $this->startParse( $title, $options, self::OT_PLAIN, true );
776
778 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
779 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
780 $text = $this->mStripState->unstripBoth( $text );
781 return $text;
782 }
783
790 public function setUser( $user ) {
791 $this->mUser = $user;
792 }
793
799 public function setTitle( $t ) {
800 if ( !$t ) {
801 $t = Title::newFromText( 'NO TITLE' );
802 }
803
804 if ( $t->hasFragment() ) {
805 # Strip the fragment to avoid various odd effects
806 $this->mTitle = $t->createFragmentTarget( '' );
807 } else {
808 $this->mTitle = $t;
809 }
810 }
811
817 public function getTitle() {
818 return $this->mTitle;
819 }
820
827 public function Title( $x = null ) {
828 return wfSetVar( $this->mTitle, $x );
829 }
830
836 public function setOutputType( $ot ) {
837 $this->mOutputType = $ot;
838 # Shortcut alias
839 $this->ot = [
840 'html' => $ot == self::OT_HTML,
841 'wiki' => $ot == self::OT_WIKI,
842 'pre' => $ot == self::OT_PREPROCESS,
843 'plain' => $ot == self::OT_PLAIN,
844 ];
845 }
846
853 public function OutputType( $x = null ) {
854 return wfSetVar( $this->mOutputType, $x );
855 }
856
862 public function getOutput() {
863 return $this->mOutput;
864 }
865
871 public function getOptions() {
872 return $this->mOptions;
873 }
874
881 public function Options( $x = null ) {
882 return wfSetVar( $this->mOptions, $x );
883 }
884
888 public function nextLinkID() {
889 return $this->mLinkID++;
890 }
891
895 public function setLinkID( $id ) {
896 $this->mLinkID = $id;
897 }
898
903 public function getFunctionLang() {
904 return $this->getTargetLanguage();
905 }
906
916 public function getTargetLanguage() {
917 $target = $this->mOptions->getTargetLanguage();
918
919 if ( $target !== null ) {
920 return $target;
921 } elseif ( $this->mOptions->getInterfaceMessage() ) {
922 return $this->mOptions->getUserLangObj();
923 } elseif ( is_null( $this->mTitle ) ) {
924 throw new MWException( __METHOD__ . ': $this->mTitle is null' );
925 }
926
927 return $this->mTitle->getPageLanguage();
928 }
929
935 public function getConverterLanguage() {
936 return $this->getTargetLanguage();
937 }
938
945 public function getUser() {
946 if ( !is_null( $this->mUser ) ) {
947 return $this->mUser;
948 }
949 return $this->mOptions->getUser();
950 }
951
957 public function getPreprocessor() {
958 if ( !isset( $this->mPreprocessor ) ) {
959 $class = $this->mPreprocessorClass;
960 $this->mPreprocessor = new $class( $this );
961 }
962 return $this->mPreprocessor;
963 }
964
971 public function getLinkRenderer() {
972 if ( !$this->mLinkRenderer ) {
973 $this->mLinkRenderer = MediaWikiServices::getInstance()
974 ->getLinkRendererFactory()->create();
975 $this->mLinkRenderer->setStubThreshold(
976 $this->getOptions()->getStubThreshold()
977 );
978 }
979
980 return $this->mLinkRenderer;
981 }
982
989 public function getMagicWordFactory() {
990 return $this->magicWordFactory;
991 }
992
999 public function getContentLanguage() {
1000 return $this->contLang;
1001 }
1002
1022 public static function extractTagsAndParams( $elements, $text, &$matches ) {
1023 static $n = 1;
1024 $stripped = '';
1025 $matches = [];
1026
1027 $taglist = implode( '|', $elements );
1028 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
1029
1030 while ( $text != '' ) {
1031 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1032 $stripped .= $p[0];
1033 if ( count( $p ) < 5 ) {
1034 break;
1035 }
1036 if ( count( $p ) > 5 ) {
1037 # comment
1038 $element = $p[4];
1039 $attributes = '';
1040 $close = '';
1041 $inside = $p[5];
1042 } else {
1043 # tag
1044 $element = $p[1];
1045 $attributes = $p[2];
1046 $close = $p[3];
1047 $inside = $p[4];
1048 }
1049
1050 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1051 $stripped .= $marker;
1052
1053 if ( $close === '/>' ) {
1054 # Empty element tag, <tag />
1055 $content = null;
1056 $text = $inside;
1057 $tail = null;
1058 } else {
1059 if ( $element === '!--' ) {
1060 $end = '/(-->)/';
1061 } else {
1062 $end = "/(<\\/$element\\s*>)/i";
1063 }
1064 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1065 $content = $q[0];
1066 if ( count( $q ) < 3 ) {
1067 # No end tag -- let it run out to the end of the text.
1068 $tail = '';
1069 $text = '';
1070 } else {
1071 $tail = $q[1];
1072 $text = $q[2];
1073 }
1074 }
1075
1076 $matches[$marker] = [ $element,
1077 $content,
1078 Sanitizer::decodeTagAttributes( $attributes ),
1079 "<$element$attributes$close$content$tail" ];
1080 }
1081 return $stripped;
1082 }
1083
1089 public function getStripList() {
1090 return $this->mStripList;
1091 }
1092
1102 public function insertStripItem( $text ) {
1103 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1104 $this->mMarkerIndex++;
1105 $this->mStripState->addGeneral( $marker, $text );
1106 return $marker;
1107 }
1108
1116 public function doTableStuff( $text ) {
1117 $lines = StringUtils::explode( "\n", $text );
1118 $out = '';
1119 $td_history = []; # Is currently a td tag open?
1120 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1121 $tr_history = []; # Is currently a tr tag open?
1122 $tr_attributes = []; # history of tr attributes
1123 $has_opened_tr = []; # Did this table open a <tr> element?
1124 $indent_level = 0; # indent level of the table
1125
1126 foreach ( $lines as $outLine ) {
1127 $line = trim( $outLine );
1128
1129 if ( $line === '' ) { # empty line, go to next line
1130 $out .= $outLine . "\n";
1131 continue;
1132 }
1133
1134 $first_character = $line[0];
1135 $first_two = substr( $line, 0, 2 );
1136 $matches = [];
1137
1138 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1139 # First check if we are starting a new table
1140 $indent_level = strlen( $matches[1] );
1141
1142 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1143 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1144
1145 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1146 array_push( $td_history, false );
1147 array_push( $last_tag_history, '' );
1148 array_push( $tr_history, false );
1149 array_push( $tr_attributes, '' );
1150 array_push( $has_opened_tr, false );
1151 } elseif ( count( $td_history ) == 0 ) {
1152 # Don't do any of the following
1153 $out .= $outLine . "\n";
1154 continue;
1155 } elseif ( $first_two === '|}' ) {
1156 # We are ending a table
1157 $line = '</table>' . substr( $line, 2 );
1158 $last_tag = array_pop( $last_tag_history );
1159
1160 if ( !array_pop( $has_opened_tr ) ) {
1161 $line = "<tr><td></td></tr>{$line}";
1162 }
1163
1164 if ( array_pop( $tr_history ) ) {
1165 $line = "</tr>{$line}";
1166 }
1167
1168 if ( array_pop( $td_history ) ) {
1169 $line = "</{$last_tag}>{$line}";
1170 }
1171 array_pop( $tr_attributes );
1172 if ( $indent_level > 0 ) {
1173 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1174 } else {
1175 $outLine = $line;
1176 }
1177 } elseif ( $first_two === '|-' ) {
1178 # Now we have a table row
1179 $line = preg_replace( '#^\|-+#', '', $line );
1180
1181 # Whats after the tag is now only attributes
1182 $attributes = $this->mStripState->unstripBoth( $line );
1183 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1184 array_pop( $tr_attributes );
1185 array_push( $tr_attributes, $attributes );
1186
1187 $line = '';
1188 $last_tag = array_pop( $last_tag_history );
1189 array_pop( $has_opened_tr );
1190 array_push( $has_opened_tr, true );
1191
1192 if ( array_pop( $tr_history ) ) {
1193 $line = '</tr>';
1194 }
1195
1196 if ( array_pop( $td_history ) ) {
1197 $line = "</{$last_tag}>{$line}";
1198 }
1199
1200 $outLine = $line;
1201 array_push( $tr_history, false );
1202 array_push( $td_history, false );
1203 array_push( $last_tag_history, '' );
1204 } elseif ( $first_character === '|'
1205 || $first_character === '!'
1206 || $first_two === '|+'
1207 ) {
1208 # This might be cell elements, td, th or captions
1209 if ( $first_two === '|+' ) {
1210 $first_character = '+';
1211 $line = substr( $line, 2 );
1212 } else {
1213 $line = substr( $line, 1 );
1214 }
1215
1216 // Implies both are valid for table headings.
1217 if ( $first_character === '!' ) {
1218 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1219 }
1220
1221 # Split up multiple cells on the same line.
1222 # FIXME : This can result in improper nesting of tags processed
1223 # by earlier parser steps.
1224 $cells = explode( '||', $line );
1225
1226 $outLine = '';
1227
1228 # Loop through each table cell
1229 foreach ( $cells as $cell ) {
1230 $previous = '';
1231 if ( $first_character !== '+' ) {
1232 $tr_after = array_pop( $tr_attributes );
1233 if ( !array_pop( $tr_history ) ) {
1234 $previous = "<tr{$tr_after}>\n";
1235 }
1236 array_push( $tr_history, true );
1237 array_push( $tr_attributes, '' );
1238 array_pop( $has_opened_tr );
1239 array_push( $has_opened_tr, true );
1240 }
1241
1242 $last_tag = array_pop( $last_tag_history );
1243
1244 if ( array_pop( $td_history ) ) {
1245 $previous = "</{$last_tag}>\n{$previous}";
1246 }
1247
1248 if ( $first_character === '|' ) {
1249 $last_tag = 'td';
1250 } elseif ( $first_character === '!' ) {
1251 $last_tag = 'th';
1252 } elseif ( $first_character === '+' ) {
1253 $last_tag = 'caption';
1254 } else {
1255 $last_tag = '';
1256 }
1257
1258 array_push( $last_tag_history, $last_tag );
1259
1260 # A cell could contain both parameters and data
1261 $cell_data = explode( '|', $cell, 2 );
1262
1263 # T2553: Note that a '|' inside an invalid link should not
1264 # be mistaken as delimiting cell parameters
1265 # Bug T153140: Neither should language converter markup.
1266 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1267 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1268 } elseif ( count( $cell_data ) == 1 ) {
1269 // Whitespace in cells is trimmed
1270 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1271 } else {
1272 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1273 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1274 // Whitespace in cells is trimmed
1275 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1276 }
1277
1278 $outLine .= $cell;
1279 array_push( $td_history, true );
1280 }
1281 }
1282 $out .= $outLine . "\n";
1283 }
1284
1285 # Closing open td, tr && table
1286 while ( count( $td_history ) > 0 ) {
1287 if ( array_pop( $td_history ) ) {
1288 $out .= "</td>\n";
1289 }
1290 if ( array_pop( $tr_history ) ) {
1291 $out .= "</tr>\n";
1292 }
1293 if ( !array_pop( $has_opened_tr ) ) {
1294 $out .= "<tr><td></td></tr>\n";
1295 }
1296
1297 $out .= "</table>\n";
1298 }
1299
1300 # Remove trailing line-ending (b/c)
1301 if ( substr( $out, -1 ) === "\n" ) {
1302 $out = substr( $out, 0, -1 );
1303 }
1304
1305 # special case: don't return empty table
1306 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1307 $out = '';
1308 }
1309
1310 return $out;
1311 }
1312
1326 public function internalParse( $text, $isMain = true, $frame = false ) {
1327 $origText = $text;
1328
1329 // Avoid PHP 7.1 warning from passing $this by reference
1330 $parser = $this;
1331
1332 # Hook to suspend the parser in this state
1333 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1334 return $text;
1335 }
1336
1337 # if $frame is provided, then use $frame for replacing any variables
1338 if ( $frame ) {
1339 # use frame depth to infer how include/noinclude tags should be handled
1340 # depth=0 means this is the top-level document; otherwise it's an included document
1341 if ( !$frame->depth ) {
1342 $flag = 0;
1343 } else {
1344 $flag = self::PTD_FOR_INCLUSION;
1345 }
1346 $dom = $this->preprocessToDom( $text, $flag );
1347 $text = $frame->expand( $dom );
1348 } else {
1349 # if $frame is not provided, then use old-style replaceVariables
1350 $text = $this->replaceVariables( $text );
1351 }
1352
1353 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1354 $text = Sanitizer::removeHTMLtags(
1355 $text,
1356 [ $this, 'attributeStripCallback' ],
1357 false,
1358 array_keys( $this->mTransparentTagHooks ),
1359 [],
1360 [ $this, 'addTrackingCategory' ]
1361 );
1362 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1363
1364 # Tables need to come after variable replacement for things to work
1365 # properly; putting them before other transformations should keep
1366 # exciting things like link expansions from showing up in surprising
1367 # places.
1368 $text = $this->doTableStuff( $text );
1369
1370 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1371
1372 $text = $this->doDoubleUnderscore( $text );
1373
1374 $text = $this->doHeadings( $text );
1375 $text = $this->replaceInternalLinks( $text );
1376 $text = $this->doAllQuotes( $text );
1377 $text = $this->replaceExternalLinks( $text );
1378
1379 # replaceInternalLinks may sometimes leave behind
1380 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1381 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1382
1383 $text = $this->doMagicLinks( $text );
1384 $text = $this->formatHeadings( $text, $origText, $isMain );
1385
1386 return $text;
1387 }
1388
1398 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1399 $text = $this->mStripState->unstripGeneral( $text );
1400
1401 // Avoid PHP 7.1 warning from passing $this by reference
1402 $parser = $this;
1403
1404 if ( $isMain ) {
1405 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1406 }
1407
1408 # Clean up special characters, only run once, next-to-last before doBlockLevels
1409 $text = Sanitizer::armorFrenchSpaces( $text );
1410
1411 $text = $this->doBlockLevels( $text, $linestart );
1412
1413 $this->replaceLinkHolders( $text );
1414
1422 if ( !( $this->mOptions->getDisableContentConversion()
1423 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1424 ) {
1425 if ( !$this->mOptions->getInterfaceMessage() ) {
1426 # The position of the convert() call should not be changed. it
1427 # assumes that the links are all replaced and the only thing left
1428 # is the <nowiki> mark.
1429 $text = $this->getTargetLanguage()->convert( $text );
1430 }
1431 }
1432
1433 $text = $this->mStripState->unstripNoWiki( $text );
1434
1435 if ( $isMain ) {
1436 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1437 }
1438
1439 $text = $this->replaceTransparentTags( $text );
1440 $text = $this->mStripState->unstripGeneral( $text );
1441
1442 $text = Sanitizer::normalizeCharReferences( $text );
1443
1444 if ( MWTidy::isEnabled() ) {
1445 if ( $this->mOptions->getTidy() ) {
1446 $text = MWTidy::tidy( $text );
1447 }
1448 } else {
1449 # attempt to sanitize at least some nesting problems
1450 # (T4702 and quite a few others)
1451 $tidyregs = [
1452 # ''Something [http://www.cool.com cool''] -->
1453 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1454 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1455 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1456 # fix up an anchor inside another anchor, only
1457 # at least for a single single nested link (T5695)
1458 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1459 '\\1\\2</a>\\3</a>\\1\\4</a>',
1460 # fix div inside inline elements- doBlockLevels won't wrap a line which
1461 # contains a div, so fix it up here; replace
1462 # div with escaped text
1463 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1464 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1465 # remove empty italic or bold tag pairs, some
1466 # introduced by rules above
1467 '/<([bi])><\/\\1>/' => '',
1468 ];
1469
1470 $text = preg_replace(
1471 array_keys( $tidyregs ),
1472 array_values( $tidyregs ),
1473 $text );
1474 }
1475
1476 if ( $isMain ) {
1477 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1478 }
1479
1480 return $text;
1481 }
1482
1494 public function doMagicLinks( $text ) {
1496 $urlChar = self::EXT_LINK_URL_CLASS;
1497 $addr = self::EXT_LINK_ADDR;
1498 $space = self::SPACE_NOT_NL; # non-newline space
1499 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1500 $spaces = "$space++"; # possessive match of 1 or more spaces
1501 $text = preg_replace_callback(
1502 '!(?: # Start cases
1503 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1504 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1505 (\b # m[3]: Free external links
1506 (?i:$prots)
1507 ($addr$urlChar*) # m[4]: Post-protocol path
1508 ) |
1509 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1510 ([0-9]+)\b |
1511 \bISBN $spaces ( # m[6]: ISBN, capture number
1512 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1513 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1514 [0-9Xx] # check digit
1515 )\b
1516 )!xu", [ $this, 'magicLinkCallback' ], $text );
1517 return $text;
1518 }
1519
1525 public function magicLinkCallback( $m ) {
1526 if ( isset( $m[1] ) && $m[1] !== '' ) {
1527 # Skip anchor
1528 return $m[0];
1529 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1530 # Skip HTML element
1531 return $m[0];
1532 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1533 # Free external link
1534 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1535 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1536 # RFC or PMID
1537 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1538 if ( !$this->mOptions->getMagicRFCLinks() ) {
1539 return $m[0];
1540 }
1541 $keyword = 'RFC';
1542 $urlmsg = 'rfcurl';
1543 $cssClass = 'mw-magiclink-rfc';
1544 $trackingCat = 'magiclink-tracking-rfc';
1545 $id = $m[5];
1546 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1547 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1548 return $m[0];
1549 }
1550 $keyword = 'PMID';
1551 $urlmsg = 'pubmedurl';
1552 $cssClass = 'mw-magiclink-pmid';
1553 $trackingCat = 'magiclink-tracking-pmid';
1554 $id = $m[5];
1555 } else {
1556 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1557 substr( $m[0], 0, 20 ) . '"' );
1558 }
1559 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1560 $this->addTrackingCategory( $trackingCat );
1561 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1562 } elseif ( isset( $m[6] ) && $m[6] !== ''
1563 && $this->mOptions->getMagicISBNLinks()
1564 ) {
1565 # ISBN
1566 $isbn = $m[6];
1567 $space = self::SPACE_NOT_NL; # non-newline space
1568 $isbn = preg_replace( "/$space/", ' ', $isbn );
1569 $num = strtr( $isbn, [
1570 '-' => '',
1571 ' ' => '',
1572 'x' => 'X',
1573 ] );
1574 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1575 return $this->getLinkRenderer()->makeKnownLink(
1576 SpecialPage::getTitleFor( 'Booksources', $num ),
1577 "ISBN $isbn",
1578 [
1579 'class' => 'internal mw-magiclink-isbn',
1580 'title' => false // suppress title attribute
1581 ]
1582 );
1583 } else {
1584 return $m[0];
1585 }
1586 }
1587
1597 public function makeFreeExternalLink( $url, $numPostProto ) {
1598 $trail = '';
1599
1600 # The characters '<' and '>' (which were escaped by
1601 # removeHTMLtags()) should not be included in
1602 # URLs, per RFC 2396.
1603 # Make &nbsp; terminate a URL as well (bug T84937)
1604 $m2 = [];
1605 if ( preg_match(
1606 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1607 $url,
1608 $m2,
1609 PREG_OFFSET_CAPTURE
1610 ) ) {
1611 $trail = substr( $url, $m2[0][1] ) . $trail;
1612 $url = substr( $url, 0, $m2[0][1] );
1613 }
1614
1615 # Move trailing punctuation to $trail
1616 $sep = ',;\.:!?';
1617 # If there is no left bracket, then consider right brackets fair game too
1618 if ( strpos( $url, '(' ) === false ) {
1619 $sep .= ')';
1620 }
1621
1622 $urlRev = strrev( $url );
1623 $numSepChars = strspn( $urlRev, $sep );
1624 # Don't break a trailing HTML entity by moving the ; into $trail
1625 # This is in hot code, so use substr_compare to avoid having to
1626 # create a new string object for the comparison
1627 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1628 # more optimization: instead of running preg_match with a $
1629 # anchor, which can be slow, do the match on the reversed
1630 # string starting at the desired offset.
1631 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1632 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1633 $numSepChars--;
1634 }
1635 }
1636 if ( $numSepChars ) {
1637 $trail = substr( $url, -$numSepChars ) . $trail;
1638 $url = substr( $url, 0, -$numSepChars );
1639 }
1640
1641 # Verify that we still have a real URL after trail removal, and
1642 # not just lone protocol
1643 if ( strlen( $trail ) >= $numPostProto ) {
1644 return $url . $trail;
1645 }
1646
1647 $url = Sanitizer::cleanUrl( $url );
1648
1649 # Is this an external image?
1650 $text = $this->maybeMakeExternalImage( $url );
1651 if ( $text === false ) {
1652 # Not an image, make a link
1653 $text = Linker::makeExternalLink( $url,
1654 $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1655 true, 'free',
1656 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1657 # Register it in the output object...
1658 $this->mOutput->addExternalLink( $url );
1659 }
1660 return $text . $trail;
1661 }
1662
1672 public function doHeadings( $text ) {
1673 for ( $i = 6; $i >= 1; --$i ) {
1674 $h = str_repeat( '=', $i );
1675 // Trim non-newline whitespace from headings
1676 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1677 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1678 }
1679 return $text;
1680 }
1681
1690 public function doAllQuotes( $text ) {
1691 $outtext = '';
1692 $lines = StringUtils::explode( "\n", $text );
1693 foreach ( $lines as $line ) {
1694 $outtext .= $this->doQuotes( $line ) . "\n";
1695 }
1696 $outtext = substr( $outtext, 0, -1 );
1697 return $outtext;
1698 }
1699
1707 public function doQuotes( $text ) {
1708 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1709 $countarr = count( $arr );
1710 if ( $countarr == 1 ) {
1711 return $text;
1712 }
1713
1714 // First, do some preliminary work. This may shift some apostrophes from
1715 // being mark-up to being text. It also counts the number of occurrences
1716 // of bold and italics mark-ups.
1717 $numbold = 0;
1718 $numitalics = 0;
1719 for ( $i = 1; $i < $countarr; $i += 2 ) {
1720 $thislen = strlen( $arr[$i] );
1721 // If there are ever four apostrophes, assume the first is supposed to
1722 // be text, and the remaining three constitute mark-up for bold text.
1723 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1724 if ( $thislen == 4 ) {
1725 $arr[$i - 1] .= "'";
1726 $arr[$i] = "'''";
1727 $thislen = 3;
1728 } elseif ( $thislen > 5 ) {
1729 // If there are more than 5 apostrophes in a row, assume they're all
1730 // text except for the last 5.
1731 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1732 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1733 $arr[$i] = "'''''";
1734 $thislen = 5;
1735 }
1736 // Count the number of occurrences of bold and italics mark-ups.
1737 if ( $thislen == 2 ) {
1738 $numitalics++;
1739 } elseif ( $thislen == 3 ) {
1740 $numbold++;
1741 } elseif ( $thislen == 5 ) {
1742 $numitalics++;
1743 $numbold++;
1744 }
1745 }
1746
1747 // If there is an odd number of both bold and italics, it is likely
1748 // that one of the bold ones was meant to be an apostrophe followed
1749 // by italics. Which one we cannot know for certain, but it is more
1750 // likely to be one that has a single-letter word before it.
1751 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1752 $firstsingleletterword = -1;
1753 $firstmultiletterword = -1;
1754 $firstspace = -1;
1755 for ( $i = 1; $i < $countarr; $i += 2 ) {
1756 if ( strlen( $arr[$i] ) == 3 ) {
1757 $x1 = substr( $arr[$i - 1], -1 );
1758 $x2 = substr( $arr[$i - 1], -2, 1 );
1759 if ( $x1 === ' ' ) {
1760 if ( $firstspace == -1 ) {
1761 $firstspace = $i;
1762 }
1763 } elseif ( $x2 === ' ' ) {
1764 $firstsingleletterword = $i;
1765 // if $firstsingleletterword is set, we don't
1766 // look at the other options, so we can bail early.
1767 break;
1768 } else {
1769 if ( $firstmultiletterword == -1 ) {
1770 $firstmultiletterword = $i;
1771 }
1772 }
1773 }
1774 }
1775
1776 // If there is a single-letter word, use it!
1777 if ( $firstsingleletterword > -1 ) {
1778 $arr[$firstsingleletterword] = "''";
1779 $arr[$firstsingleletterword - 1] .= "'";
1780 } elseif ( $firstmultiletterword > -1 ) {
1781 // If not, but there's a multi-letter word, use that one.
1782 $arr[$firstmultiletterword] = "''";
1783 $arr[$firstmultiletterword - 1] .= "'";
1784 } elseif ( $firstspace > -1 ) {
1785 // ... otherwise use the first one that has neither.
1786 // (notice that it is possible for all three to be -1 if, for example,
1787 // there is only one pentuple-apostrophe in the line)
1788 $arr[$firstspace] = "''";
1789 $arr[$firstspace - 1] .= "'";
1790 }
1791 }
1792
1793 // Now let's actually convert our apostrophic mush to HTML!
1794 $output = '';
1795 $buffer = '';
1796 $state = '';
1797 $i = 0;
1798 foreach ( $arr as $r ) {
1799 if ( ( $i % 2 ) == 0 ) {
1800 if ( $state === 'both' ) {
1801 $buffer .= $r;
1802 } else {
1803 $output .= $r;
1804 }
1805 } else {
1806 $thislen = strlen( $r );
1807 if ( $thislen == 2 ) {
1808 if ( $state === 'i' ) {
1809 $output .= '</i>';
1810 $state = '';
1811 } elseif ( $state === 'bi' ) {
1812 $output .= '</i>';
1813 $state = 'b';
1814 } elseif ( $state === 'ib' ) {
1815 $output .= '</b></i><b>';
1816 $state = 'b';
1817 } elseif ( $state === 'both' ) {
1818 $output .= '<b><i>' . $buffer . '</i>';
1819 $state = 'b';
1820 } else { // $state can be 'b' or ''
1821 $output .= '<i>';
1822 $state .= 'i';
1823 }
1824 } elseif ( $thislen == 3 ) {
1825 if ( $state === 'b' ) {
1826 $output .= '</b>';
1827 $state = '';
1828 } elseif ( $state === 'bi' ) {
1829 $output .= '</i></b><i>';
1830 $state = 'i';
1831 } elseif ( $state === 'ib' ) {
1832 $output .= '</b>';
1833 $state = 'i';
1834 } elseif ( $state === 'both' ) {
1835 $output .= '<i><b>' . $buffer . '</b>';
1836 $state = 'i';
1837 } else { // $state can be 'i' or ''
1838 $output .= '<b>';
1839 $state .= 'b';
1840 }
1841 } elseif ( $thislen == 5 ) {
1842 if ( $state === 'b' ) {
1843 $output .= '</b><i>';
1844 $state = 'i';
1845 } elseif ( $state === 'i' ) {
1846 $output .= '</i><b>';
1847 $state = 'b';
1848 } elseif ( $state === 'bi' ) {
1849 $output .= '</i></b>';
1850 $state = '';
1851 } elseif ( $state === 'ib' ) {
1852 $output .= '</b></i>';
1853 $state = '';
1854 } elseif ( $state === 'both' ) {
1855 $output .= '<i><b>' . $buffer . '</b></i>';
1856 $state = '';
1857 } else { // ($state == '')
1858 $buffer = '';
1859 $state = 'both';
1860 }
1861 }
1862 }
1863 $i++;
1864 }
1865 // Now close all remaining tags. Notice that the order is important.
1866 if ( $state === 'b' || $state === 'ib' ) {
1867 $output .= '</b>';
1868 }
1869 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1870 $output .= '</i>';
1871 }
1872 if ( $state === 'bi' ) {
1873 $output .= '</b>';
1874 }
1875 // There might be lonely ''''', so make sure we have a buffer
1876 if ( $state === 'both' && $buffer ) {
1877 $output .= '<b><i>' . $buffer . '</i></b>';
1878 }
1879 return $output;
1880 }
1881
1895 public function replaceExternalLinks( $text ) {
1896 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1897 if ( $bits === false ) {
1898 throw new MWException( "PCRE needs to be compiled with "
1899 . "--enable-unicode-properties in order for MediaWiki to function" );
1900 }
1901 $s = array_shift( $bits );
1902
1903 $i = 0;
1904 while ( $i < count( $bits ) ) {
1905 $url = $bits[$i++];
1906 $i++; // protocol
1907 $text = $bits[$i++];
1908 $trail = $bits[$i++];
1909
1910 # The characters '<' and '>' (which were escaped by
1911 # removeHTMLtags()) should not be included in
1912 # URLs, per RFC 2396.
1913 $m2 = [];
1914 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1915 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1916 $url = substr( $url, 0, $m2[0][1] );
1917 }
1918
1919 # If the link text is an image URL, replace it with an <img> tag
1920 # This happened by accident in the original parser, but some people used it extensively
1921 $img = $this->maybeMakeExternalImage( $text );
1922 if ( $img !== false ) {
1923 $text = $img;
1924 }
1925
1926 $dtrail = '';
1927
1928 # Set linktype for CSS
1929 $linktype = 'text';
1930
1931 # No link text, e.g. [http://domain.tld/some.link]
1932 if ( $text == '' ) {
1933 # Autonumber
1934 $langObj = $this->getTargetLanguage();
1935 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1936 $linktype = 'autonumber';
1937 } else {
1938 # Have link text, e.g. [http://domain.tld/some.link text]s
1939 # Check for trail
1940 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1941 }
1942
1943 // Excluding protocol-relative URLs may avoid many false positives.
1944 if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
1945 $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
1946 }
1947
1948 $url = Sanitizer::cleanUrl( $url );
1949
1950 # Use the encoded URL
1951 # This means that users can paste URLs directly into the text
1952 # Funny characters like ö aren't valid in URLs anyway
1953 # This was changed in August 2004
1954 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1955 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1956
1957 # Register link in the output object.
1958 $this->mOutput->addExternalLink( $url );
1959 }
1960
1961 return $s;
1962 }
1963
1973 public static function getExternalLinkRel( $url = false, $title = null ) {
1975 $ns = $title ? $title->getNamespace() : false;
1976 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1978 ) {
1979 return 'nofollow';
1980 }
1981 return null;
1982 }
1983
1994 public function getExternalLinkAttribs( $url ) {
1995 $attribs = [];
1996 $rel = self::getExternalLinkRel( $url, $this->mTitle );
1997
1998 $target = $this->mOptions->getExternalLinkTarget();
1999 if ( $target ) {
2000 $attribs['target'] = $target;
2001 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2002 // T133507. New windows can navigate parent cross-origin.
2003 // Including noreferrer due to lacking browser
2004 // support of noopener. Eventually noreferrer should be removed.
2005 if ( $rel !== '' ) {
2006 $rel .= ' ';
2007 }
2008 $rel .= 'noreferrer noopener';
2009 }
2010 }
2011 $attribs['rel'] = $rel;
2012 return $attribs;
2013 }
2014
2024 public static function normalizeLinkUrl( $url ) {
2025 # First, make sure unsafe characters are encoded
2026 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2027 function ( $m ) {
2028 return rawurlencode( $m[0] );
2029 },
2030 $url
2031 );
2032
2033 $ret = '';
2034 $end = strlen( $url );
2035
2036 # Fragment part - 'fragment'
2037 $start = strpos( $url, '#' );
2038 if ( $start !== false && $start < $end ) {
2039 $ret = self::normalizeUrlComponent(
2040 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2041 $end = $start;
2042 }
2043
2044 # Query part - 'query' minus &=+;
2045 $start = strpos( $url, '?' );
2046 if ( $start !== false && $start < $end ) {
2047 $ret = self::normalizeUrlComponent(
2048 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2049 $end = $start;
2050 }
2051
2052 # Scheme and path part - 'pchar'
2053 # (we assume no userinfo or encoded colons in the host)
2054 $ret = self::normalizeUrlComponent(
2055 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2056
2057 return $ret;
2058 }
2059
2060 private static function normalizeUrlComponent( $component, $unsafe ) {
2061 $callback = function ( $matches ) use ( $unsafe ) {
2062 $char = urldecode( $matches[0] );
2063 $ord = ord( $char );
2064 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2065 # Unescape it
2066 return $char;
2067 } else {
2068 # Leave it escaped, but use uppercase for a-f
2069 return strtoupper( $matches[0] );
2070 }
2071 };
2072 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2073 }
2074
2083 private function maybeMakeExternalImage( $url ) {
2084 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2085 $imagesexception = !empty( $imagesfrom );
2086 $text = false;
2087 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2088 if ( $imagesexception && is_array( $imagesfrom ) ) {
2089 $imagematch = false;
2090 foreach ( $imagesfrom as $match ) {
2091 if ( strpos( $url, $match ) === 0 ) {
2092 $imagematch = true;
2093 break;
2094 }
2095 }
2096 } elseif ( $imagesexception ) {
2097 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2098 } else {
2099 $imagematch = false;
2100 }
2101
2102 if ( $this->mOptions->getAllowExternalImages()
2103 || ( $imagesexception && $imagematch )
2104 ) {
2105 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2106 # Image found
2107 $text = Linker::makeExternalImage( $url );
2108 }
2109 }
2110 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2111 && preg_match( self::EXT_IMAGE_REGEX, $url )
2112 ) {
2113 $whitelist = explode(
2114 "\n",
2115 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2116 );
2117
2118 foreach ( $whitelist as $entry ) {
2119 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2120 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2121 continue;
2122 }
2123 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2124 # Image matches a whitelist entry
2125 $text = Linker::makeExternalImage( $url );
2126 break;
2127 }
2128 }
2129 }
2130 return $text;
2131 }
2132
2142 public function replaceInternalLinks( $s ) {
2143 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2144 return $s;
2145 }
2146
2155 public function replaceInternalLinks2( &$s ) {
2157
2158 static $tc = false, $e1, $e1_img;
2159 # the % is needed to support urlencoded titles as well
2160 if ( !$tc ) {
2161 $tc = Title::legalChars() . '#%';
2162 # Match a link having the form [[namespace:link|alternate]]trail
2163 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2164 # Match cases where there is no "]]", which might still be images
2165 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2166 }
2167
2168 $holders = new LinkHolderArray( $this );
2169
2170 # split the entire text string on occurrences of [[
2171 $a = StringUtils::explode( '[[', ' ' . $s );
2172 # get the first element (all text up to first [[), and remove the space we added
2173 $s = $a->current();
2174 $a->next();
2175 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2176 $s = substr( $s, 1 );
2177
2178 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2179 $e2 = null;
2180 if ( $useLinkPrefixExtension ) {
2181 # Match the end of a line for a word that's not followed by whitespace,
2182 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2183 $charset = $this->contLang->linkPrefixCharset();
2184 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2185 }
2186
2187 if ( is_null( $this->mTitle ) ) {
2188 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2189 }
2190 $nottalk = !$this->mTitle->isTalkPage();
2191
2192 if ( $useLinkPrefixExtension ) {
2193 $m = [];
2194 if ( preg_match( $e2, $s, $m ) ) {
2195 $first_prefix = $m[2];
2196 } else {
2197 $first_prefix = false;
2198 }
2199 } else {
2200 $prefix = '';
2201 }
2202
2203 $useSubpages = $this->areSubpagesAllowed();
2204
2205 # Loop for each link
2206 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2207 # Check for excessive memory usage
2208 if ( $holders->isBig() ) {
2209 # Too big
2210 # Do the existence check, replace the link holders and clear the array
2211 $holders->replace( $s );
2212 $holders->clear();
2213 }
2214
2215 if ( $useLinkPrefixExtension ) {
2216 if ( preg_match( $e2, $s, $m ) ) {
2217 $prefix = $m[2];
2218 $s = $m[1];
2219 } else {
2220 $prefix = '';
2221 }
2222 # first link
2223 if ( $first_prefix ) {
2224 $prefix = $first_prefix;
2225 $first_prefix = false;
2226 }
2227 }
2228
2229 $might_be_img = false;
2230
2231 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2232 $text = $m[2];
2233 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2234 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2235 # the real problem is with the $e1 regex
2236 # See T1500.
2237 # Still some problems for cases where the ] is meant to be outside punctuation,
2238 # and no image is in sight. See T4095.
2239 if ( $text !== ''
2240 && substr( $m[3], 0, 1 ) === ']'
2241 && strpos( $text, '[' ) !== false
2242 ) {
2243 $text .= ']'; # so that replaceExternalLinks($text) works later
2244 $m[3] = substr( $m[3], 1 );
2245 }
2246 # fix up urlencoded title texts
2247 if ( strpos( $m[1], '%' ) !== false ) {
2248 # Should anchors '#' also be rejected?
2249 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2250 }
2251 $trail = $m[3];
2252 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2253 # Invalid, but might be an image with a link in its caption
2254 $might_be_img = true;
2255 $text = $m[2];
2256 if ( strpos( $m[1], '%' ) !== false ) {
2257 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2258 }
2259 $trail = "";
2260 } else { # Invalid form; output directly
2261 $s .= $prefix . '[[' . $line;
2262 continue;
2263 }
2264
2265 $origLink = ltrim( $m[1], ' ' );
2266
2267 # Don't allow internal links to pages containing
2268 # PROTO: where PROTO is a valid URL protocol; these
2269 # should be external links.
2270 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2271 $s .= $prefix . '[[' . $line;
2272 continue;
2273 }
2274
2275 # Make subpage if necessary
2276 if ( $useSubpages ) {
2277 $link = $this->maybeDoSubpageLink( $origLink, $text );
2278 } else {
2279 $link = $origLink;
2280 }
2281
2282 // \x7f isn't a default legal title char, so most likely strip
2283 // markers will force us into the "invalid form" path above. But,
2284 // just in case, let's assert that xmlish tags aren't valid in
2285 // the title position.
2286 $unstrip = $this->mStripState->killMarkers( $link );
2287 $noMarkers = ( $unstrip === $link );
2288
2289 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2290 if ( $nt === null ) {
2291 $s .= $prefix . '[[' . $line;
2292 continue;
2293 }
2294
2295 $ns = $nt->getNamespace();
2296 $iw = $nt->getInterwiki();
2297
2298 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2299
2300 if ( $might_be_img ) { # if this is actually an invalid link
2301 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2302 $found = false;
2303 while ( true ) {
2304 # look at the next 'line' to see if we can close it there
2305 $a->next();
2306 $next_line = $a->current();
2307 if ( $next_line === false || $next_line === null ) {
2308 break;
2309 }
2310 $m = explode( ']]', $next_line, 3 );
2311 if ( count( $m ) == 3 ) {
2312 # the first ]] closes the inner link, the second the image
2313 $found = true;
2314 $text .= "[[{$m[0]}]]{$m[1]}";
2315 $trail = $m[2];
2316 break;
2317 } elseif ( count( $m ) == 2 ) {
2318 # if there's exactly one ]] that's fine, we'll keep looking
2319 $text .= "[[{$m[0]}]]{$m[1]}";
2320 } else {
2321 # if $next_line is invalid too, we need look no further
2322 $text .= '[[' . $next_line;
2323 break;
2324 }
2325 }
2326 if ( !$found ) {
2327 # we couldn't find the end of this imageLink, so output it raw
2328 # but don't ignore what might be perfectly normal links in the text we've examined
2329 $holders->merge( $this->replaceInternalLinks2( $text ) );
2330 $s .= "{$prefix}[[$link|$text";
2331 # note: no $trail, because without an end, there *is* no trail
2332 continue;
2333 }
2334 } else { # it's not an image, so output it raw
2335 $s .= "{$prefix}[[$link|$text";
2336 # note: no $trail, because without an end, there *is* no trail
2337 continue;
2338 }
2339 }
2340
2341 $wasblank = ( $text == '' );
2342 if ( $wasblank ) {
2343 $text = $link;
2344 if ( !$noforce ) {
2345 # Strip off leading ':'
2346 $text = substr( $text, 1 );
2347 }
2348 } else {
2349 # T6598 madness. Handle the quotes only if they come from the alternate part
2350 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2351 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2352 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2353 $text = $this->doQuotes( $text );
2354 }
2355
2356 # Link not escaped by : , create the various objects
2357 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2358 # Interwikis
2359 if (
2360 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2361 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2362 in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2363 )
2364 ) {
2365 # T26502: filter duplicates
2366 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2367 $this->mLangLinkLanguages[$iw] = true;
2368 $this->mOutput->addLanguageLink( $nt->getFullText() );
2369 }
2370
2374 $s = rtrim( $s . $prefix ) . $trail; # T175416
2375 continue;
2376 }
2377
2378 if ( $ns == NS_FILE ) {
2379 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2380 if ( $wasblank ) {
2381 # if no parameters were passed, $text
2382 # becomes something like "File:Foo.png",
2383 # which we don't want to pass on to the
2384 # image generator
2385 $text = '';
2386 } else {
2387 # recursively parse links inside the image caption
2388 # actually, this will parse them in any other parameters, too,
2389 # but it might be hard to fix that, and it doesn't matter ATM
2390 $text = $this->replaceExternalLinks( $text );
2391 $holders->merge( $this->replaceInternalLinks2( $text ) );
2392 }
2393 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2394 $s .= $prefix . $this->armorLinks(
2395 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2396 continue;
2397 }
2398 } elseif ( $ns == NS_CATEGORY ) {
2402 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2403
2404 if ( $wasblank ) {
2405 $sortkey = $this->getDefaultSort();
2406 } else {
2407 $sortkey = $text;
2408 }
2409 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2410 $sortkey = str_replace( "\n", '', $sortkey );
2411 $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2412 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2413
2414 continue;
2415 }
2416 }
2417
2418 # Self-link checking. For some languages, variants of the title are checked in
2419 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2420 # for linking to a different variant.
2421 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2422 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2423 continue;
2424 }
2425
2426 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2427 # @todo FIXME: Should do batch file existence checks, see comment below
2428 if ( $ns == NS_MEDIA ) {
2429 # Give extensions a chance to select the file revision for us
2430 $options = [];
2431 $descQuery = false;
2432 Hooks::run( 'BeforeParserFetchFileAndTitle',
2433 [ $this, $nt, &$options, &$descQuery ] );
2434 # Fetch and register the file (file title may be different via hooks)
2435 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2436 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2437 $s .= $prefix . $this->armorLinks(
2438 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2439 continue;
2440 }
2441
2442 # Some titles, such as valid special pages or files in foreign repos, should
2443 # be shown as bluelinks even though they're not included in the page table
2444 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2445 # batch file existence checks for NS_FILE and NS_MEDIA
2446 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2447 $this->mOutput->addLink( $nt );
2448 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2449 } else {
2450 # Links will be added to the output link list after checking
2451 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2452 }
2453 }
2454 return $holders;
2455 }
2456
2470 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2471 list( $inside, $trail ) = Linker::splitTrail( $trail );
2472
2473 if ( $text == '' ) {
2474 $text = htmlspecialchars( $nt->getPrefixedText() );
2475 }
2476
2477 $link = $this->getLinkRenderer()->makeKnownLink(
2478 $nt, new HtmlArmor( "$prefix$text$inside" )
2479 );
2480
2481 return $this->armorLinks( $link ) . $trail;
2482 }
2483
2494 public function armorLinks( $text ) {
2495 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2496 self::MARKER_PREFIX . "NOPARSE$1", $text );
2497 }
2498
2503 public function areSubpagesAllowed() {
2504 # Some namespaces don't allow subpages
2505 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2506 }
2507
2516 public function maybeDoSubpageLink( $target, &$text ) {
2517 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2518 }
2519
2528 public function doBlockLevels( $text, $linestart ) {
2529 return BlockLevelPass::doBlockLevels( $text, $linestart );
2530 }
2531
2543 public function getVariableValue( $index, $frame = false ) {
2546
2547 if ( is_null( $this->mTitle ) ) {
2548 // If no title set, bad things are going to happen
2549 // later. Title should always be set since this
2550 // should only be called in the middle of a parse
2551 // operation (but the unit-tests do funky stuff)
2552 throw new MWException( __METHOD__ . ' Should only be '
2553 . ' called while parsing (no title set)' );
2554 }
2555
2556 // Avoid PHP 7.1 warning from passing $this by reference
2557 $parser = $this;
2558
2563 if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) {
2564 if ( isset( $this->mVarCache[$index] ) ) {
2565 return $this->mVarCache[$index];
2566 }
2567 }
2568
2569 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2570 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2571
2572 $pageLang = $this->getFunctionLang();
2573
2574 switch ( $index ) {
2575 case '!':
2576 $value = '|';
2577 break;
2578 case 'currentmonth':
2579 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2580 break;
2581 case 'currentmonth1':
2582 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2583 break;
2584 case 'currentmonthname':
2585 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2586 break;
2587 case 'currentmonthnamegen':
2588 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2589 break;
2590 case 'currentmonthabbrev':
2591 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2592 break;
2593 case 'currentday':
2594 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2595 break;
2596 case 'currentday2':
2597 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2598 break;
2599 case 'localmonth':
2600 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2601 break;
2602 case 'localmonth1':
2603 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2604 break;
2605 case 'localmonthname':
2606 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2607 break;
2608 case 'localmonthnamegen':
2609 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2610 break;
2611 case 'localmonthabbrev':
2612 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2613 break;
2614 case 'localday':
2615 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2616 break;
2617 case 'localday2':
2618 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2619 break;
2620 case 'pagename':
2621 $value = wfEscapeWikiText( $this->mTitle->getText() );
2622 break;
2623 case 'pagenamee':
2624 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2625 break;
2626 case 'fullpagename':
2627 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2628 break;
2629 case 'fullpagenamee':
2630 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2631 break;
2632 case 'subpagename':
2633 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2634 break;
2635 case 'subpagenamee':
2636 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2637 break;
2638 case 'rootpagename':
2639 $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2640 break;
2641 case 'rootpagenamee':
2642 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2643 ' ',
2644 '_',
2645 $this->mTitle->getRootText()
2646 ) ) );
2647 break;
2648 case 'basepagename':
2649 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2650 break;
2651 case 'basepagenamee':
2652 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2653 ' ',
2654 '_',
2655 $this->mTitle->getBaseText()
2656 ) ) );
2657 break;
2658 case 'talkpagename':
2659 if ( $this->mTitle->canHaveTalkPage() ) {
2660 $talkPage = $this->mTitle->getTalkPage();
2661 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2662 } else {
2663 $value = '';
2664 }
2665 break;
2666 case 'talkpagenamee':
2667 if ( $this->mTitle->canHaveTalkPage() ) {
2668 $talkPage = $this->mTitle->getTalkPage();
2669 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2670 } else {
2671 $value = '';
2672 }
2673 break;
2674 case 'subjectpagename':
2675 $subjPage = $this->mTitle->getSubjectPage();
2676 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2677 break;
2678 case 'subjectpagenamee':
2679 $subjPage = $this->mTitle->getSubjectPage();
2680 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2681 break;
2682 case 'pageid': // requested in T25427
2683 $pageid = $this->getTitle()->getArticleID();
2684 if ( $pageid == 0 ) {
2685 # 0 means the page doesn't exist in the database,
2686 # which means the user is previewing a new page.
2687 # The vary-revision flag must be set, because the magic word
2688 # will have a different value once the page is saved.
2689 $this->mOutput->setFlag( 'vary-revision' );
2690 wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2691 }
2692 $value = $pageid ?: null;
2693 break;
2694 case 'revisionid':
2695 # Let the edit saving system know we should parse the page
2696 # *after* a revision ID has been assigned.
2697 $this->mOutput->setFlag( 'vary-revision-id' );
2698 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2699 $value = $this->mRevisionId;
2700
2701 if ( !$value ) {
2702 $rev = $this->getRevisionObject();
2703 if ( $rev ) {
2704 $value = $rev->getId();
2705 }
2706 }
2707
2708 if ( !$value ) {
2709 $value = $this->mOptions->getSpeculativeRevId();
2710 if ( $value ) {
2711 $this->mOutput->setSpeculativeRevIdUsed( $value );
2712 }
2713 }
2714 break;
2715 case 'revisionday':
2716 $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2717 break;
2718 case 'revisionday2':
2719 $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2720 break;
2721 case 'revisionmonth':
2722 $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2723 break;
2724 case 'revisionmonth1':
2725 $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2726 break;
2727 case 'revisionyear':
2728 $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
2729 break;
2730 case 'revisiontimestamp':
2731 # Let the edit saving system know we should parse the page
2732 # *after* a revision ID has been assigned. This is for null edits.
2733 $this->mOutput->setFlag( 'vary-revision' );
2734 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2735 $value = $this->getRevisionTimestamp();
2736 break;
2737 case 'revisionuser':
2738 # Let the edit saving system know we should parse the page
2739 # *after* a revision ID has been assigned for null edits.
2740 $this->mOutput->setFlag( 'vary-user' );
2741 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2742 $value = $this->getRevisionUser();
2743 break;
2744 case 'revisionsize':
2745 $value = $this->getRevisionSize();
2746 break;
2747 case 'namespace':
2748 $value = str_replace( '_', ' ',
2749 $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
2750 break;
2751 case 'namespacee':
2752 $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
2753 break;
2754 case 'namespacenumber':
2755 $value = $this->mTitle->getNamespace();
2756 break;
2757 case 'talkspace':
2758 $value = $this->mTitle->canHaveTalkPage()
2759 ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2760 : '';
2761 break;
2762 case 'talkspacee':
2763 $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2764 break;
2765 case 'subjectspace':
2766 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2767 break;
2768 case 'subjectspacee':
2769 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2770 break;
2771 case 'currentdayname':
2772 $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2773 break;
2774 case 'currentyear':
2775 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2776 break;
2777 case 'currenttime':
2778 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2779 break;
2780 case 'currenthour':
2781 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2782 break;
2783 case 'currentweek':
2784 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2785 # int to remove the padding
2786 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2787 break;
2788 case 'currentdow':
2789 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2790 break;
2791 case 'localdayname':
2792 $value = $pageLang->getWeekdayName(
2793 (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2794 );
2795 break;
2796 case 'localyear':
2797 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2798 break;
2799 case 'localtime':
2800 $value = $pageLang->time(
2801 MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2802 false,
2803 false
2804 );
2805 break;
2806 case 'localhour':
2807 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2808 break;
2809 case 'localweek':
2810 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2811 # int to remove the padding
2812 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2813 break;
2814 case 'localdow':
2815 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2816 break;
2817 case 'numberofarticles':
2818 $value = $pageLang->formatNum( SiteStats::articles() );
2819 break;
2820 case 'numberoffiles':
2821 $value = $pageLang->formatNum( SiteStats::images() );
2822 break;
2823 case 'numberofusers':
2824 $value = $pageLang->formatNum( SiteStats::users() );
2825 break;
2826 case 'numberofactiveusers':
2827 $value = $pageLang->formatNum( SiteStats::activeUsers() );
2828 break;
2829 case 'numberofpages':
2830 $value = $pageLang->formatNum( SiteStats::pages() );
2831 break;
2832 case 'numberofadmins':
2833 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2834 break;
2835 case 'numberofedits':
2836 $value = $pageLang->formatNum( SiteStats::edits() );
2837 break;
2838 case 'currenttimestamp':
2839 $value = wfTimestamp( TS_MW, $ts );
2840 break;
2841 case 'localtimestamp':
2842 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2843 break;
2844 case 'currentversion':
2846 break;
2847 case 'articlepath':
2848 return $wgArticlePath;
2849 case 'sitename':
2850 return $wgSitename;
2851 case 'server':
2852 return $wgServer;
2853 case 'servername':
2854 return $wgServerName;
2855 case 'scriptpath':
2856 return $wgScriptPath;
2857 case 'stylepath':
2858 return $wgStylePath;
2859 case 'directionmark':
2860 return $pageLang->getDirMark();
2861 case 'contentlanguage':
2862 global $wgLanguageCode;
2863 return $wgLanguageCode;
2864 case 'pagelanguage':
2865 $value = $pageLang->getCode();
2866 break;
2867 case 'cascadingsources':
2869 break;
2870 default:
2871 $ret = null;
2872 Hooks::run(
2873 'ParserGetVariableValueSwitch',
2874 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2875 );
2876
2877 return $ret;
2878 }
2879
2880 if ( $index ) {
2881 $this->mVarCache[$index] = $value;
2882 }
2883
2884 return $value;
2885 }
2886
2894 private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
2895 # Get the timezone-adjusted timestamp to be used for this revision
2896 $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
2897 # Possibly set vary-revision if there is not yet an associated revision
2898 if ( !$this->getRevisionObject() ) {
2899 # Get the timezone-adjusted timestamp $mtts seconds in the future
2900 $resThen = substr(
2901 $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
2902 $start,
2903 $len
2904 );
2905
2906 if ( $resNow !== $resThen ) {
2907 # Let the edit saving system know we should parse the page
2908 # *after* a revision ID has been assigned. This is for null edits.
2909 $this->mOutput->setFlag( 'vary-revision' );
2910 wfDebug( __METHOD__ . ": $variable used, setting vary-revision...\n" );
2911 }
2912 }
2913
2914 return $resNow;
2915 }
2916
2922 public function initialiseVariables() {
2923 $variableIDs = $this->magicWordFactory->getVariableIDs();
2924 $substIDs = $this->magicWordFactory->getSubstIDs();
2925
2926 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2927 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2928 }
2929
2952 public function preprocessToDom( $text, $flags = 0 ) {
2953 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2954 return $dom;
2955 }
2956
2964 public static function splitWhitespace( $s ) {
2965 $ltrimmed = ltrim( $s );
2966 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2967 $trimmed = rtrim( $ltrimmed );
2968 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
2969 if ( $diff > 0 ) {
2970 $w2 = substr( $ltrimmed, -$diff );
2971 } else {
2972 $w2 = '';
2973 }
2974 return [ $w1, $trimmed, $w2 ];
2975 }
2976
2997 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2998 # Is there any text? Also, Prevent too big inclusions!
2999 $textSize = strlen( $text );
3000 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3001 return $text;
3002 }
3003
3004 if ( $frame === false ) {
3005 $frame = $this->getPreprocessor()->newFrame();
3006 } elseif ( !( $frame instanceof PPFrame ) ) {
3007 wfDebug( __METHOD__ . " called using plain parameters instead of "
3008 . "a PPFrame instance. Creating custom frame.\n" );
3009 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3010 }
3011
3012 $dom = $this->preprocessToDom( $text );
3013 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3014 $text = $frame->expand( $dom, $flags );
3015
3016 return $text;
3017 }
3018
3026 public static function createAssocArgs( $args ) {
3027 $assocArgs = [];
3028 $index = 1;
3029 foreach ( $args as $arg ) {
3030 $eqpos = strpos( $arg, '=' );
3031 if ( $eqpos === false ) {
3032 $assocArgs[$index++] = $arg;
3033 } else {
3034 $name = trim( substr( $arg, 0, $eqpos ) );
3035 $value = trim( substr( $arg, $eqpos + 1 ) );
3036 if ( $value === false ) {
3037 $value = '';
3038 }
3039 if ( $name !== false ) {
3040 $assocArgs[$name] = $value;
3041 }
3042 }
3043 }
3044
3045 return $assocArgs;
3046 }
3047
3074 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3075 # does no harm if $current and $max are present but are unnecessary for the message
3076 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3077 # only during preview, and that would split the parser cache unnecessarily.
3078 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3079 ->text();
3080 $this->mOutput->addWarning( $warning );
3081 $this->addTrackingCategory( "$limitationType-category" );
3082 }
3083
3096 public function braceSubstitution( $piece, $frame ) {
3097 // Flags
3098
3099 // $text has been filled
3100 $found = false;
3101 // wiki markup in $text should be escaped
3102 $nowiki = false;
3103 // $text is HTML, armour it against wikitext transformation
3104 $isHTML = false;
3105 // Force interwiki transclusion to be done in raw mode not rendered
3106 $forceRawInterwiki = false;
3107 // $text is a DOM node needing expansion in a child frame
3108 $isChildObj = false;
3109 // $text is a DOM node needing expansion in the current frame
3110 $isLocalObj = false;
3111
3112 # Title object, where $text came from
3113 $title = false;
3114
3115 # $part1 is the bit before the first |, and must contain only title characters.
3116 # Various prefixes will be stripped from it later.
3117 $titleWithSpaces = $frame->expand( $piece['title'] );
3118 $part1 = trim( $titleWithSpaces );
3119 $titleText = false;
3120
3121 # Original title text preserved for various purposes
3122 $originalTitle = $part1;
3123
3124 # $args is a list of argument nodes, starting from index 0, not including $part1
3125 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3126 # below won't work b/c this $args isn't an object
3127 $args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3128
3129 $profileSection = null; // profile templates
3130
3131 # SUBST
3132 if ( !$found ) {
3133 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3134
3135 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3136 # Decide whether to expand template or keep wikitext as-is.
3137 if ( $this->ot['wiki'] ) {
3138 if ( $substMatch === false ) {
3139 $literal = true; # literal when in PST with no prefix
3140 } else {
3141 $literal = false; # expand when in PST with subst: or safesubst:
3142 }
3143 } else {
3144 if ( $substMatch == 'subst' ) {
3145 $literal = true; # literal when not in PST with plain subst:
3146 } else {
3147 $literal = false; # expand when not in PST with safesubst: or no prefix
3148 }
3149 }
3150 if ( $literal ) {
3151 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3152 $isLocalObj = true;
3153 $found = true;
3154 }
3155 }
3156
3157 # Variables
3158 if ( !$found && $args->getLength() == 0 ) {
3159 $id = $this->mVariables->matchStartToEnd( $part1 );
3160 if ( $id !== false ) {
3161 $text = $this->getVariableValue( $id, $frame );
3162 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3163 $this->mOutput->updateCacheExpiry(
3164 $this->magicWordFactory->getCacheTTL( $id ) );
3165 }
3166 $found = true;
3167 }
3168 }
3169
3170 # MSG, MSGNW and RAW
3171 if ( !$found ) {
3172 # Check for MSGNW:
3173 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3174 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3175 $nowiki = true;
3176 } else {
3177 # Remove obsolete MSG:
3178 $mwMsg = $this->magicWordFactory->get( 'msg' );
3179 $mwMsg->matchStartAndRemove( $part1 );
3180 }
3181
3182 # Check for RAW:
3183 $mwRaw = $this->magicWordFactory->get( 'raw' );
3184 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3185 $forceRawInterwiki = true;
3186 }
3187 }
3188
3189 # Parser functions
3190 if ( !$found ) {
3191 $colonPos = strpos( $part1, ':' );
3192 if ( $colonPos !== false ) {
3193 $func = substr( $part1, 0, $colonPos );
3194 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3195 $argsLength = $args->getLength();
3196 for ( $i = 0; $i < $argsLength; $i++ ) {
3197 $funcArgs[] = $args->item( $i );
3198 }
3199
3200 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3201
3202 // Extract any forwarded flags
3203 if ( isset( $result['title'] ) ) {
3204 $title = $result['title'];
3205 }
3206 if ( isset( $result['found'] ) ) {
3207 $found = $result['found'];
3208 }
3209 if ( array_key_exists( 'text', $result ) ) {
3210 // a string or null
3211 $text = $result['text'];
3212 }
3213 if ( isset( $result['nowiki'] ) ) {
3214 $nowiki = $result['nowiki'];
3215 }
3216 if ( isset( $result['isHTML'] ) ) {
3217 $isHTML = $result['isHTML'];
3218 }
3219 if ( isset( $result['forceRawInterwiki'] ) ) {
3220 $forceRawInterwiki = $result['forceRawInterwiki'];
3221 }
3222 if ( isset( $result['isChildObj'] ) ) {
3223 $isChildObj = $result['isChildObj'];
3224 }
3225 if ( isset( $result['isLocalObj'] ) ) {
3226 $isLocalObj = $result['isLocalObj'];
3227 }
3228 }
3229 }
3230
3231 # Finish mangling title and then check for loops.
3232 # Set $title to a Title object and $titleText to the PDBK
3233 if ( !$found ) {
3234 $ns = NS_TEMPLATE;
3235 # Split the title into page and subpage
3236 $subpage = '';
3237 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3238 if ( $part1 !== $relative ) {
3239 $part1 = $relative;
3240 $ns = $this->mTitle->getNamespace();
3241 }
3242 $title = Title::newFromText( $part1, $ns );
3243 if ( $title ) {
3244 $titleText = $title->getPrefixedText();
3245 # Check for language variants if the template is not found
3246 if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3247 $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
3248 }
3249 # Do recursion depth check
3250 $limit = $this->mOptions->getMaxTemplateDepth();
3251 if ( $frame->depth >= $limit ) {
3252 $found = true;
3253 $text = '<span class="error">'
3254 . wfMessage( 'parser-template-recursion-depth-warning' )
3255 ->numParams( $limit )->inContentLanguage()->text()
3256 . '</span>';
3257 }
3258 }
3259 }
3260
3261 # Load from database
3262 if ( !$found && $title ) {
3263 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3264 if ( !$title->isExternal() ) {
3265 if ( $title->isSpecialPage()
3266 && $this->mOptions->getAllowSpecialInclusion()
3267 && $this->ot['html']
3268 ) {
3269 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3270 // Pass the template arguments as URL parameters.
3271 // "uselang" will have no effect since the Language object
3272 // is forced to the one defined in ParserOptions.
3273 $pageArgs = [];
3274 $argsLength = $args->getLength();
3275 for ( $i = 0; $i < $argsLength; $i++ ) {
3276 $bits = $args->item( $i )->splitArg();
3277 if ( strval( $bits['index'] ) === '' ) {
3278 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3279 $value = trim( $frame->expand( $bits['value'] ) );
3280 $pageArgs[$name] = $value;
3281 }
3282 }
3283
3284 // Create a new context to execute the special page
3286 $context->setTitle( $title );
3287 $context->setRequest( new FauxRequest( $pageArgs ) );
3288 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3289 $context->setUser( $this->getUser() );
3290 } else {
3291 // If this page is cached, then we better not be per user.
3292 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3293 }
3294 $context->setLanguage( $this->mOptions->getUserLangObj() );
3295 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3296 if ( $ret ) {
3297 $text = $context->getOutput()->getHTML();
3298 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3299 $found = true;
3300 $isHTML = true;
3301 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3302 $this->mOutput->updateRuntimeAdaptiveExpiry(
3303 $specialPage->maxIncludeCacheTime()
3304 );
3305 }
3306 }
3307 } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3308 $found = false; # access denied
3309 wfDebug( __METHOD__ . ": template inclusion denied for " .
3310 $title->getPrefixedDBkey() . "\n" );
3311 } else {
3312 list( $text, $title ) = $this->getTemplateDom( $title );
3313 if ( $text !== false ) {
3314 $found = true;
3315 $isChildObj = true;
3316 }
3317 }
3318
3319 # If the title is valid but undisplayable, make a link to it
3320 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3321 $text = "[[:$titleText]]";
3322 $found = true;
3323 }
3324 } elseif ( $title->isTrans() ) {
3325 # Interwiki transclusion
3326 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3327 $text = $this->interwikiTransclude( $title, 'render' );
3328 $isHTML = true;
3329 } else {
3330 $text = $this->interwikiTransclude( $title, 'raw' );
3331 # Preprocess it like a template
3332 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3333 $isChildObj = true;
3334 }
3335 $found = true;
3336 }
3337
3338 # Do infinite loop check
3339 # This has to be done after redirect resolution to avoid infinite loops via redirects
3340 if ( !$frame->loopCheck( $title ) ) {
3341 $found = true;
3342 $text = '<span class="error">'
3343 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3344 . '</span>';
3345 $this->addTrackingCategory( 'template-loop-category' );
3346 $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3347 wfEscapeWikiText( $titleText ) )->text() );
3348 wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3349 }
3350 }
3351
3352 # If we haven't found text to substitute by now, we're done
3353 # Recover the source wikitext and return it
3354 if ( !$found ) {
3355 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3356 if ( $profileSection ) {
3357 $this->mProfiler->scopedProfileOut( $profileSection );
3358 }
3359 return [ 'object' => $text ];
3360 }
3361
3362 # Expand DOM-style return values in a child frame
3363 if ( $isChildObj ) {
3364 # Clean up argument array
3365 $newFrame = $frame->newChild( $args, $title );
3366
3367 if ( $nowiki ) {
3368 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3369 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3370 # Expansion is eligible for the empty-frame cache
3371 $text = $newFrame->cachedExpand( $titleText, $text );
3372 } else {
3373 # Uncached expansion
3374 $text = $newFrame->expand( $text );
3375 }
3376 }
3377 if ( $isLocalObj && $nowiki ) {
3378 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3379 $isLocalObj = false;
3380 }
3381
3382 if ( $profileSection ) {
3383 $this->mProfiler->scopedProfileOut( $profileSection );
3384 }
3385
3386 # Replace raw HTML by a placeholder
3387 if ( $isHTML ) {
3388 $text = $this->insertStripItem( $text );
3389 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3390 # Escape nowiki-style return values
3391 $text = wfEscapeWikiText( $text );
3392 } elseif ( is_string( $text )
3393 && !$piece['lineStart']
3394 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3395 ) {
3396 # T2529: if the template begins with a table or block-level
3397 # element, it should be treated as beginning a new line.
3398 # This behavior is somewhat controversial.
3399 $text = "\n" . $text;
3400 }
3401
3402 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3403 # Error, oversize inclusion
3404 if ( $titleText !== false ) {
3405 # Make a working, properly escaped link if possible (T25588)
3406 $text = "[[:$titleText]]";
3407 } else {
3408 # This will probably not be a working link, but at least it may
3409 # provide some hint of where the problem is
3410 preg_replace( '/^:/', '', $originalTitle );
3411 $text = "[[:$originalTitle]]";
3412 }
3413 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3414 . 'post-expand include size too large -->' );
3415 $this->limitationWarn( 'post-expand-template-inclusion' );
3416 }
3417
3418 if ( $isLocalObj ) {
3419 $ret = [ 'object' => $text ];
3420 } else {
3421 $ret = [ 'text' => $text ];
3422 }
3423
3424 return $ret;
3425 }
3426
3446 public function callParserFunction( $frame, $function, array $args = [] ) {
3447 # Case sensitive functions
3448 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3449 $function = $this->mFunctionSynonyms[1][$function];
3450 } else {
3451 # Case insensitive functions
3452 $function = $this->contLang->lc( $function );
3453 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3454 $function = $this->mFunctionSynonyms[0][$function];
3455 } else {
3456 return [ 'found' => false ];
3457 }
3458 }
3459
3460 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3461
3462 // Avoid PHP 7.1 warning from passing $this by reference
3463 $parser = $this;
3464
3465 $allArgs = [ &$parser ];
3466 if ( $flags & self::SFH_OBJECT_ARGS ) {
3467 # Convert arguments to PPNodes and collect for appending to $allArgs
3468 $funcArgs = [];
3469 foreach ( $args as $k => $v ) {
3470 if ( $v instanceof PPNode || $k === 0 ) {
3471 $funcArgs[] = $v;
3472 } else {
3473 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3474 }
3475 }
3476
3477 # Add a frame parameter, and pass the arguments as an array
3478 $allArgs[] = $frame;
3479 $allArgs[] = $funcArgs;
3480 } else {
3481 # Convert arguments to plain text and append to $allArgs
3482 foreach ( $args as $k => $v ) {
3483 if ( $v instanceof PPNode ) {
3484 $allArgs[] = trim( $frame->expand( $v ) );
3485 } elseif ( is_int( $k ) && $k >= 0 ) {
3486 $allArgs[] = trim( $v );
3487 } else {
3488 $allArgs[] = trim( "$k=$v" );
3489 }
3490 }
3491 }
3492
3493 $result = $callback( ...$allArgs );
3494
3495 # The interface for function hooks allows them to return a wikitext
3496 # string or an array containing the string and any flags. This mungs
3497 # things around to match what this method should return.
3498 if ( !is_array( $result ) ) {
3499 $result = [
3500 'found' => true,
3501 'text' => $result,
3502 ];
3503 } else {
3504 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3505 $result['text'] = $result[0];
3506 }
3507 unset( $result[0] );
3508 $result += [
3509 'found' => true,
3510 ];
3511 }
3512
3513 $noparse = true;
3514 $preprocessFlags = 0;
3515 if ( isset( $result['noparse'] ) ) {
3516 $noparse = $result['noparse'];
3517 }
3518 if ( isset( $result['preprocessFlags'] ) ) {
3519 $preprocessFlags = $result['preprocessFlags'];
3520 }
3521
3522 if ( !$noparse ) {
3523 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3524 $result['isChildObj'] = true;
3525 }
3526
3527 return $result;
3528 }
3529
3538 public function getTemplateDom( $title ) {
3539 $cacheTitle = $title;
3540 $titleText = $title->getPrefixedDBkey();
3541
3542 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3543 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3544 $title = Title::makeTitle( $ns, $dbk );
3545 $titleText = $title->getPrefixedDBkey();
3546 }
3547 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3548 return [ $this->mTplDomCache[$titleText], $title ];
3549 }
3550
3551 # Cache miss, go to the database
3552 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3553
3554 if ( $text === false ) {
3555 $this->mTplDomCache[$titleText] = false;
3556 return [ false, $title ];
3557 }
3558
3559 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3560 $this->mTplDomCache[$titleText] = $dom;
3561
3562 if ( !$title->equals( $cacheTitle ) ) {
3563 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3564 [ $title->getNamespace(), $title->getDBkey() ];
3565 }
3566
3567 return [ $dom, $title ];
3568 }
3569
3581 public function fetchCurrentRevisionOfTitle( $title ) {
3582 $cacheKey = $title->getPrefixedDBkey();
3583 if ( !$this->currentRevisionCache ) {
3584 $this->currentRevisionCache = new MapCacheLRU( 100 );
3585 }
3586 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3587 $this->currentRevisionCache->set( $cacheKey,
3588 // Defaults to Parser::statelessFetchRevision()
3589 call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3590 );
3591 }
3592 return $this->currentRevisionCache->get( $cacheKey );
3593 }
3594
3604 public static function statelessFetchRevision( Title $title, $parser = false ) {
3606
3607 return $rev;
3608 }
3609
3615 public function fetchTemplateAndTitle( $title ) {
3616 // Defaults to Parser::statelessFetchTemplate()
3617 $templateCb = $this->mOptions->getTemplateCallback();
3618 $stuff = call_user_func( $templateCb, $title, $this );
3619 // We use U+007F DELETE to distinguish strip markers from regular text.
3620 $text = $stuff['text'];
3621 if ( is_string( $stuff['text'] ) ) {
3622 $text = strtr( $text, "\x7f", "?" );
3623 }
3624 $finalTitle = $stuff['finalTitle'] ?? $title;
3625 if ( isset( $stuff['deps'] ) ) {
3626 foreach ( $stuff['deps'] as $dep ) {
3627 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3628 if ( $dep['title']->equals( $this->getTitle() ) ) {
3629 // If we transclude ourselves, the final result
3630 // will change based on the new version of the page
3631 $this->mOutput->setFlag( 'vary-revision' );
3632 }
3633 }
3634 }
3635 return [ $text, $finalTitle ];
3636 }
3637
3643 public function fetchTemplate( $title ) {
3644 return $this->fetchTemplateAndTitle( $title )[0];
3645 }
3646
3656 public static function statelessFetchTemplate( $title, $parser = false ) {
3657 $text = $skip = false;
3658 $finalTitle = $title;
3659 $deps = [];
3660
3661 # Loop to fetch the article, with up to 1 redirect
3662 // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
3663 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3664 # Give extensions a chance to select the revision instead
3665 $id = false; # Assume current
3666 Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3667 [ $parser, $title, &$skip, &$id ] );
3668
3669 if ( $skip ) {
3670 $text = false;
3671 $deps[] = [
3672 'title' => $title,
3673 'page_id' => $title->getArticleID(),
3674 'rev_id' => null
3675 ];
3676 break;
3677 }
3678 # Get the revision
3679 if ( $id ) {
3680 $rev = Revision::newFromId( $id );
3681 } elseif ( $parser ) {
3682 $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3683 } else {
3684 $rev = Revision::newFromTitle( $title );
3685 }
3686 $rev_id = $rev ? $rev->getId() : 0;
3687 # If there is no current revision, there is no page
3688 if ( $id === false && !$rev ) {
3689 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3690 $linkCache->addBadLinkObj( $title );
3691 }
3692
3693 $deps[] = [
3694 'title' => $title,
3695 'page_id' => $title->getArticleID(),
3696 'rev_id' => $rev_id ];
3697 if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3698 # We fetched a rev from a different title; register it too...
3699 $deps[] = [
3700 'title' => $rev->getTitle(),
3701 'page_id' => $rev->getPage(),
3702 'rev_id' => $rev_id ];
3703 }
3704
3705 if ( $rev ) {
3706 $content = $rev->getContent();
3707 $text = $content ? $content->getWikitextForTransclusion() : null;
3708
3709 Hooks::run( 'ParserFetchTemplate',
3710 [ $parser, $title, $rev, &$text, &$deps ] );
3711
3712 if ( $text === false || $text === null ) {
3713 $text = false;
3714 break;
3715 }
3716 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3717 $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3718 lcfirst( $title->getText() ) )->inContentLanguage();
3719 if ( !$message->exists() ) {
3720 $text = false;
3721 break;
3722 }
3723 $content = $message->content();
3724 $text = $message->plain();
3725 } else {
3726 break;
3727 }
3728 if ( !$content ) {
3729 break;
3730 }
3731 # Redirect?
3732 $finalTitle = $title;
3733 $title = $content->getRedirectTarget();
3734 }
3735 return [
3736 'text' => $text,
3737 'finalTitle' => $finalTitle,
3738 'deps' => $deps ];
3739 }
3740
3749 public function fetchFile( $title, $options = [] ) {
3750 wfDeprecated( __METHOD__, '1.32' );
3751 return $this->fetchFileAndTitle( $title, $options )[0];
3752 }
3753
3761 public function fetchFileAndTitle( $title, $options = [] ) {
3762 $file = $this->fetchFileNoRegister( $title, $options );
3763
3764 $time = $file ? $file->getTimestamp() : false;
3765 $sha1 = $file ? $file->getSha1() : false;
3766 # Register the file as a dependency...
3767 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3768 if ( $file && !$title->equals( $file->getTitle() ) ) {
3769 # Update fetched file title
3770 $title = $file->getTitle();
3771 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3772 }
3773 return [ $file, $title ];
3774 }
3775
3786 protected function fetchFileNoRegister( $title, $options = [] ) {
3787 if ( isset( $options['broken'] ) ) {
3788 $file = false; // broken thumbnail forced by hook
3789 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3790 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3791 } else { // get by (name,timestamp)
3792 $file = wfFindFile( $title, $options );
3793 }
3794 return $file;
3795 }
3796
3805 public function interwikiTransclude( $title, $action ) {
3807
3809 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3810 }
3811
3812 $url = $title->getFullURL( [ 'action' => $action ] );
3813 if ( strlen( $url ) > 1024 ) {
3814 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3815 }
3816
3817 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3818
3819 $fname = __METHOD__;
3820 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3821
3822 $data = $cache->getWithSetCallback(
3823 $cache->makeGlobalKey(
3824 'interwiki-transclude',
3825 ( $wikiId !== false ) ? $wikiId : 'external',
3826 sha1( $url )
3827 ),
3829 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3830 $req = MWHttpRequest::factory( $url, [], $fname );
3831
3832 $status = $req->execute(); // Status object
3833 if ( !$status->isOK() ) {
3834 $ttl = $cache::TTL_UNCACHEABLE;
3835 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3836 $ttl = min( $cache::TTL_LAGGED, $ttl );
3837 }
3838
3839 return [
3840 'text' => $status->isOK() ? $req->getContent() : null,
3841 'code' => $req->getStatus()
3842 ];
3843 },
3844 [
3845 'checkKeys' => ( $wikiId !== false )
3846 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3847 : [],
3848 'pcGroup' => 'interwiki-transclude:5',
3849 'pcTTL' => $cache::TTL_PROC_LONG
3850 ]
3851 );
3852
3853 if ( is_string( $data['text'] ) ) {
3854 $text = $data['text'];
3855 } elseif ( $data['code'] != 200 ) {
3856 // Though we failed to fetch the content, this status is useless.
3857 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3858 ->params( $url, $data['code'] )->inContentLanguage()->text();
3859 } else {
3860 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3861 }
3862
3863 return $text;
3864 }
3865
3875 public function argSubstitution( $piece, $frame ) {
3876 $error = false;
3877 $parts = $piece['parts'];
3878 $nameWithSpaces = $frame->expand( $piece['title'] );
3879 $argName = trim( $nameWithSpaces );
3880 $object = false;
3881 $text = $frame->getArgument( $argName );
3882 if ( $text === false && $parts->getLength() > 0
3883 && ( $this->ot['html']
3884 || $this->ot['pre']
3885 || ( $this->ot['wiki'] && $frame->isTemplate() )
3886 )
3887 ) {
3888 # No match in frame, use the supplied default
3889 $object = $parts->item( 0 )->getChildren();
3890 }
3891 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3892 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3893 $this->limitationWarn( 'post-expand-template-argument' );
3894 }
3895
3896 if ( $text === false && $object === false ) {
3897 # No match anywhere
3898 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3899 }
3900 if ( $error !== false ) {
3901 $text .= $error;
3902 }
3903 if ( $object !== false ) {
3904 $ret = [ 'object' => $object ];
3905 } else {
3906 $ret = [ 'text' => $text ];
3907 }
3908
3909 return $ret;
3910 }
3911
3927 public function extensionSubstitution( $params, $frame ) {
3928 static $errorStr = '<span class="error">';
3929 static $errorLen = 20;
3930
3931 $name = $frame->expand( $params['name'] );
3932 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3933 // Probably expansion depth or node count exceeded. Just punt the
3934 // error up.
3935 return $name;
3936 }
3937
3938 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3939 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3940 // See above
3941 return $attrText;
3942 }
3943
3944 // We can't safely check if the expansion for $content resulted in an
3945 // error, because the content could happen to be the error string
3946 // (T149622).
3947 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3948
3949 $marker = self::MARKER_PREFIX . "-$name-"
3950 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3951
3952 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3953 ( $this->ot['html'] || $this->ot['pre'] );
3954 if ( $isFunctionTag ) {
3955 $markerType = 'none';
3956 } else {
3957 $markerType = 'general';
3958 }
3959 if ( $this->ot['html'] || $isFunctionTag ) {
3960 $name = strtolower( $name );
3961 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3962 if ( isset( $params['attributes'] ) ) {
3963 $attributes = $attributes + $params['attributes'];
3964 }
3965
3966 if ( isset( $this->mTagHooks[$name] ) ) {
3967 $output = call_user_func_array( $this->mTagHooks[$name],
3968 [ $content, $attributes, $this, $frame ] );
3969 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3970 list( $callback, ) = $this->mFunctionTagHooks[$name];
3971
3972 // Avoid PHP 7.1 warning from passing $this by reference
3973 $parser = $this;
3974 $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
3975 } else {
3976 $output = '<span class="error">Invalid tag extension name: ' .
3977 htmlspecialchars( $name ) . '</span>';
3978 }
3979
3980 if ( is_array( $output ) ) {
3981 // Extract flags
3982 $flags = $output;
3983 $output = $flags[0];
3984 if ( isset( $flags['markerType'] ) ) {
3985 $markerType = $flags['markerType'];
3986 }
3987 }
3988 } else {
3989 if ( is_null( $attrText ) ) {
3990 $attrText = '';
3991 }
3992 if ( isset( $params['attributes'] ) ) {
3993 foreach ( $params['attributes'] as $attrName => $attrValue ) {
3994 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3995 htmlspecialchars( $attrValue ) . '"';
3996 }
3997 }
3998 if ( $content === null ) {
3999 $output = "<$name$attrText/>";
4000 } else {
4001 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4002 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4003 // See above
4004 return $close;
4005 }
4006 $output = "<$name$attrText>$content$close";
4007 }
4008 }
4009
4010 if ( $markerType === 'none' ) {
4011 return $output;
4012 } elseif ( $markerType === 'nowiki' ) {
4013 $this->mStripState->addNoWiki( $marker, $output );
4014 } elseif ( $markerType === 'general' ) {
4015 $this->mStripState->addGeneral( $marker, $output );
4016 } else {
4017 throw new MWException( __METHOD__ . ': invalid marker type' );
4018 }
4019 return $marker;
4020 }
4021
4029 public function incrementIncludeSize( $type, $size ) {
4030 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4031 return false;
4032 } else {
4033 $this->mIncludeSizes[$type] += $size;
4034 return true;
4035 }
4036 }
4037
4044 $this->mExpensiveFunctionCount++;
4045 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4046 }
4047
4056 public function doDoubleUnderscore( $text ) {
4057 # The position of __TOC__ needs to be recorded
4058 $mw = $this->magicWordFactory->get( 'toc' );
4059 if ( $mw->match( $text ) ) {
4060 $this->mShowToc = true;
4061 $this->mForceTocPosition = true;
4062
4063 # Set a placeholder. At the end we'll fill it in with the TOC.
4064 $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4065
4066 # Only keep the first one.
4067 $text = $mw->replace( '', $text );
4068 }
4069
4070 # Now match and remove the rest of them
4071 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4072 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4073
4074 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4075 $this->mOutput->mNoGallery = true;
4076 }
4077 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4078 $this->mShowToc = false;
4079 }
4080 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4081 && $this->mTitle->getNamespace() == NS_CATEGORY
4082 ) {
4083 $this->addTrackingCategory( 'hidden-category-category' );
4084 }
4085 # (T10068) Allow control over whether robots index a page.
4086 # __INDEX__ always overrides __NOINDEX__, see T16899
4087 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4088 $this->mOutput->setIndexPolicy( 'noindex' );
4089 $this->addTrackingCategory( 'noindex-category' );
4090 }
4091 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4092 $this->mOutput->setIndexPolicy( 'index' );
4093 $this->addTrackingCategory( 'index-category' );
4094 }
4095
4096 # Cache all double underscores in the database
4097 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4098 $this->mOutput->setProperty( $key, '' );
4099 }
4100
4101 return $text;
4102 }
4103
4109 public function addTrackingCategory( $msg ) {
4110 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4111 }
4112
4129 public function formatHeadings( $text, $origText, $isMain = true ) {
4130 global $wgMaxTocLevel;
4131
4132 # Inhibit editsection links if requested in the page
4133 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4134 $maybeShowEditLink = false;
4135 } else {
4136 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4137 }
4138
4139 # Get all headlines for numbering them and adding funky stuff like [edit]
4140 # links - this is for later, but we need the number of headlines right now
4141 # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
4142 # be trimmed here since whitespace in HTML headings is significant.
4143 $matches = [];
4144 $numMatches = preg_match_all(
4145 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4146 $text,
4147 $matches
4148 );
4149
4150 # if there are fewer than 4 headlines in the article, do not show TOC
4151 # unless it's been explicitly enabled.
4152 $enoughToc = $this->mShowToc &&
4153 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4154
4155 # Allow user to stipulate that a page should have a "new section"
4156 # link added via __NEWSECTIONLINK__
4157 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4158 $this->mOutput->setNewSection( true );
4159 }
4160
4161 # Allow user to remove the "new section"
4162 # link via __NONEWSECTIONLINK__
4163 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4164 $this->mOutput->hideNewSection( true );
4165 }
4166
4167 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4168 # override above conditions and always show TOC above first header
4169 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4170 $this->mShowToc = true;
4171 $enoughToc = true;
4172 }
4173
4174 # headline counter
4175 $headlineCount = 0;
4176 $numVisible = 0;
4177
4178 # Ugh .. the TOC should have neat indentation levels which can be
4179 # passed to the skin functions. These are determined here
4180 $toc = '';
4181 $full = '';
4182 $head = [];
4183 $sublevelCount = [];
4184 $levelCount = [];
4185 $level = 0;
4186 $prevlevel = 0;
4187 $toclevel = 0;
4188 $prevtoclevel = 0;
4189 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4190 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4191 $oldType = $this->mOutputType;
4192 $this->setOutputType( self::OT_WIKI );
4193 $frame = $this->getPreprocessor()->newFrame();
4194 $root = $this->preprocessToDom( $origText );
4195 $node = $root->getFirstChild();
4196 $byteOffset = 0;
4197 $tocraw = [];
4198 $refers = [];
4199
4200 $headlines = $numMatches !== false ? $matches[3] : [];
4201
4202 foreach ( $headlines as $headline ) {
4203 $isTemplate = false;
4204 $titleText = false;
4205 $sectionIndex = false;
4206 $numbering = '';
4207 $markerMatches = [];
4208 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4209 $serial = $markerMatches[1];
4210 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4211 $isTemplate = ( $titleText != $baseTitleText );
4212 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4213 }
4214
4215 if ( $toclevel ) {
4216 $prevlevel = $level;
4217 }
4218 $level = $matches[1][$headlineCount];
4219
4220 if ( $level > $prevlevel ) {
4221 # Increase TOC level
4222 $toclevel++;
4223 $sublevelCount[$toclevel] = 0;
4224 if ( $toclevel < $wgMaxTocLevel ) {
4225 $prevtoclevel = $toclevel;
4226 $toc .= Linker::tocIndent();
4227 $numVisible++;
4228 }
4229 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4230 # Decrease TOC level, find level to jump to
4231
4232 for ( $i = $toclevel; $i > 0; $i-- ) {
4233 if ( $levelCount[$i] == $level ) {
4234 # Found last matching level
4235 $toclevel = $i;
4236 break;
4237 } elseif ( $levelCount[$i] < $level ) {
4238 # Found first matching level below current level
4239 $toclevel = $i + 1;
4240 break;
4241 }
4242 }
4243 if ( $i == 0 ) {
4244 $toclevel = 1;
4245 }
4246 if ( $toclevel < $wgMaxTocLevel ) {
4247 if ( $prevtoclevel < $wgMaxTocLevel ) {
4248 # Unindent only if the previous toc level was shown :p
4249 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4250 $prevtoclevel = $toclevel;
4251 } else {
4252 $toc .= Linker::tocLineEnd();
4253 }
4254 }
4255 } else {
4256 # No change in level, end TOC line
4257 if ( $toclevel < $wgMaxTocLevel ) {
4258 $toc .= Linker::tocLineEnd();
4259 }
4260 }
4261
4262 $levelCount[$toclevel] = $level;
4263
4264 # count number of headlines for each level
4265 $sublevelCount[$toclevel]++;
4266 $dot = 0;
4267 for ( $i = 1; $i <= $toclevel; $i++ ) {
4268 if ( !empty( $sublevelCount[$i] ) ) {
4269 if ( $dot ) {
4270 $numbering .= '.';
4271 }
4272 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4273 $dot = 1;
4274 }
4275 }
4276
4277 # The safe header is a version of the header text safe to use for links
4278
4279 # Remove link placeholders by the link text.
4280 # <!--LINK number-->
4281 # turns into
4282 # link text with suffix
4283 # Do this before unstrip since link text can contain strip markers
4284 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4285
4286 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4287 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4288
4289 # Remove any <style> or <script> tags (T198618)
4290 $safeHeadline = preg_replace(
4291 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4292 '',
4293 $safeHeadline
4294 );
4295
4296 # Strip out HTML (first regex removes any tag not allowed)
4297 # Allowed tags are:
4298 # * <sup> and <sub> (T10393)
4299 # * <i> (T28375)
4300 # * <b> (r105284)
4301 # * <bdi> (T74884)
4302 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4303 # * <s> and <strike> (T35715)
4304 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4305 # to allow setting directionality in toc items.
4306 $tocline = preg_replace(
4307 [
4308 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4309 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4310 ],
4311 [ '', '<$1>' ],
4312 $safeHeadline
4313 );
4314
4315 # Strip '<span></span>', which is the result from the above if
4316 # <span id="foo"></span> is used to produce an additional anchor
4317 # for a section.
4318 $tocline = str_replace( '<span></span>', '', $tocline );
4319
4320 $tocline = trim( $tocline );
4321
4322 # For the anchor, strip out HTML-y stuff period
4323 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4324 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4325
4326 # Save headline for section edit hint before it's escaped
4327 $headlineHint = $safeHeadline;
4328
4329 # Decode HTML entities
4330 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4331
4332 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4333
4334 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4335 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4336 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4337 if ( $fallbackHeadline === $safeHeadline ) {
4338 # No reason to have both (in fact, we can't)
4339 $fallbackHeadline = false;
4340 }
4341
4342 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4343 # @todo FIXME: We may be changing them depending on the current locale.
4344 $arrayKey = strtolower( $safeHeadline );
4345 if ( $fallbackHeadline === false ) {
4346 $fallbackArrayKey = false;
4347 } else {
4348 $fallbackArrayKey = strtolower( $fallbackHeadline );
4349 }
4350
4351 # Create the anchor for linking from the TOC to the section
4352 $anchor = $safeHeadline;
4353 $fallbackAnchor = $fallbackHeadline;
4354 if ( isset( $refers[$arrayKey] ) ) {
4355 // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
4356 for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4357 $anchor .= "_$i";
4358 $linkAnchor .= "_$i";
4359 $refers["${arrayKey}_$i"] = true;
4360 } else {
4361 $refers[$arrayKey] = true;
4362 }
4363 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4364 // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
4365 for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4366 $fallbackAnchor .= "_$i";
4367 $refers["${fallbackArrayKey}_$i"] = true;
4368 } else {
4369 $refers[$fallbackArrayKey] = true;
4370 }
4371
4372 # Don't number the heading if it is the only one (looks silly)
4373 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4374 # the two are different if the line contains a link
4375 $headline = Html::element(
4376 'span',
4377 [ 'class' => 'mw-headline-number' ],
4378 $numbering
4379 ) . ' ' . $headline;
4380 }
4381
4382 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4383 $toc .= Linker::tocLine( $linkAnchor, $tocline,
4384 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4385 }
4386
4387 # Add the section to the section tree
4388 # Find the DOM node for this header
4389 $noOffset = ( $isTemplate || $sectionIndex === false );
4390 while ( $node && !$noOffset ) {
4391 if ( $node->getName() === 'h' ) {
4392 $bits = $node->splitHeading();
4393 if ( $bits['i'] == $sectionIndex ) {
4394 break;
4395 }
4396 }
4397 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4398 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4399 $node = $node->getNextSibling();
4400 }
4401 $tocraw[] = [
4402 'toclevel' => $toclevel,
4403 'level' => $level,
4404 'line' => $tocline,
4405 'number' => $numbering,
4406 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4407 'fromtitle' => $titleText,
4408 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4409 'anchor' => $anchor,
4410 ];
4411
4412 # give headline the correct <h#> tag
4413 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4414 // Output edit section links as markers with styles that can be customized by skins
4415 if ( $isTemplate ) {
4416 # Put a T flag in the section identifier, to indicate to extractSections()
4417 # that sections inside <includeonly> should be counted.
4418 $editsectionPage = $titleText;
4419 $editsectionSection = "T-$sectionIndex";
4420 $editsectionContent = null;
4421 } else {
4422 $editsectionPage = $this->mTitle->getPrefixedText();
4423 $editsectionSection = $sectionIndex;
4424 $editsectionContent = $headlineHint;
4425 }
4426 // We use a bit of pesudo-xml for editsection markers. The
4427 // language converter is run later on. Using a UNIQ style marker
4428 // leads to the converter screwing up the tokens when it
4429 // converts stuff. And trying to insert strip tags fails too. At
4430 // this point all real inputted tags have already been escaped,
4431 // so we don't have to worry about a user trying to input one of
4432 // these markers directly. We use a page and section attribute
4433 // to stop the language converter from converting these
4434 // important bits of data, but put the headline hint inside a
4435 // content block because the language converter is supposed to
4436 // be able to convert that piece of data.
4437 // Gets replaced with html in ParserOutput::getText
4438 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4439 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4440 if ( $editsectionContent !== null ) {
4441 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4442 } else {
4443 $editlink .= '/>';
4444 }
4445 } else {
4446 $editlink = '';
4447 }
4448 $head[$headlineCount] = Linker::makeHeadline( $level,
4449 $matches['attrib'][$headlineCount], $anchor, $headline,
4450 $editlink, $fallbackAnchor );
4451
4452 $headlineCount++;
4453 }
4454
4455 $this->setOutputType( $oldType );
4456
4457 # Never ever show TOC if no headers
4458 if ( $numVisible < 1 ) {
4459 $enoughToc = false;
4460 }
4461
4462 if ( $enoughToc ) {
4463 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4464 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4465 }
4466 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4467 $this->mOutput->setTOCHTML( $toc );
4468 $toc = self::TOC_START . $toc . self::TOC_END;
4469 }
4470
4471 if ( $isMain ) {
4472 $this->mOutput->setSections( $tocraw );
4473 }
4474
4475 # split up and insert constructed headlines
4476 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4477 $i = 0;
4478
4479 // build an array of document sections
4480 $sections = [];
4481 foreach ( $blocks as $block ) {
4482 // $head is zero-based, sections aren't.
4483 if ( empty( $head[$i - 1] ) ) {
4484 $sections[$i] = $block;
4485 } else {
4486 $sections[$i] = $head[$i - 1] . $block;
4487 }
4488
4499 Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4500
4501 $i++;
4502 }
4503
4504 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4505 // append the TOC at the beginning
4506 // Top anchor now in skin
4507 $sections[0] = $sections[0] . $toc . "\n";
4508 }
4509
4510 $full .= implode( '', $sections );
4511
4512 if ( $this->mForceTocPosition ) {
4513 return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4514 } else {
4515 return $full;
4516 }
4517 }
4518
4530 public function preSaveTransform( $text, Title $title, User $user,
4531 ParserOptions $options, $clearState = true
4532 ) {
4533 if ( $clearState ) {
4534 $magicScopeVariable = $this->lock();
4535 }
4536 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4537 $this->setUser( $user );
4538
4539 // Strip U+0000 NULL (T159174)
4540 $text = str_replace( "\000", '', $text );
4541
4542 // We still normalize line endings for backwards-compatibility
4543 // with other code that just calls PST, but this should already
4544 // be handled in TextContent subclasses
4545 $text = TextContent::normalizeLineEndings( $text );
4546
4547 if ( $options->getPreSaveTransform() ) {
4548 $text = $this->pstPass2( $text, $user );
4549 }
4550 $text = $this->mStripState->unstripBoth( $text );
4551
4552 $this->setUser( null ); # Reset
4553
4554 return $text;
4555 }
4556
4565 private function pstPass2( $text, $user ) {
4566 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4567 # $this->contLang here in order to give everyone the same signature and use the default one
4568 # rather than the one selected in each user's preferences. (see also T14815)
4569 $ts = $this->mOptions->getTimestamp();
4570 $timestamp = MWTimestamp::getLocalInstance( $ts );
4571 $ts = $timestamp->format( 'YmdHis' );
4572 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4573
4574 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4575
4576 # Variable replacement
4577 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4578 $text = $this->replaceVariables( $text );
4579
4580 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4581 # which may corrupt this parser instance via its wfMessage()->text() call-
4582
4583 # Signatures
4584 if ( strpos( $text, '~~~' ) !== false ) {
4585 $sigText = $this->getUserSig( $user );
4586 $text = strtr( $text, [
4587 '~~~~~' => $d,
4588 '~~~~' => "$sigText $d",
4589 '~~~' => $sigText
4590 ] );
4591 # The main two signature forms used above are time-sensitive
4592 $this->mOutput->setFlag( 'user-signature' );
4593 }
4594
4595 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4596 $tc = '[' . Title::legalChars() . ']';
4597 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4598
4599 // [[ns:page (context)|]]
4600 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4601 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4602 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4603 // [[ns:page (context), context|]] (using either single or double-width comma)
4604 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4605 // [[|page]] (reverse pipe trick: add context from page title)
4606 $p2 = "/\[\[\\|($tc+)]]/";
4607
4608 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4609 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4610 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4611 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4612
4613 $t = $this->mTitle->getText();
4614 $m = [];
4615 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4616 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4617 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4618 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4619 } else {
4620 # if there's no context, don't bother duplicating the title
4621 $text = preg_replace( $p2, '[[\\1]]', $text );
4622 }
4623
4624 return $text;
4625 }
4626
4641 public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4642 global $wgMaxSigChars;
4643
4644 $username = $user->getName();
4645
4646 # If not given, retrieve from the user object.
4647 if ( $nickname === false ) {
4648 $nickname = $user->getOption( 'nickname' );
4649 }
4650
4651 if ( is_null( $fancySig ) ) {
4652 $fancySig = $user->getBoolOption( 'fancysig' );
4653 }
4654
4655 $nickname = $nickname == null ? $username : $nickname;
4656
4657 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4658 $nickname = $username;
4659 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4660 } elseif ( $fancySig !== false ) {
4661 # Sig. might contain markup; validate this
4662 if ( $this->validateSig( $nickname ) !== false ) {
4663 # Validated; clean up (if needed) and return it
4664 return $this->cleanSig( $nickname, true );
4665 } else {
4666 # Failed to validate; fall back to the default
4667 $nickname = $username;
4668 wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4669 }
4670 }
4671
4672 # Make sure nickname doesnt get a sig in a sig
4673 $nickname = self::cleanSigInSig( $nickname );
4674
4675 # If we're still here, make it a link to the user page
4676 $userText = wfEscapeWikiText( $username );
4677 $nickText = wfEscapeWikiText( $nickname );
4678 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4679
4680 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4681 ->title( $this->getTitle() )->text();
4682 }
4683
4690 public function validateSig( $text ) {
4691 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4692 }
4693
4704 public function cleanSig( $text, $parsing = false ) {
4705 if ( !$parsing ) {
4706 global $wgTitle;
4707 $magicScopeVariable = $this->lock();
4708 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4709 }
4710
4711 # Option to disable this feature
4712 if ( !$this->mOptions->getCleanSignatures() ) {
4713 return $text;
4714 }
4715
4716 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4717 # => Move this logic to braceSubstitution()
4718 $substWord = $this->magicWordFactory->get( 'subst' );
4719 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4720 $substText = '{{' . $substWord->getSynonym( 0 );
4721
4722 $text = preg_replace( $substRegex, $substText, $text );
4723 $text = self::cleanSigInSig( $text );
4724 $dom = $this->preprocessToDom( $text );
4725 $frame = $this->getPreprocessor()->newFrame();
4726 $text = $frame->expand( $dom );
4727
4728 if ( !$parsing ) {
4729 $text = $this->mStripState->unstripBoth( $text );
4730 }
4731
4732 return $text;
4733 }
4734
4741 public static function cleanSigInSig( $text ) {
4742 $text = preg_replace( '/~{3,5}/', '', $text );
4743 return $text;
4744 }
4745
4755 public function startExternalParse( Title $title = null, ParserOptions $options,
4756 $outputType, $clearState = true
4757 ) {
4758 $this->startParse( $title, $options, $outputType, $clearState );
4759 }
4760
4767 private function startParse( Title $title = null, ParserOptions $options,
4768 $outputType, $clearState = true
4769 ) {
4770 $this->setTitle( $title );
4771 $this->mOptions = $options;
4772 $this->setOutputType( $outputType );
4773 if ( $clearState ) {
4774 $this->clearState();
4775 }
4776 }
4777
4786 public function transformMsg( $text, $options, $title = null ) {
4787 static $executing = false;
4788
4789 # Guard against infinite recursion
4790 if ( $executing ) {
4791 return $text;
4792 }
4793 $executing = true;
4794
4795 if ( !$title ) {
4796 global $wgTitle;
4797 $title = $wgTitle;
4798 }
4799
4800 $text = $this->preprocess( $text, $title, $options );
4801
4802 $executing = false;
4803 return $text;
4804 }
4805
4830 public function setHook( $tag, callable $callback ) {
4831 $tag = strtolower( $tag );
4832 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4833 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4834 }
4835 $oldVal = $this->mTagHooks[$tag] ?? null;
4836 $this->mTagHooks[$tag] = $callback;
4837 if ( !in_array( $tag, $this->mStripList ) ) {
4838 $this->mStripList[] = $tag;
4839 }
4840
4841 return $oldVal;
4842 }
4843
4861 public function setTransparentTagHook( $tag, callable $callback ) {
4862 $tag = strtolower( $tag );
4863 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4864 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4865 }
4866 $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
4867 $this->mTransparentTagHooks[$tag] = $callback;
4868
4869 return $oldVal;
4870 }
4871
4875 public function clearTagHooks() {
4876 $this->mTagHooks = [];
4877 $this->mFunctionTagHooks = [];
4878 $this->mStripList = $this->mDefaultStripList;
4879 }
4880
4924 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4925 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4926 $this->mFunctionHooks[$id] = [ $callback, $flags ];
4927
4928 # Add to function cache
4929 $mw = $this->magicWordFactory->get( $id );
4930 if ( !$mw ) {
4931 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4932 }
4933
4934 $synonyms = $mw->getSynonyms();
4935 $sensitive = intval( $mw->isCaseSensitive() );
4936
4937 foreach ( $synonyms as $syn ) {
4938 # Case
4939 if ( !$sensitive ) {
4940 $syn = $this->contLang->lc( $syn );
4941 }
4942 # Add leading hash
4943 if ( !( $flags & self::SFH_NO_HASH ) ) {
4944 $syn = '#' . $syn;
4945 }
4946 # Remove trailing colon
4947 if ( substr( $syn, -1, 1 ) === ':' ) {
4948 $syn = substr( $syn, 0, -1 );
4949 }
4950 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4951 }
4952 return $oldVal;
4953 }
4954
4960 public function getFunctionHooks() {
4961 $this->firstCallInit();
4962 return array_keys( $this->mFunctionHooks );
4963 }
4964
4975 public function setFunctionTagHook( $tag, callable $callback, $flags ) {
4976 $tag = strtolower( $tag );
4977 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4978 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4979 }
4980 $old = $this->mFunctionTagHooks[$tag] ?? null;
4981 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4982
4983 if ( !in_array( $tag, $this->mStripList ) ) {
4984 $this->mStripList[] = $tag;
4985 }
4986
4987 return $old;
4988 }
4989
4997 public function replaceLinkHolders( &$text, $options = 0 ) {
4998 $this->mLinkHolders->replace( $text );
4999 }
5000
5008 public function replaceLinkHoldersText( $text ) {
5009 return $this->mLinkHolders->replaceText( $text );
5010 }
5011
5025 public function renderImageGallery( $text, $params ) {
5026 $mode = false;
5027 if ( isset( $params['mode'] ) ) {
5028 $mode = $params['mode'];
5029 }
5030
5031 try {
5032 $ig = ImageGalleryBase::factory( $mode );
5033 } catch ( Exception $e ) {
5034 // If invalid type set, fallback to default.
5035 $ig = ImageGalleryBase::factory( false );
5036 }
5037
5038 $ig->setContextTitle( $this->mTitle );
5039 $ig->setShowBytes( false );
5040 $ig->setShowDimensions( false );
5041 $ig->setShowFilename( false );
5042 $ig->setParser( $this );
5043 $ig->setHideBadImages();
5044 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5045
5046 if ( isset( $params['showfilename'] ) ) {
5047 $ig->setShowFilename( true );
5048 } else {
5049 $ig->setShowFilename( false );
5050 }
5051 if ( isset( $params['caption'] ) ) {
5052 $caption = $params['caption'];
5053 $caption = htmlspecialchars( $caption );
5054 $caption = $this->replaceInternalLinks( $caption );
5055 $ig->setCaptionHtml( $caption );
5056 }
5057 if ( isset( $params['perrow'] ) ) {
5058 $ig->setPerRow( $params['perrow'] );
5059 }
5060 if ( isset( $params['widths'] ) ) {
5061 $ig->setWidths( $params['widths'] );
5062 }
5063 if ( isset( $params['heights'] ) ) {
5064 $ig->setHeights( $params['heights'] );
5065 }
5066 $ig->setAdditionalOptions( $params );
5067
5068 // Avoid PHP 7.1 warning from passing $this by reference
5069 $parser = $this;
5070 Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5071
5072 $lines = StringUtils::explode( "\n", $text );
5073 foreach ( $lines as $line ) {
5074 # match lines like these:
5075 # Image:someimage.jpg|This is some image
5076 $matches = [];
5077 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5078 # Skip empty lines
5079 if ( count( $matches ) == 0 ) {
5080 continue;
5081 }
5082
5083 if ( strpos( $matches[0], '%' ) !== false ) {
5084 $matches[1] = rawurldecode( $matches[1] );
5085 }
5086 $title = Title::newFromText( $matches[1], NS_FILE );
5087 if ( is_null( $title ) ) {
5088 # Bogus title. Ignore these so we don't bomb out later.
5089 continue;
5090 }
5091
5092 # We need to get what handler the file uses, to figure out parameters.
5093 # Note, a hook can overide the file name, and chose an entirely different
5094 # file (which potentially could be of a different type and have different handler).
5095 $options = [];
5096 $descQuery = false;
5097 Hooks::run( 'BeforeParserFetchFileAndTitle',
5098 [ $this, $title, &$options, &$descQuery ] );
5099 # Don't register it now, as TraditionalImageGallery does that later.
5100 $file = $this->fetchFileNoRegister( $title, $options );
5101 $handler = $file ? $file->getHandler() : false;
5102
5103 $paramMap = [
5104 'img_alt' => 'gallery-internal-alt',
5105 'img_link' => 'gallery-internal-link',
5106 ];
5107 if ( $handler ) {
5108 $paramMap = $paramMap + $handler->getParamMap();
5109 // We don't want people to specify per-image widths.
5110 // Additionally the width parameter would need special casing anyhow.
5111 unset( $paramMap['img_width'] );
5112 }
5113
5114 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5115
5116 $label = '';
5117 $alt = '';
5118 $link = '';
5119 $handlerOptions = [];
5120 if ( isset( $matches[3] ) ) {
5121 // look for an |alt= definition while trying not to break existing
5122 // captions with multiple pipes (|) in it, until a more sensible grammar
5123 // is defined for images in galleries
5124
5125 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5126 // splitting on '|' is a bit odd, and different from makeImage.
5127 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5128 // Protect LanguageConverter markup
5129 $parameterMatches = StringUtils::delimiterExplode(
5130 '-{', '}-', '|', $matches[3], true /* nested */
5131 );
5132
5133 foreach ( $parameterMatches as $parameterMatch ) {
5134 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5135 if ( $magicName ) {
5136 $paramName = $paramMap[$magicName];
5137
5138 switch ( $paramName ) {
5139 case 'gallery-internal-alt':
5140 $alt = $this->stripAltText( $match, false );
5141 break;
5142 case 'gallery-internal-link':
5143 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5144 if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5145 // Result of LanguageConverter::markNoConversion
5146 // invoked on an external link.
5147 $linkValue = substr( $linkValue, 4, -2 );
5148 }
5149 list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5150 if ( $type === 'link-url' ) {
5151 $link = $target;
5152 $this->mOutput->addExternalLink( $target );
5153 } elseif ( $type === 'link-title' ) {
5154 $link = $target->getLinkURL();
5155 $this->mOutput->addLink( $target );
5156 }
5157 break;
5158 default:
5159 // Must be a handler specific parameter.
5160 if ( $handler->validateParam( $paramName, $match ) ) {
5161 $handlerOptions[$paramName] = $match;
5162 } else {
5163 // Guess not, consider it as caption.
5164 wfDebug( "$parameterMatch failed parameter validation\n" );
5165 $label = $parameterMatch;
5166 }
5167 }
5168
5169 } else {
5170 // Last pipe wins.
5171 $label = $parameterMatch;
5172 }
5173 }
5174 }
5175
5176 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5177 }
5178 $html = $ig->toHTML();
5179 Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5180 return $html;
5181 }
5182
5187 public function getImageParams( $handler ) {
5188 if ( $handler ) {
5189 $handlerClass = get_class( $handler );
5190 } else {
5191 $handlerClass = '';
5192 }
5193 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5194 # Initialise static lists
5195 static $internalParamNames = [
5196 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5197 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5198 'bottom', 'text-bottom' ],
5199 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5200 'upright', 'border', 'link', 'alt', 'class' ],
5201 ];
5202 static $internalParamMap;
5203 if ( !$internalParamMap ) {
5204 $internalParamMap = [];
5205 foreach ( $internalParamNames as $type => $names ) {
5206 foreach ( $names as $name ) {
5207 // For grep: img_left, img_right, img_center, img_none,
5208 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5209 // img_bottom, img_text_bottom,
5210 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5211 // img_border, img_link, img_alt, img_class
5212 $magicName = str_replace( '-', '_', "img_$name" );
5213 $internalParamMap[$magicName] = [ $type, $name ];
5214 }
5215 }
5216 }
5217
5218 # Add handler params
5219 $paramMap = $internalParamMap;
5220 if ( $handler ) {
5221 $handlerParamMap = $handler->getParamMap();
5222 foreach ( $handlerParamMap as $magic => $paramName ) {
5223 $paramMap[$magic] = [ 'handler', $paramName ];
5224 }
5225 }
5226 $this->mImageParams[$handlerClass] = $paramMap;
5227 $this->mImageParamsMagicArray[$handlerClass] =
5228 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5229 }
5230 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5231 }
5232
5241 public function makeImage( $title, $options, $holders = false ) {
5242 # Check if the options text is of the form "options|alt text"
5243 # Options are:
5244 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5245 # * left no resizing, just left align. label is used for alt= only
5246 # * right same, but right aligned
5247 # * none same, but not aligned
5248 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5249 # * center center the image
5250 # * frame Keep original image size, no magnify-button.
5251 # * framed Same as "frame"
5252 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5253 # * upright reduce width for upright images, rounded to full __0 px
5254 # * border draw a 1px border around the image
5255 # * alt Text for HTML alt attribute (defaults to empty)
5256 # * class Set a class for img node
5257 # * link Set the target of the image link. Can be external, interwiki, or local
5258 # vertical-align values (no % or length right now):
5259 # * baseline
5260 # * sub
5261 # * super
5262 # * top
5263 # * text-top
5264 # * middle
5265 # * bottom
5266 # * text-bottom
5267
5268 # Protect LanguageConverter markup when splitting into parts
5270 '-{', '}-', '|', $options, true /* allow nesting */
5271 );
5272
5273 # Give extensions a chance to select the file revision for us
5274 $options = [];
5275 $descQuery = false;
5276 Hooks::run( 'BeforeParserFetchFileAndTitle',
5277 [ $this, $title, &$options, &$descQuery ] );
5278 # Fetch and register the file (file title may be different via hooks)
5279 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5280
5281 # Get parameter map
5282 $handler = $file ? $file->getHandler() : false;
5283
5284 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5285
5286 if ( !$file ) {
5287 $this->addTrackingCategory( 'broken-file-category' );
5288 }
5289
5290 # Process the input parameters
5291 $caption = '';
5292 $params = [ 'frame' => [], 'handler' => [],
5293 'horizAlign' => [], 'vertAlign' => [] ];
5294 $seenformat = false;
5295 foreach ( $parts as $part ) {
5296 $part = trim( $part );
5297 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5298 $validated = false;
5299 if ( isset( $paramMap[$magicName] ) ) {
5300 list( $type, $paramName ) = $paramMap[$magicName];
5301
5302 # Special case; width and height come in one variable together
5303 if ( $type === 'handler' && $paramName === 'width' ) {
5304 $parsedWidthParam = self::parseWidthParam( $value );
5305 if ( isset( $parsedWidthParam['width'] ) ) {
5306 $width = $parsedWidthParam['width'];
5307 if ( $handler->validateParam( 'width', $width ) ) {
5308 $params[$type]['width'] = $width;
5309 $validated = true;
5310 }
5311 }
5312 if ( isset( $parsedWidthParam['height'] ) ) {
5313 $height = $parsedWidthParam['height'];
5314 if ( $handler->validateParam( 'height', $height ) ) {
5315 $params[$type]['height'] = $height;
5316 $validated = true;
5317 }
5318 }
5319 # else no validation -- T15436
5320 } else {
5321 if ( $type === 'handler' ) {
5322 # Validate handler parameter
5323 $validated = $handler->validateParam( $paramName, $value );
5324 } else {
5325 # Validate internal parameters
5326 switch ( $paramName ) {
5327 case 'manualthumb':
5328 case 'alt':
5329 case 'class':
5330 # @todo FIXME: Possibly check validity here for
5331 # manualthumb? downstream behavior seems odd with
5332 # missing manual thumbs.
5333 $validated = true;
5334 $value = $this->stripAltText( $value, $holders );
5335 break;
5336 case 'link':
5337 list( $paramName, $value ) = $this->parseLinkParameter( $value );
5338 if ( $paramName ) {
5339 $validated = true;
5340 if ( $paramName === 'no-link' ) {
5341 $value = true;
5342 }
5343 if ( $paramName === 'link-url' ) {
5344 if ( $this->mOptions->getExternalLinkTarget() ) {
5345 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5346 }
5347 }
5348 }
5349 break;
5350 case 'frameless':
5351 case 'framed':
5352 case 'thumbnail':
5353 // use first appearing option, discard others.
5354 $validated = !$seenformat;
5355 $seenformat = true;
5356 break;
5357 default:
5358 # Most other things appear to be empty or numeric...
5359 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5360 }
5361 }
5362
5363 if ( $validated ) {
5364 $params[$type][$paramName] = $value;
5365 }
5366 }
5367 }
5368 if ( !$validated ) {
5369 $caption = $part;
5370 }
5371 }
5372
5373 # Process alignment parameters
5374 if ( $params['horizAlign'] ) {
5375 $params['frame']['align'] = key( $params['horizAlign'] );
5376 }
5377 if ( $params['vertAlign'] ) {
5378 $params['frame']['valign'] = key( $params['vertAlign'] );
5379 }
5380
5381 $params['frame']['caption'] = $caption;
5382
5383 # Will the image be presented in a frame, with the caption below?
5384 $imageIsFramed = isset( $params['frame']['frame'] )
5385 || isset( $params['frame']['framed'] )
5386 || isset( $params['frame']['thumbnail'] )
5387 || isset( $params['frame']['manualthumb'] );
5388
5389 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5390 # came to also set the caption, ordinary text after the image -- which
5391 # makes no sense, because that just repeats the text multiple times in
5392 # screen readers. It *also* came to set the title attribute.
5393 # Now that we have an alt attribute, we should not set the alt text to
5394 # equal the caption: that's worse than useless, it just repeats the
5395 # text. This is the framed/thumbnail case. If there's no caption, we
5396 # use the unnamed parameter for alt text as well, just for the time be-
5397 # ing, if the unnamed param is set and the alt param is not.
5398 # For the future, we need to figure out if we want to tweak this more,
5399 # e.g., introducing a title= parameter for the title; ignoring the un-
5400 # named parameter entirely for images without a caption; adding an ex-
5401 # plicit caption= parameter and preserving the old magic unnamed para-
5402 # meter for BC; ...
5403 if ( $imageIsFramed ) { # Framed image
5404 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5405 # No caption or alt text, add the filename as the alt text so
5406 # that screen readers at least get some description of the image
5407 $params['frame']['alt'] = $title->getText();
5408 }
5409 # Do not set $params['frame']['title'] because tooltips don't make sense
5410 # for framed images
5411 } else { # Inline image
5412 if ( !isset( $params['frame']['alt'] ) ) {
5413 # No alt text, use the "caption" for the alt text
5414 if ( $caption !== '' ) {
5415 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5416 } else {
5417 # No caption, fall back to using the filename for the
5418 # alt text
5419 $params['frame']['alt'] = $title->getText();
5420 }
5421 }
5422 # Use the "caption" for the tooltip text
5423 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5424 }
5425
5426 Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5427
5428 # Linker does the rest
5429 $time = $options['time'] ?? false;
5430 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5431 $time, $descQuery, $this->mOptions->getThumbSize() );
5432
5433 # Give the handler a chance to modify the parser object
5434 if ( $handler ) {
5435 $handler->parserTransformHook( $this, $file );
5436 }
5437
5438 return $ret;
5439 }
5440
5458 public function parseLinkParameter( $value ) {
5459 $chars = self::EXT_LINK_URL_CLASS;
5460 $addr = self::EXT_LINK_ADDR;
5461 $prots = $this->mUrlProtocols;
5462 $type = null;
5463 $target = false;
5464 if ( $value === '' ) {
5465 $type = 'no-link';
5466 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5467 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5468 $this->mOutput->addExternalLink( $value );
5469 $type = 'link-url';
5470 $target = $value;
5471 }
5472 } else {
5473 $linkTitle = Title::newFromText( $value );
5474 if ( $linkTitle ) {
5475 $this->mOutput->addLink( $linkTitle );
5476 $type = 'link-title';
5477 $target = $linkTitle;
5478 }
5479 }
5480 return [ $type, $target ];
5481 }
5482
5488 protected function stripAltText( $caption, $holders ) {
5489 # Strip bad stuff out of the title (tooltip). We can't just use
5490 # replaceLinkHoldersText() here, because if this function is called
5491 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5492 if ( $holders ) {
5493 $tooltip = $holders->replaceText( $caption );
5494 } else {
5495 $tooltip = $this->replaceLinkHoldersText( $caption );
5496 }
5497
5498 # make sure there are no placeholders in thumbnail attributes
5499 # that are later expanded to html- so expand them now and
5500 # remove the tags
5501 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5502 $tooltip = Sanitizer::stripAllTags( $tooltip );
5503
5504 return $tooltip;
5505 }
5506
5512 public function disableCache() {
5513 wfDebug( "Parser output marked as uncacheable.\n" );
5514 if ( !$this->mOutput ) {
5515 throw new MWException( __METHOD__ .
5516 " can only be called when actually parsing something" );
5517 }
5518 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5519 }
5520
5529 public function attributeStripCallback( &$text, $frame = false ) {
5530 $text = $this->replaceVariables( $text, $frame );
5531 $text = $this->mStripState->unstripBoth( $text );
5532 return $text;
5533 }
5534
5540 public function getTags() {
5541 $this->firstCallInit();
5542 return array_merge(
5543 array_keys( $this->mTransparentTagHooks ),
5544 array_keys( $this->mTagHooks ),
5545 array_keys( $this->mFunctionTagHooks )
5546 );
5547 }
5548
5553 public function getFunctionSynonyms() {
5554 $this->firstCallInit();
5555 return $this->mFunctionSynonyms;
5556 }
5557
5562 public function getUrlProtocols() {
5563 return $this->mUrlProtocols;
5564 }
5565
5576 public function replaceTransparentTags( $text ) {
5577 $matches = [];
5578 $elements = array_keys( $this->mTransparentTagHooks );
5579 $text = self::extractTagsAndParams( $elements, $text, $matches );
5580 $replacements = [];
5581
5582 foreach ( $matches as $marker => $data ) {
5583 list( $element, $content, $params, $tag ) = $data;
5584 $tagName = strtolower( $element );
5585 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5586 $output = call_user_func_array(
5587 $this->mTransparentTagHooks[$tagName],
5588 [ $content, $params, $this ]
5589 );
5590 } else {
5591 $output = $tag;
5592 }
5593 $replacements[$marker] = $output;
5594 }
5595 return strtr( $text, $replacements );
5596 }
5597
5627 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5628 global $wgTitle; # not generally used but removes an ugly failure mode
5629
5630 $magicScopeVariable = $this->lock();
5631 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5632 $outText = '';
5633 $frame = $this->getPreprocessor()->newFrame();
5634
5635 # Process section extraction flags
5636 $flags = 0;
5637 $sectionParts = explode( '-', $sectionId );
5638 $sectionIndex = array_pop( $sectionParts );
5639 foreach ( $sectionParts as $part ) {
5640 if ( $part === 'T' ) {
5641 $flags |= self::PTD_FOR_INCLUSION;
5642 }
5643 }
5644
5645 # Check for empty input
5646 if ( strval( $text ) === '' ) {
5647 # Only sections 0 and T-0 exist in an empty document
5648 if ( $sectionIndex == 0 ) {
5649 if ( $mode === 'get' ) {
5650 return '';
5651 } else {
5652 return $newText;
5653 }
5654 } else {
5655 if ( $mode === 'get' ) {
5656 return $newText;
5657 } else {
5658 return $text;
5659 }
5660 }
5661 }
5662
5663 # Preprocess the text
5664 $root = $this->preprocessToDom( $text, $flags );
5665
5666 # <h> nodes indicate section breaks
5667 # They can only occur at the top level, so we can find them by iterating the root's children
5668 $node = $root->getFirstChild();
5669
5670 # Find the target section
5671 if ( $sectionIndex == 0 ) {
5672 # Section zero doesn't nest, level=big
5673 $targetLevel = 1000;
5674 } else {
5675 while ( $node ) {
5676 if ( $node->getName() === 'h' ) {
5677 $bits = $node->splitHeading();
5678 if ( $bits['i'] == $sectionIndex ) {
5679 $targetLevel = $bits['level'];
5680 break;
5681 }
5682 }
5683 if ( $mode === 'replace' ) {
5684 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5685 }
5686 $node = $node->getNextSibling();
5687 }
5688 }
5689
5690 if ( !$node ) {
5691 # Not found
5692 if ( $mode === 'get' ) {
5693 return $newText;
5694 } else {
5695 return $text;
5696 }
5697 }
5698
5699 # Find the end of the section, including nested sections
5700 do {
5701 if ( $node->getName() === 'h' ) {
5702 $bits = $node->splitHeading();
5703 $curLevel = $bits['level'];
5704 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5705 break;
5706 }
5707 }
5708 if ( $mode === 'get' ) {
5709 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5710 }
5711 $node = $node->getNextSibling();
5712 } while ( $node );
5713
5714 # Write out the remainder (in replace mode only)
5715 if ( $mode === 'replace' ) {
5716 # Output the replacement text
5717 # Add two newlines on -- trailing whitespace in $newText is conventionally
5718 # stripped by the editor, so we need both newlines to restore the paragraph gap
5719 # Only add trailing whitespace if there is newText
5720 if ( $newText != "" ) {
5721 $outText .= $newText . "\n\n";
5722 }
5723
5724 while ( $node ) {
5725 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5726 $node = $node->getNextSibling();
5727 }
5728 }
5729
5730 if ( is_string( $outText ) ) {
5731 # Re-insert stripped tags
5732 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5733 }
5734
5735 return $outText;
5736 }
5737
5752 public function getSection( $text, $sectionId, $defaultText = '' ) {
5753 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5754 }
5755
5768 public function replaceSection( $oldText, $sectionId, $newText ) {
5769 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5770 }
5771
5777 public function getRevisionId() {
5778 return $this->mRevisionId;
5779 }
5780
5787 public function getRevisionObject() {
5788 if ( !is_null( $this->mRevisionObject ) ) {
5789 return $this->mRevisionObject;
5790 }
5791
5792 // NOTE: try to get the RevisionObject even if mRevisionId is null.
5793 // This is useful when parsing revision that has not yet been saved.
5794 // However, if we get back a saved revision even though we are in
5795 // preview mode, we'll have to ignore it, see below.
5796 // NOTE: This callback may be used to inject an OLD revision that was
5797 // already loaded, so "current" is a bit of a misnomer. We can't just
5798 // skip it if mRevisionId is set.
5799 $rev = call_user_func(
5800 $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5801 );
5802
5803 if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
5804 // We are in preview mode (mRevisionId is null), and the current revision callback
5805 // returned an existing revision. Ignore it and return null, it's probably the page's
5806 // current revision, which is not what we want here. Note that we do want to call the
5807 // callback to allow the unsaved revision to be injected here, e.g. for
5808 // self-transclusion previews.
5809 return null;
5810 }
5811
5812 // If the parse is for a new revision, then the callback should have
5813 // already been set to force the object and should match mRevisionId.
5814 // If not, try to fetch by mRevisionId for sanity.
5815 if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
5816 $rev = Revision::newFromId( $this->mRevisionId );
5817 }
5818
5819 $this->mRevisionObject = $rev;
5820
5821 return $this->mRevisionObject;
5822 }
5823
5829 public function getRevisionTimestamp() {
5830 if ( is_null( $this->mRevisionTimestamp ) ) {
5831 $revObject = $this->getRevisionObject();
5832 $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5833
5834 # The cryptic '' timezone parameter tells to use the site-default
5835 # timezone offset instead of the user settings.
5836 # Since this value will be saved into the parser cache, served
5837 # to other users, and potentially even used inside links and such,
5838 # it needs to be consistent for all visitors.
5839 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
5840 }
5841 return $this->mRevisionTimestamp;
5842 }
5843
5849 public function getRevisionUser() {
5850 if ( is_null( $this->mRevisionUser ) ) {
5851 $revObject = $this->getRevisionObject();
5852
5853 # if this template is subst: the revision id will be blank,
5854 # so just use the current user's name
5855 if ( $revObject ) {
5856 $this->mRevisionUser = $revObject->getUserText();
5857 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5858 $this->mRevisionUser = $this->getUser()->getName();
5859 }
5860 }
5861 return $this->mRevisionUser;
5862 }
5863
5869 public function getRevisionSize() {
5870 if ( is_null( $this->mRevisionSize ) ) {
5871 $revObject = $this->getRevisionObject();
5872
5873 # if this variable is subst: the revision id will be blank,
5874 # so just use the parser input size, because the own substituation
5875 # will change the size.
5876 if ( $revObject ) {
5877 $this->mRevisionSize = $revObject->getSize();
5878 } else {
5879 $this->mRevisionSize = $this->mInputSize;
5880 }
5881 }
5882 return $this->mRevisionSize;
5883 }
5884
5890 public function setDefaultSort( $sort ) {
5891 $this->mDefaultSort = $sort;
5892 $this->mOutput->setProperty( 'defaultsort', $sort );
5893 }
5894
5905 public function getDefaultSort() {
5906 if ( $this->mDefaultSort !== false ) {
5907 return $this->mDefaultSort;
5908 } else {
5909 return '';
5910 }
5911 }
5912
5919 public function getCustomDefaultSort() {
5920 return $this->mDefaultSort;
5921 }
5922
5923 private static function getSectionNameFromStrippedText( $text ) {
5924 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5925 $text = Sanitizer::decodeCharReferences( $text );
5926 $text = self::normalizeSectionName( $text );
5927 return $text;
5928 }
5929
5930 private static function makeAnchor( $sectionName ) {
5931 return '#' . Sanitizer::escapeIdForLink( $sectionName );
5932 }
5933
5934 private static function makeLegacyAnchor( $sectionName ) {
5935 global $wgFragmentMode;
5936 if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
5937 // ForAttribute() and ForLink() are the same for legacy encoding
5938 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
5939 } else {
5940 $id = Sanitizer::escapeIdForLink( $sectionName );
5941 }
5942
5943 return "#$id";
5944 }
5945
5954 public function guessSectionNameFromWikiText( $text ) {
5955 # Strip out wikitext links(they break the anchor)
5956 $text = $this->stripSectionName( $text );
5957 $sectionName = self::getSectionNameFromStrippedText( $text );
5958 return self::makeAnchor( $sectionName );
5959 }
5960
5970 public function guessLegacySectionNameFromWikiText( $text ) {
5971 # Strip out wikitext links(they break the anchor)
5972 $text = $this->stripSectionName( $text );
5973 $sectionName = self::getSectionNameFromStrippedText( $text );
5974 return self::makeLegacyAnchor( $sectionName );
5975 }
5976
5982 public static function guessSectionNameFromStrippedText( $text ) {
5983 $sectionName = self::getSectionNameFromStrippedText( $text );
5984 return self::makeAnchor( $sectionName );
5985 }
5986
5993 private static function normalizeSectionName( $text ) {
5994 # T90902: ensure the same normalization is applied for IDs as to links
5995 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
5996 try {
5997
5998 $parts = $titleParser->splitTitleString( "#$text" );
5999 } catch ( MalformedTitleException $ex ) {
6000 return $text;
6001 }
6002 return $parts['fragment'];
6003 }
6004
6019 public function stripSectionName( $text ) {
6020 # Strip internal link markup
6021 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6022 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6023
6024 # Strip external link markup
6025 # @todo FIXME: Not tolerant to blank link text
6026 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6027 # on how many empty links there are on the page - need to figure that out.
6028 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6029
6030 # Parse wikitext quotes (italics & bold)
6031 $text = $this->doQuotes( $text );
6032
6033 # Strip HTML tags
6034 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6035 return $text;
6036 }
6037
6048 public function testSrvus( $text, Title $title, ParserOptions $options,
6049 $outputType = self::OT_HTML
6050 ) {
6051 $magicScopeVariable = $this->lock();
6052 $this->startParse( $title, $options, $outputType, true );
6053
6054 $text = $this->replaceVariables( $text );
6055 $text = $this->mStripState->unstripBoth( $text );
6056 $text = Sanitizer::removeHTMLtags( $text );
6057 return $text;
6058 }
6059
6066 public function testPst( $text, Title $title, ParserOptions $options ) {
6067 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6068 }
6069
6076 public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6077 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6078 }
6079
6096 public function markerSkipCallback( $s, $callback ) {
6097 $i = 0;
6098 $out = '';
6099 while ( $i < strlen( $s ) ) {
6100 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6101 if ( $markerStart === false ) {
6102 $out .= call_user_func( $callback, substr( $s, $i ) );
6103 break;
6104 } else {
6105 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6106 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6107 if ( $markerEnd === false ) {
6108 $out .= substr( $s, $markerStart );
6109 break;
6110 } else {
6111 $markerEnd += strlen( self::MARKER_SUFFIX );
6112 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6113 $i = $markerEnd;
6114 }
6115 }
6116 }
6117 return $out;
6118 }
6119
6126 public function killMarkers( $text ) {
6127 return $this->mStripState->killMarkers( $text );
6128 }
6129
6147 public function serializeHalfParsedText( $text ) {
6148 wfDeprecated( __METHOD__, '1.31' );
6149 $data = [
6150 'text' => $text,
6151 'version' => self::HALF_PARSED_VERSION,
6152 'stripState' => $this->mStripState->getSubState( $text ),
6153 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6154 ];
6155 return $data;
6156 }
6157
6174 public function unserializeHalfParsedText( $data ) {
6175 wfDeprecated( __METHOD__, '1.31' );
6176 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6177 throw new MWException( __METHOD__ . ': invalid version' );
6178 }
6179
6180 # First, extract the strip state.
6181 $texts = [ $data['text'] ];
6182 $texts = $this->mStripState->merge( $data['stripState'], $texts );
6183
6184 # Now renumber links
6185 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6186
6187 # Should be good to go.
6188 return $texts[0];
6189 }
6190
6201 public function isValidHalfParsedText( $data ) {
6202 wfDeprecated( __METHOD__, '1.31' );
6203 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6204 }
6205
6215 public static function parseWidthParam( $value, $parseHeight = true ) {
6216 $parsedWidthParam = [];
6217 if ( $value === '' ) {
6218 return $parsedWidthParam;
6219 }
6220 $m = [];
6221 # (T15500) In both cases (width/height and width only),
6222 # permit trailing "px" for backward compatibility.
6223 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6224 $width = intval( $m[1] );
6225 $height = intval( $m[2] );
6226 $parsedWidthParam['width'] = $width;
6227 $parsedWidthParam['height'] = $height;
6228 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6229 $width = intval( $value );
6230 $parsedWidthParam['width'] = $width;
6231 }
6232 return $parsedWidthParam;
6233 }
6234
6244 protected function lock() {
6245 if ( $this->mInParse ) {
6246 throw new MWException( "Parser state cleared while parsing. "
6247 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6248 }
6249
6250 // Save the backtrace when locking, so that if some code tries locking again,
6251 // we can print the lock owner's backtrace for easier debugging
6252 $e = new Exception;
6253 $this->mInParse = $e->getTraceAsString();
6254
6255 $recursiveCheck = new ScopedCallback( function () {
6256 $this->mInParse = false;
6257 } );
6258
6259 return $recursiveCheck;
6260 }
6261
6272 public static function stripOuterParagraph( $html ) {
6273 $m = [];
6274 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6275 if ( strpos( $m[1], '</p>' ) === false ) {
6276 $html = $m[1];
6277 }
6278 }
6279
6280 return $html;
6281 }
6282
6293 public function getFreshParser() {
6294 if ( $this->mInParse ) {
6295 return $this->factory->create();
6296 } else {
6297 return $this;
6298 }
6299 }
6300
6307 public function enableOOUI() {
6308 OutputPage::setupOOUI();
6309 $this->mOutput->setEnableOOUI( true );
6310 }
6311}
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
we sometimes make exceptions for this Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally NO WARRANTY BECAUSE THE PROGRAM IS LICENSED FREE OF THERE IS NO WARRANTY FOR THE TO THE EXTENT PERMITTED BY APPLICABLE LAW EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND OR OTHER PARTIES PROVIDE THE PROGRAM AS IS WITHOUT WARRANTY OF ANY EITHER EXPRESSED OR BUT NOT LIMITED THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU SHOULD THE PROGRAM PROVE YOU ASSUME THE COST OF ALL NECESSARY REPAIR OR CORRECTION IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT OR ANY OTHER PARTY WHO MAY MODIFY AND OR REDISTRIBUTE THE PROGRAM AS PERMITTED BE LIABLE TO YOU FOR INCLUDING ANY INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new and you want it to be of the greatest possible use to the public
Definition COPYING.txt:285
$wgLanguageCode
Site language code.
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgServerName
Server name.
$wgExtraInterlanguageLinkPrefixes
List of additional interwiki prefixes that should be treated as interlanguage links (i....
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
$wgSitename
Name of the site.
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
$wgScriptPath
The path we should point to.
$wgFragmentMode
How should section IDs be encoded? This array can contain 1 or 2 elements, each of them can be one of...
$wgTranscludeCacheExpiry
Expiry time for transcluded templates cached in object cache.
$wgMaxSigChars
Maximum number of Unicode characters in signature.
$wgEnableScaryTranscluding
Enable interwiki transcluding.
$wgStylePath
The URL path of the skins directory.
$wgServer
URL of the server.
$wgMaxTocLevel
Maximum indent level of toc.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfHostname()
Fetch server name for use in error reporting etc.
wfFindFile( $title, $options=[])
Find a file.
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.
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.
wfIsHHVM()
Check if we are running under HHVM.
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:121
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition api.php:57
$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
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
Internationalisation code.
Definition Language.php:35
static tocList( $toc, $lang=false)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition Linker.php:1567
static makeMediaLinkFile(Title $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:766
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1531
static makeImageLink(Parser $parser, Title $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:301
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:248
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1355
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:1505
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:1664
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:826
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1641
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1516
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1555
MediaWiki exception.
static tidy( $text)
Interface with html tidy.
Definition MWTidy.php:46
static isEnabled()
Definition MWTidy.php:58
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.
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.
Set options of the Parser.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:68
addTrackingCategory( $msg)
Definition Parser.php:4109
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:916
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:5829
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:2060
bool string $mInParse
Recursive call protection.
Definition Parser.php:254
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3927
const TOC_END
Definition Parser.php:137
$mDefaultStripList
Definition Parser.php:149
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:5890
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6272
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2024
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:769
MagicWordFactory $magicWordFactory
Definition Parser.php:265
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition Parser.php:2503
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:334
ParserOutput $mOutput
Definition Parser.php:181
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2083
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4741
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3786
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:989
LinkRenderer $mLinkRenderer
Definition Parser.php:262
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:724
$mHighestExpansionDepth
Definition Parser.php:196
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5025
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:5919
static getSectionNameFromStrippedText( $text)
Definition Parser.php:5923
stripAltText( $caption, $holders)
Definition Parser.php:5488
getOptions()
Get the ParserOptions object.
Definition Parser.php:871
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4704
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:4530
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:4767
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:5849
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3604
replaceExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:1895
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2997
getFunctionSynonyms()
Definition Parser.php:5553
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
Definition Parser.php:80
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:2470
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:2494
Language $contLang
Definition Parser.php:268
$mHeadings
Definition Parser.php:198
Title $mTitle
Definition Parser.php:218
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2155
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:4975
static makeLegacyAnchor( $sectionName)
Definition Parser.php:5934
const PTD_FOR_INCLUSION
Definition Parser.php:105
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:5982
$mTplDomCache
Definition Parser.php:198
$mGeneratedPPNodeCount
Definition Parser.php:196
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1494
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
Definition Parser.php:6174
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:3074
LinkHolderArray $mLinkHolders
Definition Parser.php:193
$mFunctionTagHooks
Definition Parser.php:147
$mRevisionId
Definition Parser.php:222
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5241
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
Definition Parser.php:5512
pstPass2( $text, $user)
Pre-save transform helper function.
Definition Parser.php:4565
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Definition Parser.php:4786
getRevisionSize()
Get the size of the revision.
Definition Parser.php:5869
const TOC_START
Definition Parser.php:136
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition Parser.php:5627
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6019
$mDefaultSort
Definition Parser.php:197
$mTagHooks
Definition Parser.php:143
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition Parser.php:5993
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition Parser.php:1597
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:750
$mFunctionHooks
Definition Parser.php:145
$mShowToc
Definition Parser.php:200
getRevisionTimestampSubstring( $start, $len, $mtts, $variable)
Definition Parser.php:2894
getImageParams( $handler)
Definition Parser.php:5187
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition Parser.php:6147
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:4129
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1398
getUrlProtocols()
Definition Parser.php:5562
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:4924
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3538
$mRevisionSize
Definition Parser.php:225
bool $mFirstCall
Whether firstCallInit still needs to be called.
Definition Parser.php:157
doTableStuff( $text)
parse the wiki syntax used to render tables
Definition Parser.php:1116
getTitle()
Accessor for the Title object.
Definition Parser.php:817
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:1973
ParserOptions $mOptions
Definition Parser.php:213
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition Parser.php:6048
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:74
MagicWordArray $mSubstWords
Definition Parser.php:169
$mUrlProtocols
Definition Parser.php:171
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
Definition Parser.php:5576
MapCacheLRU null $currentRevisionCache
Definition Parser.php:248
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:971
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition Parser.php:2964
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:1994
setOutputType( $ot)
Set the output type.
Definition Parser.php:836
__construct(array $conf=[], MagicWordFactory $magicWordFactory=null, Language $contLang=null, ParserFactory $factory=null, $urlProtocols=null, SpecialPageFactory $spFactory=null)
Definition Parser.php:284
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:903
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2543
StripState $mStripState
Definition Parser.php:187
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition Parser.php:544
$mInputSize
Definition Parser.php:227
MagicWordArray $mVariables
Definition Parser.php:164
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1089
$mOutputType
Definition Parser.php:219
$mImageParamsMagicArray
Definition Parser.php:152
setUser( $user)
Set the current user.
Definition Parser.php:790
$mAutonumber
Definition Parser.php:182
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6096
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4830
getTags()
Accessor.
Definition Parser.php:5540
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:4056
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3875
replaceInternalLinks( $s)
Process [[ ]] wikilinks.
Definition Parser.php:2142
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:240
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:679
$mRevisionUser
Definition Parser.php:224
string $mUniqPrefix
Deprecated accessor for the strip marker prefix.
Definition Parser.php:233
$mTplRedirCache
Definition Parser.php:198
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3761
$mFunctionSynonyms
Definition Parser.php:146
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5752
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1326
$mRevisionTimestamp
Definition Parser.php:223
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:1022
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5768
interwikiTransclude( $title, $action)
Transclude an interwiki link.
Definition Parser.php:3805
setTitle( $t)
Set the context title.
Definition Parser.php:799
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:4029
User $mUser
Definition Parser.php:205
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
Definition Parser.php:2516
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:5787
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2528
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3615
$mPPNodeCount
Definition Parser.php:196
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:5905
const MARKER_PREFIX
Definition Parser.php:133
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:5008
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:4960
doAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1690
$mMarkerIndex
Definition Parser.php:153
const EXT_LINK_ADDR
Definition Parser.php:95
$mExpensiveFunctionCount
Definition Parser.php:199
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:999
Preprocessor $mPreprocessor
Definition Parser.php:175
$mDoubleUnderscores
Definition Parser.php:198
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Definition Parser.php:3581
getOutput()
Get the ParserOutput object.
Definition Parser.php:862
$mVarCache
Definition Parser.php:150
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:881
$mImageParams
Definition Parser.php:151
Title( $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:827
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:707
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:5954
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4875
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:4997
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:5777
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6215
$mIncludeCount
Definition Parser.php:189
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4690
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:957
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition Parser.php:5458
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:5970
const EXT_LINK_URL_CLASS
Definition Parser.php:92
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:853
magicLinkCallback( $m)
Definition Parser.php:1525
$mIncludeSizes
Definition Parser.php:196
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:322
fetchFile( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3749
setLinkID( $id)
Definition Parser.php:895
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:945
doQuotes( $text)
Helper function for doAllQuotes()
Definition Parser.php:1707
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3643
doHeadings( $text)
Parse headers and return html.
Definition Parser.php:1672
$mExtLinkBracketedRegex
Definition Parser.php:171
const EXT_IMAGE_REGEX
Definition Parser.php:98
$mRevIdForTs
Definition Parser.php:226
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:1102
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
Definition Parser.php:4861
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:4043
testPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:6076
static makeAnchor( $sectionName)
Definition Parser.php:5930
const SPACE_NOT_NL
Definition Parser.php:102
SpecialPageFactory $specialPageFactory
Definition Parser.php:274
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:357
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:2952
$mForceTocPosition
Definition Parser.php:200
clearState()
Clear Parser state.
Definition Parser.php:377
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6126
ParserFactory $factory
Definition Parser.php:271
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6307
nextLinkID()
Definition Parser.php:888
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:4641
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3096
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5529
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
Definition Parser.php:2922
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
Definition Parser.php:6201
lock()
Lock the current instance of the parser.
Definition Parser.php:6244
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition Parser.php:3026
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:437
$mStripList
Definition Parser.php:148
testPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:6066
startExternalParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Set up some variables which are usually set up in parse() so that an external function can call some ...
Definition Parser.php:4755
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6293
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3446
SectionProfiler $mProfiler
Definition Parser.php:257
$mTransparentTagHooks
Definition Parser.php:144
getConverterLanguage()
Get the language object for language conversion.
Definition Parser.php:935
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3656
Variant of the Message class.
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:59
Group all the pieces relevant to the context of a request into one instance.
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition Revision.php:133
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:114
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 delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <....
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
static delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
Represents a title within MediaWiki.
Definition Title.php:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:47
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:592
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition deferred.txt:5
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global then executing the whole list after the page is displayed We don t do anything smart like collating updates to the same table or such because the list is almost always going to have just one item on if so it s not worth the trouble Since there is a job queue in the jobs table
Definition deferred.txt:16
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of normal(non-web) applications -- they might conflict with distributors' policies
this hook is for auditing only $req
Definition hooks.txt:1018
namespace being checked & $result
Definition hooks.txt:2385
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition hooks.txt:1873
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1841
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc next in line in page history
Definition hooks.txt:1818
either a plain
Definition hooks.txt:2105
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2050
null means default in associative array form
Definition hooks.txt:2045
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message key
Definition hooks.txt:2214
usually copyright or history_copyright This message must be in HTML not wikitext if the section is included from a template to be included in the link
Definition hooks.txt:3108
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2885
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:994
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2055
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped broken
Definition hooks.txt:2050
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition hooks.txt:2335
if the prop value should be in the metadata multi language array format
Definition hooks.txt:1690
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2054
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:894
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition hooks.txt:2062
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3106
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:815
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:2063
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1071
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:933
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2317
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1818
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2226
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
!hooks source !endhooks !test Non existent language !input< source lang="doesnotexist"> foobar</source > ! result< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > foobar</pre ></div > !end !test No language specified ! wikitext< source > foo</source > ! html< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > foo</pre ></div > !end !test No language specified(no wellformed xml) !! config !! wikitext< source > bar</source > !! html< div class
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition globals.txt:62
const OT_WIKI
Definition Defines.php:185
const SFH_NO_HASH
Definition Defines.php:197
const SFH_OBJECT_ARGS
Definition Defines.php:198
const NS_FILE
Definition Defines.php:70
const NS_TEMPLATE
Definition Defines.php:74
const NS_SPECIAL
Definition Defines.php:53
const OT_PLAIN
Definition Defines.php:188
const OT_PREPROCESS
Definition Defines.php:186
const OT_HTML
Definition Defines.php:184
const NS_MEDIA
Definition Defines.php:52
const NS_CATEGORY
Definition Defines.php:78
const OT_MSG
Definition Defines.php:187
$wgArticlePath
Definition img_auth.php:46
const NO_TEMPLATES
const RECOVER_ORIG
const NO_ARGS
const STRIP_COMMENTS
There are three types of nodes:
$cache
Definition mcc.php:33
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$buffer
$content
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres as and are nearing end of but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition postgres.txt:36
$sort
const DB_REPLICA
Definition defines.php:25
$lines
Definition router.php:61
$params