MediaWiki REL1_33
Parser.php
Go to the documentation of this file.
1<?php
27use Wikimedia\ScopedCallback;
28
69class Parser {
75 const VERSION = '1.6.4';
76
82
83 # Flags for Parser::setFunctionHook
84 const SFH_NO_HASH = 1;
85 const SFH_OBJECT_ARGS = 2;
86
87 # Constants needed for external link processing
88 # Everything except bracket, space, or control characters
89 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
90 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
91 # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
92 # uses to replace invalid HTML characters.
93 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
94 # Simplified expression to match an IPv4 or IPv6 address, or
95 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
96 const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
97 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
98 // phpcs:ignore Generic.Files.LineLength
99 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
100 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
101
102 # Regular expression for a non-newline space
103 const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
104
105 # Flags for preprocessToDom
107
108 # Allowed values for $this->mOutputType
109 # Parameter to startExternalParse().
110 const OT_HTML = 1; # like parse()
111 const OT_WIKI = 2; # like preSaveTransform()
113 const OT_MSG = 3;
114 const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
115
133 const MARKER_SUFFIX = "-QINU`\"'\x7f";
134 const MARKER_PREFIX = "\x7f'\"`UNIQ-";
135
136 # Markers used for wrapping the table of contents
137 const TOC_START = '<mw:toc>';
138 const TOC_END = '</mw:toc>';
139
141 const MAX_TTS = 900;
142
143 # Persistent:
144 public $mTagHooks = [];
146 public $mFunctionHooks = [];
147 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
149 public $mStripList = [];
151 public $mVarCache = [];
152 public $mImageParams = [];
154 public $mMarkerIndex = 0;
158 public $mFirstCall = true;
159
160 # Initialised by initialiseVariables()
161
166
171 # Initialised in constructor
172 public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
173
174 # Initialized in getPreprocessor()
177
178 # Cleared with clearState():
182 public $mOutput;
184
189
195
196 public $mLinkID;
197 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
199 public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
200 public $mExpensiveFunctionCount; # number of expensive parser function calls
202
206 public $mUser; # User object; only used when doing pre-save transform
207
208 # Temporary
209 # These are variables reset at least once per parse regardless of $clearState
210
214 public $mOptions;
215
219 public $mTitle; # Title context, used for self-link rendering and similar things
220 public $mOutputType; # Output type, one of the OT_xxx constants
221 public $ot; # Shortcut alias, see setOutputType()
222 public $mRevisionObject; # The revision object of the specified revision ID
223 public $mRevisionId; # ID to display in {{REVISIONID}} tags
224 public $mRevisionTimestamp; # The timestamp of the specified revision ID
225 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
226 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
227 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
228 public $mInputSize = false; # For {{PAGESIZE}} on current page.
229
234 public $mUniqPrefix = self::MARKER_PREFIX;
235
242
250
255 public $mInParse = false;
256
258 protected $mProfiler;
259
263 protected $mLinkRenderer;
264
267
269 private $contLang;
270
272 private $factory;
273
276
278 private $siteConfig;
279
282
293 public function __construct(
294 array $parserConf = [], MagicWordFactory $magicWordFactory = null,
295 Language $contLang = null, ParserFactory $factory = null, $urlProtocols = null,
296 SpecialPageFactory $spFactory = null, Config $siteConfig = null,
297 LinkRendererFactory $linkRendererFactory = null
298 ) {
299 $this->mConf = $parserConf;
300 $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
301 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
302 self::EXT_LINK_ADDR .
303 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
304 if ( isset( $parserConf['preprocessorClass'] ) ) {
305 $this->mPreprocessorClass = $parserConf['preprocessorClass'];
306 } elseif ( wfIsHHVM() ) {
307 # Under HHVM Preprocessor_Hash is much faster than Preprocessor_DOM
308 $this->mPreprocessorClass = Preprocessor_Hash::class;
309 } elseif ( extension_loaded( 'domxml' ) ) {
310 # PECL extension that conflicts with the core DOM extension (T15770)
311 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
312 $this->mPreprocessorClass = Preprocessor_Hash::class;
313 } elseif ( extension_loaded( 'dom' ) ) {
314 $this->mPreprocessorClass = Preprocessor_DOM::class;
315 } else {
316 $this->mPreprocessorClass = Preprocessor_Hash::class;
317 }
318 wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
319
320 $services = MediaWikiServices::getInstance();
321 $this->magicWordFactory = $magicWordFactory ??
322 $services->getMagicWordFactory();
323
324 $this->contLang = $contLang ?? $services->getContentLanguage();
325
326 $this->factory = $factory ?? $services->getParserFactory();
327 $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
328 $this->siteConfig = $siteConfig ?? MediaWikiServices::getInstance()->getMainConfig();
329
330 $this->linkRendererFactory =
331 $linkRendererFactory ?? MediaWikiServices::getInstance()->getLinkRendererFactory();
332 }
333
337 public function __destruct() {
338 if ( isset( $this->mLinkHolders ) ) {
339 unset( $this->mLinkHolders );
340 }
341 foreach ( $this as $name => $value ) {
342 unset( $this->$name );
343 }
344 }
345
349 public function __clone() {
350 $this->mInParse = false;
351
352 // T58226: When you create a reference "to" an object field, that
353 // makes the object field itself be a reference too (until the other
354 // reference goes out of scope). When cloning, any field that's a
355 // reference is copied as a reference in the new object. Both of these
356 // are defined PHP5 behaviors, as inconvenient as it is for us when old
357 // hooks from PHP4 days are passing fields by reference.
358 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
359 // Make a non-reference copy of the field, then rebind the field to
360 // reference the new copy.
361 $tmp = $this->$k;
362 $this->$k =& $tmp;
363 unset( $tmp );
364 }
365
366 Hooks::run( 'ParserCloned', [ $this ] );
367 }
368
372 public function firstCallInit() {
373 if ( !$this->mFirstCall ) {
374 return;
375 }
376 $this->mFirstCall = false;
377
379 CoreTagHooks::register( $this );
380 $this->initialiseVariables();
381
382 // Avoid PHP 7.1 warning from passing $this by reference
383 $parser = $this;
384 Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
385 }
386
392 public function clearState() {
393 $this->firstCallInit();
394 $this->mOutput = new ParserOutput;
395 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
396 $this->mAutonumber = 0;
397 $this->mIncludeCount = [];
398 $this->mLinkHolders = new LinkHolderArray( $this );
399 $this->mLinkID = 0;
400 $this->mRevisionObject = $this->mRevisionTimestamp =
401 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
402 $this->mVarCache = [];
403 $this->mUser = null;
404 $this->mLangLinkLanguages = [];
405 $this->currentRevisionCache = null;
406
407 $this->mStripState = new StripState( $this );
408
409 # Clear these on every parse, T6549
410 $this->mTplRedirCache = $this->mTplDomCache = [];
411
412 $this->mShowToc = true;
413 $this->mForceTocPosition = false;
414 $this->mIncludeSizes = [
415 'post-expand' => 0,
416 'arg' => 0,
417 ];
418 $this->mPPNodeCount = 0;
419 $this->mGeneratedPPNodeCount = 0;
420 $this->mHighestExpansionDepth = 0;
421 $this->mDefaultSort = false;
422 $this->mHeadings = [];
423 $this->mDoubleUnderscores = [];
424 $this->mExpensiveFunctionCount = 0;
425
426 # Fix cloning
427 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
428 $this->mPreprocessor = null;
429 }
430
431 $this->mProfiler = new SectionProfiler();
432
433 // Avoid PHP 7.1 warning from passing $this by reference
434 $parser = $this;
435 Hooks::run( 'ParserClearState', [ &$parser ] );
436 }
437
452 public function parse(
453 $text, Title $title, ParserOptions $options,
454 $linestart = true, $clearState = true, $revid = null
455 ) {
456 if ( $clearState ) {
457 // We use U+007F DELETE to construct strip markers, so we have to make
458 // sure that this character does not occur in the input text.
459 $text = strtr( $text, "\x7f", "?" );
460 $magicScopeVariable = $this->lock();
461 }
462 // Strip U+0000 NULL (T159174)
463 $text = str_replace( "\000", '', $text );
464
465 $this->startParse( $title, $options, self::OT_HTML, $clearState );
466
467 $this->currentRevisionCache = null;
468 $this->mInputSize = strlen( $text );
469 if ( $this->mOptions->getEnableLimitReport() ) {
470 $this->mOutput->resetParseStartTime();
471 }
472
473 $oldRevisionId = $this->mRevisionId;
474 $oldRevisionObject = $this->mRevisionObject;
475 $oldRevisionTimestamp = $this->mRevisionTimestamp;
476 $oldRevisionUser = $this->mRevisionUser;
477 $oldRevisionSize = $this->mRevisionSize;
478 if ( $revid !== null ) {
479 $this->mRevisionId = $revid;
480 $this->mRevisionObject = null;
481 $this->mRevisionTimestamp = null;
482 $this->mRevisionUser = null;
483 $this->mRevisionSize = null;
484 }
485
486 // Avoid PHP 7.1 warning from passing $this by reference
487 $parser = $this;
488 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
489 # No more strip!
490 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
491 $text = $this->internalParse( $text );
492 Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
493
494 $text = $this->internalParseHalfParsed( $text, true, $linestart );
495
503 if ( !( $options->getDisableTitleConversion()
504 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
505 || isset( $this->mDoubleUnderscores['notitleconvert'] )
506 || $this->mOutput->getDisplayTitle() !== false )
507 ) {
508 $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
509 if ( $convruletitle ) {
510 $this->mOutput->setTitleText( $convruletitle );
511 } else {
512 $titleText = $this->getTargetLanguage()->convertTitle( $title );
513 $this->mOutput->setTitleText( $titleText );
514 }
515 }
516
517 # Compute runtime adaptive expiry if set
518 $this->mOutput->finalizeAdaptiveCacheExpiry();
519
520 # Warn if too many heavyweight parser functions were used
521 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
522 $this->limitationWarn( 'expensive-parserfunction',
523 $this->mExpensiveFunctionCount,
524 $this->mOptions->getExpensiveParserFunctionLimit()
525 );
526 }
527
528 # Information on limits, for the benefit of users who try to skirt them
529 if ( $this->mOptions->getEnableLimitReport() ) {
530 $text .= $this->makeLimitReport();
531 }
532
533 # Wrap non-interface parser output in a <div> so it can be targeted
534 # with CSS (T37247)
535 $class = $this->mOptions->getWrapOutputClass();
536 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
537 $this->mOutput->addWrapperDivClass( $class );
538 }
539
540 $this->mOutput->setText( $text );
541
542 $this->mRevisionId = $oldRevisionId;
543 $this->mRevisionObject = $oldRevisionObject;
544 $this->mRevisionTimestamp = $oldRevisionTimestamp;
545 $this->mRevisionUser = $oldRevisionUser;
546 $this->mRevisionSize = $oldRevisionSize;
547 $this->mInputSize = false;
548 $this->currentRevisionCache = null;
549
550 return $this->mOutput;
551 }
552
559 protected function makeLimitReport() {
560 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
561
562 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
563 if ( $cpuTime !== null ) {
564 $this->mOutput->setLimitReportData( 'limitreport-cputime',
565 sprintf( "%.3f", $cpuTime )
566 );
567 }
568
569 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
570 $this->mOutput->setLimitReportData( 'limitreport-walltime',
571 sprintf( "%.3f", $wallTime )
572 );
573
574 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
575 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
576 );
577 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
578 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
579 );
580 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
581 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
582 );
583 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
584 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
585 );
586 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
587 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
588 );
589 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
590 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
591 );
592
593 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
594 $this->mOutput->setLimitReportData( $key, $value );
595 }
596
597 Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
598
599 $limitReport = "NewPP limit report\n";
600 if ( $this->siteConfig->get( 'ShowHostnames' ) ) {
601 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
602 }
603 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
604 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
605 $limitReport .= 'Dynamic content: ' .
606 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
607 "\n";
608
609 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
610 if ( Hooks::run( 'ParserLimitReportFormat',
611 [ $key, &$value, &$limitReport, false, false ]
612 ) ) {
613 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
614 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
615 ->inLanguage( 'en' )->useDatabase( false );
616 if ( !$valueMsg->exists() ) {
617 $valueMsg = new RawMessage( '$1' );
618 }
619 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
620 $valueMsg->params( $value );
621 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
622 }
623 }
624 }
625 // Since we're not really outputting HTML, decode the entities and
626 // then re-encode the things that need hiding inside HTML comments.
627 $limitReport = htmlspecialchars_decode( $limitReport );
628
629 // Sanitize for comment. Note '‐' in the replacement is U+2010,
630 // which looks much like the problematic '-'.
631 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
632 $text = "\n<!-- \n$limitReport-->\n";
633
634 // Add on template profiling data in human/machine readable way
635 $dataByFunc = $this->mProfiler->getFunctionStats();
636 uasort( $dataByFunc, function ( $a, $b ) {
637 return $b['real'] <=> $a['real']; // descending order
638 } );
639 $profileReport = [];
640 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
641 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
642 $item['%real'], $item['real'], $item['calls'],
643 htmlspecialchars( $item['name'] ) );
644 }
645 $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
646 $text .= implode( "\n", $profileReport ) . "\n-->\n";
647
648 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
649
650 // Add other cache related metadata
651 if ( $this->siteConfig->get( 'ShowHostnames' ) ) {
652 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
653 }
654 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
655 $this->mOutput->getCacheTime() );
656 $this->mOutput->setLimitReportData( 'cachereport-ttl',
657 $this->mOutput->getCacheExpiry() );
658 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
659 $this->mOutput->hasDynamicContent() );
660
661 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
662 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
663 $this->mTitle->getPrefixedDBkey() );
664 }
665 return $text;
666 }
667
692 public function recursiveTagParse( $text, $frame = false ) {
693 // Avoid PHP 7.1 warning from passing $this by reference
694 $parser = $this;
695 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
696 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
697 $text = $this->internalParse( $text, false, $frame );
698 return $text;
699 }
700
720 public function recursiveTagParseFully( $text, $frame = false ) {
721 $text = $this->recursiveTagParse( $text, $frame );
722 $text = $this->internalParseHalfParsed( $text, false );
723 return $text;
724 }
725
737 public function preprocess( $text, Title $title = null,
738 ParserOptions $options, $revid = null, $frame = false
739 ) {
740 $magicScopeVariable = $this->lock();
741 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
742 if ( $revid !== null ) {
743 $this->mRevisionId = $revid;
744 }
745 // Avoid PHP 7.1 warning from passing $this by reference
746 $parser = $this;
747 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
748 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
749 $text = $this->replaceVariables( $text, $frame );
750 $text = $this->mStripState->unstripBoth( $text );
751 return $text;
752 }
753
763 public function recursivePreprocess( $text, $frame = false ) {
764 $text = $this->replaceVariables( $text, $frame );
765 $text = $this->mStripState->unstripBoth( $text );
766 return $text;
767 }
768
782 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
783 $msg = new RawMessage( $text );
784 $text = $msg->params( $params )->plain();
785
786 # Parser (re)initialisation
787 $magicScopeVariable = $this->lock();
788 $this->startParse( $title, $options, self::OT_PLAIN, true );
789
791 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
792 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
793 $text = $this->mStripState->unstripBoth( $text );
794 return $text;
795 }
796
803 public function setUser( $user ) {
804 $this->mUser = $user;
805 }
806
812 public function setTitle( $t ) {
813 if ( !$t ) {
814 $t = Title::newFromText( 'NO TITLE' );
815 }
816
817 if ( $t->hasFragment() ) {
818 # Strip the fragment to avoid various odd effects
819 $this->mTitle = $t->createFragmentTarget( '' );
820 } else {
821 $this->mTitle = $t;
822 }
823 }
824
830 public function getTitle() {
831 return $this->mTitle;
832 }
833
840 public function Title( $x = null ) {
841 return wfSetVar( $this->mTitle, $x );
842 }
843
849 public function setOutputType( $ot ) {
850 $this->mOutputType = $ot;
851 # Shortcut alias
852 $this->ot = [
853 'html' => $ot == self::OT_HTML,
854 'wiki' => $ot == self::OT_WIKI,
855 'pre' => $ot == self::OT_PREPROCESS,
856 'plain' => $ot == self::OT_PLAIN,
857 ];
858 }
859
866 public function OutputType( $x = null ) {
867 return wfSetVar( $this->mOutputType, $x );
868 }
869
875 public function getOutput() {
876 return $this->mOutput;
877 }
878
884 public function getOptions() {
885 return $this->mOptions;
886 }
887
894 public function Options( $x = null ) {
895 return wfSetVar( $this->mOptions, $x );
896 }
897
901 public function nextLinkID() {
902 return $this->mLinkID++;
903 }
904
908 public function setLinkID( $id ) {
909 $this->mLinkID = $id;
910 }
911
916 public function getFunctionLang() {
917 return $this->getTargetLanguage();
918 }
919
929 public function getTargetLanguage() {
930 $target = $this->mOptions->getTargetLanguage();
931
932 if ( $target !== null ) {
933 return $target;
934 } elseif ( $this->mOptions->getInterfaceMessage() ) {
935 return $this->mOptions->getUserLangObj();
936 } elseif ( is_null( $this->mTitle ) ) {
937 throw new MWException( __METHOD__ . ': $this->mTitle is null' );
938 }
939
940 return $this->mTitle->getPageLanguage();
941 }
942
948 public function getConverterLanguage() {
949 return $this->getTargetLanguage();
950 }
951
958 public function getUser() {
959 if ( !is_null( $this->mUser ) ) {
960 return $this->mUser;
961 }
962 return $this->mOptions->getUser();
963 }
964
970 public function getPreprocessor() {
971 if ( !isset( $this->mPreprocessor ) ) {
972 $class = $this->mPreprocessorClass;
973 $this->mPreprocessor = new $class( $this );
974 }
975 return $this->mPreprocessor;
976 }
977
984 public function getLinkRenderer() {
985 // XXX We make the LinkRenderer with current options and then cache it forever
986 if ( !$this->mLinkRenderer ) {
987 $this->mLinkRenderer = $this->linkRendererFactory->create();
988 $this->mLinkRenderer->setStubThreshold(
989 $this->getOptions()->getStubThreshold()
990 );
991 }
992
993 return $this->mLinkRenderer;
994 }
995
1002 public function getMagicWordFactory() {
1003 return $this->magicWordFactory;
1004 }
1005
1012 public function getContentLanguage() {
1013 return $this->contLang;
1014 }
1015
1035 public static function extractTagsAndParams( $elements, $text, &$matches ) {
1036 static $n = 1;
1037 $stripped = '';
1038 $matches = [];
1039
1040 $taglist = implode( '|', $elements );
1041 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1042
1043 while ( $text != '' ) {
1044 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1045 $stripped .= $p[0];
1046 if ( count( $p ) < 5 ) {
1047 break;
1048 }
1049 if ( count( $p ) > 5 ) {
1050 # comment
1051 $element = $p[4];
1052 $attributes = '';
1053 $close = '';
1054 $inside = $p[5];
1055 } else {
1056 # tag
1057 list( , $element, $attributes, $close, $inside ) = $p;
1058 }
1059
1060 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1061 $stripped .= $marker;
1062
1063 if ( $close === '/>' ) {
1064 # Empty element tag, <tag />
1065 $content = null;
1066 $text = $inside;
1067 $tail = null;
1068 } else {
1069 if ( $element === '!--' ) {
1070 $end = '/(-->)/';
1071 } else {
1072 $end = "/(<\\/$element\\s*>)/i";
1073 }
1074 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1075 $content = $q[0];
1076 if ( count( $q ) < 3 ) {
1077 # No end tag -- let it run out to the end of the text.
1078 $tail = '';
1079 $text = '';
1080 } else {
1081 list( , $tail, $text ) = $q;
1082 }
1083 }
1084
1085 $matches[$marker] = [ $element,
1086 $content,
1087 Sanitizer::decodeTagAttributes( $attributes ),
1088 "<$element$attributes$close$content$tail" ];
1089 }
1090 return $stripped;
1091 }
1092
1098 public function getStripList() {
1099 return $this->mStripList;
1100 }
1101
1111 public function insertStripItem( $text ) {
1112 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1113 $this->mMarkerIndex++;
1114 $this->mStripState->addGeneral( $marker, $text );
1115 return $marker;
1116 }
1117
1125 public function doTableStuff( $text ) {
1126 $lines = StringUtils::explode( "\n", $text );
1127 $out = '';
1128 $td_history = []; # Is currently a td tag open?
1129 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1130 $tr_history = []; # Is currently a tr tag open?
1131 $tr_attributes = []; # history of tr attributes
1132 $has_opened_tr = []; # Did this table open a <tr> element?
1133 $indent_level = 0; # indent level of the table
1134
1135 foreach ( $lines as $outLine ) {
1136 $line = trim( $outLine );
1137
1138 if ( $line === '' ) { # empty line, go to next line
1139 $out .= $outLine . "\n";
1140 continue;
1141 }
1142
1143 $first_character = $line[0];
1144 $first_two = substr( $line, 0, 2 );
1145 $matches = [];
1146
1147 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1148 # First check if we are starting a new table
1149 $indent_level = strlen( $matches[1] );
1150
1151 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1152 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1153
1154 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1155 array_push( $td_history, false );
1156 array_push( $last_tag_history, '' );
1157 array_push( $tr_history, false );
1158 array_push( $tr_attributes, '' );
1159 array_push( $has_opened_tr, false );
1160 } elseif ( count( $td_history ) == 0 ) {
1161 # Don't do any of the following
1162 $out .= $outLine . "\n";
1163 continue;
1164 } elseif ( $first_two === '|}' ) {
1165 # We are ending a table
1166 $line = '</table>' . substr( $line, 2 );
1167 $last_tag = array_pop( $last_tag_history );
1168
1169 if ( !array_pop( $has_opened_tr ) ) {
1170 $line = "<tr><td></td></tr>{$line}";
1171 }
1172
1173 if ( array_pop( $tr_history ) ) {
1174 $line = "</tr>{$line}";
1175 }
1176
1177 if ( array_pop( $td_history ) ) {
1178 $line = "</{$last_tag}>{$line}";
1179 }
1180 array_pop( $tr_attributes );
1181 if ( $indent_level > 0 ) {
1182 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1183 } else {
1184 $outLine = $line;
1185 }
1186 } elseif ( $first_two === '|-' ) {
1187 # Now we have a table row
1188 $line = preg_replace( '#^\|-+#', '', $line );
1189
1190 # Whats after the tag is now only attributes
1191 $attributes = $this->mStripState->unstripBoth( $line );
1192 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1193 array_pop( $tr_attributes );
1194 array_push( $tr_attributes, $attributes );
1195
1196 $line = '';
1197 $last_tag = array_pop( $last_tag_history );
1198 array_pop( $has_opened_tr );
1199 array_push( $has_opened_tr, true );
1200
1201 if ( array_pop( $tr_history ) ) {
1202 $line = '</tr>';
1203 }
1204
1205 if ( array_pop( $td_history ) ) {
1206 $line = "</{$last_tag}>{$line}";
1207 }
1208
1209 $outLine = $line;
1210 array_push( $tr_history, false );
1211 array_push( $td_history, false );
1212 array_push( $last_tag_history, '' );
1213 } elseif ( $first_character === '|'
1214 || $first_character === '!'
1215 || $first_two === '|+'
1216 ) {
1217 # This might be cell elements, td, th or captions
1218 if ( $first_two === '|+' ) {
1219 $first_character = '+';
1220 $line = substr( $line, 2 );
1221 } else {
1222 $line = substr( $line, 1 );
1223 }
1224
1225 // Implies both are valid for table headings.
1226 if ( $first_character === '!' ) {
1227 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1228 }
1229
1230 # Split up multiple cells on the same line.
1231 # FIXME : This can result in improper nesting of tags processed
1232 # by earlier parser steps.
1233 $cells = explode( '||', $line );
1234
1235 $outLine = '';
1236
1237 # Loop through each table cell
1238 foreach ( $cells as $cell ) {
1239 $previous = '';
1240 if ( $first_character !== '+' ) {
1241 $tr_after = array_pop( $tr_attributes );
1242 if ( !array_pop( $tr_history ) ) {
1243 $previous = "<tr{$tr_after}>\n";
1244 }
1245 array_push( $tr_history, true );
1246 array_push( $tr_attributes, '' );
1247 array_pop( $has_opened_tr );
1248 array_push( $has_opened_tr, true );
1249 }
1250
1251 $last_tag = array_pop( $last_tag_history );
1252
1253 if ( array_pop( $td_history ) ) {
1254 $previous = "</{$last_tag}>\n{$previous}";
1255 }
1256
1257 if ( $first_character === '|' ) {
1258 $last_tag = 'td';
1259 } elseif ( $first_character === '!' ) {
1260 $last_tag = 'th';
1261 } elseif ( $first_character === '+' ) {
1262 $last_tag = 'caption';
1263 } else {
1264 $last_tag = '';
1265 }
1266
1267 array_push( $last_tag_history, $last_tag );
1268
1269 # A cell could contain both parameters and data
1270 $cell_data = explode( '|', $cell, 2 );
1271
1272 # T2553: Note that a '|' inside an invalid link should not
1273 # be mistaken as delimiting cell parameters
1274 # Bug T153140: Neither should language converter markup.
1275 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1276 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1277 } elseif ( count( $cell_data ) == 1 ) {
1278 // Whitespace in cells is trimmed
1279 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1280 } else {
1281 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1282 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1283 // Whitespace in cells is trimmed
1284 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1285 }
1286
1287 $outLine .= $cell;
1288 array_push( $td_history, true );
1289 }
1290 }
1291 $out .= $outLine . "\n";
1292 }
1293
1294 # Closing open td, tr && table
1295 while ( count( $td_history ) > 0 ) {
1296 if ( array_pop( $td_history ) ) {
1297 $out .= "</td>\n";
1298 }
1299 if ( array_pop( $tr_history ) ) {
1300 $out .= "</tr>\n";
1301 }
1302 if ( !array_pop( $has_opened_tr ) ) {
1303 $out .= "<tr><td></td></tr>\n";
1304 }
1305
1306 $out .= "</table>\n";
1307 }
1308
1309 # Remove trailing line-ending (b/c)
1310 if ( substr( $out, -1 ) === "\n" ) {
1311 $out = substr( $out, 0, -1 );
1312 }
1313
1314 # special case: don't return empty table
1315 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1316 $out = '';
1317 }
1318
1319 return $out;
1320 }
1321
1335 public function internalParse( $text, $isMain = true, $frame = false ) {
1336 $origText = $text;
1337
1338 // Avoid PHP 7.1 warning from passing $this by reference
1339 $parser = $this;
1340
1341 # Hook to suspend the parser in this state
1342 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1343 return $text;
1344 }
1345
1346 # if $frame is provided, then use $frame for replacing any variables
1347 if ( $frame ) {
1348 # use frame depth to infer how include/noinclude tags should be handled
1349 # depth=0 means this is the top-level document; otherwise it's an included document
1350 if ( !$frame->depth ) {
1351 $flag = 0;
1352 } else {
1353 $flag = self::PTD_FOR_INCLUSION;
1354 }
1355 $dom = $this->preprocessToDom( $text, $flag );
1356 $text = $frame->expand( $dom );
1357 } else {
1358 # if $frame is not provided, then use old-style replaceVariables
1359 $text = $this->replaceVariables( $text );
1360 }
1361
1362 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1363 $text = Sanitizer::removeHTMLtags(
1364 $text,
1365 [ $this, 'attributeStripCallback' ],
1366 false,
1367 array_keys( $this->mTransparentTagHooks ),
1368 [],
1369 [ $this, 'addTrackingCategory' ]
1370 );
1371 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1372
1373 # Tables need to come after variable replacement for things to work
1374 # properly; putting them before other transformations should keep
1375 # exciting things like link expansions from showing up in surprising
1376 # places.
1377 $text = $this->doTableStuff( $text );
1378
1379 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1380
1381 $text = $this->doDoubleUnderscore( $text );
1382
1383 $text = $this->doHeadings( $text );
1384 $text = $this->replaceInternalLinks( $text );
1385 $text = $this->doAllQuotes( $text );
1386 $text = $this->replaceExternalLinks( $text );
1387
1388 # replaceInternalLinks may sometimes leave behind
1389 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1390 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1391
1392 $text = $this->doMagicLinks( $text );
1393 $text = $this->formatHeadings( $text, $origText, $isMain );
1394
1395 return $text;
1396 }
1397
1407 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1408 $text = $this->mStripState->unstripGeneral( $text );
1409
1410 // Avoid PHP 7.1 warning from passing $this by reference
1411 $parser = $this;
1412
1413 if ( $isMain ) {
1414 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1415 }
1416
1417 # Clean up special characters, only run once, next-to-last before doBlockLevels
1418 $text = Sanitizer::armorFrenchSpaces( $text );
1419
1420 $text = $this->doBlockLevels( $text, $linestart );
1421
1422 $this->replaceLinkHolders( $text );
1423
1431 if ( !( $this->mOptions->getDisableContentConversion()
1432 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1433 && !$this->mOptions->getInterfaceMessage()
1434 ) {
1435 # The position of the convert() call should not be changed. it
1436 # assumes that the links are all replaced and the only thing left
1437 # is the <nowiki> mark.
1438 $text = $this->getTargetLanguage()->convert( $text );
1439 }
1440
1441 $text = $this->mStripState->unstripNoWiki( $text );
1442
1443 if ( $isMain ) {
1444 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1445 }
1446
1447 $text = $this->replaceTransparentTags( $text );
1448 $text = $this->mStripState->unstripGeneral( $text );
1449
1450 $text = Sanitizer::normalizeCharReferences( $text );
1451
1452 if ( MWTidy::isEnabled() ) {
1453 if ( $this->mOptions->getTidy() ) {
1454 $text = MWTidy::tidy( $text );
1455 }
1456 } else {
1457 # attempt to sanitize at least some nesting problems
1458 # (T4702 and quite a few others)
1459 # This code path is buggy and deprecated!
1460 wfDeprecated( 'disabling tidy', '1.33' );
1461 $tidyregs = [
1462 # ''Something [http://www.cool.com cool''] -->
1463 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1464 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1465 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1466 # fix up an anchor inside another anchor, only
1467 # at least for a single single nested link (T5695)
1468 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1469 '\\1\\2</a>\\3</a>\\1\\4</a>',
1470 # fix div inside inline elements- doBlockLevels won't wrap a line which
1471 # contains a div, so fix it up here; replace
1472 # div with escaped text
1473 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1474 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1475 # remove empty italic or bold tag pairs, some
1476 # introduced by rules above
1477 '/<([bi])><\/\\1>/' => '',
1478 ];
1479
1480 $text = preg_replace(
1481 array_keys( $tidyregs ),
1482 array_values( $tidyregs ),
1483 $text );
1484 }
1485
1486 if ( $isMain ) {
1487 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1488 }
1489
1490 return $text;
1491 }
1492
1504 public function doMagicLinks( $text ) {
1506 $urlChar = self::EXT_LINK_URL_CLASS;
1507 $addr = self::EXT_LINK_ADDR;
1508 $space = self::SPACE_NOT_NL; # non-newline space
1509 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1510 $spaces = "$space++"; # possessive match of 1 or more spaces
1511 $text = preg_replace_callback(
1512 '!(?: # Start cases
1513 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1514 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1515 (\b # m[3]: Free external links
1516 (?i:$prots)
1517 ($addr$urlChar*) # m[4]: Post-protocol path
1518 ) |
1519 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1520 ([0-9]+)\b |
1521 \bISBN $spaces ( # m[6]: ISBN, capture number
1522 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1523 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1524 [0-9Xx] # check digit
1525 )\b
1526 )!xu", [ $this, 'magicLinkCallback' ], $text );
1527 return $text;
1528 }
1529
1535 public function magicLinkCallback( $m ) {
1536 if ( isset( $m[1] ) && $m[1] !== '' ) {
1537 # Skip anchor
1538 return $m[0];
1539 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1540 # Skip HTML element
1541 return $m[0];
1542 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1543 # Free external link
1544 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1545 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1546 # RFC or PMID
1547 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1548 if ( !$this->mOptions->getMagicRFCLinks() ) {
1549 return $m[0];
1550 }
1551 $keyword = 'RFC';
1552 $urlmsg = 'rfcurl';
1553 $cssClass = 'mw-magiclink-rfc';
1554 $trackingCat = 'magiclink-tracking-rfc';
1555 $id = $m[5];
1556 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1557 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1558 return $m[0];
1559 }
1560 $keyword = 'PMID';
1561 $urlmsg = 'pubmedurl';
1562 $cssClass = 'mw-magiclink-pmid';
1563 $trackingCat = 'magiclink-tracking-pmid';
1564 $id = $m[5];
1565 } else {
1566 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1567 substr( $m[0], 0, 20 ) . '"' );
1568 }
1569 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1570 $this->addTrackingCategory( $trackingCat );
1571 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1572 } elseif ( isset( $m[6] ) && $m[6] !== ''
1573 && $this->mOptions->getMagicISBNLinks()
1574 ) {
1575 # ISBN
1576 $isbn = $m[6];
1577 $space = self::SPACE_NOT_NL; # non-newline space
1578 $isbn = preg_replace( "/$space/", ' ', $isbn );
1579 $num = strtr( $isbn, [
1580 '-' => '',
1581 ' ' => '',
1582 'x' => 'X',
1583 ] );
1584 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1585 return $this->getLinkRenderer()->makeKnownLink(
1586 SpecialPage::getTitleFor( 'Booksources', $num ),
1587 "ISBN $isbn",
1588 [
1589 'class' => 'internal mw-magiclink-isbn',
1590 'title' => false // suppress title attribute
1591 ]
1592 );
1593 } else {
1594 return $m[0];
1595 }
1596 }
1597
1607 public function makeFreeExternalLink( $url, $numPostProto ) {
1608 $trail = '';
1609
1610 # The characters '<' and '>' (which were escaped by
1611 # removeHTMLtags()) should not be included in
1612 # URLs, per RFC 2396.
1613 # Make &nbsp; terminate a URL as well (bug T84937)
1614 $m2 = [];
1615 if ( preg_match(
1616 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1617 $url,
1618 $m2,
1619 PREG_OFFSET_CAPTURE
1620 ) ) {
1621 $trail = substr( $url, $m2[0][1] ) . $trail;
1622 $url = substr( $url, 0, $m2[0][1] );
1623 }
1624
1625 # Move trailing punctuation to $trail
1626 $sep = ',;\.:!?';
1627 # If there is no left bracket, then consider right brackets fair game too
1628 if ( strpos( $url, '(' ) === false ) {
1629 $sep .= ')';
1630 }
1631
1632 $urlRev = strrev( $url );
1633 $numSepChars = strspn( $urlRev, $sep );
1634 # Don't break a trailing HTML entity by moving the ; into $trail
1635 # This is in hot code, so use substr_compare to avoid having to
1636 # create a new string object for the comparison
1637 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1638 # more optimization: instead of running preg_match with a $
1639 # anchor, which can be slow, do the match on the reversed
1640 # string starting at the desired offset.
1641 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1642 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1643 $numSepChars--;
1644 }
1645 }
1646 if ( $numSepChars ) {
1647 $trail = substr( $url, -$numSepChars ) . $trail;
1648 $url = substr( $url, 0, -$numSepChars );
1649 }
1650
1651 # Verify that we still have a real URL after trail removal, and
1652 # not just lone protocol
1653 if ( strlen( $trail ) >= $numPostProto ) {
1654 return $url . $trail;
1655 }
1656
1657 $url = Sanitizer::cleanUrl( $url );
1658
1659 # Is this an external image?
1660 $text = $this->maybeMakeExternalImage( $url );
1661 if ( $text === false ) {
1662 # Not an image, make a link
1663 $text = Linker::makeExternalLink( $url,
1664 $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1665 true, 'free',
1666 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1667 # Register it in the output object...
1668 $this->mOutput->addExternalLink( $url );
1669 }
1670 return $text . $trail;
1671 }
1672
1682 public function doHeadings( $text ) {
1683 for ( $i = 6; $i >= 1; --$i ) {
1684 $h = str_repeat( '=', $i );
1685 // Trim non-newline whitespace from headings
1686 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1687 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1688 }
1689 return $text;
1690 }
1691
1700 public function doAllQuotes( $text ) {
1701 $outtext = '';
1702 $lines = StringUtils::explode( "\n", $text );
1703 foreach ( $lines as $line ) {
1704 $outtext .= $this->doQuotes( $line ) . "\n";
1705 }
1706 $outtext = substr( $outtext, 0, -1 );
1707 return $outtext;
1708 }
1709
1717 public function doQuotes( $text ) {
1718 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1719 $countarr = count( $arr );
1720 if ( $countarr == 1 ) {
1721 return $text;
1722 }
1723
1724 // First, do some preliminary work. This may shift some apostrophes from
1725 // being mark-up to being text. It also counts the number of occurrences
1726 // of bold and italics mark-ups.
1727 $numbold = 0;
1728 $numitalics = 0;
1729 for ( $i = 1; $i < $countarr; $i += 2 ) {
1730 $thislen = strlen( $arr[$i] );
1731 // If there are ever four apostrophes, assume the first is supposed to
1732 // be text, and the remaining three constitute mark-up for bold text.
1733 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1734 if ( $thislen == 4 ) {
1735 $arr[$i - 1] .= "'";
1736 $arr[$i] = "'''";
1737 $thislen = 3;
1738 } elseif ( $thislen > 5 ) {
1739 // If there are more than 5 apostrophes in a row, assume they're all
1740 // text except for the last 5.
1741 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1742 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1743 $arr[$i] = "'''''";
1744 $thislen = 5;
1745 }
1746 // Count the number of occurrences of bold and italics mark-ups.
1747 if ( $thislen == 2 ) {
1748 $numitalics++;
1749 } elseif ( $thislen == 3 ) {
1750 $numbold++;
1751 } elseif ( $thislen == 5 ) {
1752 $numitalics++;
1753 $numbold++;
1754 }
1755 }
1756
1757 // If there is an odd number of both bold and italics, it is likely
1758 // that one of the bold ones was meant to be an apostrophe followed
1759 // by italics. Which one we cannot know for certain, but it is more
1760 // likely to be one that has a single-letter word before it.
1761 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1762 $firstsingleletterword = -1;
1763 $firstmultiletterword = -1;
1764 $firstspace = -1;
1765 for ( $i = 1; $i < $countarr; $i += 2 ) {
1766 if ( strlen( $arr[$i] ) == 3 ) {
1767 $x1 = substr( $arr[$i - 1], -1 );
1768 $x2 = substr( $arr[$i - 1], -2, 1 );
1769 if ( $x1 === ' ' ) {
1770 if ( $firstspace == -1 ) {
1771 $firstspace = $i;
1772 }
1773 } elseif ( $x2 === ' ' ) {
1774 $firstsingleletterword = $i;
1775 // if $firstsingleletterword is set, we don't
1776 // look at the other options, so we can bail early.
1777 break;
1778 } elseif ( $firstmultiletterword == -1 ) {
1779 $firstmultiletterword = $i;
1780 }
1781 }
1782 }
1783
1784 // If there is a single-letter word, use it!
1785 if ( $firstsingleletterword > -1 ) {
1786 $arr[$firstsingleletterword] = "''";
1787 $arr[$firstsingleletterword - 1] .= "'";
1788 } elseif ( $firstmultiletterword > -1 ) {
1789 // If not, but there's a multi-letter word, use that one.
1790 $arr[$firstmultiletterword] = "''";
1791 $arr[$firstmultiletterword - 1] .= "'";
1792 } elseif ( $firstspace > -1 ) {
1793 // ... otherwise use the first one that has neither.
1794 // (notice that it is possible for all three to be -1 if, for example,
1795 // there is only one pentuple-apostrophe in the line)
1796 $arr[$firstspace] = "''";
1797 $arr[$firstspace - 1] .= "'";
1798 }
1799 }
1800
1801 // Now let's actually convert our apostrophic mush to HTML!
1802 $output = '';
1803 $buffer = '';
1804 $state = '';
1805 $i = 0;
1806 foreach ( $arr as $r ) {
1807 if ( ( $i % 2 ) == 0 ) {
1808 if ( $state === 'both' ) {
1809 $buffer .= $r;
1810 } else {
1811 $output .= $r;
1812 }
1813 } else {
1814 $thislen = strlen( $r );
1815 if ( $thislen == 2 ) {
1816 if ( $state === 'i' ) {
1817 $output .= '</i>';
1818 $state = '';
1819 } elseif ( $state === 'bi' ) {
1820 $output .= '</i>';
1821 $state = 'b';
1822 } elseif ( $state === 'ib' ) {
1823 $output .= '</b></i><b>';
1824 $state = 'b';
1825 } elseif ( $state === 'both' ) {
1826 $output .= '<b><i>' . $buffer . '</i>';
1827 $state = 'b';
1828 } else { // $state can be 'b' or ''
1829 $output .= '<i>';
1830 $state .= 'i';
1831 }
1832 } elseif ( $thislen == 3 ) {
1833 if ( $state === 'b' ) {
1834 $output .= '</b>';
1835 $state = '';
1836 } elseif ( $state === 'bi' ) {
1837 $output .= '</i></b><i>';
1838 $state = 'i';
1839 } elseif ( $state === 'ib' ) {
1840 $output .= '</b>';
1841 $state = 'i';
1842 } elseif ( $state === 'both' ) {
1843 $output .= '<i><b>' . $buffer . '</b>';
1844 $state = 'i';
1845 } else { // $state can be 'i' or ''
1846 $output .= '<b>';
1847 $state .= 'b';
1848 }
1849 } elseif ( $thislen == 5 ) {
1850 if ( $state === 'b' ) {
1851 $output .= '</b><i>';
1852 $state = 'i';
1853 } elseif ( $state === 'i' ) {
1854 $output .= '</i><b>';
1855 $state = 'b';
1856 } elseif ( $state === 'bi' ) {
1857 $output .= '</i></b>';
1858 $state = '';
1859 } elseif ( $state === 'ib' ) {
1860 $output .= '</b></i>';
1861 $state = '';
1862 } elseif ( $state === 'both' ) {
1863 $output .= '<i><b>' . $buffer . '</b></i>';
1864 $state = '';
1865 } else { // ($state == '')
1866 $buffer = '';
1867 $state = 'both';
1868 }
1869 }
1870 }
1871 $i++;
1872 }
1873 // Now close all remaining tags. Notice that the order is important.
1874 if ( $state === 'b' || $state === 'ib' ) {
1875 $output .= '</b>';
1876 }
1877 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1878 $output .= '</i>';
1879 }
1880 if ( $state === 'bi' ) {
1881 $output .= '</b>';
1882 }
1883 // There might be lonely ''''', so make sure we have a buffer
1884 if ( $state === 'both' && $buffer ) {
1885 $output .= '<b><i>' . $buffer . '</i></b>';
1886 }
1887 return $output;
1888 }
1889
1903 public function replaceExternalLinks( $text ) {
1904 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1905 if ( $bits === false ) {
1906 throw new MWException( "PCRE needs to be compiled with "
1907 . "--enable-unicode-properties in order for MediaWiki to function" );
1908 }
1909 $s = array_shift( $bits );
1910
1911 $i = 0;
1912 while ( $i < count( $bits ) ) {
1913 $url = $bits[$i++];
1914 $i++; // protocol
1915 $text = $bits[$i++];
1916 $trail = $bits[$i++];
1917
1918 # The characters '<' and '>' (which were escaped by
1919 # removeHTMLtags()) should not be included in
1920 # URLs, per RFC 2396.
1921 $m2 = [];
1922 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1923 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1924 $url = substr( $url, 0, $m2[0][1] );
1925 }
1926
1927 # If the link text is an image URL, replace it with an <img> tag
1928 # This happened by accident in the original parser, but some people used it extensively
1929 $img = $this->maybeMakeExternalImage( $text );
1930 if ( $img !== false ) {
1931 $text = $img;
1932 }
1933
1934 $dtrail = '';
1935
1936 # Set linktype for CSS
1937 $linktype = 'text';
1938
1939 # No link text, e.g. [http://domain.tld/some.link]
1940 if ( $text == '' ) {
1941 # Autonumber
1942 $langObj = $this->getTargetLanguage();
1943 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1944 $linktype = 'autonumber';
1945 } else {
1946 # Have link text, e.g. [http://domain.tld/some.link text]s
1947 # Check for trail
1948 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1949 }
1950
1951 // Excluding protocol-relative URLs may avoid many false positives.
1952 if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
1953 $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
1954 }
1955
1956 $url = Sanitizer::cleanUrl( $url );
1957
1958 # Use the encoded URL
1959 # This means that users can paste URLs directly into the text
1960 # Funny characters like ö aren't valid in URLs anyway
1961 # This was changed in August 2004
1962 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1963 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1964
1965 # Register link in the output object.
1966 $this->mOutput->addExternalLink( $url );
1967 }
1968
1969 return $s;
1970 }
1971
1981 public static function getExternalLinkRel( $url = false, $title = null ) {
1983 $ns = $title ? $title->getNamespace() : false;
1984 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1986 ) {
1987 return 'nofollow';
1988 }
1989 return null;
1990 }
1991
2002 public function getExternalLinkAttribs( $url ) {
2003 $attribs = [];
2004 $rel = self::getExternalLinkRel( $url, $this->mTitle );
2005
2006 $target = $this->mOptions->getExternalLinkTarget();
2007 if ( $target ) {
2008 $attribs['target'] = $target;
2009 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2010 // T133507. New windows can navigate parent cross-origin.
2011 // Including noreferrer due to lacking browser
2012 // support of noopener. Eventually noreferrer should be removed.
2013 if ( $rel !== '' ) {
2014 $rel .= ' ';
2015 }
2016 $rel .= 'noreferrer noopener';
2017 }
2018 }
2019 $attribs['rel'] = $rel;
2020 return $attribs;
2021 }
2022
2032 public static function normalizeLinkUrl( $url ) {
2033 # Test for RFC 3986 IPv6 syntax
2034 $scheme = '[a-z][a-z0-9+.-]*:';
2035 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2036 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2037 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2038 IP::isValid( rawurldecode( $m[1] ) )
2039 ) {
2040 $isIPv6 = rawurldecode( $m[1] );
2041 } else {
2042 $isIPv6 = false;
2043 }
2044
2045 # Make sure unsafe characters are encoded
2046 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2047 function ( $m ) {
2048 return rawurlencode( $m[0] );
2049 },
2050 $url
2051 );
2052
2053 $ret = '';
2054 $end = strlen( $url );
2055
2056 # Fragment part - 'fragment'
2057 $start = strpos( $url, '#' );
2058 if ( $start !== false && $start < $end ) {
2059 $ret = self::normalizeUrlComponent(
2060 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2061 $end = $start;
2062 }
2063
2064 # Query part - 'query' minus &=+;
2065 $start = strpos( $url, '?' );
2066 if ( $start !== false && $start < $end ) {
2067 $ret = self::normalizeUrlComponent(
2068 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2069 $end = $start;
2070 }
2071
2072 # Scheme and path part - 'pchar'
2073 # (we assume no userinfo or encoded colons in the host)
2074 $ret = self::normalizeUrlComponent(
2075 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2076
2077 # Fix IPv6 syntax
2078 if ( $isIPv6 !== false ) {
2079 $ipv6Host = "%5B({$isIPv6})%5D";
2080 $ret = preg_replace(
2081 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2082 "$1[$2]",
2083 $ret
2084 );
2085 }
2086
2087 return $ret;
2088 }
2089
2090 private static function normalizeUrlComponent( $component, $unsafe ) {
2091 $callback = function ( $matches ) use ( $unsafe ) {
2092 $char = urldecode( $matches[0] );
2093 $ord = ord( $char );
2094 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2095 # Unescape it
2096 return $char;
2097 } else {
2098 # Leave it escaped, but use uppercase for a-f
2099 return strtoupper( $matches[0] );
2100 }
2101 };
2102 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2103 }
2104
2113 private function maybeMakeExternalImage( $url ) {
2114 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2115 $imagesexception = !empty( $imagesfrom );
2116 $text = false;
2117 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2118 if ( $imagesexception && is_array( $imagesfrom ) ) {
2119 $imagematch = false;
2120 foreach ( $imagesfrom as $match ) {
2121 if ( strpos( $url, $match ) === 0 ) {
2122 $imagematch = true;
2123 break;
2124 }
2125 }
2126 } elseif ( $imagesexception ) {
2127 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2128 } else {
2129 $imagematch = false;
2130 }
2131
2132 if ( $this->mOptions->getAllowExternalImages()
2133 || ( $imagesexception && $imagematch )
2134 ) {
2135 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2136 # Image found
2137 $text = Linker::makeExternalImage( $url );
2138 }
2139 }
2140 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2141 && preg_match( self::EXT_IMAGE_REGEX, $url )
2142 ) {
2143 $whitelist = explode(
2144 "\n",
2145 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2146 );
2147
2148 foreach ( $whitelist as $entry ) {
2149 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2150 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2151 continue;
2152 }
2153 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2154 # Image matches a whitelist entry
2155 $text = Linker::makeExternalImage( $url );
2156 break;
2157 }
2158 }
2159 }
2160 return $text;
2161 }
2162
2172 public function replaceInternalLinks( $s ) {
2173 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2174 return $s;
2175 }
2176
2185 public function replaceInternalLinks2( &$s ) {
2186 static $tc = false, $e1, $e1_img;
2187 # the % is needed to support urlencoded titles as well
2188 if ( !$tc ) {
2189 $tc = Title::legalChars() . '#%';
2190 # Match a link having the form [[namespace:link|alternate]]trail
2191 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2192 # Match cases where there is no "]]", which might still be images
2193 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2194 }
2195
2196 $holders = new LinkHolderArray( $this );
2197
2198 # split the entire text string on occurrences of [[
2199 $a = StringUtils::explode( '[[', ' ' . $s );
2200 # get the first element (all text up to first [[), and remove the space we added
2201 $s = $a->current();
2202 $a->next();
2203 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2204 $s = substr( $s, 1 );
2205
2206 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2207 $e2 = null;
2208 if ( $useLinkPrefixExtension ) {
2209 # Match the end of a line for a word that's not followed by whitespace,
2210 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2211 $charset = $this->contLang->linkPrefixCharset();
2212 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2213 }
2214
2215 if ( is_null( $this->mTitle ) ) {
2216 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2217 }
2218 $nottalk = !$this->mTitle->isTalkPage();
2219
2220 if ( $useLinkPrefixExtension ) {
2221 $m = [];
2222 if ( preg_match( $e2, $s, $m ) ) {
2223 $first_prefix = $m[2];
2224 } else {
2225 $first_prefix = false;
2226 }
2227 } else {
2228 $prefix = '';
2229 }
2230
2231 $useSubpages = $this->areSubpagesAllowed();
2232
2233 # Loop for each link
2234 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2235 # Check for excessive memory usage
2236 if ( $holders->isBig() ) {
2237 # Too big
2238 # Do the existence check, replace the link holders and clear the array
2239 $holders->replace( $s );
2240 $holders->clear();
2241 }
2242
2243 if ( $useLinkPrefixExtension ) {
2244 if ( preg_match( $e2, $s, $m ) ) {
2245 list( , $s, $prefix ) = $m;
2246 } else {
2247 $prefix = '';
2248 }
2249 # first link
2250 if ( $first_prefix ) {
2251 $prefix = $first_prefix;
2252 $first_prefix = false;
2253 }
2254 }
2255
2256 $might_be_img = false;
2257
2258 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2259 $text = $m[2];
2260 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2261 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2262 # the real problem is with the $e1 regex
2263 # See T1500.
2264 # Still some problems for cases where the ] is meant to be outside punctuation,
2265 # and no image is in sight. See T4095.
2266 if ( $text !== ''
2267 && substr( $m[3], 0, 1 ) === ']'
2268 && strpos( $text, '[' ) !== false
2269 ) {
2270 $text .= ']'; # so that replaceExternalLinks($text) works later
2271 $m[3] = substr( $m[3], 1 );
2272 }
2273 # fix up urlencoded title texts
2274 if ( strpos( $m[1], '%' ) !== false ) {
2275 # Should anchors '#' also be rejected?
2276 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2277 }
2278 $trail = $m[3];
2279 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2280 # Invalid, but might be an image with a link in its caption
2281 $might_be_img = true;
2282 $text = $m[2];
2283 if ( strpos( $m[1], '%' ) !== false ) {
2284 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2285 }
2286 $trail = "";
2287 } else { # Invalid form; output directly
2288 $s .= $prefix . '[[' . $line;
2289 continue;
2290 }
2291
2292 $origLink = ltrim( $m[1], ' ' );
2293
2294 # Don't allow internal links to pages containing
2295 # PROTO: where PROTO is a valid URL protocol; these
2296 # should be external links.
2297 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2298 $s .= $prefix . '[[' . $line;
2299 continue;
2300 }
2301
2302 # Make subpage if necessary
2303 if ( $useSubpages ) {
2304 $link = $this->maybeDoSubpageLink( $origLink, $text );
2305 } else {
2306 $link = $origLink;
2307 }
2308
2309 // \x7f isn't a default legal title char, so most likely strip
2310 // markers will force us into the "invalid form" path above. But,
2311 // just in case, let's assert that xmlish tags aren't valid in
2312 // the title position.
2313 $unstrip = $this->mStripState->killMarkers( $link );
2314 $noMarkers = ( $unstrip === $link );
2315
2316 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2317 if ( $nt === null ) {
2318 $s .= $prefix . '[[' . $line;
2319 continue;
2320 }
2321
2322 $ns = $nt->getNamespace();
2323 $iw = $nt->getInterwiki();
2324
2325 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2326
2327 if ( $might_be_img ) { # if this is actually an invalid link
2328 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2329 $found = false;
2330 while ( true ) {
2331 # look at the next 'line' to see if we can close it there
2332 $a->next();
2333 $next_line = $a->current();
2334 if ( $next_line === false || $next_line === null ) {
2335 break;
2336 }
2337 $m = explode( ']]', $next_line, 3 );
2338 if ( count( $m ) == 3 ) {
2339 # the first ]] closes the inner link, the second the image
2340 $found = true;
2341 $text .= "[[{$m[0]}]]{$m[1]}";
2342 $trail = $m[2];
2343 break;
2344 } elseif ( count( $m ) == 2 ) {
2345 # if there's exactly one ]] that's fine, we'll keep looking
2346 $text .= "[[{$m[0]}]]{$m[1]}";
2347 } else {
2348 # if $next_line is invalid too, we need look no further
2349 $text .= '[[' . $next_line;
2350 break;
2351 }
2352 }
2353 if ( !$found ) {
2354 # we couldn't find the end of this imageLink, so output it raw
2355 # but don't ignore what might be perfectly normal links in the text we've examined
2356 $holders->merge( $this->replaceInternalLinks2( $text ) );
2357 $s .= "{$prefix}[[$link|$text";
2358 # note: no $trail, because without an end, there *is* no trail
2359 continue;
2360 }
2361 } else { # it's not an image, so output it raw
2362 $s .= "{$prefix}[[$link|$text";
2363 # note: no $trail, because without an end, there *is* no trail
2364 continue;
2365 }
2366 }
2367
2368 $wasblank = ( $text == '' );
2369 if ( $wasblank ) {
2370 $text = $link;
2371 if ( !$noforce ) {
2372 # Strip off leading ':'
2373 $text = substr( $text, 1 );
2374 }
2375 } else {
2376 # T6598 madness. Handle the quotes only if they come from the alternate part
2377 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2378 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2379 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2380 $text = $this->doQuotes( $text );
2381 }
2382
2383 # Link not escaped by : , create the various objects
2384 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2385 # Interwikis
2386 if (
2387 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2388 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2389 in_array( $iw, $this->siteConfig->get( 'ExtraInterlanguageLinkPrefixes' ) )
2390 )
2391 ) {
2392 # T26502: filter duplicates
2393 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2394 $this->mLangLinkLanguages[$iw] = true;
2395 $this->mOutput->addLanguageLink( $nt->getFullText() );
2396 }
2397
2401 $s = rtrim( $s . $prefix ) . $trail; # T175416
2402 continue;
2403 }
2404
2405 if ( $ns == NS_FILE ) {
2406 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2407 if ( $wasblank ) {
2408 # if no parameters were passed, $text
2409 # becomes something like "File:Foo.png",
2410 # which we don't want to pass on to the
2411 # image generator
2412 $text = '';
2413 } else {
2414 # recursively parse links inside the image caption
2415 # actually, this will parse them in any other parameters, too,
2416 # but it might be hard to fix that, and it doesn't matter ATM
2417 $text = $this->replaceExternalLinks( $text );
2418 $holders->merge( $this->replaceInternalLinks2( $text ) );
2419 }
2420 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2421 $s .= $prefix . $this->armorLinks(
2422 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2423 continue;
2424 }
2425 } elseif ( $ns == NS_CATEGORY ) {
2429 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2430
2431 if ( $wasblank ) {
2432 $sortkey = $this->getDefaultSort();
2433 } else {
2434 $sortkey = $text;
2435 }
2436 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2437 $sortkey = str_replace( "\n", '', $sortkey );
2438 $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2439 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2440
2441 continue;
2442 }
2443 }
2444
2445 # Self-link checking. For some languages, variants of the title are checked in
2446 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2447 # for linking to a different variant.
2448 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2449 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2450 continue;
2451 }
2452
2453 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2454 # @todo FIXME: Should do batch file existence checks, see comment below
2455 if ( $ns == NS_MEDIA ) {
2456 # Give extensions a chance to select the file revision for us
2457 $options = [];
2458 $descQuery = false;
2459 Hooks::run( 'BeforeParserFetchFileAndTitle',
2460 [ $this, $nt, &$options, &$descQuery ] );
2461 # Fetch and register the file (file title may be different via hooks)
2462 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2463 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2464 $s .= $prefix . $this->armorLinks(
2465 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2466 continue;
2467 }
2468
2469 # Some titles, such as valid special pages or files in foreign repos, should
2470 # be shown as bluelinks even though they're not included in the page table
2471 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2472 # batch file existence checks for NS_FILE and NS_MEDIA
2473 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2474 $this->mOutput->addLink( $nt );
2475 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2476 } else {
2477 # Links will be added to the output link list after checking
2478 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2479 }
2480 }
2481 return $holders;
2482 }
2483
2497 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2498 list( $inside, $trail ) = Linker::splitTrail( $trail );
2499
2500 if ( $text == '' ) {
2501 $text = htmlspecialchars( $nt->getPrefixedText() );
2502 }
2503
2504 $link = $this->getLinkRenderer()->makeKnownLink(
2505 $nt, new HtmlArmor( "$prefix$text$inside" )
2506 );
2507
2508 return $this->armorLinks( $link ) . $trail;
2509 }
2510
2521 public function armorLinks( $text ) {
2522 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2523 self::MARKER_PREFIX . "NOPARSE$1", $text );
2524 }
2525
2530 public function areSubpagesAllowed() {
2531 # Some namespaces don't allow subpages
2532 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2533 }
2534
2543 public function maybeDoSubpageLink( $target, &$text ) {
2544 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2545 }
2546
2555 public function doBlockLevels( $text, $linestart ) {
2556 return BlockLevelPass::doBlockLevels( $text, $linestart );
2557 }
2558
2570 public function getVariableValue( $index, $frame = false ) {
2571 if ( is_null( $this->mTitle ) ) {
2572 // If no title set, bad things are going to happen
2573 // later. Title should always be set since this
2574 // should only be called in the middle of a parse
2575 // operation (but the unit-tests do funky stuff)
2576 throw new MWException( __METHOD__ . ' Should only be '
2577 . ' called while parsing (no title set)' );
2578 }
2579
2580 // Avoid PHP 7.1 warning from passing $this by reference
2581 $parser = $this;
2582
2587 if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] )
2588 && isset( $this->mVarCache[$index] )
2589 ) {
2590 return $this->mVarCache[$index];
2591 }
2592
2593 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2594 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2595
2596 // In miser mode, disable words that always cause double-parses on page save (T137900)
2597 static $slowRevWords = [ 'revisionid' => true ]; // @TODO: 'revisiontimestamp'
2598 if (
2599 isset( $slowRevWords[$index] ) &&
2600 $this->siteConfig->get( 'MiserMode' ) &&
2601 !$this->mOptions->getInterfaceMessage() &&
2602 // @TODO: disallow this word on all namespaces
2603 MWNamespace::isContent( $this->mTitle->getNamespace() )
2604 ) {
2605 return $this->mRevisionId ? '-' : '';
2606 };
2607
2608 $pageLang = $this->getFunctionLang();
2609
2610 switch ( $index ) {
2611 case '!':
2612 $value = '|';
2613 break;
2614 case 'currentmonth':
2615 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2616 break;
2617 case 'currentmonth1':
2618 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2619 break;
2620 case 'currentmonthname':
2621 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2622 break;
2623 case 'currentmonthnamegen':
2624 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2625 break;
2626 case 'currentmonthabbrev':
2627 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2628 break;
2629 case 'currentday':
2630 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2631 break;
2632 case 'currentday2':
2633 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2634 break;
2635 case 'localmonth':
2636 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2637 break;
2638 case 'localmonth1':
2639 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2640 break;
2641 case 'localmonthname':
2642 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2643 break;
2644 case 'localmonthnamegen':
2645 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2646 break;
2647 case 'localmonthabbrev':
2648 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2649 break;
2650 case 'localday':
2651 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2652 break;
2653 case 'localday2':
2654 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2655 break;
2656 case 'pagename':
2657 $value = wfEscapeWikiText( $this->mTitle->getText() );
2658 break;
2659 case 'pagenamee':
2660 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2661 break;
2662 case 'fullpagename':
2663 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2664 break;
2665 case 'fullpagenamee':
2666 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2667 break;
2668 case 'subpagename':
2669 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2670 break;
2671 case 'subpagenamee':
2672 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2673 break;
2674 case 'rootpagename':
2675 $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2676 break;
2677 case 'rootpagenamee':
2678 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2679 ' ',
2680 '_',
2681 $this->mTitle->getRootText()
2682 ) ) );
2683 break;
2684 case 'basepagename':
2685 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2686 break;
2687 case 'basepagenamee':
2688 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2689 ' ',
2690 '_',
2691 $this->mTitle->getBaseText()
2692 ) ) );
2693 break;
2694 case 'talkpagename':
2695 if ( $this->mTitle->canHaveTalkPage() ) {
2696 $talkPage = $this->mTitle->getTalkPage();
2697 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2698 } else {
2699 $value = '';
2700 }
2701 break;
2702 case 'talkpagenamee':
2703 if ( $this->mTitle->canHaveTalkPage() ) {
2704 $talkPage = $this->mTitle->getTalkPage();
2705 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2706 } else {
2707 $value = '';
2708 }
2709 break;
2710 case 'subjectpagename':
2711 $subjPage = $this->mTitle->getSubjectPage();
2712 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2713 break;
2714 case 'subjectpagenamee':
2715 $subjPage = $this->mTitle->getSubjectPage();
2716 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2717 break;
2718 case 'pageid': // requested in T25427
2719 $pageid = $this->getTitle()->getArticleID();
2720 if ( $pageid == 0 ) {
2721 # 0 means the page doesn't exist in the database,
2722 # which means the user is previewing a new page.
2723 # The vary-revision flag must be set, because the magic word
2724 # will have a different value once the page is saved.
2725 $this->mOutput->setFlag( 'vary-revision' );
2726 wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2727 }
2728 $value = $pageid ?: null;
2729 break;
2730 case 'revisionid':
2731 # Let the edit saving system know we should parse the page
2732 # *after* a revision ID has been assigned.
2733 $this->mOutput->setFlag( 'vary-revision-id' );
2734 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2735 $value = $this->mRevisionId;
2736
2737 if ( !$value ) {
2738 $rev = $this->getRevisionObject();
2739 if ( $rev ) {
2740 $value = $rev->getId();
2741 }
2742 }
2743
2744 if ( !$value ) {
2745 $value = $this->mOptions->getSpeculativeRevId();
2746 if ( $value ) {
2747 $this->mOutput->setSpeculativeRevIdUsed( $value );
2748 }
2749 }
2750 break;
2751 case 'revisionday':
2752 $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2753 break;
2754 case 'revisionday2':
2755 $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2756 break;
2757 case 'revisionmonth':
2758 $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2759 break;
2760 case 'revisionmonth1':
2761 $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2762 break;
2763 case 'revisionyear':
2764 $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
2765 break;
2766 case 'revisiontimestamp':
2767 # Let the edit saving system know we should parse the page
2768 # *after* a revision ID has been assigned. This is for null edits.
2769 $this->mOutput->setFlag( 'vary-revision' );
2770 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2771 $value = $this->getRevisionTimestamp();
2772 break;
2773 case 'revisionuser':
2774 # Let the edit saving system know we should parse the page
2775 # *after* a revision ID has been assigned for null edits.
2776 $this->mOutput->setFlag( 'vary-user' );
2777 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2778 $value = $this->getRevisionUser();
2779 break;
2780 case 'revisionsize':
2781 $value = $this->getRevisionSize();
2782 break;
2783 case 'namespace':
2784 $value = str_replace( '_', ' ',
2785 $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
2786 break;
2787 case 'namespacee':
2788 $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
2789 break;
2790 case 'namespacenumber':
2791 $value = $this->mTitle->getNamespace();
2792 break;
2793 case 'talkspace':
2794 $value = $this->mTitle->canHaveTalkPage()
2795 ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2796 : '';
2797 break;
2798 case 'talkspacee':
2799 $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2800 break;
2801 case 'subjectspace':
2802 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2803 break;
2804 case 'subjectspacee':
2805 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2806 break;
2807 case 'currentdayname':
2808 $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2809 break;
2810 case 'currentyear':
2811 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2812 break;
2813 case 'currenttime':
2814 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2815 break;
2816 case 'currenthour':
2817 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2818 break;
2819 case 'currentweek':
2820 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2821 # int to remove the padding
2822 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2823 break;
2824 case 'currentdow':
2825 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2826 break;
2827 case 'localdayname':
2828 $value = $pageLang->getWeekdayName(
2829 (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2830 );
2831 break;
2832 case 'localyear':
2833 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2834 break;
2835 case 'localtime':
2836 $value = $pageLang->time(
2837 MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2838 false,
2839 false
2840 );
2841 break;
2842 case 'localhour':
2843 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2844 break;
2845 case 'localweek':
2846 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2847 # int to remove the padding
2848 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2849 break;
2850 case 'localdow':
2851 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2852 break;
2853 case 'numberofarticles':
2854 $value = $pageLang->formatNum( SiteStats::articles() );
2855 break;
2856 case 'numberoffiles':
2857 $value = $pageLang->formatNum( SiteStats::images() );
2858 break;
2859 case 'numberofusers':
2860 $value = $pageLang->formatNum( SiteStats::users() );
2861 break;
2862 case 'numberofactiveusers':
2863 $value = $pageLang->formatNum( SiteStats::activeUsers() );
2864 break;
2865 case 'numberofpages':
2866 $value = $pageLang->formatNum( SiteStats::pages() );
2867 break;
2868 case 'numberofadmins':
2869 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2870 break;
2871 case 'numberofedits':
2872 $value = $pageLang->formatNum( SiteStats::edits() );
2873 break;
2874 case 'currenttimestamp':
2875 $value = wfTimestamp( TS_MW, $ts );
2876 break;
2877 case 'localtimestamp':
2878 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2879 break;
2880 case 'currentversion':
2882 break;
2883 case 'articlepath':
2884 return $this->siteConfig->get( 'ArticlePath' );
2885 case 'sitename':
2886 return $this->siteConfig->get( 'Sitename' );
2887 case 'server':
2888 return $this->siteConfig->get( 'Server' );
2889 case 'servername':
2890 return $this->siteConfig->get( 'ServerName' );
2891 case 'scriptpath':
2892 return $this->siteConfig->get( 'ScriptPath' );
2893 case 'stylepath':
2894 return $this->siteConfig->get( 'StylePath' );
2895 case 'directionmark':
2896 return $pageLang->getDirMark();
2897 case 'contentlanguage':
2898 return $this->siteConfig->get( 'LanguageCode' );
2899 case 'pagelanguage':
2900 $value = $pageLang->getCode();
2901 break;
2902 case 'cascadingsources':
2904 break;
2905 default:
2906 $ret = null;
2907 Hooks::run(
2908 'ParserGetVariableValueSwitch',
2909 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2910 );
2911
2912 return $ret;
2913 }
2914
2915 if ( $index ) {
2916 $this->mVarCache[$index] = $value;
2917 }
2918
2919 return $value;
2920 }
2921
2929 private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
2930 # Get the timezone-adjusted timestamp to be used for this revision
2931 $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
2932 # Possibly set vary-revision if there is not yet an associated revision
2933 if ( !$this->getRevisionObject() ) {
2934 # Get the timezone-adjusted timestamp $mtts seconds in the future
2935 $resThen = substr(
2936 $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
2937 $start,
2938 $len
2939 );
2940
2941 if ( $resNow !== $resThen ) {
2942 # Let the edit saving system know we should parse the page
2943 # *after* a revision ID has been assigned. This is for null edits.
2944 $this->mOutput->setFlag( 'vary-revision' );
2945 wfDebug( __METHOD__ . ": $variable used, setting vary-revision...\n" );
2946 }
2947 }
2948
2949 return $resNow;
2950 }
2951
2957 public function initialiseVariables() {
2958 $variableIDs = $this->magicWordFactory->getVariableIDs();
2959 $substIDs = $this->magicWordFactory->getSubstIDs();
2960
2961 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2962 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2963 }
2964
2987 public function preprocessToDom( $text, $flags = 0 ) {
2988 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2989 return $dom;
2990 }
2991
2999 public static function splitWhitespace( $s ) {
3000 $ltrimmed = ltrim( $s );
3001 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3002 $trimmed = rtrim( $ltrimmed );
3003 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3004 if ( $diff > 0 ) {
3005 $w2 = substr( $ltrimmed, -$diff );
3006 } else {
3007 $w2 = '';
3008 }
3009 return [ $w1, $trimmed, $w2 ];
3010 }
3011
3032 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3033 # Is there any text? Also, Prevent too big inclusions!
3034 $textSize = strlen( $text );
3035 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3036 return $text;
3037 }
3038
3039 if ( $frame === false ) {
3040 $frame = $this->getPreprocessor()->newFrame();
3041 } elseif ( !( $frame instanceof PPFrame ) ) {
3042 wfDebug( __METHOD__ . " called using plain parameters instead of "
3043 . "a PPFrame instance. Creating custom frame.\n" );
3044 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3045 }
3046
3047 $dom = $this->preprocessToDom( $text );
3048 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3049 $text = $frame->expand( $dom, $flags );
3050
3051 return $text;
3052 }
3053
3061 public static function createAssocArgs( $args ) {
3062 $assocArgs = [];
3063 $index = 1;
3064 foreach ( $args as $arg ) {
3065 $eqpos = strpos( $arg, '=' );
3066 if ( $eqpos === false ) {
3067 $assocArgs[$index++] = $arg;
3068 } else {
3069 $name = trim( substr( $arg, 0, $eqpos ) );
3070 $value = trim( substr( $arg, $eqpos + 1 ) );
3071 if ( $value === false ) {
3072 $value = '';
3073 }
3074 if ( $name !== false ) {
3075 $assocArgs[$name] = $value;
3076 }
3077 }
3078 }
3079
3080 return $assocArgs;
3081 }
3082
3109 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3110 # does no harm if $current and $max are present but are unnecessary for the message
3111 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3112 # only during preview, and that would split the parser cache unnecessarily.
3113 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3114 ->text();
3115 $this->mOutput->addWarning( $warning );
3116 $this->addTrackingCategory( "$limitationType-category" );
3117 }
3118
3131 public function braceSubstitution( $piece, $frame ) {
3132 // Flags
3133
3134 // $text has been filled
3135 $found = false;
3136 // wiki markup in $text should be escaped
3137 $nowiki = false;
3138 // $text is HTML, armour it against wikitext transformation
3139 $isHTML = false;
3140 // Force interwiki transclusion to be done in raw mode not rendered
3141 $forceRawInterwiki = false;
3142 // $text is a DOM node needing expansion in a child frame
3143 $isChildObj = false;
3144 // $text is a DOM node needing expansion in the current frame
3145 $isLocalObj = false;
3146
3147 # Title object, where $text came from
3148 $title = false;
3149
3150 # $part1 is the bit before the first |, and must contain only title characters.
3151 # Various prefixes will be stripped from it later.
3152 $titleWithSpaces = $frame->expand( $piece['title'] );
3153 $part1 = trim( $titleWithSpaces );
3154 $titleText = false;
3155
3156 # Original title text preserved for various purposes
3157 $originalTitle = $part1;
3158
3159 # $args is a list of argument nodes, starting from index 0, not including $part1
3160 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3161 # below won't work b/c this $args isn't an object
3162 $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
3163
3164 $profileSection = null; // profile templates
3165
3166 # SUBST
3167 if ( !$found ) {
3168 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3169
3170 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3171 # Decide whether to expand template or keep wikitext as-is.
3172 if ( $this->ot['wiki'] ) {
3173 if ( $substMatch === false ) {
3174 $literal = true; # literal when in PST with no prefix
3175 } else {
3176 $literal = false; # expand when in PST with subst: or safesubst:
3177 }
3178 } else {
3179 if ( $substMatch == 'subst' ) {
3180 $literal = true; # literal when not in PST with plain subst:
3181 } else {
3182 $literal = false; # expand when not in PST with safesubst: or no prefix
3183 }
3184 }
3185 if ( $literal ) {
3186 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3187 $isLocalObj = true;
3188 $found = true;
3189 }
3190 }
3191
3192 # Variables
3193 if ( !$found && $args->getLength() == 0 ) {
3194 $id = $this->mVariables->matchStartToEnd( $part1 );
3195 if ( $id !== false ) {
3196 $text = $this->getVariableValue( $id, $frame );
3197 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3198 $this->mOutput->updateCacheExpiry(
3199 $this->magicWordFactory->getCacheTTL( $id ) );
3200 }
3201 $found = true;
3202 }
3203 }
3204
3205 # MSG, MSGNW and RAW
3206 if ( !$found ) {
3207 # Check for MSGNW:
3208 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3209 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3210 $nowiki = true;
3211 } else {
3212 # Remove obsolete MSG:
3213 $mwMsg = $this->magicWordFactory->get( 'msg' );
3214 $mwMsg->matchStartAndRemove( $part1 );
3215 }
3216
3217 # Check for RAW:
3218 $mwRaw = $this->magicWordFactory->get( 'raw' );
3219 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3220 $forceRawInterwiki = true;
3221 }
3222 }
3223
3224 # Parser functions
3225 if ( !$found ) {
3226 $colonPos = strpos( $part1, ':' );
3227 if ( $colonPos !== false ) {
3228 $func = substr( $part1, 0, $colonPos );
3229 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3230 $argsLength = $args->getLength();
3231 for ( $i = 0; $i < $argsLength; $i++ ) {
3232 $funcArgs[] = $args->item( $i );
3233 }
3234
3235 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3236
3237 // Extract any forwarded flags
3238 if ( isset( $result['title'] ) ) {
3239 $title = $result['title'];
3240 }
3241 if ( isset( $result['found'] ) ) {
3242 $found = $result['found'];
3243 }
3244 if ( array_key_exists( 'text', $result ) ) {
3245 // a string or null
3246 $text = $result['text'];
3247 }
3248 if ( isset( $result['nowiki'] ) ) {
3249 $nowiki = $result['nowiki'];
3250 }
3251 if ( isset( $result['isHTML'] ) ) {
3252 $isHTML = $result['isHTML'];
3253 }
3254 if ( isset( $result['forceRawInterwiki'] ) ) {
3255 $forceRawInterwiki = $result['forceRawInterwiki'];
3256 }
3257 if ( isset( $result['isChildObj'] ) ) {
3258 $isChildObj = $result['isChildObj'];
3259 }
3260 if ( isset( $result['isLocalObj'] ) ) {
3261 $isLocalObj = $result['isLocalObj'];
3262 }
3263 }
3264 }
3265
3266 # Finish mangling title and then check for loops.
3267 # Set $title to a Title object and $titleText to the PDBK
3268 if ( !$found ) {
3269 $ns = NS_TEMPLATE;
3270 # Split the title into page and subpage
3271 $subpage = '';
3272 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3273 if ( $part1 !== $relative ) {
3274 $part1 = $relative;
3275 $ns = $this->mTitle->getNamespace();
3276 }
3277 $title = Title::newFromText( $part1, $ns );
3278 if ( $title ) {
3279 $titleText = $title->getPrefixedText();
3280 # Check for language variants if the template is not found
3281 if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3282 $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
3283 }
3284 # Do recursion depth check
3285 $limit = $this->mOptions->getMaxTemplateDepth();
3286 if ( $frame->depth >= $limit ) {
3287 $found = true;
3288 $text = '<span class="error">'
3289 . wfMessage( 'parser-template-recursion-depth-warning' )
3290 ->numParams( $limit )->inContentLanguage()->text()
3291 . '</span>';
3292 }
3293 }
3294 }
3295
3296 # Load from database
3297 if ( !$found && $title ) {
3298 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3299 if ( !$title->isExternal() ) {
3300 if ( $title->isSpecialPage()
3301 && $this->mOptions->getAllowSpecialInclusion()
3302 && $this->ot['html']
3303 ) {
3304 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3305 // Pass the template arguments as URL parameters.
3306 // "uselang" will have no effect since the Language object
3307 // is forced to the one defined in ParserOptions.
3308 $pageArgs = [];
3309 $argsLength = $args->getLength();
3310 for ( $i = 0; $i < $argsLength; $i++ ) {
3311 $bits = $args->item( $i )->splitArg();
3312 if ( strval( $bits['index'] ) === '' ) {
3313 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3314 $value = trim( $frame->expand( $bits['value'] ) );
3315 $pageArgs[$name] = $value;
3316 }
3317 }
3318
3319 // Create a new context to execute the special page
3321 $context->setTitle( $title );
3322 $context->setRequest( new FauxRequest( $pageArgs ) );
3323 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3324 $context->setUser( $this->getUser() );
3325 } else {
3326 // If this page is cached, then we better not be per user.
3327 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3328 }
3329 $context->setLanguage( $this->mOptions->getUserLangObj() );
3330 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3331 if ( $ret ) {
3332 $text = $context->getOutput()->getHTML();
3333 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3334 $found = true;
3335 $isHTML = true;
3336 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3337 $this->mOutput->updateRuntimeAdaptiveExpiry(
3338 $specialPage->maxIncludeCacheTime()
3339 );
3340 }
3341 }
3342 } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3343 $found = false; # access denied
3344 wfDebug( __METHOD__ . ": template inclusion denied for " .
3345 $title->getPrefixedDBkey() . "\n" );
3346 } else {
3347 list( $text, $title ) = $this->getTemplateDom( $title );
3348 if ( $text !== false ) {
3349 $found = true;
3350 $isChildObj = true;
3351 }
3352 }
3353
3354 # If the title is valid but undisplayable, make a link to it
3355 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3356 $text = "[[:$titleText]]";
3357 $found = true;
3358 }
3359 } elseif ( $title->isTrans() ) {
3360 # Interwiki transclusion
3361 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3362 $text = $this->interwikiTransclude( $title, 'render' );
3363 $isHTML = true;
3364 } else {
3365 $text = $this->interwikiTransclude( $title, 'raw' );
3366 # Preprocess it like a template
3367 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3368 $isChildObj = true;
3369 }
3370 $found = true;
3371 }
3372
3373 # Do infinite loop check
3374 # This has to be done after redirect resolution to avoid infinite loops via redirects
3375 if ( !$frame->loopCheck( $title ) ) {
3376 $found = true;
3377 $text = '<span class="error">'
3378 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3379 . '</span>';
3380 $this->addTrackingCategory( 'template-loop-category' );
3381 $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3382 wfEscapeWikiText( $titleText ) )->text() );
3383 wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3384 }
3385 }
3386
3387 # If we haven't found text to substitute by now, we're done
3388 # Recover the source wikitext and return it
3389 if ( !$found ) {
3390 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3391 if ( $profileSection ) {
3392 $this->mProfiler->scopedProfileOut( $profileSection );
3393 }
3394 return [ 'object' => $text ];
3395 }
3396
3397 # Expand DOM-style return values in a child frame
3398 if ( $isChildObj ) {
3399 # Clean up argument array
3400 $newFrame = $frame->newChild( $args, $title );
3401
3402 if ( $nowiki ) {
3403 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3404 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3405 # Expansion is eligible for the empty-frame cache
3406 $text = $newFrame->cachedExpand( $titleText, $text );
3407 } else {
3408 # Uncached expansion
3409 $text = $newFrame->expand( $text );
3410 }
3411 }
3412 if ( $isLocalObj && $nowiki ) {
3413 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3414 $isLocalObj = false;
3415 }
3416
3417 if ( $profileSection ) {
3418 $this->mProfiler->scopedProfileOut( $profileSection );
3419 }
3420
3421 # Replace raw HTML by a placeholder
3422 if ( $isHTML ) {
3423 $text = $this->insertStripItem( $text );
3424 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3425 # Escape nowiki-style return values
3426 $text = wfEscapeWikiText( $text );
3427 } elseif ( is_string( $text )
3428 && !$piece['lineStart']
3429 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3430 ) {
3431 # T2529: if the template begins with a table or block-level
3432 # element, it should be treated as beginning a new line.
3433 # This behavior is somewhat controversial.
3434 $text = "\n" . $text;
3435 }
3436
3437 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3438 # Error, oversize inclusion
3439 if ( $titleText !== false ) {
3440 # Make a working, properly escaped link if possible (T25588)
3441 $text = "[[:$titleText]]";
3442 } else {
3443 # This will probably not be a working link, but at least it may
3444 # provide some hint of where the problem is
3445 preg_replace( '/^:/', '', $originalTitle );
3446 $text = "[[:$originalTitle]]";
3447 }
3448 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3449 . 'post-expand include size too large -->' );
3450 $this->limitationWarn( 'post-expand-template-inclusion' );
3451 }
3452
3453 if ( $isLocalObj ) {
3454 $ret = [ 'object' => $text ];
3455 } else {
3456 $ret = [ 'text' => $text ];
3457 }
3458
3459 return $ret;
3460 }
3461
3481 public function callParserFunction( $frame, $function, array $args = [] ) {
3482 # Case sensitive functions
3483 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3484 $function = $this->mFunctionSynonyms[1][$function];
3485 } else {
3486 # Case insensitive functions
3487 $function = $this->contLang->lc( $function );
3488 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3489 $function = $this->mFunctionSynonyms[0][$function];
3490 } else {
3491 return [ 'found' => false ];
3492 }
3493 }
3494
3495 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3496
3497 // Avoid PHP 7.1 warning from passing $this by reference
3498 $parser = $this;
3499
3500 $allArgs = [ &$parser ];
3501 if ( $flags & self::SFH_OBJECT_ARGS ) {
3502 # Convert arguments to PPNodes and collect for appending to $allArgs
3503 $funcArgs = [];
3504 foreach ( $args as $k => $v ) {
3505 if ( $v instanceof PPNode || $k === 0 ) {
3506 $funcArgs[] = $v;
3507 } else {
3508 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3509 }
3510 }
3511
3512 # Add a frame parameter, and pass the arguments as an array
3513 $allArgs[] = $frame;
3514 $allArgs[] = $funcArgs;
3515 } else {
3516 # Convert arguments to plain text and append to $allArgs
3517 foreach ( $args as $k => $v ) {
3518 if ( $v instanceof PPNode ) {
3519 $allArgs[] = trim( $frame->expand( $v ) );
3520 } elseif ( is_int( $k ) && $k >= 0 ) {
3521 $allArgs[] = trim( $v );
3522 } else {
3523 $allArgs[] = trim( "$k=$v" );
3524 }
3525 }
3526 }
3527
3528 $result = $callback( ...$allArgs );
3529
3530 # The interface for function hooks allows them to return a wikitext
3531 # string or an array containing the string and any flags. This mungs
3532 # things around to match what this method should return.
3533 if ( !is_array( $result ) ) {
3534 $result = [
3535 'found' => true,
3536 'text' => $result,
3537 ];
3538 } else {
3539 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3540 $result['text'] = $result[0];
3541 }
3542 unset( $result[0] );
3543 $result += [
3544 'found' => true,
3545 ];
3546 }
3547
3548 $noparse = true;
3549 $preprocessFlags = 0;
3550 if ( isset( $result['noparse'] ) ) {
3551 $noparse = $result['noparse'];
3552 }
3553 if ( isset( $result['preprocessFlags'] ) ) {
3554 $preprocessFlags = $result['preprocessFlags'];
3555 }
3556
3557 if ( !$noparse ) {
3558 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3559 $result['isChildObj'] = true;
3560 }
3561
3562 return $result;
3563 }
3564
3573 public function getTemplateDom( $title ) {
3574 $cacheTitle = $title;
3575 $titleText = $title->getPrefixedDBkey();
3576
3577 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3578 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3579 $title = Title::makeTitle( $ns, $dbk );
3580 $titleText = $title->getPrefixedDBkey();
3581 }
3582 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3583 return [ $this->mTplDomCache[$titleText], $title ];
3584 }
3585
3586 # Cache miss, go to the database
3587 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3588
3589 if ( $text === false ) {
3590 $this->mTplDomCache[$titleText] = false;
3591 return [ false, $title ];
3592 }
3593
3594 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3595 $this->mTplDomCache[$titleText] = $dom;
3596
3597 if ( !$title->equals( $cacheTitle ) ) {
3598 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3599 [ $title->getNamespace(), $title->getDBkey() ];
3600 }
3601
3602 return [ $dom, $title ];
3603 }
3604
3616 public function fetchCurrentRevisionOfTitle( $title ) {
3617 $cacheKey = $title->getPrefixedDBkey();
3618 if ( !$this->currentRevisionCache ) {
3619 $this->currentRevisionCache = new MapCacheLRU( 100 );
3620 }
3621 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3622 $this->currentRevisionCache->set( $cacheKey,
3623 // Defaults to Parser::statelessFetchRevision()
3624 call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3625 );
3626 }
3627 return $this->currentRevisionCache->get( $cacheKey );
3628 }
3629
3639 public static function statelessFetchRevision( Title $title, $parser = false ) {
3641
3642 return $rev;
3643 }
3644
3650 public function fetchTemplateAndTitle( $title ) {
3651 // Defaults to Parser::statelessFetchTemplate()
3652 $templateCb = $this->mOptions->getTemplateCallback();
3653 $stuff = call_user_func( $templateCb, $title, $this );
3654 // We use U+007F DELETE to distinguish strip markers from regular text.
3655 $text = $stuff['text'];
3656 if ( is_string( $stuff['text'] ) ) {
3657 $text = strtr( $text, "\x7f", "?" );
3658 }
3659 $finalTitle = $stuff['finalTitle'] ?? $title;
3660 if ( isset( $stuff['deps'] ) ) {
3661 foreach ( $stuff['deps'] as $dep ) {
3662 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3663 if ( $dep['title']->equals( $this->getTitle() ) ) {
3664 // If we transclude ourselves, the final result
3665 // will change based on the new version of the page
3666 $this->mOutput->setFlag( 'vary-revision' );
3667 }
3668 }
3669 }
3670 return [ $text, $finalTitle ];
3671 }
3672
3678 public function fetchTemplate( $title ) {
3679 return $this->fetchTemplateAndTitle( $title )[0];
3680 }
3681
3691 public static function statelessFetchTemplate( $title, $parser = false ) {
3692 $text = $skip = false;
3693 $finalTitle = $title;
3694 $deps = [];
3695
3696 # Loop to fetch the article, with up to 1 redirect
3697 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3698 # Give extensions a chance to select the revision instead
3699 $id = false; # Assume current
3700 Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3701 [ $parser, $title, &$skip, &$id ] );
3702
3703 if ( $skip ) {
3704 $text = false;
3705 $deps[] = [
3706 'title' => $title,
3707 'page_id' => $title->getArticleID(),
3708 'rev_id' => null
3709 ];
3710 break;
3711 }
3712 # Get the revision
3713 if ( $id ) {
3714 $rev = Revision::newFromId( $id );
3715 } elseif ( $parser ) {
3716 $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3717 } else {
3719 }
3720 $rev_id = $rev ? $rev->getId() : 0;
3721 # If there is no current revision, there is no page
3722 if ( $id === false && !$rev ) {
3723 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3724 $linkCache->addBadLinkObj( $title );
3725 }
3726
3727 $deps[] = [
3728 'title' => $title,
3729 'page_id' => $title->getArticleID(),
3730 'rev_id' => $rev_id ];
3731 if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3732 # We fetched a rev from a different title; register it too...
3733 $deps[] = [
3734 'title' => $rev->getTitle(),
3735 'page_id' => $rev->getPage(),
3736 'rev_id' => $rev_id ];
3737 }
3738
3739 if ( $rev ) {
3740 $content = $rev->getContent();
3741 $text = $content ? $content->getWikitextForTransclusion() : null;
3742
3743 Hooks::run( 'ParserFetchTemplate',
3744 [ $parser, $title, $rev, &$text, &$deps ] );
3745
3746 if ( $text === false || $text === null ) {
3747 $text = false;
3748 break;
3749 }
3750 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3751 $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3752 lcfirst( $title->getText() ) )->inContentLanguage();
3753 if ( !$message->exists() ) {
3754 $text = false;
3755 break;
3756 }
3757 $content = $message->content();
3758 $text = $message->plain();
3759 } else {
3760 break;
3761 }
3762 if ( !$content ) {
3763 break;
3764 }
3765 # Redirect?
3766 $finalTitle = $title;
3767 $title = $content->getRedirectTarget();
3768 }
3769 return [
3770 'text' => $text,
3771 'finalTitle' => $finalTitle,
3772 'deps' => $deps ];
3773 }
3774
3783 public function fetchFile( $title, $options = [] ) {
3784 wfDeprecated( __METHOD__, '1.32' );
3785 return $this->fetchFileAndTitle( $title, $options )[0];
3786 }
3787
3795 public function fetchFileAndTitle( $title, $options = [] ) {
3796 $file = $this->fetchFileNoRegister( $title, $options );
3797
3798 $time = $file ? $file->getTimestamp() : false;
3799 $sha1 = $file ? $file->getSha1() : false;
3800 # Register the file as a dependency...
3801 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3802 if ( $file && !$title->equals( $file->getTitle() ) ) {
3803 # Update fetched file title
3804 $title = $file->getTitle();
3805 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3806 }
3807 return [ $file, $title ];
3808 }
3809
3820 protected function fetchFileNoRegister( $title, $options = [] ) {
3821 if ( isset( $options['broken'] ) ) {
3822 $file = false; // broken thumbnail forced by hook
3823 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3824 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3825 } else { // get by (name,timestamp)
3827 }
3828 return $file;
3829 }
3830
3839 public function interwikiTransclude( $title, $action ) {
3840 if ( !$this->siteConfig->get( 'EnableScaryTranscluding' ) ) {
3841 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3842 }
3843
3844 $url = $title->getFullURL( [ 'action' => $action ] );
3845 if ( strlen( $url ) > 1024 ) {
3846 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3847 }
3848
3849 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3850
3851 $fname = __METHOD__;
3852 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3853
3854 $data = $cache->getWithSetCallback(
3855 $cache->makeGlobalKey(
3856 'interwiki-transclude',
3857 ( $wikiId !== false ) ? $wikiId : 'external',
3858 sha1( $url )
3859 ),
3860 $this->siteConfig->get( 'TranscludeCacheExpiry' ),
3861 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3862 $req = MWHttpRequest::factory( $url, [], $fname );
3863
3864 $status = $req->execute(); // Status object
3865 if ( !$status->isOK() ) {
3866 $ttl = $cache::TTL_UNCACHEABLE;
3867 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3868 $ttl = min( $cache::TTL_LAGGED, $ttl );
3869 }
3870
3871 return [
3872 'text' => $status->isOK() ? $req->getContent() : null,
3873 'code' => $req->getStatus()
3874 ];
3875 },
3876 [
3877 'checkKeys' => ( $wikiId !== false )
3878 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3879 : [],
3880 'pcGroup' => 'interwiki-transclude:5',
3881 'pcTTL' => $cache::TTL_PROC_LONG
3882 ]
3883 );
3884
3885 if ( is_string( $data['text'] ) ) {
3886 $text = $data['text'];
3887 } elseif ( $data['code'] != 200 ) {
3888 // Though we failed to fetch the content, this status is useless.
3889 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3890 ->params( $url, $data['code'] )->inContentLanguage()->text();
3891 } else {
3892 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3893 }
3894
3895 return $text;
3896 }
3897
3907 public function argSubstitution( $piece, $frame ) {
3908 $error = false;
3909 $parts = $piece['parts'];
3910 $nameWithSpaces = $frame->expand( $piece['title'] );
3911 $argName = trim( $nameWithSpaces );
3912 $object = false;
3913 $text = $frame->getArgument( $argName );
3914 if ( $text === false && $parts->getLength() > 0
3915 && ( $this->ot['html']
3916 || $this->ot['pre']
3917 || ( $this->ot['wiki'] && $frame->isTemplate() )
3918 )
3919 ) {
3920 # No match in frame, use the supplied default
3921 $object = $parts->item( 0 )->getChildren();
3922 }
3923 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3924 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3925 $this->limitationWarn( 'post-expand-template-argument' );
3926 }
3927
3928 if ( $text === false && $object === false ) {
3929 # No match anywhere
3930 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3931 }
3932 if ( $error !== false ) {
3933 $text .= $error;
3934 }
3935 if ( $object !== false ) {
3936 $ret = [ 'object' => $object ];
3937 } else {
3938 $ret = [ 'text' => $text ];
3939 }
3940
3941 return $ret;
3942 }
3943
3959 public function extensionSubstitution( $params, $frame ) {
3960 static $errorStr = '<span class="error">';
3961 static $errorLen = 20;
3962
3963 $name = $frame->expand( $params['name'] );
3964 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3965 // Probably expansion depth or node count exceeded. Just punt the
3966 // error up.
3967 return $name;
3968 }
3969
3970 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3971 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3972 // See above
3973 return $attrText;
3974 }
3975
3976 // We can't safely check if the expansion for $content resulted in an
3977 // error, because the content could happen to be the error string
3978 // (T149622).
3979 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3980
3981 $marker = self::MARKER_PREFIX . "-$name-"
3982 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3983
3984 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3985 ( $this->ot['html'] || $this->ot['pre'] );
3986 if ( $isFunctionTag ) {
3987 $markerType = 'none';
3988 } else {
3989 $markerType = 'general';
3990 }
3991 if ( $this->ot['html'] || $isFunctionTag ) {
3992 $name = strtolower( $name );
3993 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3994 if ( isset( $params['attributes'] ) ) {
3995 $attributes += $params['attributes'];
3996 }
3997
3998 if ( isset( $this->mTagHooks[$name] ) ) {
3999 $output = call_user_func_array( $this->mTagHooks[$name],
4000 [ $content, $attributes, $this, $frame ] );
4001 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4002 list( $callback, ) = $this->mFunctionTagHooks[$name];
4003
4004 // Avoid PHP 7.1 warning from passing $this by reference
4005 $parser = $this;
4006 $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
4007 } else {
4008 $output = '<span class="error">Invalid tag extension name: ' .
4009 htmlspecialchars( $name ) . '</span>';
4010 }
4011
4012 if ( is_array( $output ) ) {
4013 // Extract flags
4014 $flags = $output;
4015 $output = $flags[0];
4016 if ( isset( $flags['markerType'] ) ) {
4017 $markerType = $flags['markerType'];
4018 }
4019 }
4020 } else {
4021 if ( is_null( $attrText ) ) {
4022 $attrText = '';
4023 }
4024 if ( isset( $params['attributes'] ) ) {
4025 foreach ( $params['attributes'] as $attrName => $attrValue ) {
4026 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4027 htmlspecialchars( $attrValue ) . '"';
4028 }
4029 }
4030 if ( $content === null ) {
4031 $output = "<$name$attrText/>";
4032 } else {
4033 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4034 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4035 // See above
4036 return $close;
4037 }
4038 $output = "<$name$attrText>$content$close";
4039 }
4040 }
4041
4042 if ( $markerType === 'none' ) {
4043 return $output;
4044 } elseif ( $markerType === 'nowiki' ) {
4045 $this->mStripState->addNoWiki( $marker, $output );
4046 } elseif ( $markerType === 'general' ) {
4047 $this->mStripState->addGeneral( $marker, $output );
4048 } else {
4049 throw new MWException( __METHOD__ . ': invalid marker type' );
4050 }
4051 return $marker;
4052 }
4053
4061 public function incrementIncludeSize( $type, $size ) {
4062 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4063 return false;
4064 } else {
4065 $this->mIncludeSizes[$type] += $size;
4066 return true;
4067 }
4068 }
4069
4076 $this->mExpensiveFunctionCount++;
4077 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4078 }
4079
4088 public function doDoubleUnderscore( $text ) {
4089 # The position of __TOC__ needs to be recorded
4090 $mw = $this->magicWordFactory->get( 'toc' );
4091 if ( $mw->match( $text ) ) {
4092 $this->mShowToc = true;
4093 $this->mForceTocPosition = true;
4094
4095 # Set a placeholder. At the end we'll fill it in with the TOC.
4096 $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4097
4098 # Only keep the first one.
4099 $text = $mw->replace( '', $text );
4100 }
4101
4102 # Now match and remove the rest of them
4103 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4104 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4105
4106 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4107 $this->mOutput->mNoGallery = true;
4108 }
4109 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4110 $this->mShowToc = false;
4111 }
4112 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4113 && $this->mTitle->getNamespace() == NS_CATEGORY
4114 ) {
4115 $this->addTrackingCategory( 'hidden-category-category' );
4116 }
4117 # (T10068) Allow control over whether robots index a page.
4118 # __INDEX__ always overrides __NOINDEX__, see T16899
4119 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4120 $this->mOutput->setIndexPolicy( 'noindex' );
4121 $this->addTrackingCategory( 'noindex-category' );
4122 }
4123 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4124 $this->mOutput->setIndexPolicy( 'index' );
4125 $this->addTrackingCategory( 'index-category' );
4126 }
4127
4128 # Cache all double underscores in the database
4129 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4130 $this->mOutput->setProperty( $key, '' );
4131 }
4132
4133 return $text;
4134 }
4135
4141 public function addTrackingCategory( $msg ) {
4142 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4143 }
4144
4161 public function formatHeadings( $text, $origText, $isMain = true ) {
4162 # Inhibit editsection links if requested in the page
4163 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4164 $maybeShowEditLink = false;
4165 } else {
4166 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4167 }
4168
4169 # Get all headlines for numbering them and adding funky stuff like [edit]
4170 # links - this is for later, but we need the number of headlines right now
4171 # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
4172 # be trimmed here since whitespace in HTML headings is significant.
4173 $matches = [];
4174 $numMatches = preg_match_all(
4175 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4176 $text,
4177 $matches
4178 );
4179
4180 # if there are fewer than 4 headlines in the article, do not show TOC
4181 # unless it's been explicitly enabled.
4182 $enoughToc = $this->mShowToc &&
4183 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4184
4185 # Allow user to stipulate that a page should have a "new section"
4186 # link added via __NEWSECTIONLINK__
4187 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4188 $this->mOutput->setNewSection( true );
4189 }
4190
4191 # Allow user to remove the "new section"
4192 # link via __NONEWSECTIONLINK__
4193 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4194 $this->mOutput->hideNewSection( true );
4195 }
4196
4197 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4198 # override above conditions and always show TOC above first header
4199 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4200 $this->mShowToc = true;
4201 $enoughToc = true;
4202 }
4203
4204 # headline counter
4205 $headlineCount = 0;
4206 $numVisible = 0;
4207
4208 # Ugh .. the TOC should have neat indentation levels which can be
4209 # passed to the skin functions. These are determined here
4210 $toc = '';
4211 $full = '';
4212 $head = [];
4213 $sublevelCount = [];
4214 $levelCount = [];
4215 $level = 0;
4216 $prevlevel = 0;
4217 $toclevel = 0;
4218 $prevtoclevel = 0;
4219 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4220 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4221 $oldType = $this->mOutputType;
4222 $this->setOutputType( self::OT_WIKI );
4223 $frame = $this->getPreprocessor()->newFrame();
4224 $root = $this->preprocessToDom( $origText );
4225 $node = $root->getFirstChild();
4226 $byteOffset = 0;
4227 $tocraw = [];
4228 $refers = [];
4229
4230 $headlines = $numMatches !== false ? $matches[3] : [];
4231
4232 $maxTocLevel = $this->siteConfig->get( 'MaxTocLevel' );
4233 foreach ( $headlines as $headline ) {
4234 $isTemplate = false;
4235 $titleText = false;
4236 $sectionIndex = false;
4237 $numbering = '';
4238 $markerMatches = [];
4239 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4240 $serial = $markerMatches[1];
4241 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4242 $isTemplate = ( $titleText != $baseTitleText );
4243 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4244 }
4245
4246 if ( $toclevel ) {
4247 $prevlevel = $level;
4248 }
4249 $level = $matches[1][$headlineCount];
4250
4251 if ( $level > $prevlevel ) {
4252 # Increase TOC level
4253 $toclevel++;
4254 $sublevelCount[$toclevel] = 0;
4255 if ( $toclevel < $maxTocLevel ) {
4256 $prevtoclevel = $toclevel;
4257 $toc .= Linker::tocIndent();
4258 $numVisible++;
4259 }
4260 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4261 # Decrease TOC level, find level to jump to
4262
4263 for ( $i = $toclevel; $i > 0; $i-- ) {
4264 if ( $levelCount[$i] == $level ) {
4265 # Found last matching level
4266 $toclevel = $i;
4267 break;
4268 } elseif ( $levelCount[$i] < $level ) {
4269 # Found first matching level below current level
4270 $toclevel = $i + 1;
4271 break;
4272 }
4273 }
4274 if ( $i == 0 ) {
4275 $toclevel = 1;
4276 }
4277 if ( $toclevel < $maxTocLevel ) {
4278 if ( $prevtoclevel < $maxTocLevel ) {
4279 # Unindent only if the previous toc level was shown :p
4280 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4281 $prevtoclevel = $toclevel;
4282 } else {
4283 $toc .= Linker::tocLineEnd();
4284 }
4285 }
4286 } else {
4287 # No change in level, end TOC line
4288 if ( $toclevel < $maxTocLevel ) {
4289 $toc .= Linker::tocLineEnd();
4290 }
4291 }
4292
4293 $levelCount[$toclevel] = $level;
4294
4295 # count number of headlines for each level
4296 $sublevelCount[$toclevel]++;
4297 $dot = 0;
4298 for ( $i = 1; $i <= $toclevel; $i++ ) {
4299 if ( !empty( $sublevelCount[$i] ) ) {
4300 if ( $dot ) {
4301 $numbering .= '.';
4302 }
4303 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4304 $dot = 1;
4305 }
4306 }
4307
4308 # The safe header is a version of the header text safe to use for links
4309
4310 # Remove link placeholders by the link text.
4311 # <!--LINK number-->
4312 # turns into
4313 # link text with suffix
4314 # Do this before unstrip since link text can contain strip markers
4315 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4316
4317 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4318 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4319
4320 # Remove any <style> or <script> tags (T198618)
4321 $safeHeadline = preg_replace(
4322 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4323 '',
4324 $safeHeadline
4325 );
4326
4327 # Strip out HTML (first regex removes any tag not allowed)
4328 # Allowed tags are:
4329 # * <sup> and <sub> (T10393)
4330 # * <i> (T28375)
4331 # * <b> (r105284)
4332 # * <bdi> (T74884)
4333 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4334 # * <s> and <strike> (T35715)
4335 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4336 # to allow setting directionality in toc items.
4337 $tocline = preg_replace(
4338 [
4339 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4340 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4341 ],
4342 [ '', '<$1>' ],
4343 $safeHeadline
4344 );
4345
4346 # Strip '<span></span>', which is the result from the above if
4347 # <span id="foo"></span> is used to produce an additional anchor
4348 # for a section.
4349 $tocline = str_replace( '<span></span>', '', $tocline );
4350
4351 $tocline = trim( $tocline );
4352
4353 # For the anchor, strip out HTML-y stuff period
4354 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4355 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4356
4357 # Save headline for section edit hint before it's escaped
4358 $headlineHint = $safeHeadline;
4359
4360 # Decode HTML entities
4361 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4362
4363 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4364
4365 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4366 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4367 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4368 if ( $fallbackHeadline === $safeHeadline ) {
4369 # No reason to have both (in fact, we can't)
4370 $fallbackHeadline = false;
4371 }
4372
4373 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4374 # @todo FIXME: We may be changing them depending on the current locale.
4375 $arrayKey = strtolower( $safeHeadline );
4376 if ( $fallbackHeadline === false ) {
4377 $fallbackArrayKey = false;
4378 } else {
4379 $fallbackArrayKey = strtolower( $fallbackHeadline );
4380 }
4381
4382 # Create the anchor for linking from the TOC to the section
4383 $anchor = $safeHeadline;
4384 $fallbackAnchor = $fallbackHeadline;
4385 if ( isset( $refers[$arrayKey] ) ) {
4386 // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4387 for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4388 $anchor .= "_$i";
4389 $linkAnchor .= "_$i";
4390 $refers["${arrayKey}_$i"] = true;
4391 } else {
4392 $refers[$arrayKey] = true;
4393 }
4394 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4395 // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4396 for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4397 $fallbackAnchor .= "_$i";
4398 $refers["${fallbackArrayKey}_$i"] = true;
4399 } else {
4400 $refers[$fallbackArrayKey] = true;
4401 }
4402
4403 # Don't number the heading if it is the only one (looks silly)
4404 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4405 # the two are different if the line contains a link
4406 $headline = Html::element(
4407 'span',
4408 [ 'class' => 'mw-headline-number' ],
4409 $numbering
4410 ) . ' ' . $headline;
4411 }
4412
4413 if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4414 $toc .= Linker::tocLine( $linkAnchor, $tocline,
4415 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4416 }
4417
4418 # Add the section to the section tree
4419 # Find the DOM node for this header
4420 $noOffset = ( $isTemplate || $sectionIndex === false );
4421 while ( $node && !$noOffset ) {
4422 if ( $node->getName() === 'h' ) {
4423 $bits = $node->splitHeading();
4424 if ( $bits['i'] == $sectionIndex ) {
4425 break;
4426 }
4427 }
4428 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4429 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4430 $node = $node->getNextSibling();
4431 }
4432 $tocraw[] = [
4433 'toclevel' => $toclevel,
4434 'level' => $level,
4435 'line' => $tocline,
4436 'number' => $numbering,
4437 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4438 'fromtitle' => $titleText,
4439 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4440 'anchor' => $anchor,
4441 ];
4442
4443 # give headline the correct <h#> tag
4444 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4445 // Output edit section links as markers with styles that can be customized by skins
4446 if ( $isTemplate ) {
4447 # Put a T flag in the section identifier, to indicate to extractSections()
4448 # that sections inside <includeonly> should be counted.
4449 $editsectionPage = $titleText;
4450 $editsectionSection = "T-$sectionIndex";
4451 $editsectionContent = null;
4452 } else {
4453 $editsectionPage = $this->mTitle->getPrefixedText();
4454 $editsectionSection = $sectionIndex;
4455 $editsectionContent = $headlineHint;
4456 }
4457 // We use a bit of pesudo-xml for editsection markers. The
4458 // language converter is run later on. Using a UNIQ style marker
4459 // leads to the converter screwing up the tokens when it
4460 // converts stuff. And trying to insert strip tags fails too. At
4461 // this point all real inputted tags have already been escaped,
4462 // so we don't have to worry about a user trying to input one of
4463 // these markers directly. We use a page and section attribute
4464 // to stop the language converter from converting these
4465 // important bits of data, but put the headline hint inside a
4466 // content block because the language converter is supposed to
4467 // be able to convert that piece of data.
4468 // Gets replaced with html in ParserOutput::getText
4469 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4470 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4471 if ( $editsectionContent !== null ) {
4472 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4473 } else {
4474 $editlink .= '/>';
4475 }
4476 } else {
4477 $editlink = '';
4478 }
4479 $head[$headlineCount] = Linker::makeHeadline( $level,
4480 $matches['attrib'][$headlineCount], $anchor, $headline,
4481 $editlink, $fallbackAnchor );
4482
4483 $headlineCount++;
4484 }
4485
4486 $this->setOutputType( $oldType );
4487
4488 # Never ever show TOC if no headers
4489 if ( $numVisible < 1 ) {
4490 $enoughToc = false;
4491 }
4492
4493 if ( $enoughToc ) {
4494 if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4495 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4496 }
4497 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4498 $this->mOutput->setTOCHTML( $toc );
4499 $toc = self::TOC_START . $toc . self::TOC_END;
4500 }
4501
4502 if ( $isMain ) {
4503 $this->mOutput->setSections( $tocraw );
4504 }
4505
4506 # split up and insert constructed headlines
4507 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4508 $i = 0;
4509
4510 // build an array of document sections
4511 $sections = [];
4512 foreach ( $blocks as $block ) {
4513 // $head is zero-based, sections aren't.
4514 if ( empty( $head[$i - 1] ) ) {
4515 $sections[$i] = $block;
4516 } else {
4517 $sections[$i] = $head[$i - 1] . $block;
4518 }
4519
4530 Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4531
4532 $i++;
4533 }
4534
4535 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4536 // append the TOC at the beginning
4537 // Top anchor now in skin
4538 $sections[0] .= $toc . "\n";
4539 }
4540
4541 $full .= implode( '', $sections );
4542
4543 if ( $this->mForceTocPosition ) {
4544 return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4545 } else {
4546 return $full;
4547 }
4548 }
4549
4561 public function preSaveTransform( $text, Title $title, User $user,
4562 ParserOptions $options, $clearState = true
4563 ) {
4564 if ( $clearState ) {
4565 $magicScopeVariable = $this->lock();
4566 }
4567 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4568 $this->setUser( $user );
4569
4570 // Strip U+0000 NULL (T159174)
4571 $text = str_replace( "\000", '', $text );
4572
4573 // We still normalize line endings for backwards-compatibility
4574 // with other code that just calls PST, but this should already
4575 // be handled in TextContent subclasses
4576 $text = TextContent::normalizeLineEndings( $text );
4577
4578 if ( $options->getPreSaveTransform() ) {
4579 $text = $this->pstPass2( $text, $user );
4580 }
4581 $text = $this->mStripState->unstripBoth( $text );
4582
4583 $this->setUser( null ); # Reset
4584
4585 return $text;
4586 }
4587
4596 private function pstPass2( $text, $user ) {
4597 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4598 # $this->contLang here in order to give everyone the same signature and use the default one
4599 # rather than the one selected in each user's preferences. (see also T14815)
4600 $ts = $this->mOptions->getTimestamp();
4601 $timestamp = MWTimestamp::getLocalInstance( $ts );
4602 $ts = $timestamp->format( 'YmdHis' );
4603 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4604
4605 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4606
4607 # Variable replacement
4608 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4609 $text = $this->replaceVariables( $text );
4610
4611 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4612 # which may corrupt this parser instance via its wfMessage()->text() call-
4613
4614 # Signatures
4615 if ( strpos( $text, '~~~' ) !== false ) {
4616 $sigText = $this->getUserSig( $user );
4617 $text = strtr( $text, [
4618 '~~~~~' => $d,
4619 '~~~~' => "$sigText $d",
4620 '~~~' => $sigText
4621 ] );
4622 # The main two signature forms used above are time-sensitive
4623 $this->mOutput->setFlag( 'user-signature' );
4624 }
4625
4626 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4627 $tc = '[' . Title::legalChars() . ']';
4628 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4629
4630 // [[ns:page (context)|]]
4631 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4632 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4633 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4634 // [[ns:page (context), context|]] (using either single or double-width comma)
4635 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4636 // [[|page]] (reverse pipe trick: add context from page title)
4637 $p2 = "/\[\[\\|($tc+)]]/";
4638
4639 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4640 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4641 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4642 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4643
4644 $t = $this->mTitle->getText();
4645 $m = [];
4646 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4647 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4648 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4649 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4650 } else {
4651 # if there's no context, don't bother duplicating the title
4652 $text = preg_replace( $p2, '[[\\1]]', $text );
4653 }
4654
4655 return $text;
4656 }
4657
4672 public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4673 $username = $user->getName();
4674
4675 # If not given, retrieve from the user object.
4676 if ( $nickname === false ) {
4677 $nickname = $user->getOption( 'nickname' );
4678 }
4679
4680 if ( is_null( $fancySig ) ) {
4681 $fancySig = $user->getBoolOption( 'fancysig' );
4682 }
4683
4684 $nickname = $nickname == null ? $username : $nickname;
4685
4686 if ( mb_strlen( $nickname ) > $this->siteConfig->get( 'MaxSigChars' ) ) {
4687 $nickname = $username;
4688 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4689 } elseif ( $fancySig !== false ) {
4690 # Sig. might contain markup; validate this
4691 if ( $this->validateSig( $nickname ) !== false ) {
4692 # Validated; clean up (if needed) and return it
4693 return $this->cleanSig( $nickname, true );
4694 } else {
4695 # Failed to validate; fall back to the default
4696 $nickname = $username;
4697 wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4698 }
4699 }
4700
4701 # Make sure nickname doesnt get a sig in a sig
4702 $nickname = self::cleanSigInSig( $nickname );
4703
4704 # If we're still here, make it a link to the user page
4705 $userText = wfEscapeWikiText( $username );
4706 $nickText = wfEscapeWikiText( $nickname );
4707 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4708
4709 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4710 ->title( $this->getTitle() )->text();
4711 }
4712
4719 public function validateSig( $text ) {
4720 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4721 }
4722
4733 public function cleanSig( $text, $parsing = false ) {
4734 if ( !$parsing ) {
4735 global $wgTitle;
4736 $magicScopeVariable = $this->lock();
4737 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4738 }
4739
4740 # Option to disable this feature
4741 if ( !$this->mOptions->getCleanSignatures() ) {
4742 return $text;
4743 }
4744
4745 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4746 # => Move this logic to braceSubstitution()
4747 $substWord = $this->magicWordFactory->get( 'subst' );
4748 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4749 $substText = '{{' . $substWord->getSynonym( 0 );
4750
4751 $text = preg_replace( $substRegex, $substText, $text );
4752 $text = self::cleanSigInSig( $text );
4753 $dom = $this->preprocessToDom( $text );
4754 $frame = $this->getPreprocessor()->newFrame();
4755 $text = $frame->expand( $dom );
4756
4757 if ( !$parsing ) {
4758 $text = $this->mStripState->unstripBoth( $text );
4759 }
4760
4761 return $text;
4762 }
4763
4770 public static function cleanSigInSig( $text ) {
4771 $text = preg_replace( '/~{3,5}/', '', $text );
4772 return $text;
4773 }
4774
4784 public function startExternalParse( Title $title = null, ParserOptions $options,
4785 $outputType, $clearState = true
4786 ) {
4787 $this->startParse( $title, $options, $outputType, $clearState );
4788 }
4789
4796 private function startParse( Title $title = null, ParserOptions $options,
4797 $outputType, $clearState = true
4798 ) {
4799 $this->setTitle( $title );
4800 $this->mOptions = $options;
4801 $this->setOutputType( $outputType );
4802 if ( $clearState ) {
4803 $this->clearState();
4804 }
4805 }
4806
4815 public function transformMsg( $text, $options, $title = null ) {
4816 static $executing = false;
4817
4818 # Guard against infinite recursion
4819 if ( $executing ) {
4820 return $text;
4821 }
4822 $executing = true;
4823
4824 if ( !$title ) {
4825 global $wgTitle;
4826 $title = $wgTitle;
4827 }
4828
4829 $text = $this->preprocess( $text, $title, $options );
4830
4831 $executing = false;
4832 return $text;
4833 }
4834
4859 public function setHook( $tag, callable $callback ) {
4860 $tag = strtolower( $tag );
4861 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4862 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4863 }
4864 $oldVal = $this->mTagHooks[$tag] ?? null;
4865 $this->mTagHooks[$tag] = $callback;
4866 if ( !in_array( $tag, $this->mStripList ) ) {
4867 $this->mStripList[] = $tag;
4868 }
4869
4870 return $oldVal;
4871 }
4872
4890 public function setTransparentTagHook( $tag, callable $callback ) {
4891 $tag = strtolower( $tag );
4892 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4893 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4894 }
4895 $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
4896 $this->mTransparentTagHooks[$tag] = $callback;
4897
4898 return $oldVal;
4899 }
4900
4904 public function clearTagHooks() {
4905 $this->mTagHooks = [];
4906 $this->mFunctionTagHooks = [];
4907 $this->mStripList = $this->mDefaultStripList;
4908 }
4909
4953 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4954 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4955 $this->mFunctionHooks[$id] = [ $callback, $flags ];
4956
4957 # Add to function cache
4958 $mw = $this->magicWordFactory->get( $id );
4959 if ( !$mw ) {
4960 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4961 }
4962
4963 $synonyms = $mw->getSynonyms();
4964 $sensitive = intval( $mw->isCaseSensitive() );
4965
4966 foreach ( $synonyms as $syn ) {
4967 # Case
4968 if ( !$sensitive ) {
4969 $syn = $this->contLang->lc( $syn );
4970 }
4971 # Add leading hash
4972 if ( !( $flags & self::SFH_NO_HASH ) ) {
4973 $syn = '#' . $syn;
4974 }
4975 # Remove trailing colon
4976 if ( substr( $syn, -1, 1 ) === ':' ) {
4977 $syn = substr( $syn, 0, -1 );
4978 }
4979 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4980 }
4981 return $oldVal;
4982 }
4983
4989 public function getFunctionHooks() {
4990 $this->firstCallInit();
4991 return array_keys( $this->mFunctionHooks );
4992 }
4993
5004 public function setFunctionTagHook( $tag, callable $callback, $flags ) {
5005 $tag = strtolower( $tag );
5006 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5007 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5008 }
5009 $old = $this->mFunctionTagHooks[$tag] ?? null;
5010 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5011
5012 if ( !in_array( $tag, $this->mStripList ) ) {
5013 $this->mStripList[] = $tag;
5014 }
5015
5016 return $old;
5017 }
5018
5026 public function replaceLinkHolders( &$text, $options = 0 ) {
5027 $this->mLinkHolders->replace( $text );
5028 }
5029
5037 public function replaceLinkHoldersText( $text ) {
5038 return $this->mLinkHolders->replaceText( $text );
5039 }
5040
5054 public function renderImageGallery( $text, $params ) {
5055 $mode = false;
5056 if ( isset( $params['mode'] ) ) {
5057 $mode = $params['mode'];
5058 }
5059
5060 try {
5061 $ig = ImageGalleryBase::factory( $mode );
5062 } catch ( Exception $e ) {
5063 // If invalid type set, fallback to default.
5064 $ig = ImageGalleryBase::factory( false );
5065 }
5066
5067 $ig->setContextTitle( $this->mTitle );
5068 $ig->setShowBytes( false );
5069 $ig->setShowDimensions( false );
5070 $ig->setShowFilename( false );
5071 $ig->setParser( $this );
5072 $ig->setHideBadImages();
5073 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5074
5075 if ( isset( $params['showfilename'] ) ) {
5076 $ig->setShowFilename( true );
5077 } else {
5078 $ig->setShowFilename( false );
5079 }
5080 if ( isset( $params['caption'] ) ) {
5081 // NOTE: We aren't passing a frame here or below. Frame info
5082 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5083 // See T107332#4030581
5084 $caption = $this->recursiveTagParse( $params['caption'] );
5085 $ig->setCaptionHtml( $caption );
5086 }
5087 if ( isset( $params['perrow'] ) ) {
5088 $ig->setPerRow( $params['perrow'] );
5089 }
5090 if ( isset( $params['widths'] ) ) {
5091 $ig->setWidths( $params['widths'] );
5092 }
5093 if ( isset( $params['heights'] ) ) {
5094 $ig->setHeights( $params['heights'] );
5095 }
5096 $ig->setAdditionalOptions( $params );
5097
5098 // Avoid PHP 7.1 warning from passing $this by reference
5099 $parser = $this;
5100 Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5101
5102 $lines = StringUtils::explode( "\n", $text );
5103 foreach ( $lines as $line ) {
5104 # match lines like these:
5105 # Image:someimage.jpg|This is some image
5106 $matches = [];
5107 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5108 # Skip empty lines
5109 if ( count( $matches ) == 0 ) {
5110 continue;
5111 }
5112
5113 if ( strpos( $matches[0], '%' ) !== false ) {
5114 $matches[1] = rawurldecode( $matches[1] );
5115 }
5116 $title = Title::newFromText( $matches[1], NS_FILE );
5117 if ( is_null( $title ) ) {
5118 # Bogus title. Ignore these so we don't bomb out later.
5119 continue;
5120 }
5121
5122 # We need to get what handler the file uses, to figure out parameters.
5123 # Note, a hook can overide the file name, and chose an entirely different
5124 # file (which potentially could be of a different type and have different handler).
5125 $options = [];
5126 $descQuery = false;
5127 Hooks::run( 'BeforeParserFetchFileAndTitle',
5128 [ $this, $title, &$options, &$descQuery ] );
5129 # Don't register it now, as TraditionalImageGallery does that later.
5130 $file = $this->fetchFileNoRegister( $title, $options );
5131 $handler = $file ? $file->getHandler() : false;
5132
5133 $paramMap = [
5134 'img_alt' => 'gallery-internal-alt',
5135 'img_link' => 'gallery-internal-link',
5136 ];
5137 if ( $handler ) {
5138 $paramMap += $handler->getParamMap();
5139 // We don't want people to specify per-image widths.
5140 // Additionally the width parameter would need special casing anyhow.
5141 unset( $paramMap['img_width'] );
5142 }
5143
5144 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5145
5146 $label = '';
5147 $alt = '';
5148 $link = '';
5149 $handlerOptions = [];
5150 if ( isset( $matches[3] ) ) {
5151 // look for an |alt= definition while trying not to break existing
5152 // captions with multiple pipes (|) in it, until a more sensible grammar
5153 // is defined for images in galleries
5154
5155 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5156 // splitting on '|' is a bit odd, and different from makeImage.
5157 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5158 // Protect LanguageConverter markup
5159 $parameterMatches = StringUtils::delimiterExplode(
5160 '-{', '}-', '|', $matches[3], true /* nested */
5161 );
5162
5163 foreach ( $parameterMatches as $parameterMatch ) {
5164 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5165 if ( $magicName ) {
5166 $paramName = $paramMap[$magicName];
5167
5168 switch ( $paramName ) {
5169 case 'gallery-internal-alt':
5170 $alt = $this->stripAltText( $match, false );
5171 break;
5172 case 'gallery-internal-link':
5173 $linkValue = $this->stripAltText( $match, false );
5174 if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5175 // Result of LanguageConverter::markNoConversion
5176 // invoked on an external link.
5177 $linkValue = substr( $linkValue, 4, -2 );
5178 }
5179 list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5180 if ( $type === 'link-url' ) {
5181 $link = $target;
5182 $this->mOutput->addExternalLink( $target );
5183 } elseif ( $type === 'link-title' ) {
5184 $link = $target->getLinkURL();
5185 $this->mOutput->addLink( $target );
5186 }
5187 break;
5188 default:
5189 // Must be a handler specific parameter.
5190 if ( $handler->validateParam( $paramName, $match ) ) {
5191 $handlerOptions[$paramName] = $match;
5192 } else {
5193 // Guess not, consider it as caption.
5194 wfDebug( "$parameterMatch failed parameter validation\n" );
5195 $label = $parameterMatch;
5196 }
5197 }
5198
5199 } else {
5200 // Last pipe wins.
5201 $label = $parameterMatch;
5202 }
5203 }
5204 }
5205
5206 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5207 }
5208 $html = $ig->toHTML();
5209 Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5210 return $html;
5211 }
5212
5217 public function getImageParams( $handler ) {
5218 if ( $handler ) {
5219 $handlerClass = get_class( $handler );
5220 } else {
5221 $handlerClass = '';
5222 }
5223 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5224 # Initialise static lists
5225 static $internalParamNames = [
5226 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5227 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5228 'bottom', 'text-bottom' ],
5229 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5230 'upright', 'border', 'link', 'alt', 'class' ],
5231 ];
5232 static $internalParamMap;
5233 if ( !$internalParamMap ) {
5234 $internalParamMap = [];
5235 foreach ( $internalParamNames as $type => $names ) {
5236 foreach ( $names as $name ) {
5237 // For grep: img_left, img_right, img_center, img_none,
5238 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5239 // img_bottom, img_text_bottom,
5240 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5241 // img_border, img_link, img_alt, img_class
5242 $magicName = str_replace( '-', '_', "img_$name" );
5243 $internalParamMap[$magicName] = [ $type, $name ];
5244 }
5245 }
5246 }
5247
5248 # Add handler params
5249 $paramMap = $internalParamMap;
5250 if ( $handler ) {
5251 $handlerParamMap = $handler->getParamMap();
5252 foreach ( $handlerParamMap as $magic => $paramName ) {
5253 $paramMap[$magic] = [ 'handler', $paramName ];
5254 }
5255 }
5256 $this->mImageParams[$handlerClass] = $paramMap;
5257 $this->mImageParamsMagicArray[$handlerClass] =
5258 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5259 }
5260 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5261 }
5262
5271 public function makeImage( $title, $options, $holders = false ) {
5272 # Check if the options text is of the form "options|alt text"
5273 # Options are:
5274 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5275 # * left no resizing, just left align. label is used for alt= only
5276 # * right same, but right aligned
5277 # * none same, but not aligned
5278 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5279 # * center center the image
5280 # * frame Keep original image size, no magnify-button.
5281 # * framed Same as "frame"
5282 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5283 # * upright reduce width for upright images, rounded to full __0 px
5284 # * border draw a 1px border around the image
5285 # * alt Text for HTML alt attribute (defaults to empty)
5286 # * class Set a class for img node
5287 # * link Set the target of the image link. Can be external, interwiki, or local
5288 # vertical-align values (no % or length right now):
5289 # * baseline
5290 # * sub
5291 # * super
5292 # * top
5293 # * text-top
5294 # * middle
5295 # * bottom
5296 # * text-bottom
5297
5298 # Protect LanguageConverter markup when splitting into parts
5300 '-{', '}-', '|', $options, true /* allow nesting */
5301 );
5302
5303 # Give extensions a chance to select the file revision for us
5304 $options = [];
5305 $descQuery = false;
5306 Hooks::run( 'BeforeParserFetchFileAndTitle',
5307 [ $this, $title, &$options, &$descQuery ] );
5308 # Fetch and register the file (file title may be different via hooks)
5309 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5310
5311 # Get parameter map
5312 $handler = $file ? $file->getHandler() : false;
5313
5314 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5315
5316 if ( !$file ) {
5317 $this->addTrackingCategory( 'broken-file-category' );
5318 }
5319
5320 # Process the input parameters
5321 $caption = '';
5322 $params = [ 'frame' => [], 'handler' => [],
5323 'horizAlign' => [], 'vertAlign' => [] ];
5324 $seenformat = false;
5325 foreach ( $parts as $part ) {
5326 $part = trim( $part );
5327 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5328 $validated = false;
5329 if ( isset( $paramMap[$magicName] ) ) {
5330 list( $type, $paramName ) = $paramMap[$magicName];
5331
5332 # Special case; width and height come in one variable together
5333 if ( $type === 'handler' && $paramName === 'width' ) {
5334 $parsedWidthParam = self::parseWidthParam( $value );
5335 if ( isset( $parsedWidthParam['width'] ) ) {
5336 $width = $parsedWidthParam['width'];
5337 if ( $handler->validateParam( 'width', $width ) ) {
5338 $params[$type]['width'] = $width;
5339 $validated = true;
5340 }
5341 }
5342 if ( isset( $parsedWidthParam['height'] ) ) {
5343 $height = $parsedWidthParam['height'];
5344 if ( $handler->validateParam( 'height', $height ) ) {
5345 $params[$type]['height'] = $height;
5346 $validated = true;
5347 }
5348 }
5349 # else no validation -- T15436
5350 } else {
5351 if ( $type === 'handler' ) {
5352 # Validate handler parameter
5353 $validated = $handler->validateParam( $paramName, $value );
5354 } else {
5355 # Validate internal parameters
5356 switch ( $paramName ) {
5357 case 'manualthumb':
5358 case 'alt':
5359 case 'class':
5360 # @todo FIXME: Possibly check validity here for
5361 # manualthumb? downstream behavior seems odd with
5362 # missing manual thumbs.
5363 $validated = true;
5364 $value = $this->stripAltText( $value, $holders );
5365 break;
5366 case 'link':
5367 list( $paramName, $value ) =
5368 $this->parseLinkParameter(
5369 $this->stripAltText( $value, $holders )
5370 );
5371 if ( $paramName ) {
5372 $validated = true;
5373 if ( $paramName === 'no-link' ) {
5374 $value = true;
5375 }
5376 if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5377 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5378 }
5379 }
5380 break;
5381 case 'frameless':
5382 case 'framed':
5383 case 'thumbnail':
5384 // use first appearing option, discard others.
5385 $validated = !$seenformat;
5386 $seenformat = true;
5387 break;
5388 default:
5389 # Most other things appear to be empty or numeric...
5390 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5391 }
5392 }
5393
5394 if ( $validated ) {
5395 $params[$type][$paramName] = $value;
5396 }
5397 }
5398 }
5399 if ( !$validated ) {
5400 $caption = $part;
5401 }
5402 }
5403
5404 # Process alignment parameters
5405 if ( $params['horizAlign'] ) {
5406 $params['frame']['align'] = key( $params['horizAlign'] );
5407 }
5408 if ( $params['vertAlign'] ) {
5409 $params['frame']['valign'] = key( $params['vertAlign'] );
5410 }
5411
5412 $params['frame']['caption'] = $caption;
5413
5414 # Will the image be presented in a frame, with the caption below?
5415 $imageIsFramed = isset( $params['frame']['frame'] )
5416 || isset( $params['frame']['framed'] )
5417 || isset( $params['frame']['thumbnail'] )
5418 || isset( $params['frame']['manualthumb'] );
5419
5420 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5421 # came to also set the caption, ordinary text after the image -- which
5422 # makes no sense, because that just repeats the text multiple times in
5423 # screen readers. It *also* came to set the title attribute.
5424 # Now that we have an alt attribute, we should not set the alt text to
5425 # equal the caption: that's worse than useless, it just repeats the
5426 # text. This is the framed/thumbnail case. If there's no caption, we
5427 # use the unnamed parameter for alt text as well, just for the time be-
5428 # ing, if the unnamed param is set and the alt param is not.
5429 # For the future, we need to figure out if we want to tweak this more,
5430 # e.g., introducing a title= parameter for the title; ignoring the un-
5431 # named parameter entirely for images without a caption; adding an ex-
5432 # plicit caption= parameter and preserving the old magic unnamed para-
5433 # meter for BC; ...
5434 if ( $imageIsFramed ) { # Framed image
5435 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5436 # No caption or alt text, add the filename as the alt text so
5437 # that screen readers at least get some description of the image
5438 $params['frame']['alt'] = $title->getText();
5439 }
5440 # Do not set $params['frame']['title'] because tooltips don't make sense
5441 # for framed images
5442 } else { # Inline image
5443 if ( !isset( $params['frame']['alt'] ) ) {
5444 # No alt text, use the "caption" for the alt text
5445 if ( $caption !== '' ) {
5446 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5447 } else {
5448 # No caption, fall back to using the filename for the
5449 # alt text
5450 $params['frame']['alt'] = $title->getText();
5451 }
5452 }
5453 # Use the "caption" for the tooltip text
5454 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5455 }
5456 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5457
5458 Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5459
5460 # Linker does the rest
5461 $time = $options['time'] ?? false;
5462 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5463 $time, $descQuery, $this->mOptions->getThumbSize() );
5464
5465 # Give the handler a chance to modify the parser object
5466 if ( $handler ) {
5467 $handler->parserTransformHook( $this, $file );
5468 }
5469
5470 return $ret;
5471 }
5472
5491 public function parseLinkParameter( $value ) {
5492 $chars = self::EXT_LINK_URL_CLASS;
5493 $addr = self::EXT_LINK_ADDR;
5494 $prots = $this->mUrlProtocols;
5495 $type = null;
5496 $target = false;
5497 if ( $value === '' ) {
5498 $type = 'no-link';
5499 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5500 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5501 $this->mOutput->addExternalLink( $value );
5502 $type = 'link-url';
5503 $target = $value;
5504 }
5505 } else {
5506 $linkTitle = Title::newFromText( $value );
5507 if ( $linkTitle ) {
5508 $this->mOutput->addLink( $linkTitle );
5509 $type = 'link-title';
5510 $target = $linkTitle;
5511 }
5512 }
5513 return [ $type, $target ];
5514 }
5515
5521 protected function stripAltText( $caption, $holders ) {
5522 # Strip bad stuff out of the title (tooltip). We can't just use
5523 # replaceLinkHoldersText() here, because if this function is called
5524 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5525 if ( $holders ) {
5526 $tooltip = $holders->replaceText( $caption );
5527 } else {
5528 $tooltip = $this->replaceLinkHoldersText( $caption );
5529 }
5530
5531 # make sure there are no placeholders in thumbnail attributes
5532 # that are later expanded to html- so expand them now and
5533 # remove the tags
5534 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5535 # Compatibility hack! In HTML certain entity references not terminated
5536 # by a semicolon are decoded (but not if we're in an attribute; that's
5537 # how link URLs get away without properly escaping & in queries).
5538 # But wikitext has always required semicolon-termination of entities,
5539 # so encode & where needed to avoid decode of semicolon-less entities.
5540 # See T209236 and
5541 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5542 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5543 $tooltip = preg_replace( "/
5544 & # 1. entity prefix
5545 (?= # 2. followed by:
5546 (?: # a. one of the legacy semicolon-less named entities
5547 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5548 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5549 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5550 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5551 U(?:acute|circ|grave|uml)|Yacute|
5552 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5553 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5554 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5555 frac(?:1(?:2|4)|34)|
5556 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5557 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5558 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5559 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5560 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5561 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5562 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5563 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5564 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5565 )
5566 (?:[^;]|$)) # b. and not followed by a semicolon
5567 # S = study, for efficiency
5568 /Sx", '&amp;', $tooltip );
5569 $tooltip = Sanitizer::stripAllTags( $tooltip );
5570
5571 return $tooltip;
5572 }
5573
5579 public function disableCache() {
5580 wfDebug( "Parser output marked as uncacheable.\n" );
5581 if ( !$this->mOutput ) {
5582 throw new MWException( __METHOD__ .
5583 " can only be called when actually parsing something" );
5584 }
5585 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5586 }
5587
5596 public function attributeStripCallback( &$text, $frame = false ) {
5597 $text = $this->replaceVariables( $text, $frame );
5598 $text = $this->mStripState->unstripBoth( $text );
5599 return $text;
5600 }
5601
5607 public function getTags() {
5608 $this->firstCallInit();
5609 return array_merge(
5610 array_keys( $this->mTransparentTagHooks ),
5611 array_keys( $this->mTagHooks ),
5612 array_keys( $this->mFunctionTagHooks )
5613 );
5614 }
5615
5620 public function getFunctionSynonyms() {
5621 $this->firstCallInit();
5622 return $this->mFunctionSynonyms;
5623 }
5624
5629 public function getUrlProtocols() {
5630 return $this->mUrlProtocols;
5631 }
5632
5643 public function replaceTransparentTags( $text ) {
5644 $matches = [];
5645 $elements = array_keys( $this->mTransparentTagHooks );
5646 $text = self::extractTagsAndParams( $elements, $text, $matches );
5647 $replacements = [];
5648
5649 foreach ( $matches as $marker => $data ) {
5650 list( $element, $content, $params, $tag ) = $data;
5651 $tagName = strtolower( $element );
5652 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5653 $output = call_user_func_array(
5654 $this->mTransparentTagHooks[$tagName],
5655 [ $content, $params, $this ]
5656 );
5657 } else {
5658 $output = $tag;
5659 }
5660 $replacements[$marker] = $output;
5661 }
5662 return strtr( $text, $replacements );
5663 }
5664
5694 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5695 global $wgTitle; # not generally used but removes an ugly failure mode
5696
5697 $magicScopeVariable = $this->lock();
5698 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5699 $outText = '';
5700 $frame = $this->getPreprocessor()->newFrame();
5701
5702 # Process section extraction flags
5703 $flags = 0;
5704 $sectionParts = explode( '-', $sectionId );
5705 $sectionIndex = array_pop( $sectionParts );
5706 foreach ( $sectionParts as $part ) {
5707 if ( $part === 'T' ) {
5708 $flags |= self::PTD_FOR_INCLUSION;
5709 }
5710 }
5711
5712 # Check for empty input
5713 if ( strval( $text ) === '' ) {
5714 # Only sections 0 and T-0 exist in an empty document
5715 if ( $sectionIndex == 0 ) {
5716 if ( $mode === 'get' ) {
5717 return '';
5718 }
5719
5720 return $newText;
5721 } else {
5722 if ( $mode === 'get' ) {
5723 return $newText;
5724 }
5725
5726 return $text;
5727 }
5728 }
5729
5730 # Preprocess the text
5731 $root = $this->preprocessToDom( $text, $flags );
5732
5733 # <h> nodes indicate section breaks
5734 # They can only occur at the top level, so we can find them by iterating the root's children
5735 $node = $root->getFirstChild();
5736
5737 # Find the target section
5738 if ( $sectionIndex == 0 ) {
5739 # Section zero doesn't nest, level=big
5740 $targetLevel = 1000;
5741 } else {
5742 while ( $node ) {
5743 if ( $node->getName() === 'h' ) {
5744 $bits = $node->splitHeading();
5745 if ( $bits['i'] == $sectionIndex ) {
5746 $targetLevel = $bits['level'];
5747 break;
5748 }
5749 }
5750 if ( $mode === 'replace' ) {
5751 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5752 }
5753 $node = $node->getNextSibling();
5754 }
5755 }
5756
5757 if ( !$node ) {
5758 # Not found
5759 if ( $mode === 'get' ) {
5760 return $newText;
5761 } else {
5762 return $text;
5763 }
5764 }
5765
5766 # Find the end of the section, including nested sections
5767 do {
5768 if ( $node->getName() === 'h' ) {
5769 $bits = $node->splitHeading();
5770 $curLevel = $bits['level'];
5771 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5772 break;
5773 }
5774 }
5775 if ( $mode === 'get' ) {
5776 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5777 }
5778 $node = $node->getNextSibling();
5779 } while ( $node );
5780
5781 # Write out the remainder (in replace mode only)
5782 if ( $mode === 'replace' ) {
5783 # Output the replacement text
5784 # Add two newlines on -- trailing whitespace in $newText is conventionally
5785 # stripped by the editor, so we need both newlines to restore the paragraph gap
5786 # Only add trailing whitespace if there is newText
5787 if ( $newText != "" ) {
5788 $outText .= $newText . "\n\n";
5789 }
5790
5791 while ( $node ) {
5792 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5793 $node = $node->getNextSibling();
5794 }
5795 }
5796
5797 if ( is_string( $outText ) ) {
5798 # Re-insert stripped tags
5799 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5800 }
5801
5802 return $outText;
5803 }
5804
5819 public function getSection( $text, $sectionId, $defaultText = '' ) {
5820 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5821 }
5822
5835 public function replaceSection( $oldText, $sectionId, $newText ) {
5836 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5837 }
5838
5844 public function getRevisionId() {
5845 return $this->mRevisionId;
5846 }
5847
5854 public function getRevisionObject() {
5855 if ( !is_null( $this->mRevisionObject ) ) {
5856 return $this->mRevisionObject;
5857 }
5858
5859 // NOTE: try to get the RevisionObject even if mRevisionId is null.
5860 // This is useful when parsing revision that has not yet been saved.
5861 // However, if we get back a saved revision even though we are in
5862 // preview mode, we'll have to ignore it, see below.
5863 // NOTE: This callback may be used to inject an OLD revision that was
5864 // already loaded, so "current" is a bit of a misnomer. We can't just
5865 // skip it if mRevisionId is set.
5866 $rev = call_user_func(
5867 $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5868 );
5869
5870 if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
5871 // We are in preview mode (mRevisionId is null), and the current revision callback
5872 // returned an existing revision. Ignore it and return null, it's probably the page's
5873 // current revision, which is not what we want here. Note that we do want to call the
5874 // callback to allow the unsaved revision to be injected here, e.g. for
5875 // self-transclusion previews.
5876 return null;
5877 }
5878
5879 // If the parse is for a new revision, then the callback should have
5880 // already been set to force the object and should match mRevisionId.
5881 // If not, try to fetch by mRevisionId for sanity.
5882 if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
5883 $rev = Revision::newFromId( $this->mRevisionId );
5884 }
5885
5886 $this->mRevisionObject = $rev;
5887
5888 return $this->mRevisionObject;
5889 }
5890
5896 public function getRevisionTimestamp() {
5897 if ( is_null( $this->mRevisionTimestamp ) ) {
5898 $revObject = $this->getRevisionObject();
5899 $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5900
5901 # The cryptic '' timezone parameter tells to use the site-default
5902 # timezone offset instead of the user settings.
5903 # Since this value will be saved into the parser cache, served
5904 # to other users, and potentially even used inside links and such,
5905 # it needs to be consistent for all visitors.
5906 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
5907 }
5908 return $this->mRevisionTimestamp;
5909 }
5910
5916 public function getRevisionUser() {
5917 if ( is_null( $this->mRevisionUser ) ) {
5918 $revObject = $this->getRevisionObject();
5919
5920 # if this template is subst: the revision id will be blank,
5921 # so just use the current user's name
5922 if ( $revObject ) {
5923 $this->mRevisionUser = $revObject->getUserText();
5924 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5925 $this->mRevisionUser = $this->getUser()->getName();
5926 }
5927 }
5928 return $this->mRevisionUser;
5929 }
5930
5936 public function getRevisionSize() {
5937 if ( is_null( $this->mRevisionSize ) ) {
5938 $revObject = $this->getRevisionObject();
5939
5940 # if this variable is subst: the revision id will be blank,
5941 # so just use the parser input size, because the own substituation
5942 # will change the size.
5943 if ( $revObject ) {
5944 $this->mRevisionSize = $revObject->getSize();
5945 } else {
5946 $this->mRevisionSize = $this->mInputSize;
5947 }
5948 }
5949 return $this->mRevisionSize;
5950 }
5951
5957 public function setDefaultSort( $sort ) {
5958 $this->mDefaultSort = $sort;
5959 $this->mOutput->setProperty( 'defaultsort', $sort );
5960 }
5961
5972 public function getDefaultSort() {
5973 if ( $this->mDefaultSort !== false ) {
5974 return $this->mDefaultSort;
5975 } else {
5976 return '';
5977 }
5978 }
5979
5986 public function getCustomDefaultSort() {
5987 return $this->mDefaultSort;
5988 }
5989
5990 private static function getSectionNameFromStrippedText( $text ) {
5991 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5992 $text = Sanitizer::decodeCharReferences( $text );
5993 $text = self::normalizeSectionName( $text );
5994 return $text;
5995 }
5996
5997 private static function makeAnchor( $sectionName ) {
5998 return '#' . Sanitizer::escapeIdForLink( $sectionName );
5999 }
6000
6001 private function makeLegacyAnchor( $sectionName ) {
6002 $fragmentMode = $this->siteConfig->get( 'FragmentMode' );
6003 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6004 // ForAttribute() and ForLink() are the same for legacy encoding
6005 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6006 } else {
6007 $id = Sanitizer::escapeIdForLink( $sectionName );
6008 }
6009
6010 return "#$id";
6011 }
6012
6021 public function guessSectionNameFromWikiText( $text ) {
6022 # Strip out wikitext links(they break the anchor)
6023 $text = $this->stripSectionName( $text );
6024 $sectionName = self::getSectionNameFromStrippedText( $text );
6025 return self::makeAnchor( $sectionName );
6026 }
6027
6037 public function guessLegacySectionNameFromWikiText( $text ) {
6038 # Strip out wikitext links(they break the anchor)
6039 $text = $this->stripSectionName( $text );
6040 $sectionName = self::getSectionNameFromStrippedText( $text );
6041 return $this->makeLegacyAnchor( $sectionName );
6042 }
6043
6049 public static function guessSectionNameFromStrippedText( $text ) {
6050 $sectionName = self::getSectionNameFromStrippedText( $text );
6051 return self::makeAnchor( $sectionName );
6052 }
6053
6060 private static function normalizeSectionName( $text ) {
6061 # T90902: ensure the same normalization is applied for IDs as to links
6062 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6063 try {
6064
6065 $parts = $titleParser->splitTitleString( "#$text" );
6066 } catch ( MalformedTitleException $ex ) {
6067 return $text;
6068 }
6069 return $parts['fragment'];
6070 }
6071
6086 public function stripSectionName( $text ) {
6087 # Strip internal link markup
6088 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6089 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6090
6091 # Strip external link markup
6092 # @todo FIXME: Not tolerant to blank link text
6093 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6094 # on how many empty links there are on the page - need to figure that out.
6095 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6096
6097 # Parse wikitext quotes (italics & bold)
6098 $text = $this->doQuotes( $text );
6099
6100 # Strip HTML tags
6101 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6102 return $text;
6103 }
6104
6115 public function testSrvus( $text, Title $title, ParserOptions $options,
6116 $outputType = self::OT_HTML
6117 ) {
6118 $magicScopeVariable = $this->lock();
6119 $this->startParse( $title, $options, $outputType, true );
6120
6121 $text = $this->replaceVariables( $text );
6122 $text = $this->mStripState->unstripBoth( $text );
6123 $text = Sanitizer::removeHTMLtags( $text );
6124 return $text;
6125 }
6126
6133 public function testPst( $text, Title $title, ParserOptions $options ) {
6134 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6135 }
6136
6143 public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6144 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6145 }
6146
6163 public function markerSkipCallback( $s, $callback ) {
6164 $i = 0;
6165 $out = '';
6166 while ( $i < strlen( $s ) ) {
6167 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6168 if ( $markerStart === false ) {
6169 $out .= call_user_func( $callback, substr( $s, $i ) );
6170 break;
6171 } else {
6172 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6173 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6174 if ( $markerEnd === false ) {
6175 $out .= substr( $s, $markerStart );
6176 break;
6177 } else {
6178 $markerEnd += strlen( self::MARKER_SUFFIX );
6179 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6180 $i = $markerEnd;
6181 }
6182 }
6183 }
6184 return $out;
6185 }
6186
6193 public function killMarkers( $text ) {
6194 return $this->mStripState->killMarkers( $text );
6195 }
6196
6214 public function serializeHalfParsedText( $text ) {
6215 wfDeprecated( __METHOD__, '1.31' );
6216 $data = [
6217 'text' => $text,
6218 'version' => self::HALF_PARSED_VERSION,
6219 'stripState' => $this->mStripState->getSubState( $text ),
6220 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6221 ];
6222 return $data;
6223 }
6224
6242 wfDeprecated( __METHOD__, '1.31' );
6243 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6244 throw new MWException( __METHOD__ . ': invalid version' );
6245 }
6246
6247 # First, extract the strip state.
6248 $texts = [ $data['text'] ];
6249 $texts = $this->mStripState->merge( $data['stripState'], $texts );
6250
6251 # Now renumber links
6252 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6253
6254 # Should be good to go.
6255 return $texts[0];
6256 }
6257
6268 public function isValidHalfParsedText( $data ) {
6269 wfDeprecated( __METHOD__, '1.31' );
6270 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6271 }
6272
6282 public static function parseWidthParam( $value, $parseHeight = true ) {
6283 $parsedWidthParam = [];
6284 if ( $value === '' ) {
6285 return $parsedWidthParam;
6286 }
6287 $m = [];
6288 # (T15500) In both cases (width/height and width only),
6289 # permit trailing "px" for backward compatibility.
6290 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6291 $width = intval( $m[1] );
6292 $height = intval( $m[2] );
6293 $parsedWidthParam['width'] = $width;
6294 $parsedWidthParam['height'] = $height;
6295 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6296 $width = intval( $value );
6297 $parsedWidthParam['width'] = $width;
6298 }
6299 return $parsedWidthParam;
6300 }
6301
6311 protected function lock() {
6312 if ( $this->mInParse ) {
6313 throw new MWException( "Parser state cleared while parsing. "
6314 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6315 }
6316
6317 // Save the backtrace when locking, so that if some code tries locking again,
6318 // we can print the lock owner's backtrace for easier debugging
6319 $e = new Exception;
6320 $this->mInParse = $e->getTraceAsString();
6321
6322 $recursiveCheck = new ScopedCallback( function () {
6323 $this->mInParse = false;
6324 } );
6325
6326 return $recursiveCheck;
6327 }
6328
6339 public static function stripOuterParagraph( $html ) {
6340 $m = [];
6341 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6342 $html = $m[1];
6343 }
6344
6345 return $html;
6346 }
6347
6358 public function getFreshParser() {
6359 if ( $this->mInParse ) {
6360 return $this->factory->create();
6361 } else {
6362 return $this;
6363 }
6364 }
6365
6372 public function enableOOUI() {
6373 OutputPage::setupOOUI();
6374 $this->mOutput->setEnableOOUI( true );
6375 }
6376}
and(b) You must cause any modified files to carry prominent notices stating that You changed the files
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation and configuration files Object form shall mean any form resulting from mechanical transformation or translation of a Source including but not limited to compiled object generated and conversions to other media types Work shall mean the work of whether in Source or Object made available under the as indicated by a copyright notice that is included in or attached to the whether in Source or Object that is based or other modifications as a an original work of authorship For the purposes of this Derivative Works shall not include works that remain separable or merely the Work and Derivative Works thereof Contribution shall mean any work of including the original version of the Work and any modifications or additions to that Work or Derivative Works that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner For the purposes of this submitted means any form of or written communication sent to the Licensor or its including but not limited to communication on electronic mailing source code control and issue tracking systems that are managed or on behalf the Licensor for the purpose of discussing and improving the but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work Grant of Copyright License Subject to the terms and conditions of this each Contributor hereby grants to You a non no royalty irrevocable copyright license to prepare Derivative Works publicly display
If you want to remove the page from your watchlist later
target page
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
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
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:123
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:36
static makeMediaLinkFile(Title $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:775
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1576
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:307
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:251
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1394
static tocList( $toc, $lang=null)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition Linker.php:1613
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:168
static tocIndent()
Add another level to the Table of Contents.
Definition Linker.php:1550
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:1714
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:842
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1693
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1561
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1600
MediaWiki exception.
static tidy( $text)
Interface with Remex tidy.
Definition MWTidy.php:42
static isEnabled()
Definition MWTidy.php:54
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.
Factory to create LinkRender objects.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Factory for handling the special page list and generating SpecialPage objects.
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:69
addTrackingCategory( $msg)
Definition Parser.php:4141
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:929
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:5896
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:2090
bool string $mInParse
Recursive call protection.
Definition Parser.php:255
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3959
const TOC_END
Definition Parser.php:138
$mDefaultStripList
Definition Parser.php:150
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:5957
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6339
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2032
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:782
MagicWordFactory $magicWordFactory
Definition Parser.php:266
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition Parser.php:2530
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:349
ParserOutput $mOutput
Definition Parser.php:182
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2113
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4770
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3820
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1002
LinkRenderer $mLinkRenderer
Definition Parser.php:263
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:737
$mHighestExpansionDepth
Definition Parser.php:197
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5054
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:5986
static getSectionNameFromStrippedText( $text)
Definition Parser.php:5990
stripAltText( $caption, $holders)
Definition Parser.php:5521
getOptions()
Get the ParserOptions object.
Definition Parser.php:884
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4733
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:4561
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:4796
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:5916
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3639
replaceExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:1903
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:3032
getFunctionSynonyms()
Definition Parser.php:5620
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
Definition Parser.php:81
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:2497
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:2521
Language $contLang
Definition Parser.php:269
$mHeadings
Definition Parser.php:199
Title $mTitle
Definition Parser.php:219
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2185
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:5004
const PTD_FOR_INCLUSION
Definition Parser.php:106
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:6049
$mTplDomCache
Definition Parser.php:199
$mGeneratedPPNodeCount
Definition Parser.php:197
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1504
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
Definition Parser.php:6241
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:3109
LinkHolderArray $mLinkHolders
Definition Parser.php:194
$mFunctionTagHooks
Definition Parser.php:148
$mRevisionId
Definition Parser.php:223
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5271
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
Definition Parser.php:5579
pstPass2( $text, $user)
Pre-save transform helper function.
Definition Parser.php:4596
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Definition Parser.php:4815
getRevisionSize()
Get the size of the revision.
Definition Parser.php:5936
const TOC_START
Definition Parser.php:137
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition Parser.php:5694
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6086
$mDefaultSort
Definition Parser.php:198
$mTagHooks
Definition Parser.php:144
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition Parser.php:6060
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition Parser.php:1607
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:763
$mFunctionHooks
Definition Parser.php:146
$mShowToc
Definition Parser.php:201
getRevisionTimestampSubstring( $start, $len, $mtts, $variable)
Definition Parser.php:2929
getImageParams( $handler)
Definition Parser.php:5217
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition Parser.php:6214
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:4161
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1407
getUrlProtocols()
Definition Parser.php:5629
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:4953
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3573
$mRevisionSize
Definition Parser.php:226
bool $mFirstCall
Whether firstCallInit still needs to be called.
Definition Parser.php:158
__construct(array $parserConf=[], MagicWordFactory $magicWordFactory=null, Language $contLang=null, ParserFactory $factory=null, $urlProtocols=null, SpecialPageFactory $spFactory=null, Config $siteConfig=null, LinkRendererFactory $linkRendererFactory=null)
Definition Parser.php:293
doTableStuff( $text)
parse the wiki syntax used to render tables
Definition Parser.php:1125
getTitle()
Accessor for the Title object.
Definition Parser.php:830
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:1981
ParserOptions $mOptions
Definition Parser.php:214
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition Parser.php:6115
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:75
MagicWordArray $mSubstWords
Definition Parser.php:170
$mUrlProtocols
Definition Parser.php:172
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
Definition Parser.php:5643
MapCacheLRU null $currentRevisionCache
Definition Parser.php:249
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:984
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition Parser.php:2999
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2002
setOutputType( $ot)
Set the output type.
Definition Parser.php:849
makeLegacyAnchor( $sectionName)
Definition Parser.php:6001
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:916
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2570
StripState $mStripState
Definition Parser.php:188
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition Parser.php:559
$mInputSize
Definition Parser.php:228
MagicWordArray $mVariables
Definition Parser.php:165
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1098
$mOutputType
Definition Parser.php:220
$mImageParamsMagicArray
Definition Parser.php:153
setUser( $user)
Set the current user.
Definition Parser.php:803
$mAutonumber
Definition Parser.php:183
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6163
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4859
getTags()
Accessor.
Definition Parser.php:5607
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:4088
Config $siteConfig
Definition Parser.php:278
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3907
replaceInternalLinks( $s)
Process [[ ]] wikilinks.
Definition Parser.php:2172
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:241
LinkRendererFactory $linkRendererFactory
Definition Parser.php:281
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:692
$mRevisionUser
Definition Parser.php:225
string $mUniqPrefix
Deprecated accessor for the strip marker prefix.
Definition Parser.php:234
$mTplRedirCache
Definition Parser.php:199
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3795
$mFunctionSynonyms
Definition Parser.php:147
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5819
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1335
$mRevisionTimestamp
Definition Parser.php:224
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:1035
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5835
interwikiTransclude( $title, $action)
Transclude an interwiki link.
Definition Parser.php:3839
setTitle( $t)
Set the context title.
Definition Parser.php:812
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:4061
User $mUser
Definition Parser.php:206
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
Definition Parser.php:2543
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:5854
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2555
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3650
$mPPNodeCount
Definition Parser.php:197
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:5972
const MARKER_PREFIX
Definition Parser.php:134
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:5037
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:4989
doAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1700
$mMarkerIndex
Definition Parser.php:154
const EXT_LINK_ADDR
Definition Parser.php:96
$mExpensiveFunctionCount
Definition Parser.php:200
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1012
Preprocessor $mPreprocessor
Definition Parser.php:176
$mDoubleUnderscores
Definition Parser.php:199
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Definition Parser.php:3616
getOutput()
Get the ParserOutput object.
Definition Parser.php:875
$mVarCache
Definition Parser.php:151
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:894
$mImageParams
Definition Parser.php:152
Title( $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:840
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:720
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6021
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4904
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:5026
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:5844
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6282
$mIncludeCount
Definition Parser.php:190
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4719
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:970
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition Parser.php:5491
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:6037
const EXT_LINK_URL_CLASS
Definition Parser.php:93
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:866
magicLinkCallback( $m)
Definition Parser.php:1535
$mIncludeSizes
Definition Parser.php:197
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:337
fetchFile( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3783
setLinkID( $id)
Definition Parser.php:908
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:958
doQuotes( $text)
Helper function for doAllQuotes()
Definition Parser.php:1717
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3678
doHeadings( $text)
Parse headers and return html.
Definition Parser.php:1682
$mExtLinkBracketedRegex
Definition Parser.php:172
const EXT_IMAGE_REGEX
Definition Parser.php:99
$mRevIdForTs
Definition Parser.php:227
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:1111
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
Definition Parser.php:4890
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:4075
testPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:6143
static makeAnchor( $sectionName)
Definition Parser.php:5997
const SPACE_NOT_NL
Definition Parser.php:103
SpecialPageFactory $specialPageFactory
Definition Parser.php:275
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:372
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:2987
$mForceTocPosition
Definition Parser.php:201
clearState()
Clear Parser state.
Definition Parser.php:392
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6193
ParserFactory $factory
Definition Parser.php:272
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6372
nextLinkID()
Definition Parser.php:901
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:4672
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3131
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5596
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
Definition Parser.php:2957
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
Definition Parser.php:6268
lock()
Lock the current instance of the parser.
Definition Parser.php:6311
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition Parser.php:3061
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:452
$mStripList
Definition Parser.php:149
testPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:6133
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:4784
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6358
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3481
SectionProfiler $mProfiler
Definition Parser.php:258
$mTransparentTagHooks
Definition Parser.php:145
getConverterLanguage()
Get the language object for language conversion.
Definition Parser.php:948
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3691
Variant of the Message class.
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:61
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:137
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:118
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:40
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
=Architecture==Two class hierarchies are used to provide the functionality associated with the different content models:*Content interface(and AbstractContent base class) define functionality that acts on the concrete content of a page, and *ContentHandler base class provides functionality specific to a content model, but not acting on concrete content. The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content. These Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text. All manipulation and analysis of page content must be done via the appropriate methods of the Content object. For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers. The ContentHandler object for a given content model can be obtained using ContentHandler::getForModelID($id). Also Title, WikiPage and Revision now have getContentHandler() methods for convenience. ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting on the content of some page. ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to create a Content object of the appropriate type. However, it is recommended to instead use WikiPage::getContent() resp. Revision::getContent() to get a page 's content as a Content object. These two methods should be the ONLY way in which page content is accessed. Another important function of ContentHandler objects is to define custom action handlers for a content model, see ContentHandler::getActionOverrides(). This is similar to what WikiPage::getActionOverrides() was already doing.==Serialization==With the ContentHandler facility, page content no longer has to be text based. Objects implementing the Content interface are used to represent and handle the content internally. For storage and data exchange, each content model supports at least one serialization format via ContentHandler::serializeContent($content). The list of supported formats for a given content model can be accessed using ContentHandler::getSupportedFormats(). Content serialization formats are identified using MIME type like strings. The following formats are built in:*text/x-wiki - wikitext *text/javascript - for js pages *text/css - for css pages *text/plain - for future use, e.g. with plain text messages. *text/html - for future use, e.g. with plain html messages. *application/vnd.php.serialized - for future use with the api and for extensions *application/json - for future use with the api, and for use by extensions *application/xml - for future use with the api, and for use by extensions In PHP, use the corresponding CONTENT_FORMAT_XXX constant. Note that when using the API to access page content, especially action=edit, action=parse and action=query &prop=revisions, the model and format of the content should always be handled explicitly. Without that information, interpretation of the provided content is not reliable. The same applies to XML dumps generated via maintenance/dumpBackup.php or Special:Export. Also note that the API will provide encapsulated, serialized content - so if the API was called with format=json, and contentformat is also json(or rather, application/json), the page content is represented as a string containing an escaped json structure. Extensions that use JSON to serialize some types of page content may provide specialized API modules that allow access to that content in a more natural form.==Compatibility==The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least for pages that contain wikitext or other text based content. However, a number of functions and hooks have been deprecated in favor of new versions that are aware of the page 's content model, and will now generate warnings when used. Most importantly, the following functions have been deprecated:*Revisions::getText() is deprecated in favor Revisions::getContent() *WikiPage::getText() is deprecated in favor WikiPage::getContent() Also, the old Article::getContent()(which returns text) is superceded by Article::getContentObject(). However, both methods should be avoided since they do not provide clean access to the page 's actual content. For instance, they may return a system message for non-existing pages. Use WikiPage::getContent() instead. Code that relies on a textual representation of the page content should eventually be rewritten. However, ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page. Its behavior is controlled by $wgContentHandlerTextFallback it
Some information about database access in MediaWiki By Tim January Database layout For information about the MediaWiki database such as a description of the tables and their please see
Definition database.txt:18
For a write use something like
Definition database.txt:28
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 that
Definition deferred.txt:13
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 as
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
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition globals.txt:10
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:194
const SFH_NO_HASH
Definition Defines.php:206
const SFH_OBJECT_ARGS
Definition Defines.php:207
const NS_FILE
Definition Defines.php:79
const NS_MEDIAWIKI
Definition Defines.php:81
const NS_TEMPLATE
Definition Defines.php:83
const NS_SPECIAL
Definition Defines.php:62
const OT_PLAIN
Definition Defines.php:197
const OT_PREPROCESS
Definition Defines.php:195
const OT_HTML
Definition Defines.php:193
const NS_MEDIA
Definition Defines.php:61
const NS_CATEGORY
Definition Defines.php:87
const OT_MSG
Definition Defines.php:196
this hook is for auditing only $req
Definition hooks.txt:979
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1802
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition hooks.txt:1834
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1991
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:855
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:894
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:1779
either a plain
Definition hooks.txt:2054
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:1266
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:1999
null means default in associative array form
Definition hooks.txt:1994
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:2163
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:3071
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:2848
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
null for the local wiki Added in
Definition hooks.txt:1588
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:2004
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:1999
you don t have to do a grep find to see where the $wgReverseTitle variable is used
Definition hooks.txt:115
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:2290
if the prop value should be in the metadata multi language array format
Definition hooks.txt:1651
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:2003
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
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:2011
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3069
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:782
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:2012
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going on
Definition hooks.txt:86
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
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:2272
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:1779
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2175
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 and or sell copies of the and to permit persons to whom the Software is furnished to do so
Definition LICENSE.txt:13
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Interface for configuration instances.
Definition Config.php:28
const NO_TEMPLATES
const RECOVER_ORIG
const NO_ARGS
const STRIP_COMMENTS
There are three types of nodes:
magicword txt Magic Words are some phrases used in the wikitext They are used for two things
Definition magicword.txt:4
$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
In both all secondary updates will be triggered handle like object that caches derived data representing a revision
$content
This document provides an overview of the usage of PageUpdater and that is
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 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:30
$sort
const DB_REPLICA
Definition defines.php:25
$lines
Definition router.php:61
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition router.php:42
$params