MediaWiki REL1_28
Parser.php
Go to the documentation of this file.
1<?php
25use Wikimedia\ScopedCallback;
26
70class Parser {
76 const VERSION = '1.6.4';
77
83
84 # Flags for Parser::setFunctionHook
85 const SFH_NO_HASH = 1;
86 const SFH_OBJECT_ARGS = 2;
87
88 # Constants needed for external link processing
89 # Everything except bracket, space, or control characters
90 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
91 # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
92 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
93 # Simplified expression to match an IPv4 or IPv6 address, or
94 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
95 const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
96 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
97 // @codingStandardsIgnoreStart Generic.Files.LineLength
98 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
99 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
100 // @codingStandardsIgnoreEnd
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
140 # Persistent:
141 public $mTagHooks = [];
143 public $mFunctionHooks = [];
144 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
146 public $mStripList = [];
148 public $mVarCache = [];
149 public $mImageParams = [];
151 public $mMarkerIndex = 0;
152 public $mFirstCall = true;
153
154 # Initialised by initialiseVariables()
155
160
165 # Initialised in constructor
166 public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
167
168 # Initialized in getPreprocessor()
171
172 # Cleared with clearState():
176 public $mOutput;
178
183
189
190 public $mLinkID;
191 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
193 public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
194 public $mExpensiveFunctionCount; # number of expensive parser function calls
196
200 public $mUser; # User object; only used when doing pre-save transform
201
202 # Temporary
203 # These are variables reset at least once per parse regardless of $clearState
204
208 public $mOptions;
209
213 public $mTitle; # Title context, used for self-link rendering and similar things
214 public $mOutputType; # Output type, one of the OT_xxx constants
215 public $ot; # Shortcut alias, see setOutputType()
216 public $mRevisionObject; # The revision object of the specified revision ID
217 public $mRevisionId; # ID to display in {{REVISIONID}} tags
218 public $mRevisionTimestamp; # The timestamp of the specified revision ID
219 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
220 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
221 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
222 public $mInputSize = false; # For {{PAGESIZE}} on current page.
223
228 public $mUniqPrefix = Parser::MARKER_PREFIX;
229
236
244
249 public $mInParse = false;
250
252 protected $mProfiler;
253
257 protected $mLinkRenderer;
258
262 public function __construct( $conf = [] ) {
263 $this->mConf = $conf;
264 $this->mUrlProtocols = wfUrlProtocols();
265 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
266 self::EXT_LINK_ADDR .
267 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
268 if ( isset( $conf['preprocessorClass'] ) ) {
269 $this->mPreprocessorClass = $conf['preprocessorClass'];
270 } elseif ( defined( 'HPHP_VERSION' ) ) {
271 # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
272 $this->mPreprocessorClass = 'Preprocessor_Hash';
273 } elseif ( extension_loaded( 'domxml' ) ) {
274 # PECL extension that conflicts with the core DOM extension (bug 13770)
275 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
276 $this->mPreprocessorClass = 'Preprocessor_Hash';
277 } elseif ( extension_loaded( 'dom' ) ) {
278 $this->mPreprocessorClass = 'Preprocessor_DOM';
279 } else {
280 $this->mPreprocessorClass = 'Preprocessor_Hash';
281 }
282 wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
283 }
284
288 public function __destruct() {
289 if ( isset( $this->mLinkHolders ) ) {
290 unset( $this->mLinkHolders );
291 }
292 foreach ( $this as $name => $value ) {
293 unset( $this->$name );
294 }
295 }
296
300 public function __clone() {
301 $this->mInParse = false;
302
303 // Bug 56226: When you create a reference "to" an object field, that
304 // makes the object field itself be a reference too (until the other
305 // reference goes out of scope). When cloning, any field that's a
306 // reference is copied as a reference in the new object. Both of these
307 // are defined PHP5 behaviors, as inconvenient as it is for us when old
308 // hooks from PHP4 days are passing fields by reference.
309 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
310 // Make a non-reference copy of the field, then rebind the field to
311 // reference the new copy.
312 $tmp = $this->$k;
313 $this->$k =& $tmp;
314 unset( $tmp );
315 }
316
317 Hooks::run( 'ParserCloned', [ $this ] );
318 }
319
323 public function firstCallInit() {
324 if ( !$this->mFirstCall ) {
325 return;
326 }
327 $this->mFirstCall = false;
328
330 CoreTagHooks::register( $this );
331 $this->initialiseVariables();
332
333 // Avoid PHP 7.1 warning from passing $this by reference
334 $parser = $this;
335 Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
336 }
337
343 public function clearState() {
344 if ( $this->mFirstCall ) {
345 $this->firstCallInit();
346 }
347 $this->mOutput = new ParserOutput;
348 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
349 $this->mAutonumber = 0;
350 $this->mIncludeCount = [];
351 $this->mLinkHolders = new LinkHolderArray( $this );
352 $this->mLinkID = 0;
353 $this->mRevisionObject = $this->mRevisionTimestamp =
354 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
355 $this->mVarCache = [];
356 $this->mUser = null;
357 $this->mLangLinkLanguages = [];
358 $this->currentRevisionCache = null;
359
360 $this->mStripState = new StripState;
361
362 # Clear these on every parse, bug 4549
363 $this->mTplRedirCache = $this->mTplDomCache = [];
364
365 $this->mShowToc = true;
366 $this->mForceTocPosition = false;
367 $this->mIncludeSizes = [
368 'post-expand' => 0,
369 'arg' => 0,
370 ];
371 $this->mPPNodeCount = 0;
372 $this->mGeneratedPPNodeCount = 0;
373 $this->mHighestExpansionDepth = 0;
374 $this->mDefaultSort = false;
375 $this->mHeadings = [];
376 $this->mDoubleUnderscores = [];
377 $this->mExpensiveFunctionCount = 0;
378
379 # Fix cloning
380 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
381 $this->mPreprocessor = null;
382 }
383
384 $this->mProfiler = new SectionProfiler();
385
386 // Avoid PHP 7.1 warning from passing $this by reference
387 $parser = $this;
388 Hooks::run( 'ParserClearState', [ &$parser ] );
389 }
390
403 public function parse(
404 $text, Title $title, ParserOptions $options,
405 $linestart = true, $clearState = true, $revid = null
406 ) {
413
414 if ( $clearState ) {
415 // We use U+007F DELETE to construct strip markers, so we have to make
416 // sure that this character does not occur in the input text.
417 $text = strtr( $text, "\x7f", "?" );
418 $magicScopeVariable = $this->lock();
419 }
420
421 $this->startParse( $title, $options, self::OT_HTML, $clearState );
422
423 $this->currentRevisionCache = null;
424 $this->mInputSize = strlen( $text );
425 if ( $this->mOptions->getEnableLimitReport() ) {
426 $this->mOutput->resetParseStartTime();
427 }
428
429 $oldRevisionId = $this->mRevisionId;
430 $oldRevisionObject = $this->mRevisionObject;
431 $oldRevisionTimestamp = $this->mRevisionTimestamp;
432 $oldRevisionUser = $this->mRevisionUser;
433 $oldRevisionSize = $this->mRevisionSize;
434 if ( $revid !== null ) {
435 $this->mRevisionId = $revid;
436 $this->mRevisionObject = null;
437 $this->mRevisionTimestamp = null;
438 $this->mRevisionUser = null;
439 $this->mRevisionSize = null;
440 }
441
442 // Avoid PHP 7.1 warning from passing $this by reference
443 $parser = $this;
444 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
445 # No more strip!
446 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
447 $text = $this->internalParse( $text );
448 Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
449
450 $text = $this->internalParseHalfParsed( $text, true, $linestart );
451
459 if ( !( $options->getDisableTitleConversion()
460 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
461 || isset( $this->mDoubleUnderscores['notitleconvert'] )
462 || $this->mOutput->getDisplayTitle() !== false )
463 ) {
464 $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
465 if ( $convruletitle ) {
466 $this->mOutput->setTitleText( $convruletitle );
467 } else {
468 $titleText = $this->getConverterLanguage()->convertTitle( $title );
469 $this->mOutput->setTitleText( $titleText );
470 }
471 }
472
473 # Done parsing! Compute runtime adaptive expiry if set
474 $this->mOutput->finalizeAdaptiveCacheExpiry();
475
476 # Warn if too many heavyweight parser functions were used
477 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
478 $this->limitationWarn( 'expensive-parserfunction',
479 $this->mExpensiveFunctionCount,
480 $this->mOptions->getExpensiveParserFunctionLimit()
481 );
482 }
483
484 # Information on include size limits, for the benefit of users who try to skirt them
485 if ( $this->mOptions->getEnableLimitReport() ) {
486 $max = $this->mOptions->getMaxIncludeSize();
487
488 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
489 if ( $cpuTime !== null ) {
490 $this->mOutput->setLimitReportData( 'limitreport-cputime',
491 sprintf( "%.3f", $cpuTime )
492 );
493 }
494
495 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
496 $this->mOutput->setLimitReportData( 'limitreport-walltime',
497 sprintf( "%.3f", $wallTime )
498 );
499
500 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
501 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
502 );
503 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
504 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
505 );
506 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
507 [ $this->mIncludeSizes['post-expand'], $max ]
508 );
509 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
510 [ $this->mIncludeSizes['arg'], $max ]
511 );
512 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
513 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
514 );
515 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
516 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
517 );
518 Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
519
520 $limitReport = "NewPP limit report\n";
521 if ( $wgShowHostnames ) {
522 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
523 }
524 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
525 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
526 $limitReport .= 'Dynamic content: ' .
527 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
528 "\n";
529
530 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
531 if ( Hooks::run( 'ParserLimitReportFormat',
532 [ $key, &$value, &$limitReport, false, false ]
533 ) ) {
534 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
535 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
536 ->inLanguage( 'en' )->useDatabase( false );
537 if ( !$valueMsg->exists() ) {
538 $valueMsg = new RawMessage( '$1' );
539 }
540 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
541 $valueMsg->params( $value );
542 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
543 }
544 }
545 }
546 // Since we're not really outputting HTML, decode the entities and
547 // then re-encode the things that need hiding inside HTML comments.
548 $limitReport = htmlspecialchars_decode( $limitReport );
549 Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ] );
550
551 // Sanitize for comment. Note '‐' in the replacement is U+2010,
552 // which looks much like the problematic '-'.
553 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
554 $text .= "\n<!-- \n$limitReport-->\n";
555
556 // Add on template profiling data
557 $dataByFunc = $this->mProfiler->getFunctionStats();
558 uasort( $dataByFunc, function ( $a, $b ) {
559 return $a['real'] < $b['real']; // descending order
560 } );
561 $profileReport = "Transclusion expansion time report (%,ms,calls,template)\n";
562 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
563 $profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n",
564 $item['%real'], $item['real'], $item['calls'],
565 htmlspecialchars( $item['name'] ) );
566 }
567 $text .= "\n<!-- \n$profileReport-->\n";
568
569 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
570 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
571 $this->mTitle->getPrefixedDBkey() );
572 }
573 }
574 $this->mOutput->setText( $text );
575
576 $this->mRevisionId = $oldRevisionId;
577 $this->mRevisionObject = $oldRevisionObject;
578 $this->mRevisionTimestamp = $oldRevisionTimestamp;
579 $this->mRevisionUser = $oldRevisionUser;
580 $this->mRevisionSize = $oldRevisionSize;
581 $this->mInputSize = false;
582 $this->currentRevisionCache = null;
583
584 return $this->mOutput;
585 }
586
609 public function recursiveTagParse( $text, $frame = false ) {
610 // Avoid PHP 7.1 warning from passing $this by reference
611 $parser = $this;
612 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
613 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
614 $text = $this->internalParse( $text, false, $frame );
615 return $text;
616 }
617
635 public function recursiveTagParseFully( $text, $frame = false ) {
636 $text = $this->recursiveTagParse( $text, $frame );
637 $text = $this->internalParseHalfParsed( $text, false );
638 return $text;
639 }
640
652 public function preprocess( $text, Title $title = null,
653 ParserOptions $options, $revid = null, $frame = false
654 ) {
655 $magicScopeVariable = $this->lock();
656 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
657 if ( $revid !== null ) {
658 $this->mRevisionId = $revid;
659 }
660 // Avoid PHP 7.1 warning from passing $this by reference
661 $parser = $this;
662 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
663 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
664 $text = $this->replaceVariables( $text, $frame );
665 $text = $this->mStripState->unstripBoth( $text );
666 return $text;
667 }
668
678 public function recursivePreprocess( $text, $frame = false ) {
679 $text = $this->replaceVariables( $text, $frame );
680 $text = $this->mStripState->unstripBoth( $text );
681 return $text;
682 }
683
697 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
698 $msg = new RawMessage( $text );
699 $text = $msg->params( $params )->plain();
700
701 # Parser (re)initialisation
702 $magicScopeVariable = $this->lock();
703 $this->startParse( $title, $options, self::OT_PLAIN, true );
704
706 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
707 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
708 $text = $this->mStripState->unstripBoth( $text );
709 return $text;
710 }
711
718 public static function getRandomString() {
719 wfDeprecated( __METHOD__, '1.26' );
720 return wfRandomString( 16 );
721 }
722
729 public function setUser( $user ) {
730 $this->mUser = $user;
731 }
732
739 public function uniqPrefix() {
740 wfDeprecated( __METHOD__, '1.26' );
741 return self::MARKER_PREFIX;
742 }
743
749 public function setTitle( $t ) {
750 if ( !$t ) {
751 $t = Title::newFromText( 'NO TITLE' );
752 }
753
754 if ( $t->hasFragment() ) {
755 # Strip the fragment to avoid various odd effects
756 $this->mTitle = $t->createFragmentTarget( '' );
757 } else {
758 $this->mTitle = $t;
759 }
760 }
761
767 public function getTitle() {
768 return $this->mTitle;
769 }
770
777 public function Title( $x = null ) {
778 return wfSetVar( $this->mTitle, $x );
779 }
780
786 public function setOutputType( $ot ) {
787 $this->mOutputType = $ot;
788 # Shortcut alias
789 $this->ot = [
790 'html' => $ot == self::OT_HTML,
791 'wiki' => $ot == self::OT_WIKI,
792 'pre' => $ot == self::OT_PREPROCESS,
793 'plain' => $ot == self::OT_PLAIN,
794 ];
795 }
796
803 public function OutputType( $x = null ) {
804 return wfSetVar( $this->mOutputType, $x );
805 }
806
812 public function getOutput() {
813 return $this->mOutput;
814 }
815
821 public function getOptions() {
822 return $this->mOptions;
823 }
824
831 public function Options( $x = null ) {
832 return wfSetVar( $this->mOptions, $x );
833 }
834
838 public function nextLinkID() {
839 return $this->mLinkID++;
840 }
841
845 public function setLinkID( $id ) {
846 $this->mLinkID = $id;
847 }
848
853 public function getFunctionLang() {
854 return $this->getTargetLanguage();
855 }
856
866 public function getTargetLanguage() {
867 $target = $this->mOptions->getTargetLanguage();
868
869 if ( $target !== null ) {
870 return $target;
871 } elseif ( $this->mOptions->getInterfaceMessage() ) {
872 return $this->mOptions->getUserLangObj();
873 } elseif ( is_null( $this->mTitle ) ) {
874 throw new MWException( __METHOD__ . ': $this->mTitle is null' );
875 }
876
877 return $this->mTitle->getPageLanguage();
878 }
879
884 public function getConverterLanguage() {
885 return $this->getTargetLanguage();
886 }
887
894 public function getUser() {
895 if ( !is_null( $this->mUser ) ) {
896 return $this->mUser;
897 }
898 return $this->mOptions->getUser();
899 }
900
906 public function getPreprocessor() {
907 if ( !isset( $this->mPreprocessor ) ) {
908 $class = $this->mPreprocessorClass;
909 $this->mPreprocessor = new $class( $this );
910 }
911 return $this->mPreprocessor;
912 }
913
920 public function getLinkRenderer() {
921 if ( !$this->mLinkRenderer ) {
922 $this->mLinkRenderer = MediaWikiServices::getInstance()
923 ->getLinkRendererFactory()->create();
924 $this->mLinkRenderer->setStubThreshold(
925 $this->getOptions()->getStubThreshold()
926 );
927 }
928
929 return $this->mLinkRenderer;
930 }
931
953 public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
954 if ( $uniq_prefix !== null ) {
955 wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
956 }
957 static $n = 1;
958 $stripped = '';
959 $matches = [];
960
961 $taglist = implode( '|', $elements );
962 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
963
964 while ( $text != '' ) {
965 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
966 $stripped .= $p[0];
967 if ( count( $p ) < 5 ) {
968 break;
969 }
970 if ( count( $p ) > 5 ) {
971 # comment
972 $element = $p[4];
973 $attributes = '';
974 $close = '';
975 $inside = $p[5];
976 } else {
977 # tag
978 $element = $p[1];
979 $attributes = $p[2];
980 $close = $p[3];
981 $inside = $p[4];
982 }
983
984 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
985 $stripped .= $marker;
986
987 if ( $close === '/>' ) {
988 # Empty element tag, <tag />
989 $content = null;
990 $text = $inside;
991 $tail = null;
992 } else {
993 if ( $element === '!--' ) {
994 $end = '/(-->)/';
995 } else {
996 $end = "/(<\\/$element\\s*>)/i";
997 }
998 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
999 $content = $q[0];
1000 if ( count( $q ) < 3 ) {
1001 # No end tag -- let it run out to the end of the text.
1002 $tail = '';
1003 $text = '';
1004 } else {
1005 $tail = $q[1];
1006 $text = $q[2];
1007 }
1008 }
1009
1010 $matches[$marker] = [ $element,
1011 $content,
1012 Sanitizer::decodeTagAttributes( $attributes ),
1013 "<$element$attributes$close$content$tail" ];
1014 }
1015 return $stripped;
1016 }
1017
1023 public function getStripList() {
1024 return $this->mStripList;
1025 }
1026
1036 public function insertStripItem( $text ) {
1037 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1038 $this->mMarkerIndex++;
1039 $this->mStripState->addGeneral( $marker, $text );
1040 return $marker;
1041 }
1042
1050 public function doTableStuff( $text ) {
1051
1052 $lines = StringUtils::explode( "\n", $text );
1053 $out = '';
1054 $td_history = []; # Is currently a td tag open?
1055 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1056 $tr_history = []; # Is currently a tr tag open?
1057 $tr_attributes = []; # history of tr attributes
1058 $has_opened_tr = []; # Did this table open a <tr> element?
1059 $indent_level = 0; # indent level of the table
1060
1061 foreach ( $lines as $outLine ) {
1062 $line = trim( $outLine );
1063
1064 if ( $line === '' ) { # empty line, go to next line
1065 $out .= $outLine . "\n";
1066 continue;
1067 }
1068
1069 $first_character = $line[0];
1070 $first_two = substr( $line, 0, 2 );
1071 $matches = [];
1072
1073 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1074 # First check if we are starting a new table
1075 $indent_level = strlen( $matches[1] );
1076
1077 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1078 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1079
1080 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1081 array_push( $td_history, false );
1082 array_push( $last_tag_history, '' );
1083 array_push( $tr_history, false );
1084 array_push( $tr_attributes, '' );
1085 array_push( $has_opened_tr, false );
1086 } elseif ( count( $td_history ) == 0 ) {
1087 # Don't do any of the following
1088 $out .= $outLine . "\n";
1089 continue;
1090 } elseif ( $first_two === '|}' ) {
1091 # We are ending a table
1092 $line = '</table>' . substr( $line, 2 );
1093 $last_tag = array_pop( $last_tag_history );
1094
1095 if ( !array_pop( $has_opened_tr ) ) {
1096 $line = "<tr><td></td></tr>{$line}";
1097 }
1098
1099 if ( array_pop( $tr_history ) ) {
1100 $line = "</tr>{$line}";
1101 }
1102
1103 if ( array_pop( $td_history ) ) {
1104 $line = "</{$last_tag}>{$line}";
1105 }
1106 array_pop( $tr_attributes );
1107 $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1108 } elseif ( $first_two === '|-' ) {
1109 # Now we have a table row
1110 $line = preg_replace( '#^\|-+#', '', $line );
1111
1112 # Whats after the tag is now only attributes
1113 $attributes = $this->mStripState->unstripBoth( $line );
1114 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1115 array_pop( $tr_attributes );
1116 array_push( $tr_attributes, $attributes );
1117
1118 $line = '';
1119 $last_tag = array_pop( $last_tag_history );
1120 array_pop( $has_opened_tr );
1121 array_push( $has_opened_tr, true );
1122
1123 if ( array_pop( $tr_history ) ) {
1124 $line = '</tr>';
1125 }
1126
1127 if ( array_pop( $td_history ) ) {
1128 $line = "</{$last_tag}>{$line}";
1129 }
1130
1131 $outLine = $line;
1132 array_push( $tr_history, false );
1133 array_push( $td_history, false );
1134 array_push( $last_tag_history, '' );
1135 } elseif ( $first_character === '|'
1136 || $first_character === '!'
1137 || $first_two === '|+'
1138 ) {
1139 # This might be cell elements, td, th or captions
1140 if ( $first_two === '|+' ) {
1141 $first_character = '+';
1142 $line = substr( $line, 2 );
1143 } else {
1144 $line = substr( $line, 1 );
1145 }
1146
1147 // Implies both are valid for table headings.
1148 if ( $first_character === '!' ) {
1149 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1150 }
1151
1152 # Split up multiple cells on the same line.
1153 # FIXME : This can result in improper nesting of tags processed
1154 # by earlier parser steps.
1155 $cells = explode( '||', $line );
1156
1157 $outLine = '';
1158
1159 # Loop through each table cell
1160 foreach ( $cells as $cell ) {
1161 $previous = '';
1162 if ( $first_character !== '+' ) {
1163 $tr_after = array_pop( $tr_attributes );
1164 if ( !array_pop( $tr_history ) ) {
1165 $previous = "<tr{$tr_after}>\n";
1166 }
1167 array_push( $tr_history, true );
1168 array_push( $tr_attributes, '' );
1169 array_pop( $has_opened_tr );
1170 array_push( $has_opened_tr, true );
1171 }
1172
1173 $last_tag = array_pop( $last_tag_history );
1174
1175 if ( array_pop( $td_history ) ) {
1176 $previous = "</{$last_tag}>\n{$previous}";
1177 }
1178
1179 if ( $first_character === '|' ) {
1180 $last_tag = 'td';
1181 } elseif ( $first_character === '!' ) {
1182 $last_tag = 'th';
1183 } elseif ( $first_character === '+' ) {
1184 $last_tag = 'caption';
1185 } else {
1186 $last_tag = '';
1187 }
1188
1189 array_push( $last_tag_history, $last_tag );
1190
1191 # A cell could contain both parameters and data
1192 $cell_data = explode( '|', $cell, 2 );
1193
1194 # Bug 553: Note that a '|' inside an invalid link should not
1195 # be mistaken as delimiting cell parameters
1196 if ( strpos( $cell_data[0], '[[' ) !== false ) {
1197 $cell = "{$previous}<{$last_tag}>{$cell}";
1198 } elseif ( count( $cell_data ) == 1 ) {
1199 $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1200 } else {
1201 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1202 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1203 $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1204 }
1205
1206 $outLine .= $cell;
1207 array_push( $td_history, true );
1208 }
1209 }
1210 $out .= $outLine . "\n";
1211 }
1212
1213 # Closing open td, tr && table
1214 while ( count( $td_history ) > 0 ) {
1215 if ( array_pop( $td_history ) ) {
1216 $out .= "</td>\n";
1217 }
1218 if ( array_pop( $tr_history ) ) {
1219 $out .= "</tr>\n";
1220 }
1221 if ( !array_pop( $has_opened_tr ) ) {
1222 $out .= "<tr><td></td></tr>\n";
1223 }
1224
1225 $out .= "</table>\n";
1226 }
1227
1228 # Remove trailing line-ending (b/c)
1229 if ( substr( $out, -1 ) === "\n" ) {
1230 $out = substr( $out, 0, -1 );
1231 }
1232
1233 # special case: don't return empty table
1234 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1235 $out = '';
1236 }
1237
1238 return $out;
1239 }
1240
1253 public function internalParse( $text, $isMain = true, $frame = false ) {
1254
1255 $origText = $text;
1256
1257 // Avoid PHP 7.1 warning from passing $this by reference
1258 $parser = $this;
1259
1260 # Hook to suspend the parser in this state
1261 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1262 return $text;
1263 }
1264
1265 # if $frame is provided, then use $frame for replacing any variables
1266 if ( $frame ) {
1267 # use frame depth to infer how include/noinclude tags should be handled
1268 # depth=0 means this is the top-level document; otherwise it's an included document
1269 if ( !$frame->depth ) {
1270 $flag = 0;
1271 } else {
1273 }
1274 $dom = $this->preprocessToDom( $text, $flag );
1275 $text = $frame->expand( $dom );
1276 } else {
1277 # if $frame is not provided, then use old-style replaceVariables
1278 $text = $this->replaceVariables( $text );
1279 }
1280
1281 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1282 $text = Sanitizer::removeHTMLtags(
1283 $text,
1284 [ $this, 'attributeStripCallback' ],
1285 false,
1286 array_keys( $this->mTransparentTagHooks ),
1287 [],
1288 [ $this, 'addTrackingCategory' ]
1289 );
1290 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1291
1292 # Tables need to come after variable replacement for things to work
1293 # properly; putting them before other transformations should keep
1294 # exciting things like link expansions from showing up in surprising
1295 # places.
1296 $text = $this->doTableStuff( $text );
1297
1298 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1299
1300 $text = $this->doDoubleUnderscore( $text );
1301
1302 $text = $this->doHeadings( $text );
1303 $text = $this->replaceInternalLinks( $text );
1304 $text = $this->doAllQuotes( $text );
1305 $text = $this->replaceExternalLinks( $text );
1306
1307 # replaceInternalLinks may sometimes leave behind
1308 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1309 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1310
1311 $text = $this->doMagicLinks( $text );
1312 $text = $this->formatHeadings( $text, $origText, $isMain );
1313
1314 return $text;
1315 }
1316
1326 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1327 $text = $this->mStripState->unstripGeneral( $text );
1328
1329 // Avoid PHP 7.1 warning from passing $this by reference
1330 $parser = $this;
1331
1332 if ( $isMain ) {
1333 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1334 }
1335
1336 # Clean up special characters, only run once, next-to-last before doBlockLevels
1337 $fixtags = [
1338 # french spaces, last one Guillemet-left
1339 # only if there is something before the space
1340 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1341 # french spaces, Guillemet-right
1342 '/(\\302\\253) /' => '\\1&#160;',
1343 '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1344 ];
1345 $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1346
1347 $text = $this->doBlockLevels( $text, $linestart );
1348
1349 $this->replaceLinkHolders( $text );
1350
1358 if ( !( $this->mOptions->getDisableContentConversion()
1359 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1360 ) {
1361 if ( !$this->mOptions->getInterfaceMessage() ) {
1362 # The position of the convert() call should not be changed. it
1363 # assumes that the links are all replaced and the only thing left
1364 # is the <nowiki> mark.
1365 $text = $this->getConverterLanguage()->convert( $text );
1366 }
1367 }
1368
1369 $text = $this->mStripState->unstripNoWiki( $text );
1370
1371 if ( $isMain ) {
1372 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1373 }
1374
1375 $text = $this->replaceTransparentTags( $text );
1376 $text = $this->mStripState->unstripGeneral( $text );
1377
1378 $text = Sanitizer::normalizeCharReferences( $text );
1379
1380 if ( MWTidy::isEnabled() ) {
1381 if ( $this->mOptions->getTidy() ) {
1382 $text = MWTidy::tidy( $text );
1383 }
1384 } else {
1385 # attempt to sanitize at least some nesting problems
1386 # (bug #2702 and quite a few others)
1387 $tidyregs = [
1388 # ''Something [http://www.cool.com cool''] -->
1389 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1390 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1391 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1392 # fix up an anchor inside another anchor, only
1393 # at least for a single single nested link (bug 3695)
1394 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1395 '\\1\\2</a>\\3</a>\\1\\4</a>',
1396 # fix div inside inline elements- doBlockLevels won't wrap a line which
1397 # contains a div, so fix it up here; replace
1398 # div with escaped text
1399 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1400 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1401 # remove empty italic or bold tag pairs, some
1402 # introduced by rules above
1403 '/<([bi])><\/\\1>/' => '',
1404 ];
1405
1406 $text = preg_replace(
1407 array_keys( $tidyregs ),
1408 array_values( $tidyregs ),
1409 $text );
1410 }
1411
1412 if ( $isMain ) {
1413 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1414 }
1415
1416 return $text;
1417 }
1418
1430 public function doMagicLinks( $text ) {
1432 $urlChar = self::EXT_LINK_URL_CLASS;
1433 $addr = self::EXT_LINK_ADDR;
1434 $space = self::SPACE_NOT_NL; # non-newline space
1435 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1436 $spaces = "$space++"; # possessive match of 1 or more spaces
1437 $text = preg_replace_callback(
1438 '!(?: # Start cases
1439 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1440 (<.*?>) | # m[2]: Skip stuff inside
1441 # HTML elements' . "
1442 (\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1443 # m[4]: Post-protocol path
1444 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1445 ([0-9]+)\b |
1446 \bISBN $spaces ( # m[6]: ISBN, capture number
1447 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1448 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1449 [0-9Xx] # check digit
1450 )\b
1451 )!xu", [ $this, 'magicLinkCallback' ], $text );
1452 return $text;
1453 }
1454
1460 public function magicLinkCallback( $m ) {
1461 if ( isset( $m[1] ) && $m[1] !== '' ) {
1462 # Skip anchor
1463 return $m[0];
1464 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1465 # Skip HTML element
1466 return $m[0];
1467 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1468 # Free external link
1469 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1470 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1471 # RFC or PMID
1472 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1473 if ( !$this->mOptions->getMagicRFCLinks() ) {
1474 return $m[0];
1475 }
1476 $keyword = 'RFC';
1477 $urlmsg = 'rfcurl';
1478 $cssClass = 'mw-magiclink-rfc';
1479 $trackingCat = 'magiclink-tracking-rfc';
1480 $id = $m[5];
1481 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1482 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1483 return $m[0];
1484 }
1485 $keyword = 'PMID';
1486 $urlmsg = 'pubmedurl';
1487 $cssClass = 'mw-magiclink-pmid';
1488 $trackingCat = 'magiclink-tracking-pmid';
1489 $id = $m[5];
1490 } else {
1491 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1492 substr( $m[0], 0, 20 ) . '"' );
1493 }
1494 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1495 $this->addTrackingCategory( $trackingCat );
1496 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1497 } elseif ( isset( $m[6] ) && $m[6] !== ''
1498 && $this->mOptions->getMagicISBNLinks()
1499 ) {
1500 # ISBN
1501 $isbn = $m[6];
1502 $space = self::SPACE_NOT_NL; # non-newline space
1503 $isbn = preg_replace( "/$space/", ' ', $isbn );
1504 $num = strtr( $isbn, [
1505 '-' => '',
1506 ' ' => '',
1507 'x' => 'X',
1508 ] );
1509 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1510 return $this->getLinkRenderer()->makeKnownLink(
1511 SpecialPage::getTitleFor( 'Booksources', $num ),
1512 "ISBN $isbn",
1513 [
1514 'class' => 'internal mw-magiclink-isbn',
1515 'title' => false // suppress title attribute
1516 ]
1517 );
1518 } else {
1519 return $m[0];
1520 }
1521 }
1522
1532 public function makeFreeExternalLink( $url, $numPostProto ) {
1533 $trail = '';
1534
1535 # The characters '<' and '>' (which were escaped by
1536 # removeHTMLtags()) should not be included in
1537 # URLs, per RFC 2396.
1538 # Make &nbsp; terminate a URL as well (bug T84937)
1539 $m2 = [];
1540 if ( preg_match(
1541 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1542 $url,
1543 $m2,
1544 PREG_OFFSET_CAPTURE
1545 ) ) {
1546 $trail = substr( $url, $m2[0][1] ) . $trail;
1547 $url = substr( $url, 0, $m2[0][1] );
1548 }
1549
1550 # Move trailing punctuation to $trail
1551 $sep = ',;\.:!?';
1552 # If there is no left bracket, then consider right brackets fair game too
1553 if ( strpos( $url, '(' ) === false ) {
1554 $sep .= ')';
1555 }
1556
1557 $urlRev = strrev( $url );
1558 $numSepChars = strspn( $urlRev, $sep );
1559 # Don't break a trailing HTML entity by moving the ; into $trail
1560 # This is in hot code, so use substr_compare to avoid having to
1561 # create a new string object for the comparison
1562 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1563 # more optimization: instead of running preg_match with a $
1564 # anchor, which can be slow, do the match on the reversed
1565 # string starting at the desired offset.
1566 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1567 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1568 $numSepChars--;
1569 }
1570 }
1571 if ( $numSepChars ) {
1572 $trail = substr( $url, -$numSepChars ) . $trail;
1573 $url = substr( $url, 0, -$numSepChars );
1574 }
1575
1576 # Verify that we still have a real URL after trail removal, and
1577 # not just lone protocol
1578 if ( strlen( $trail ) >= $numPostProto ) {
1579 return $url . $trail;
1580 }
1581
1582 $url = Sanitizer::cleanUrl( $url );
1583
1584 # Is this an external image?
1585 $text = $this->maybeMakeExternalImage( $url );
1586 if ( $text === false ) {
1587 # Not an image, make a link
1588 $text = Linker::makeExternalLink( $url,
1589 $this->getConverterLanguage()->markNoConversion( $url, true ),
1590 true, 'free',
1591 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1592 # Register it in the output object...
1593 $this->mOutput->addExternalLink( $url );
1594 }
1595 return $text . $trail;
1596 }
1597
1607 public function doHeadings( $text ) {
1608 for ( $i = 6; $i >= 1; --$i ) {
1609 $h = str_repeat( '=', $i );
1610 $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1611 }
1612 return $text;
1613 }
1614
1623 public function doAllQuotes( $text ) {
1624 $outtext = '';
1625 $lines = StringUtils::explode( "\n", $text );
1626 foreach ( $lines as $line ) {
1627 $outtext .= $this->doQuotes( $line ) . "\n";
1628 }
1629 $outtext = substr( $outtext, 0, -1 );
1630 return $outtext;
1631 }
1632
1640 public function doQuotes( $text ) {
1641 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1642 $countarr = count( $arr );
1643 if ( $countarr == 1 ) {
1644 return $text;
1645 }
1646
1647 // First, do some preliminary work. This may shift some apostrophes from
1648 // being mark-up to being text. It also counts the number of occurrences
1649 // of bold and italics mark-ups.
1650 $numbold = 0;
1651 $numitalics = 0;
1652 for ( $i = 1; $i < $countarr; $i += 2 ) {
1653 $thislen = strlen( $arr[$i] );
1654 // If there are ever four apostrophes, assume the first is supposed to
1655 // be text, and the remaining three constitute mark-up for bold text.
1656 // (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1657 if ( $thislen == 4 ) {
1658 $arr[$i - 1] .= "'";
1659 $arr[$i] = "'''";
1660 $thislen = 3;
1661 } elseif ( $thislen > 5 ) {
1662 // If there are more than 5 apostrophes in a row, assume they're all
1663 // text except for the last 5.
1664 // (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1665 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1666 $arr[$i] = "'''''";
1667 $thislen = 5;
1668 }
1669 // Count the number of occurrences of bold and italics mark-ups.
1670 if ( $thislen == 2 ) {
1671 $numitalics++;
1672 } elseif ( $thislen == 3 ) {
1673 $numbold++;
1674 } elseif ( $thislen == 5 ) {
1675 $numitalics++;
1676 $numbold++;
1677 }
1678 }
1679
1680 // If there is an odd number of both bold and italics, it is likely
1681 // that one of the bold ones was meant to be an apostrophe followed
1682 // by italics. Which one we cannot know for certain, but it is more
1683 // likely to be one that has a single-letter word before it.
1684 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1685 $firstsingleletterword = -1;
1686 $firstmultiletterword = -1;
1687 $firstspace = -1;
1688 for ( $i = 1; $i < $countarr; $i += 2 ) {
1689 if ( strlen( $arr[$i] ) == 3 ) {
1690 $x1 = substr( $arr[$i - 1], -1 );
1691 $x2 = substr( $arr[$i - 1], -2, 1 );
1692 if ( $x1 === ' ' ) {
1693 if ( $firstspace == -1 ) {
1694 $firstspace = $i;
1695 }
1696 } elseif ( $x2 === ' ' ) {
1697 $firstsingleletterword = $i;
1698 // if $firstsingleletterword is set, we don't
1699 // look at the other options, so we can bail early.
1700 break;
1701 } else {
1702 if ( $firstmultiletterword == -1 ) {
1703 $firstmultiletterword = $i;
1704 }
1705 }
1706 }
1707 }
1708
1709 // If there is a single-letter word, use it!
1710 if ( $firstsingleletterword > -1 ) {
1711 $arr[$firstsingleletterword] = "''";
1712 $arr[$firstsingleletterword - 1] .= "'";
1713 } elseif ( $firstmultiletterword > -1 ) {
1714 // If not, but there's a multi-letter word, use that one.
1715 $arr[$firstmultiletterword] = "''";
1716 $arr[$firstmultiletterword - 1] .= "'";
1717 } elseif ( $firstspace > -1 ) {
1718 // ... otherwise use the first one that has neither.
1719 // (notice that it is possible for all three to be -1 if, for example,
1720 // there is only one pentuple-apostrophe in the line)
1721 $arr[$firstspace] = "''";
1722 $arr[$firstspace - 1] .= "'";
1723 }
1724 }
1725
1726 // Now let's actually convert our apostrophic mush to HTML!
1727 $output = '';
1728 $buffer = '';
1729 $state = '';
1730 $i = 0;
1731 foreach ( $arr as $r ) {
1732 if ( ( $i % 2 ) == 0 ) {
1733 if ( $state === 'both' ) {
1734 $buffer .= $r;
1735 } else {
1736 $output .= $r;
1737 }
1738 } else {
1739 $thislen = strlen( $r );
1740 if ( $thislen == 2 ) {
1741 if ( $state === 'i' ) {
1742 $output .= '</i>';
1743 $state = '';
1744 } elseif ( $state === 'bi' ) {
1745 $output .= '</i>';
1746 $state = 'b';
1747 } elseif ( $state === 'ib' ) {
1748 $output .= '</b></i><b>';
1749 $state = 'b';
1750 } elseif ( $state === 'both' ) {
1751 $output .= '<b><i>' . $buffer . '</i>';
1752 $state = 'b';
1753 } else { // $state can be 'b' or ''
1754 $output .= '<i>';
1755 $state .= 'i';
1756 }
1757 } elseif ( $thislen == 3 ) {
1758 if ( $state === 'b' ) {
1759 $output .= '</b>';
1760 $state = '';
1761 } elseif ( $state === 'bi' ) {
1762 $output .= '</i></b><i>';
1763 $state = 'i';
1764 } elseif ( $state === 'ib' ) {
1765 $output .= '</b>';
1766 $state = 'i';
1767 } elseif ( $state === 'both' ) {
1768 $output .= '<i><b>' . $buffer . '</b>';
1769 $state = 'i';
1770 } else { // $state can be 'i' or ''
1771 $output .= '<b>';
1772 $state .= 'b';
1773 }
1774 } elseif ( $thislen == 5 ) {
1775 if ( $state === 'b' ) {
1776 $output .= '</b><i>';
1777 $state = 'i';
1778 } elseif ( $state === 'i' ) {
1779 $output .= '</i><b>';
1780 $state = 'b';
1781 } elseif ( $state === 'bi' ) {
1782 $output .= '</i></b>';
1783 $state = '';
1784 } elseif ( $state === 'ib' ) {
1785 $output .= '</b></i>';
1786 $state = '';
1787 } elseif ( $state === 'both' ) {
1788 $output .= '<i><b>' . $buffer . '</b></i>';
1789 $state = '';
1790 } else { // ($state == '')
1791 $buffer = '';
1792 $state = 'both';
1793 }
1794 }
1795 }
1796 $i++;
1797 }
1798 // Now close all remaining tags. Notice that the order is important.
1799 if ( $state === 'b' || $state === 'ib' ) {
1800 $output .= '</b>';
1801 }
1802 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1803 $output .= '</i>';
1804 }
1805 if ( $state === 'bi' ) {
1806 $output .= '</b>';
1807 }
1808 // There might be lonely ''''', so make sure we have a buffer
1809 if ( $state === 'both' && $buffer ) {
1810 $output .= '<b><i>' . $buffer . '</i></b>';
1811 }
1812 return $output;
1813 }
1814
1828 public function replaceExternalLinks( $text ) {
1829
1830 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1831 if ( $bits === false ) {
1832 throw new MWException( "PCRE needs to be compiled with "
1833 . "--enable-unicode-properties in order for MediaWiki to function" );
1834 }
1835 $s = array_shift( $bits );
1836
1837 $i = 0;
1838 while ( $i < count( $bits ) ) {
1839 $url = $bits[$i++];
1840 $i++; // protocol
1841 $text = $bits[$i++];
1842 $trail = $bits[$i++];
1843
1844 # The characters '<' and '>' (which were escaped by
1845 # removeHTMLtags()) should not be included in
1846 # URLs, per RFC 2396.
1847 $m2 = [];
1848 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1849 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1850 $url = substr( $url, 0, $m2[0][1] );
1851 }
1852
1853 # If the link text is an image URL, replace it with an <img> tag
1854 # This happened by accident in the original parser, but some people used it extensively
1855 $img = $this->maybeMakeExternalImage( $text );
1856 if ( $img !== false ) {
1857 $text = $img;
1858 }
1859
1860 $dtrail = '';
1861
1862 # Set linktype for CSS - if URL==text, link is essentially free
1863 $linktype = ( $text === $url ) ? 'free' : 'text';
1864
1865 # No link text, e.g. [http://domain.tld/some.link]
1866 if ( $text == '' ) {
1867 # Autonumber
1868 $langObj = $this->getTargetLanguage();
1869 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1870 $linktype = 'autonumber';
1871 } else {
1872 # Have link text, e.g. [http://domain.tld/some.link text]s
1873 # Check for trail
1874 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1875 }
1876
1877 $text = $this->getConverterLanguage()->markNoConversion( $text );
1878
1879 $url = Sanitizer::cleanUrl( $url );
1880
1881 # Use the encoded URL
1882 # This means that users can paste URLs directly into the text
1883 # Funny characters like ö aren't valid in URLs anyway
1884 # This was changed in August 2004
1885 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1886 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1887
1888 # Register link in the output object.
1889 $this->mOutput->addExternalLink( $url );
1890 }
1891
1892 return $s;
1893 }
1894
1904 public static function getExternalLinkRel( $url = false, $title = null ) {
1906 $ns = $title ? $title->getNamespace() : false;
1907 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1909 ) {
1910 return 'nofollow';
1911 }
1912 return null;
1913 }
1914
1925 public function getExternalLinkAttribs( $url ) {
1926 $attribs = [];
1927 $rel = self::getExternalLinkRel( $url, $this->mTitle );
1928
1929 $target = $this->mOptions->getExternalLinkTarget();
1930 if ( $target ) {
1931 $attribs['target'] = $target;
1932 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1933 // T133507. New windows can navigate parent cross-origin.
1934 // Including noreferrer due to lacking browser
1935 // support of noopener. Eventually noreferrer should be removed.
1936 if ( $rel !== '' ) {
1937 $rel .= ' ';
1938 }
1939 $rel .= 'noreferrer noopener';
1940 }
1941 }
1942 $attribs['rel'] = $rel;
1943 return $attribs;
1944 }
1945
1953 public static function replaceUnusualEscapes( $url ) {
1954 wfDeprecated( __METHOD__, '1.24' );
1955 return self::normalizeLinkUrl( $url );
1956 }
1957
1967 public static function normalizeLinkUrl( $url ) {
1968 # First, make sure unsafe characters are encoded
1969 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1970 function ( $m ) {
1971 return rawurlencode( $m[0] );
1972 },
1973 $url
1974 );
1975
1976 $ret = '';
1977 $end = strlen( $url );
1978
1979 # Fragment part - 'fragment'
1980 $start = strpos( $url, '#' );
1981 if ( $start !== false && $start < $end ) {
1982 $ret = self::normalizeUrlComponent(
1983 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1984 $end = $start;
1985 }
1986
1987 # Query part - 'query' minus &=+;
1988 $start = strpos( $url, '?' );
1989 if ( $start !== false && $start < $end ) {
1990 $ret = self::normalizeUrlComponent(
1991 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1992 $end = $start;
1993 }
1994
1995 # Scheme and path part - 'pchar'
1996 # (we assume no userinfo or encoded colons in the host)
1997 $ret = self::normalizeUrlComponent(
1998 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1999
2000 return $ret;
2001 }
2002
2003 private static function normalizeUrlComponent( $component, $unsafe ) {
2004 $callback = function ( $matches ) use ( $unsafe ) {
2005 $char = urldecode( $matches[0] );
2006 $ord = ord( $char );
2007 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2008 # Unescape it
2009 return $char;
2010 } else {
2011 # Leave it escaped, but use uppercase for a-f
2012 return strtoupper( $matches[0] );
2013 }
2014 };
2015 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2016 }
2017
2026 private function maybeMakeExternalImage( $url ) {
2027 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2028 $imagesexception = !empty( $imagesfrom );
2029 $text = false;
2030 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2031 if ( $imagesexception && is_array( $imagesfrom ) ) {
2032 $imagematch = false;
2033 foreach ( $imagesfrom as $match ) {
2034 if ( strpos( $url, $match ) === 0 ) {
2035 $imagematch = true;
2036 break;
2037 }
2038 }
2039 } elseif ( $imagesexception ) {
2040 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2041 } else {
2042 $imagematch = false;
2043 }
2044
2045 if ( $this->mOptions->getAllowExternalImages()
2046 || ( $imagesexception && $imagematch )
2047 ) {
2048 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2049 # Image found
2050 $text = Linker::makeExternalImage( $url );
2051 }
2052 }
2053 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2054 && preg_match( self::EXT_IMAGE_REGEX, $url )
2055 ) {
2056 $whitelist = explode(
2057 "\n",
2058 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2059 );
2060
2061 foreach ( $whitelist as $entry ) {
2062 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2063 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2064 continue;
2065 }
2066 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2067 # Image matches a whitelist entry
2068 $text = Linker::makeExternalImage( $url );
2069 break;
2070 }
2071 }
2072 }
2073 return $text;
2074 }
2075
2085 public function replaceInternalLinks( $s ) {
2086 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2087 return $s;
2088 }
2089
2098 public function replaceInternalLinks2( &$s ) {
2100
2101 static $tc = false, $e1, $e1_img;
2102 # the % is needed to support urlencoded titles as well
2103 if ( !$tc ) {
2104 $tc = Title::legalChars() . '#%';
2105 # Match a link having the form [[namespace:link|alternate]]trail
2106 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2107 # Match cases where there is no "]]", which might still be images
2108 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2109 }
2110
2111 $holders = new LinkHolderArray( $this );
2112
2113 # split the entire text string on occurrences of [[
2114 $a = StringUtils::explode( '[[', ' ' . $s );
2115 # get the first element (all text up to first [[), and remove the space we added
2116 $s = $a->current();
2117 $a->next();
2118 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2119 $s = substr( $s, 1 );
2120
2121 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2122 $e2 = null;
2123 if ( $useLinkPrefixExtension ) {
2124 # Match the end of a line for a word that's not followed by whitespace,
2125 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2127 $charset = $wgContLang->linkPrefixCharset();
2128 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2129 }
2130
2131 if ( is_null( $this->mTitle ) ) {
2132 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2133 }
2134 $nottalk = !$this->mTitle->isTalkPage();
2135
2136 if ( $useLinkPrefixExtension ) {
2137 $m = [];
2138 if ( preg_match( $e2, $s, $m ) ) {
2139 $first_prefix = $m[2];
2140 } else {
2141 $first_prefix = false;
2142 }
2143 } else {
2144 $prefix = '';
2145 }
2146
2147 $useSubpages = $this->areSubpagesAllowed();
2148
2149 // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2150 # Loop for each link
2151 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2152 // @codingStandardsIgnoreEnd
2153
2154 # Check for excessive memory usage
2155 if ( $holders->isBig() ) {
2156 # Too big
2157 # Do the existence check, replace the link holders and clear the array
2158 $holders->replace( $s );
2159 $holders->clear();
2160 }
2161
2162 if ( $useLinkPrefixExtension ) {
2163 if ( preg_match( $e2, $s, $m ) ) {
2164 $prefix = $m[2];
2165 $s = $m[1];
2166 } else {
2167 $prefix = '';
2168 }
2169 # first link
2170 if ( $first_prefix ) {
2171 $prefix = $first_prefix;
2172 $first_prefix = false;
2173 }
2174 }
2175
2176 $might_be_img = false;
2177
2178 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2179 $text = $m[2];
2180 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2181 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2182 # the real problem is with the $e1 regex
2183 # See bug 1300.
2184 # Still some problems for cases where the ] is meant to be outside punctuation,
2185 # and no image is in sight. See bug 2095.
2186 if ( $text !== ''
2187 && substr( $m[3], 0, 1 ) === ']'
2188 && strpos( $text, '[' ) !== false
2189 ) {
2190 $text .= ']'; # so that replaceExternalLinks($text) works later
2191 $m[3] = substr( $m[3], 1 );
2192 }
2193 # fix up urlencoded title texts
2194 if ( strpos( $m[1], '%' ) !== false ) {
2195 # Should anchors '#' also be rejected?
2196 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2197 }
2198 $trail = $m[3];
2199 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2200 # Invalid, but might be an image with a link in its caption
2201 $might_be_img = true;
2202 $text = $m[2];
2203 if ( strpos( $m[1], '%' ) !== false ) {
2204 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2205 }
2206 $trail = "";
2207 } else { # Invalid form; output directly
2208 $s .= $prefix . '[[' . $line;
2209 continue;
2210 }
2211
2212 $origLink = $m[1];
2213
2214 # Don't allow internal links to pages containing
2215 # PROTO: where PROTO is a valid URL protocol; these
2216 # should be external links.
2217 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2218 $s .= $prefix . '[[' . $line;
2219 continue;
2220 }
2221
2222 # Make subpage if necessary
2223 if ( $useSubpages ) {
2224 $link = $this->maybeDoSubpageLink( $origLink, $text );
2225 } else {
2226 $link = $origLink;
2227 }
2228
2229 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2230 if ( !$noforce ) {
2231 # Strip off leading ':'
2232 $link = substr( $link, 1 );
2233 }
2234
2235 $unstrip = $this->mStripState->unstripNoWiki( $link );
2236 $nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2237 if ( $nt === null ) {
2238 $s .= $prefix . '[[' . $line;
2239 continue;
2240 }
2241
2242 $ns = $nt->getNamespace();
2243 $iw = $nt->getInterwiki();
2244
2245 if ( $might_be_img ) { # if this is actually an invalid link
2246 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2247 $found = false;
2248 while ( true ) {
2249 # look at the next 'line' to see if we can close it there
2250 $a->next();
2251 $next_line = $a->current();
2252 if ( $next_line === false || $next_line === null ) {
2253 break;
2254 }
2255 $m = explode( ']]', $next_line, 3 );
2256 if ( count( $m ) == 3 ) {
2257 # the first ]] closes the inner link, the second the image
2258 $found = true;
2259 $text .= "[[{$m[0]}]]{$m[1]}";
2260 $trail = $m[2];
2261 break;
2262 } elseif ( count( $m ) == 2 ) {
2263 # if there's exactly one ]] that's fine, we'll keep looking
2264 $text .= "[[{$m[0]}]]{$m[1]}";
2265 } else {
2266 # if $next_line is invalid too, we need look no further
2267 $text .= '[[' . $next_line;
2268 break;
2269 }
2270 }
2271 if ( !$found ) {
2272 # we couldn't find the end of this imageLink, so output it raw
2273 # but don't ignore what might be perfectly normal links in the text we've examined
2274 $holders->merge( $this->replaceInternalLinks2( $text ) );
2275 $s .= "{$prefix}[[$link|$text";
2276 # note: no $trail, because without an end, there *is* no trail
2277 continue;
2278 }
2279 } else { # it's not an image, so output it raw
2280 $s .= "{$prefix}[[$link|$text";
2281 # note: no $trail, because without an end, there *is* no trail
2282 continue;
2283 }
2284 }
2285
2286 $wasblank = ( $text == '' );
2287 if ( $wasblank ) {
2288 $text = $link;
2289 } else {
2290 # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2291 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2292 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2293 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2294 $text = $this->doQuotes( $text );
2295 }
2296
2297 # Link not escaped by : , create the various objects
2298 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2299 # Interwikis
2300 if (
2301 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2302 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2303 in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2304 )
2305 ) {
2306 # Bug 24502: filter duplicates
2307 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2308 $this->mLangLinkLanguages[$iw] = true;
2309 $this->mOutput->addLanguageLink( $nt->getFullText() );
2310 }
2311
2312 $s = rtrim( $s . $prefix );
2313 $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2314 continue;
2315 }
2316
2317 if ( $ns == NS_FILE ) {
2318 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2319 if ( $wasblank ) {
2320 # if no parameters were passed, $text
2321 # becomes something like "File:Foo.png",
2322 # which we don't want to pass on to the
2323 # image generator
2324 $text = '';
2325 } else {
2326 # recursively parse links inside the image caption
2327 # actually, this will parse them in any other parameters, too,
2328 # but it might be hard to fix that, and it doesn't matter ATM
2329 $text = $this->replaceExternalLinks( $text );
2330 $holders->merge( $this->replaceInternalLinks2( $text ) );
2331 }
2332 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2333 $s .= $prefix . $this->armorLinks(
2334 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2335 continue;
2336 }
2337 } elseif ( $ns == NS_CATEGORY ) {
2338 $s = rtrim( $s . "\n" ); # bug 87
2339
2340 if ( $wasblank ) {
2341 $sortkey = $this->getDefaultSort();
2342 } else {
2343 $sortkey = $text;
2344 }
2345 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2346 $sortkey = str_replace( "\n", '', $sortkey );
2347 $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2348 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2349
2353 $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2354
2355 continue;
2356 }
2357 }
2358
2359 # Self-link checking. For some languages, variants of the title are checked in
2360 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2361 # for linking to a different variant.
2362 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2363 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2364 continue;
2365 }
2366
2367 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2368 # @todo FIXME: Should do batch file existence checks, see comment below
2369 if ( $ns == NS_MEDIA ) {
2370 # Give extensions a chance to select the file revision for us
2371 $options = [];
2372 $descQuery = false;
2373 Hooks::run( 'BeforeParserFetchFileAndTitle',
2374 [ $this, $nt, &$options, &$descQuery ] );
2375 # Fetch and register the file (file title may be different via hooks)
2376 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2377 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2378 $s .= $prefix . $this->armorLinks(
2379 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2380 continue;
2381 }
2382
2383 # Some titles, such as valid special pages or files in foreign repos, should
2384 # be shown as bluelinks even though they're not included in the page table
2385 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2386 # batch file existence checks for NS_FILE and NS_MEDIA
2387 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2388 $this->mOutput->addLink( $nt );
2389 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2390 } else {
2391 # Links will be added to the output link list after checking
2392 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2393 }
2394 }
2395 return $holders;
2396 }
2397
2411 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2412 list( $inside, $trail ) = Linker::splitTrail( $trail );
2413
2414 if ( $text == '' ) {
2415 $text = htmlspecialchars( $nt->getPrefixedText() );
2416 }
2417
2418 $link = $this->getLinkRenderer()->makeKnownLink(
2419 $nt, new HtmlArmor( "$prefix$text$inside" )
2420 );
2421
2422 return $this->armorLinks( $link ) . $trail;
2423 }
2424
2435 public function armorLinks( $text ) {
2436 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2437 self::MARKER_PREFIX . "NOPARSE$1", $text );
2438 }
2439
2444 public function areSubpagesAllowed() {
2445 # Some namespaces don't allow subpages
2446 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2447 }
2448
2457 public function maybeDoSubpageLink( $target, &$text ) {
2458 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2459 }
2460
2469 public function doBlockLevels( $text, $linestart ) {
2470 return BlockLevelPass::doBlockLevels( $text, $linestart );
2471 }
2472
2484 public function getVariableValue( $index, $frame = false ) {
2487
2488 if ( is_null( $this->mTitle ) ) {
2489 // If no title set, bad things are going to happen
2490 // later. Title should always be set since this
2491 // should only be called in the middle of a parse
2492 // operation (but the unit-tests do funky stuff)
2493 throw new MWException( __METHOD__ . ' Should only be '
2494 . ' called while parsing (no title set)' );
2495 }
2496
2497 // Avoid PHP 7.1 warning from passing $this by reference
2498 $parser = $this;
2499
2504 if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) {
2505 if ( isset( $this->mVarCache[$index] ) ) {
2506 return $this->mVarCache[$index];
2507 }
2508 }
2509
2510 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2511 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2512
2513 $pageLang = $this->getFunctionLang();
2514
2515 switch ( $index ) {
2516 case '!':
2517 $value = '|';
2518 break;
2519 case 'currentmonth':
2520 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2521 break;
2522 case 'currentmonth1':
2523 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2524 break;
2525 case 'currentmonthname':
2526 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2527 break;
2528 case 'currentmonthnamegen':
2529 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2530 break;
2531 case 'currentmonthabbrev':
2532 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2533 break;
2534 case 'currentday':
2535 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2536 break;
2537 case 'currentday2':
2538 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2539 break;
2540 case 'localmonth':
2541 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2542 break;
2543 case 'localmonth1':
2544 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2545 break;
2546 case 'localmonthname':
2547 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2548 break;
2549 case 'localmonthnamegen':
2550 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2551 break;
2552 case 'localmonthabbrev':
2553 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2554 break;
2555 case 'localday':
2556 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2557 break;
2558 case 'localday2':
2559 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2560 break;
2561 case 'pagename':
2562 $value = wfEscapeWikiText( $this->mTitle->getText() );
2563 break;
2564 case 'pagenamee':
2565 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2566 break;
2567 case 'fullpagename':
2568 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2569 break;
2570 case 'fullpagenamee':
2571 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2572 break;
2573 case 'subpagename':
2574 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2575 break;
2576 case 'subpagenamee':
2577 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2578 break;
2579 case 'rootpagename':
2580 $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2581 break;
2582 case 'rootpagenamee':
2583 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2584 ' ',
2585 '_',
2586 $this->mTitle->getRootText()
2587 ) ) );
2588 break;
2589 case 'basepagename':
2590 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2591 break;
2592 case 'basepagenamee':
2593 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2594 ' ',
2595 '_',
2596 $this->mTitle->getBaseText()
2597 ) ) );
2598 break;
2599 case 'talkpagename':
2600 if ( $this->mTitle->canTalk() ) {
2601 $talkPage = $this->mTitle->getTalkPage();
2602 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2603 } else {
2604 $value = '';
2605 }
2606 break;
2607 case 'talkpagenamee':
2608 if ( $this->mTitle->canTalk() ) {
2609 $talkPage = $this->mTitle->getTalkPage();
2610 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2611 } else {
2612 $value = '';
2613 }
2614 break;
2615 case 'subjectpagename':
2616 $subjPage = $this->mTitle->getSubjectPage();
2617 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2618 break;
2619 case 'subjectpagenamee':
2620 $subjPage = $this->mTitle->getSubjectPage();
2621 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2622 break;
2623 case 'pageid': // requested in bug 23427
2624 $pageid = $this->getTitle()->getArticleID();
2625 if ( $pageid == 0 ) {
2626 # 0 means the page doesn't exist in the database,
2627 # which means the user is previewing a new page.
2628 # The vary-revision flag must be set, because the magic word
2629 # will have a different value once the page is saved.
2630 $this->mOutput->setFlag( 'vary-revision' );
2631 wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2632 }
2633 $value = $pageid ? $pageid : null;
2634 break;
2635 case 'revisionid':
2636 # Let the edit saving system know we should parse the page
2637 # *after* a revision ID has been assigned.
2638 $this->mOutput->setFlag( 'vary-revision-id' );
2639 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2640 $value = $this->mRevisionId;
2641 if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
2642 $value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
2643 $this->mOutput->setSpeculativeRevIdUsed( $value );
2644 }
2645 break;
2646 case 'revisionday':
2647 # Let the edit saving system know we should parse the page
2648 # *after* a revision ID has been assigned. This is for null edits.
2649 $this->mOutput->setFlag( 'vary-revision' );
2650 wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2651 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2652 break;
2653 case 'revisionday2':
2654 # Let the edit saving system know we should parse the page
2655 # *after* a revision ID has been assigned. This is for null edits.
2656 $this->mOutput->setFlag( 'vary-revision' );
2657 wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2658 $value = substr( $this->getRevisionTimestamp(), 6, 2 );
2659 break;
2660 case 'revisionmonth':
2661 # Let the edit saving system know we should parse the page
2662 # *after* a revision ID has been assigned. This is for null edits.
2663 $this->mOutput->setFlag( 'vary-revision' );
2664 wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2665 $value = substr( $this->getRevisionTimestamp(), 4, 2 );
2666 break;
2667 case 'revisionmonth1':
2668 # Let the edit saving system know we should parse the page
2669 # *after* a revision ID has been assigned. This is for null edits.
2670 $this->mOutput->setFlag( 'vary-revision' );
2671 wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2672 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2673 break;
2674 case 'revisionyear':
2675 # Let the edit saving system know we should parse the page
2676 # *after* a revision ID has been assigned. This is for null edits.
2677 $this->mOutput->setFlag( 'vary-revision' );
2678 wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2679 $value = substr( $this->getRevisionTimestamp(), 0, 4 );
2680 break;
2681 case 'revisiontimestamp':
2682 # Let the edit saving system know we should parse the page
2683 # *after* a revision ID has been assigned. This is for null edits.
2684 $this->mOutput->setFlag( 'vary-revision' );
2685 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2686 $value = $this->getRevisionTimestamp();
2687 break;
2688 case 'revisionuser':
2689 # Let the edit saving system know we should parse the page
2690 # *after* a revision ID has been assigned for null edits.
2691 $this->mOutput->setFlag( 'vary-user' );
2692 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2693 $value = $this->getRevisionUser();
2694 break;
2695 case 'revisionsize':
2696 $value = $this->getRevisionSize();
2697 break;
2698 case 'namespace':
2699 $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2700 break;
2701 case 'namespacee':
2702 $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2703 break;
2704 case 'namespacenumber':
2705 $value = $this->mTitle->getNamespace();
2706 break;
2707 case 'talkspace':
2708 $value = $this->mTitle->canTalk()
2709 ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2710 : '';
2711 break;
2712 case 'talkspacee':
2713 $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2714 break;
2715 case 'subjectspace':
2716 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2717 break;
2718 case 'subjectspacee':
2719 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2720 break;
2721 case 'currentdayname':
2722 $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2723 break;
2724 case 'currentyear':
2725 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2726 break;
2727 case 'currenttime':
2728 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2729 break;
2730 case 'currenthour':
2731 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2732 break;
2733 case 'currentweek':
2734 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2735 # int to remove the padding
2736 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2737 break;
2738 case 'currentdow':
2739 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2740 break;
2741 case 'localdayname':
2742 $value = $pageLang->getWeekdayName(
2743 (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2744 );
2745 break;
2746 case 'localyear':
2747 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2748 break;
2749 case 'localtime':
2750 $value = $pageLang->time(
2751 MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2752 false,
2753 false
2754 );
2755 break;
2756 case 'localhour':
2757 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2758 break;
2759 case 'localweek':
2760 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
2761 # int to remove the padding
2762 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2763 break;
2764 case 'localdow':
2765 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2766 break;
2767 case 'numberofarticles':
2768 $value = $pageLang->formatNum( SiteStats::articles() );
2769 break;
2770 case 'numberoffiles':
2771 $value = $pageLang->formatNum( SiteStats::images() );
2772 break;
2773 case 'numberofusers':
2774 $value = $pageLang->formatNum( SiteStats::users() );
2775 break;
2776 case 'numberofactiveusers':
2777 $value = $pageLang->formatNum( SiteStats::activeUsers() );
2778 break;
2779 case 'numberofpages':
2780 $value = $pageLang->formatNum( SiteStats::pages() );
2781 break;
2782 case 'numberofadmins':
2783 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2784 break;
2785 case 'numberofedits':
2786 $value = $pageLang->formatNum( SiteStats::edits() );
2787 break;
2788 case 'currenttimestamp':
2789 $value = wfTimestamp( TS_MW, $ts );
2790 break;
2791 case 'localtimestamp':
2792 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2793 break;
2794 case 'currentversion':
2796 break;
2797 case 'articlepath':
2798 return $wgArticlePath;
2799 case 'sitename':
2800 return $wgSitename;
2801 case 'server':
2802 return $wgServer;
2803 case 'servername':
2804 return $wgServerName;
2805 case 'scriptpath':
2806 return $wgScriptPath;
2807 case 'stylepath':
2808 return $wgStylePath;
2809 case 'directionmark':
2810 return $pageLang->getDirMark();
2811 case 'contentlanguage':
2813 return $wgLanguageCode;
2814 case 'cascadingsources':
2816 break;
2817 default:
2818 $ret = null;
2819 Hooks::run(
2820 'ParserGetVariableValueSwitch',
2821 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2822 );
2823
2824 return $ret;
2825 }
2826
2827 if ( $index ) {
2828 $this->mVarCache[$index] = $value;
2829 }
2830
2831 return $value;
2832 }
2833
2839 public function initialiseVariables() {
2840 $variableIDs = MagicWord::getVariableIDs();
2841 $substIDs = MagicWord::getSubstIDs();
2842
2843 $this->mVariables = new MagicWordArray( $variableIDs );
2844 $this->mSubstWords = new MagicWordArray( $substIDs );
2845 }
2846
2869 public function preprocessToDom( $text, $flags = 0 ) {
2870 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2871 return $dom;
2872 }
2873
2881 public static function splitWhitespace( $s ) {
2882 $ltrimmed = ltrim( $s );
2883 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2884 $trimmed = rtrim( $ltrimmed );
2885 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
2886 if ( $diff > 0 ) {
2887 $w2 = substr( $ltrimmed, -$diff );
2888 } else {
2889 $w2 = '';
2890 }
2891 return [ $w1, $trimmed, $w2 ];
2892 }
2893
2914 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2915 # Is there any text? Also, Prevent too big inclusions!
2916 $textSize = strlen( $text );
2917 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2918 return $text;
2919 }
2920
2921 if ( $frame === false ) {
2922 $frame = $this->getPreprocessor()->newFrame();
2923 } elseif ( !( $frame instanceof PPFrame ) ) {
2924 wfDebug( __METHOD__ . " called using plain parameters instead of "
2925 . "a PPFrame instance. Creating custom frame.\n" );
2926 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2927 }
2928
2929 $dom = $this->preprocessToDom( $text );
2930 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2931 $text = $frame->expand( $dom, $flags );
2932
2933 return $text;
2934 }
2935
2943 public static function createAssocArgs( $args ) {
2944 $assocArgs = [];
2945 $index = 1;
2946 foreach ( $args as $arg ) {
2947 $eqpos = strpos( $arg, '=' );
2948 if ( $eqpos === false ) {
2949 $assocArgs[$index++] = $arg;
2950 } else {
2951 $name = trim( substr( $arg, 0, $eqpos ) );
2952 $value = trim( substr( $arg, $eqpos + 1 ) );
2953 if ( $value === false ) {
2954 $value = '';
2955 }
2956 if ( $name !== false ) {
2957 $assocArgs[$name] = $value;
2958 }
2959 }
2960 }
2961
2962 return $assocArgs;
2963 }
2964
2991 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2992 # does no harm if $current and $max are present but are unnecessary for the message
2993 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2994 # only during preview, and that would split the parser cache unnecessarily.
2995 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2996 ->text();
2997 $this->mOutput->addWarning( $warning );
2998 $this->addTrackingCategory( "$limitationType-category" );
2999 }
3000
3013 public function braceSubstitution( $piece, $frame ) {
3014
3015 // Flags
3016
3017 // $text has been filled
3018 $found = false;
3019 // wiki markup in $text should be escaped
3020 $nowiki = false;
3021 // $text is HTML, armour it against wikitext transformation
3022 $isHTML = false;
3023 // Force interwiki transclusion to be done in raw mode not rendered
3024 $forceRawInterwiki = false;
3025 // $text is a DOM node needing expansion in a child frame
3026 $isChildObj = false;
3027 // $text is a DOM node needing expansion in the current frame
3028 $isLocalObj = false;
3029
3030 # Title object, where $text came from
3031 $title = false;
3032
3033 # $part1 is the bit before the first |, and must contain only title characters.
3034 # Various prefixes will be stripped from it later.
3035 $titleWithSpaces = $frame->expand( $piece['title'] );
3036 $part1 = trim( $titleWithSpaces );
3037 $titleText = false;
3038
3039 # Original title text preserved for various purposes
3040 $originalTitle = $part1;
3041
3042 # $args is a list of argument nodes, starting from index 0, not including $part1
3043 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3044 # below won't work b/c this $args isn't an object
3045 $args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3046
3047 $profileSection = null; // profile templates
3048
3049 # SUBST
3050 if ( !$found ) {
3051 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3052
3053 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3054 # Decide whether to expand template or keep wikitext as-is.
3055 if ( $this->ot['wiki'] ) {
3056 if ( $substMatch === false ) {
3057 $literal = true; # literal when in PST with no prefix
3058 } else {
3059 $literal = false; # expand when in PST with subst: or safesubst:
3060 }
3061 } else {
3062 if ( $substMatch == 'subst' ) {
3063 $literal = true; # literal when not in PST with plain subst:
3064 } else {
3065 $literal = false; # expand when not in PST with safesubst: or no prefix
3066 }
3067 }
3068 if ( $literal ) {
3069 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3070 $isLocalObj = true;
3071 $found = true;
3072 }
3073 }
3074
3075 # Variables
3076 if ( !$found && $args->getLength() == 0 ) {
3077 $id = $this->mVariables->matchStartToEnd( $part1 );
3078 if ( $id !== false ) {
3079 $text = $this->getVariableValue( $id, $frame );
3080 if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3081 $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3082 }
3083 $found = true;
3084 }
3085 }
3086
3087 # MSG, MSGNW and RAW
3088 if ( !$found ) {
3089 # Check for MSGNW:
3090 $mwMsgnw = MagicWord::get( 'msgnw' );
3091 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3092 $nowiki = true;
3093 } else {
3094 # Remove obsolete MSG:
3095 $mwMsg = MagicWord::get( 'msg' );
3096 $mwMsg->matchStartAndRemove( $part1 );
3097 }
3098
3099 # Check for RAW:
3100 $mwRaw = MagicWord::get( 'raw' );
3101 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3102 $forceRawInterwiki = true;
3103 }
3104 }
3105
3106 # Parser functions
3107 if ( !$found ) {
3108 $colonPos = strpos( $part1, ':' );
3109 if ( $colonPos !== false ) {
3110 $func = substr( $part1, 0, $colonPos );
3111 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3112 $argsLength = $args->getLength();
3113 for ( $i = 0; $i < $argsLength; $i++ ) {
3114 $funcArgs[] = $args->item( $i );
3115 }
3116 try {
3117 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3118 } catch ( Exception $ex ) {
3119 throw $ex;
3120 }
3121
3122 # The interface for parser functions allows for extracting
3123 # flags into the local scope. Extract any forwarded flags
3124 # here.
3125 extract( $result );
3126 }
3127 }
3128
3129 # Finish mangling title and then check for loops.
3130 # Set $title to a Title object and $titleText to the PDBK
3131 if ( !$found ) {
3132 $ns = NS_TEMPLATE;
3133 # Split the title into page and subpage
3134 $subpage = '';
3135 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3136 if ( $part1 !== $relative ) {
3137 $part1 = $relative;
3138 $ns = $this->mTitle->getNamespace();
3139 }
3140 $title = Title::newFromText( $part1, $ns );
3141 if ( $title ) {
3142 $titleText = $title->getPrefixedText();
3143 # Check for language variants if the template is not found
3144 if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3145 $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3146 }
3147 # Do recursion depth check
3148 $limit = $this->mOptions->getMaxTemplateDepth();
3149 if ( $frame->depth >= $limit ) {
3150 $found = true;
3151 $text = '<span class="error">'
3152 . wfMessage( 'parser-template-recursion-depth-warning' )
3153 ->numParams( $limit )->inContentLanguage()->text()
3154 . '</span>';
3155 }
3156 }
3157 }
3158
3159 # Load from database
3160 if ( !$found && $title ) {
3161 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3162 if ( !$title->isExternal() ) {
3163 if ( $title->isSpecialPage()
3164 && $this->mOptions->getAllowSpecialInclusion()
3165 && $this->ot['html']
3166 ) {
3167 $specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3168 // Pass the template arguments as URL parameters.
3169 // "uselang" will have no effect since the Language object
3170 // is forced to the one defined in ParserOptions.
3171 $pageArgs = [];
3172 $argsLength = $args->getLength();
3173 for ( $i = 0; $i < $argsLength; $i++ ) {
3174 $bits = $args->item( $i )->splitArg();
3175 if ( strval( $bits['index'] ) === '' ) {
3176 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3177 $value = trim( $frame->expand( $bits['value'] ) );
3178 $pageArgs[$name] = $value;
3179 }
3180 }
3181
3182 // Create a new context to execute the special page
3185 $context->setRequest( new FauxRequest( $pageArgs ) );
3186 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3187 $context->setUser( $this->getUser() );
3188 } else {
3189 // If this page is cached, then we better not be per user.
3190 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3191 }
3192 $context->setLanguage( $this->mOptions->getUserLangObj() );
3194 $title, $context, $this->getLinkRenderer() );
3195 if ( $ret ) {
3196 $text = $context->getOutput()->getHTML();
3197 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3198 $found = true;
3199 $isHTML = true;
3200 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3201 $this->mOutput->updateRuntimeAdaptiveExpiry(
3202 $specialPage->maxIncludeCacheTime()
3203 );
3204 }
3205 }
3206 } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3207 $found = false; # access denied
3208 wfDebug( __METHOD__ . ": template inclusion denied for " .
3209 $title->getPrefixedDBkey() . "\n" );
3210 } else {
3211 list( $text, $title ) = $this->getTemplateDom( $title );
3212 if ( $text !== false ) {
3213 $found = true;
3214 $isChildObj = true;
3215 }
3216 }
3217
3218 # If the title is valid but undisplayable, make a link to it
3219 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3220 $text = "[[:$titleText]]";
3221 $found = true;
3222 }
3223 } elseif ( $title->isTrans() ) {
3224 # Interwiki transclusion
3225 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3226 $text = $this->interwikiTransclude( $title, 'render' );
3227 $isHTML = true;
3228 } else {
3229 $text = $this->interwikiTransclude( $title, 'raw' );
3230 # Preprocess it like a template
3231 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3232 $isChildObj = true;
3233 }
3234 $found = true;
3235 }
3236
3237 # Do infinite loop check
3238 # This has to be done after redirect resolution to avoid infinite loops via redirects
3239 if ( !$frame->loopCheck( $title ) ) {
3240 $found = true;
3241 $text = '<span class="error">'
3242 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3243 . '</span>';
3244 wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3245 }
3246 }
3247
3248 # If we haven't found text to substitute by now, we're done
3249 # Recover the source wikitext and return it
3250 if ( !$found ) {
3251 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3252 if ( $profileSection ) {
3253 $this->mProfiler->scopedProfileOut( $profileSection );
3254 }
3255 return [ 'object' => $text ];
3256 }
3257
3258 # Expand DOM-style return values in a child frame
3259 if ( $isChildObj ) {
3260 # Clean up argument array
3261 $newFrame = $frame->newChild( $args, $title );
3262
3263 if ( $nowiki ) {
3264 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3265 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3266 # Expansion is eligible for the empty-frame cache
3267 $text = $newFrame->cachedExpand( $titleText, $text );
3268 } else {
3269 # Uncached expansion
3270 $text = $newFrame->expand( $text );
3271 }
3272 }
3273 if ( $isLocalObj && $nowiki ) {
3274 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3275 $isLocalObj = false;
3276 }
3277
3278 if ( $profileSection ) {
3279 $this->mProfiler->scopedProfileOut( $profileSection );
3280 }
3281
3282 # Replace raw HTML by a placeholder
3283 if ( $isHTML ) {
3284 $text = $this->insertStripItem( $text );
3285 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3286 # Escape nowiki-style return values
3287 $text = wfEscapeWikiText( $text );
3288 } elseif ( is_string( $text )
3289 && !$piece['lineStart']
3290 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3291 ) {
3292 # Bug 529: if the template begins with a table or block-level
3293 # element, it should be treated as beginning a new line.
3294 # This behavior is somewhat controversial.
3295 $text = "\n" . $text;
3296 }
3297
3298 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3299 # Error, oversize inclusion
3300 if ( $titleText !== false ) {
3301 # Make a working, properly escaped link if possible (bug 23588)
3302 $text = "[[:$titleText]]";
3303 } else {
3304 # This will probably not be a working link, but at least it may
3305 # provide some hint of where the problem is
3306 preg_replace( '/^:/', '', $originalTitle );
3307 $text = "[[:$originalTitle]]";
3308 }
3309 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3310 . 'post-expand include size too large -->' );
3311 $this->limitationWarn( 'post-expand-template-inclusion' );
3312 }
3313
3314 if ( $isLocalObj ) {
3315 $ret = [ 'object' => $text ];
3316 } else {
3317 $ret = [ 'text' => $text ];
3318 }
3319
3320 return $ret;
3321 }
3322
3342 public function callParserFunction( $frame, $function, array $args = [] ) {
3344
3345 # Case sensitive functions
3346 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3347 $function = $this->mFunctionSynonyms[1][$function];
3348 } else {
3349 # Case insensitive functions
3350 $function = $wgContLang->lc( $function );
3351 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3352 $function = $this->mFunctionSynonyms[0][$function];
3353 } else {
3354 return [ 'found' => false ];
3355 }
3356 }
3357
3358 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3359
3360 # Workaround for PHP bug 35229 and similar
3361 if ( !is_callable( $callback ) ) {
3362 throw new MWException( "Tag hook for $function is not callable\n" );
3363 }
3364
3365 // Avoid PHP 7.1 warning from passing $this by reference
3366 $parser = $this;
3367
3368 $allArgs = [ &$parser ];
3369 if ( $flags & self::SFH_OBJECT_ARGS ) {
3370 # Convert arguments to PPNodes and collect for appending to $allArgs
3371 $funcArgs = [];
3372 foreach ( $args as $k => $v ) {
3373 if ( $v instanceof PPNode || $k === 0 ) {
3374 $funcArgs[] = $v;
3375 } else {
3376 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3377 }
3378 }
3379
3380 # Add a frame parameter, and pass the arguments as an array
3381 $allArgs[] = $frame;
3382 $allArgs[] = $funcArgs;
3383 } else {
3384 # Convert arguments to plain text and append to $allArgs
3385 foreach ( $args as $k => $v ) {
3386 if ( $v instanceof PPNode ) {
3387 $allArgs[] = trim( $frame->expand( $v ) );
3388 } elseif ( is_int( $k ) && $k >= 0 ) {
3389 $allArgs[] = trim( $v );
3390 } else {
3391 $allArgs[] = trim( "$k=$v" );
3392 }
3393 }
3394 }
3395
3396 $result = call_user_func_array( $callback, $allArgs );
3397
3398 # The interface for function hooks allows them to return a wikitext
3399 # string or an array containing the string and any flags. This mungs
3400 # things around to match what this method should return.
3401 if ( !is_array( $result ) ) {
3402 $result =[
3403 'found' => true,
3404 'text' => $result,
3405 ];
3406 } else {
3407 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3408 $result['text'] = $result[0];
3409 }
3410 unset( $result[0] );
3411 $result += [
3412 'found' => true,
3413 ];
3414 }
3415
3416 $noparse = true;
3417 $preprocessFlags = 0;
3418 if ( isset( $result['noparse'] ) ) {
3419 $noparse = $result['noparse'];
3420 }
3421 if ( isset( $result['preprocessFlags'] ) ) {
3422 $preprocessFlags = $result['preprocessFlags'];
3423 }
3424
3425 if ( !$noparse ) {
3426 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3427 $result['isChildObj'] = true;
3428 }
3429
3430 return $result;
3431 }
3432
3441 public function getTemplateDom( $title ) {
3442 $cacheTitle = $title;
3443 $titleText = $title->getPrefixedDBkey();
3444
3445 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3446 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3447 $title = Title::makeTitle( $ns, $dbk );
3448 $titleText = $title->getPrefixedDBkey();
3449 }
3450 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3451 return [ $this->mTplDomCache[$titleText], $title ];
3452 }
3453
3454 # Cache miss, go to the database
3455 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3456
3457 if ( $text === false ) {
3458 $this->mTplDomCache[$titleText] = false;
3459 return [ false, $title ];
3460 }
3461
3462 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3463 $this->mTplDomCache[$titleText] = $dom;
3464
3465 if ( !$title->equals( $cacheTitle ) ) {
3466 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3467 [ $title->getNamespace(), $cdb = $title->getDBkey() ];
3468 }
3469
3470 return [ $dom, $title ];
3471 }
3472
3484 public function fetchCurrentRevisionOfTitle( $title ) {
3485 $cacheKey = $title->getPrefixedDBkey();
3486 if ( !$this->currentRevisionCache ) {
3487 $this->currentRevisionCache = new MapCacheLRU( 100 );
3488 }
3489 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3490 $this->currentRevisionCache->set( $cacheKey,
3491 // Defaults to Parser::statelessFetchRevision()
3492 call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3493 );
3494 }
3495 return $this->currentRevisionCache->get( $cacheKey );
3496 }
3497
3507 public static function statelessFetchRevision( Title $title, $parser = false ) {
3508 $pageId = $title->getArticleID();
3509 $revId = $title->getLatestRevID();
3510
3512 if ( $rev ) {
3513 $rev->setTitle( $title );
3514 }
3515
3516 return $rev;
3517 }
3518
3524 public function fetchTemplateAndTitle( $title ) {
3525 // Defaults to Parser::statelessFetchTemplate()
3526 $templateCb = $this->mOptions->getTemplateCallback();
3527 $stuff = call_user_func( $templateCb, $title, $this );
3528 // We use U+007F DELETE to distinguish strip markers from regular text.
3529 $text = $stuff['text'];
3530 if ( is_string( $stuff['text'] ) ) {
3531 $text = strtr( $text, "\x7f", "?" );
3532 }
3533 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3534 if ( isset( $stuff['deps'] ) ) {
3535 foreach ( $stuff['deps'] as $dep ) {
3536 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3537 if ( $dep['title']->equals( $this->getTitle() ) ) {
3538 // If we transclude ourselves, the final result
3539 // will change based on the new version of the page
3540 $this->mOutput->setFlag( 'vary-revision' );
3541 }
3542 }
3543 }
3544 return [ $text, $finalTitle ];
3545 }
3546
3552 public function fetchTemplate( $title ) {
3553 return $this->fetchTemplateAndTitle( $title )[0];
3554 }
3555
3565 public static function statelessFetchTemplate( $title, $parser = false ) {
3566 $text = $skip = false;
3567 $finalTitle = $title;
3568 $deps = [];
3569
3570 # Loop to fetch the article, with up to 1 redirect
3571 // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3572 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3573 // @codingStandardsIgnoreEnd
3574 # Give extensions a chance to select the revision instead
3575 $id = false; # Assume current
3576 Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3577 [ $parser, $title, &$skip, &$id ] );
3578
3579 if ( $skip ) {
3580 $text = false;
3581 $deps[] = [
3582 'title' => $title,
3583 'page_id' => $title->getArticleID(),
3584 'rev_id' => null
3585 ];
3586 break;
3587 }
3588 # Get the revision
3589 if ( $id ) {
3590 $rev = Revision::newFromId( $id );
3591 } elseif ( $parser ) {
3592 $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3593 } else {
3595 }
3596 $rev_id = $rev ? $rev->getId() : 0;
3597 # If there is no current revision, there is no page
3598 if ( $id === false && !$rev ) {
3599 $linkCache = LinkCache::singleton();
3600 $linkCache->addBadLinkObj( $title );
3601 }
3602
3603 $deps[] = [
3604 'title' => $title,
3605 'page_id' => $title->getArticleID(),
3606 'rev_id' => $rev_id ];
3607 if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3608 # We fetched a rev from a different title; register it too...
3609 $deps[] = [
3610 'title' => $rev->getTitle(),
3611 'page_id' => $rev->getPage(),
3612 'rev_id' => $rev_id ];
3613 }
3614
3615 if ( $rev ) {
3616 $content = $rev->getContent();
3617 $text = $content ? $content->getWikitextForTransclusion() : null;
3618
3619 if ( $text === false || $text === null ) {
3620 $text = false;
3621 break;
3622 }
3623 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3625 $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3626 if ( !$message->exists() ) {
3627 $text = false;
3628 break;
3629 }
3630 $content = $message->content();
3631 $text = $message->plain();
3632 } else {
3633 break;
3634 }
3635 if ( !$content ) {
3636 break;
3637 }
3638 # Redirect?
3639 $finalTitle = $title;
3640 $title = $content->getRedirectTarget();
3641 }
3642 return [
3643 'text' => $text,
3644 'finalTitle' => $finalTitle,
3645 'deps' => $deps ];
3646 }
3647
3655 public function fetchFile( $title, $options = [] ) {
3656 return $this->fetchFileAndTitle( $title, $options )[0];
3657 }
3658
3666 public function fetchFileAndTitle( $title, $options = [] ) {
3667 $file = $this->fetchFileNoRegister( $title, $options );
3668
3669 $time = $file ? $file->getTimestamp() : false;
3670 $sha1 = $file ? $file->getSha1() : false;
3671 # Register the file as a dependency...
3672 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3673 if ( $file && !$title->equals( $file->getTitle() ) ) {
3674 # Update fetched file title
3675 $title = $file->getTitle();
3676 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3677 }
3678 return [ $file, $title ];
3679 }
3680
3691 protected function fetchFileNoRegister( $title, $options = [] ) {
3692 if ( isset( $options['broken'] ) ) {
3693 $file = false; // broken thumbnail forced by hook
3694 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3695 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3696 } else { // get by (name,timestamp)
3697 $file = wfFindFile( $title, $options );
3698 }
3699 return $file;
3700 }
3701
3710 public function interwikiTransclude( $title, $action ) {
3712
3714 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3715 }
3716
3717 $url = $title->getFullURL( [ 'action' => $action ] );
3718
3719 if ( strlen( $url ) > 255 ) {
3720 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3721 }
3722 return $this->fetchScaryTemplateMaybeFromCache( $url );
3723 }
3724
3729 public function fetchScaryTemplateMaybeFromCache( $url ) {
3731 $dbr = wfGetDB( DB_REPLICA );
3732 $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3733 $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3734 [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
3735 if ( $obj ) {
3736 return $obj->tc_contents;
3737 }
3738
3739 $req = MWHttpRequest::factory( $url, [], __METHOD__ );
3740 $status = $req->execute(); // Status object
3741 if ( $status->isOK() ) {
3742 $text = $req->getContent();
3743 } elseif ( $req->getStatus() != 200 ) {
3744 // Though we failed to fetch the content, this status is useless.
3745 return wfMessage( 'scarytranscludefailed-httpstatus' )
3746 ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3747 } else {
3748 return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3749 }
3750
3751 $dbw = wfGetDB( DB_MASTER );
3752 $dbw->replace( 'transcache', [ 'tc_url' ], [
3753 'tc_url' => $url,
3754 'tc_time' => $dbw->timestamp( time() ),
3755 'tc_contents' => $text
3756 ] );
3757 return $text;
3758 }
3759
3769 public function argSubstitution( $piece, $frame ) {
3770
3771 $error = false;
3772 $parts = $piece['parts'];
3773 $nameWithSpaces = $frame->expand( $piece['title'] );
3774 $argName = trim( $nameWithSpaces );
3775 $object = false;
3776 $text = $frame->getArgument( $argName );
3777 if ( $text === false && $parts->getLength() > 0
3778 && ( $this->ot['html']
3779 || $this->ot['pre']
3780 || ( $this->ot['wiki'] && $frame->isTemplate() )
3781 )
3782 ) {
3783 # No match in frame, use the supplied default
3784 $object = $parts->item( 0 )->getChildren();
3785 }
3786 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3787 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3788 $this->limitationWarn( 'post-expand-template-argument' );
3789 }
3790
3791 if ( $text === false && $object === false ) {
3792 # No match anywhere
3793 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3794 }
3795 if ( $error !== false ) {
3796 $text .= $error;
3797 }
3798 if ( $object !== false ) {
3799 $ret = [ 'object' => $object ];
3800 } else {
3801 $ret = [ 'text' => $text ];
3802 }
3803
3804 return $ret;
3805 }
3806
3822 public function extensionSubstitution( $params, $frame ) {
3823 static $errorStr = '<span class="error">';
3824 static $errorLen = 20;
3825
3826 $name = $frame->expand( $params['name'] );
3827 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3828 // Probably expansion depth or node count exceeded. Just punt the
3829 // error up.
3830 return $name;
3831 }
3832
3833 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3834 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3835 // See above
3836 return $attrText;
3837 }
3838
3839 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3840 if ( substr( $content, 0, $errorLen ) === $errorStr ) {
3841 // See above
3842 return $content;
3843 }
3844
3845 $marker = self::MARKER_PREFIX . "-$name-"
3846 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3847
3848 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3849 ( $this->ot['html'] || $this->ot['pre'] );
3850 if ( $isFunctionTag ) {
3851 $markerType = 'none';
3852 } else {
3853 $markerType = 'general';
3854 }
3855 if ( $this->ot['html'] || $isFunctionTag ) {
3856 $name = strtolower( $name );
3857 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3858 if ( isset( $params['attributes'] ) ) {
3859 $attributes = $attributes + $params['attributes'];
3860 }
3861
3862 if ( isset( $this->mTagHooks[$name] ) ) {
3863 # Workaround for PHP bug 35229 and similar
3864 if ( !is_callable( $this->mTagHooks[$name] ) ) {
3865 throw new MWException( "Tag hook for $name is not callable\n" );
3866 }
3867 $output = call_user_func_array( $this->mTagHooks[$name],
3868 [ $content, $attributes, $this, $frame ] );
3869 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3870 list( $callback, ) = $this->mFunctionTagHooks[$name];
3871 if ( !is_callable( $callback ) ) {
3872 throw new MWException( "Tag hook for $name is not callable\n" );
3873 }
3874
3875 // Avoid PHP 7.1 warning from passing $this by reference
3876 $parser = $this;
3877 $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
3878 } else {
3879 $output = '<span class="error">Invalid tag extension name: ' .
3880 htmlspecialchars( $name ) . '</span>';
3881 }
3882
3883 if ( is_array( $output ) ) {
3884 # Extract flags to local scope (to override $markerType)
3885 $flags = $output;
3886 $output = $flags[0];
3887 unset( $flags[0] );
3888 extract( $flags );
3889 }
3890 } else {
3891 if ( is_null( $attrText ) ) {
3892 $attrText = '';
3893 }
3894 if ( isset( $params['attributes'] ) ) {
3895 foreach ( $params['attributes'] as $attrName => $attrValue ) {
3896 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3897 htmlspecialchars( $attrValue ) . '"';
3898 }
3899 }
3900 if ( $content === null ) {
3901 $output = "<$name$attrText/>";
3902 } else {
3903 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3904 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3905 // See above
3906 return $close;
3907 }
3908 $output = "<$name$attrText>$content$close";
3909 }
3910 }
3911
3912 if ( $markerType === 'none' ) {
3913 return $output;
3914 } elseif ( $markerType === 'nowiki' ) {
3915 $this->mStripState->addNoWiki( $marker, $output );
3916 } elseif ( $markerType === 'general' ) {
3917 $this->mStripState->addGeneral( $marker, $output );
3918 } else {
3919 throw new MWException( __METHOD__ . ': invalid marker type' );
3920 }
3921 return $marker;
3922 }
3923
3931 public function incrementIncludeSize( $type, $size ) {
3932 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3933 return false;
3934 } else {
3935 $this->mIncludeSizes[$type] += $size;
3936 return true;
3937 }
3938 }
3939
3946 $this->mExpensiveFunctionCount++;
3947 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3948 }
3949
3958 public function doDoubleUnderscore( $text ) {
3959
3960 # The position of __TOC__ needs to be recorded
3961 $mw = MagicWord::get( 'toc' );
3962 if ( $mw->match( $text ) ) {
3963 $this->mShowToc = true;
3964 $this->mForceTocPosition = true;
3965
3966 # Set a placeholder. At the end we'll fill it in with the TOC.
3967 $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
3968
3969 # Only keep the first one.
3970 $text = $mw->replace( '', $text );
3971 }
3972
3973 # Now match and remove the rest of them
3975 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3976
3977 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3978 $this->mOutput->mNoGallery = true;
3979 }
3980 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3981 $this->mShowToc = false;
3982 }
3983 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
3984 && $this->mTitle->getNamespace() == NS_CATEGORY
3985 ) {
3986 $this->addTrackingCategory( 'hidden-category-category' );
3987 }
3988 # (bug 8068) Allow control over whether robots index a page.
3989 # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
3990 # is not desirable, the last one on the page should win.
3991 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
3992 $this->mOutput->setIndexPolicy( 'noindex' );
3993 $this->addTrackingCategory( 'noindex-category' );
3994 }
3995 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
3996 $this->mOutput->setIndexPolicy( 'index' );
3997 $this->addTrackingCategory( 'index-category' );
3998 }
3999
4000 # Cache all double underscores in the database
4001 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4002 $this->mOutput->setProperty( $key, '' );
4003 }
4004
4005 return $text;
4006 }
4007
4013 public function addTrackingCategory( $msg ) {
4014 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4015 }
4016
4033 public function formatHeadings( $text, $origText, $isMain = true ) {
4035
4036 # Inhibit editsection links if requested in the page
4037 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4038 $maybeShowEditLink = $showEditLink = false;
4039 } else {
4040 $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4041 $showEditLink = $this->mOptions->getEditSection();
4042 }
4043 if ( $showEditLink ) {
4044 $this->mOutput->setEditSectionTokens( true );
4045 }
4046
4047 # Get all headlines for numbering them and adding funky stuff like [edit]
4048 # links - this is for later, but we need the number of headlines right now
4049 $matches = [];
4050 $numMatches = preg_match_all(
4051 '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4052 $text,
4053 $matches
4054 );
4055
4056 # if there are fewer than 4 headlines in the article, do not show TOC
4057 # unless it's been explicitly enabled.
4058 $enoughToc = $this->mShowToc &&
4059 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4060
4061 # Allow user to stipulate that a page should have a "new section"
4062 # link added via __NEWSECTIONLINK__
4063 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4064 $this->mOutput->setNewSection( true );
4065 }
4066
4067 # Allow user to remove the "new section"
4068 # link via __NONEWSECTIONLINK__
4069 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4070 $this->mOutput->hideNewSection( true );
4071 }
4072
4073 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4074 # override above conditions and always show TOC above first header
4075 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4076 $this->mShowToc = true;
4077 $enoughToc = true;
4078 }
4079
4080 # headline counter
4081 $headlineCount = 0;
4082 $numVisible = 0;
4083
4084 # Ugh .. the TOC should have neat indentation levels which can be
4085 # passed to the skin functions. These are determined here
4086 $toc = '';
4087 $full = '';
4088 $head = [];
4089 $sublevelCount = [];
4090 $levelCount = [];
4091 $level = 0;
4092 $prevlevel = 0;
4093 $toclevel = 0;
4094 $prevtoclevel = 0;
4095 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4096 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4097 $oldType = $this->mOutputType;
4098 $this->setOutputType( self::OT_WIKI );
4099 $frame = $this->getPreprocessor()->newFrame();
4100 $root = $this->preprocessToDom( $origText );
4101 $node = $root->getFirstChild();
4102 $byteOffset = 0;
4103 $tocraw = [];
4104 $refers = [];
4105
4106 $headlines = $numMatches !== false ? $matches[3] : [];
4107
4108 foreach ( $headlines as $headline ) {
4109 $isTemplate = false;
4110 $titleText = false;
4111 $sectionIndex = false;
4112 $numbering = '';
4113 $markerMatches = [];
4114 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4115 $serial = $markerMatches[1];
4116 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4117 $isTemplate = ( $titleText != $baseTitleText );
4118 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4119 }
4120
4121 if ( $toclevel ) {
4122 $prevlevel = $level;
4123 }
4124 $level = $matches[1][$headlineCount];
4125
4126 if ( $level > $prevlevel ) {
4127 # Increase TOC level
4128 $toclevel++;
4129 $sublevelCount[$toclevel] = 0;
4130 if ( $toclevel < $wgMaxTocLevel ) {
4131 $prevtoclevel = $toclevel;
4132 $toc .= Linker::tocIndent();
4133 $numVisible++;
4134 }
4135 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4136 # Decrease TOC level, find level to jump to
4137
4138 for ( $i = $toclevel; $i > 0; $i-- ) {
4139 if ( $levelCount[$i] == $level ) {
4140 # Found last matching level
4141 $toclevel = $i;
4142 break;
4143 } elseif ( $levelCount[$i] < $level ) {
4144 # Found first matching level below current level
4145 $toclevel = $i + 1;
4146 break;
4147 }
4148 }
4149 if ( $i == 0 ) {
4150 $toclevel = 1;
4151 }
4152 if ( $toclevel < $wgMaxTocLevel ) {
4153 if ( $prevtoclevel < $wgMaxTocLevel ) {
4154 # Unindent only if the previous toc level was shown :p
4155 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4156 $prevtoclevel = $toclevel;
4157 } else {
4158 $toc .= Linker::tocLineEnd();
4159 }
4160 }
4161 } else {
4162 # No change in level, end TOC line
4163 if ( $toclevel < $wgMaxTocLevel ) {
4164 $toc .= Linker::tocLineEnd();
4165 }
4166 }
4167
4168 $levelCount[$toclevel] = $level;
4169
4170 # count number of headlines for each level
4171 $sublevelCount[$toclevel]++;
4172 $dot = 0;
4173 for ( $i = 1; $i <= $toclevel; $i++ ) {
4174 if ( !empty( $sublevelCount[$i] ) ) {
4175 if ( $dot ) {
4176 $numbering .= '.';
4177 }
4178 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4179 $dot = 1;
4180 }
4181 }
4182
4183 # The safe header is a version of the header text safe to use for links
4184
4185 # Remove link placeholders by the link text.
4186 # <!--LINK number-->
4187 # turns into
4188 # link text with suffix
4189 # Do this before unstrip since link text can contain strip markers
4190 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4191
4192 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4193 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4194
4195 # Strip out HTML (first regex removes any tag not allowed)
4196 # Allowed tags are:
4197 # * <sup> and <sub> (bug 8393)
4198 # * <i> (bug 26375)
4199 # * <b> (r105284)
4200 # * <bdi> (bug 72884)
4201 # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4202 # * <s> and <strike> (T35715)
4203 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4204 # to allow setting directionality in toc items.
4205 $tocline = preg_replace(
4206 [
4207 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4208 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4209 ],
4210 [ '', '<$1>' ],
4211 $safeHeadline
4212 );
4213
4214 # Strip '<span></span>', which is the result from the above if
4215 # <span id="foo"></span> is used to produce an additional anchor
4216 # for a section.
4217 $tocline = str_replace( '<span></span>', '', $tocline );
4218
4219 $tocline = trim( $tocline );
4220
4221 # For the anchor, strip out HTML-y stuff period
4222 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4223 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4224
4225 # Save headline for section edit hint before it's escaped
4226 $headlineHint = $safeHeadline;
4227
4228 if ( $wgExperimentalHtmlIds ) {
4229 # For reverse compatibility, provide an id that's
4230 # HTML4-compatible, like we used to.
4231 # It may be worth noting, academically, that it's possible for
4232 # the legacy anchor to conflict with a non-legacy headline
4233 # anchor on the page. In this case likely the "correct" thing
4234 # would be to either drop the legacy anchors or make sure
4235 # they're numbered first. However, this would require people
4236 # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4237 # manually, so let's not bother worrying about it.
4238 $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4239 [ 'noninitial', 'legacy' ] );
4240 $safeHeadline = Sanitizer::escapeId( $safeHeadline );
4241
4242 if ( $legacyHeadline == $safeHeadline ) {
4243 # No reason to have both (in fact, we can't)
4244 $legacyHeadline = false;
4245 }
4246 } else {
4247 $legacyHeadline = false;
4248 $safeHeadline = Sanitizer::escapeId( $safeHeadline,
4249 'noninitial' );
4250 }
4251
4252 # HTML names must be case-insensitively unique (bug 10721).
4253 # This does not apply to Unicode characters per
4254 # http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4255 # @todo FIXME: We may be changing them depending on the current locale.
4256 $arrayKey = strtolower( $safeHeadline );
4257 if ( $legacyHeadline === false ) {
4258 $legacyArrayKey = false;
4259 } else {
4260 $legacyArrayKey = strtolower( $legacyHeadline );
4261 }
4262
4263 # Create the anchor for linking from the TOC to the section
4264 $anchor = $safeHeadline;
4265 $legacyAnchor = $legacyHeadline;
4266 if ( isset( $refers[$arrayKey] ) ) {
4267 // @codingStandardsIgnoreStart
4268 for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4269 // @codingStandardsIgnoreEnd
4270 $anchor .= "_$i";
4271 $refers["${arrayKey}_$i"] = true;
4272 } else {
4273 $refers[$arrayKey] = true;
4274 }
4275 if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4276 // @codingStandardsIgnoreStart
4277 for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4278 // @codingStandardsIgnoreEnd
4279 $legacyAnchor .= "_$i";
4280 $refers["${legacyArrayKey}_$i"] = true;
4281 } else {
4282 $refers[$legacyArrayKey] = true;
4283 }
4284
4285 # Don't number the heading if it is the only one (looks silly)
4286 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4287 # the two are different if the line contains a link
4288 $headline = Html::element(
4289 'span',
4290 [ 'class' => 'mw-headline-number' ],
4291 $numbering
4292 ) . ' ' . $headline;
4293 }
4294
4295 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4296 $toc .= Linker::tocLine( $anchor, $tocline,
4297 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4298 }
4299
4300 # Add the section to the section tree
4301 # Find the DOM node for this header
4302 $noOffset = ( $isTemplate || $sectionIndex === false );
4303 while ( $node && !$noOffset ) {
4304 if ( $node->getName() === 'h' ) {
4305 $bits = $node->splitHeading();
4306 if ( $bits['i'] == $sectionIndex ) {
4307 break;
4308 }
4309 }
4310 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4311 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4312 $node = $node->getNextSibling();
4313 }
4314 $tocraw[] = [
4315 'toclevel' => $toclevel,
4316 'level' => $level,
4317 'line' => $tocline,
4318 'number' => $numbering,
4319 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4320 'fromtitle' => $titleText,
4321 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4322 'anchor' => $anchor,
4323 ];
4324
4325 # give headline the correct <h#> tag
4326 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4327 // Output edit section links as markers with styles that can be customized by skins
4328 if ( $isTemplate ) {
4329 # Put a T flag in the section identifier, to indicate to extractSections()
4330 # that sections inside <includeonly> should be counted.
4331 $editsectionPage = $titleText;
4332 $editsectionSection = "T-$sectionIndex";
4333 $editsectionContent = null;
4334 } else {
4335 $editsectionPage = $this->mTitle->getPrefixedText();
4336 $editsectionSection = $sectionIndex;
4337 $editsectionContent = $headlineHint;
4338 }
4339 // We use a bit of pesudo-xml for editsection markers. The
4340 // language converter is run later on. Using a UNIQ style marker
4341 // leads to the converter screwing up the tokens when it
4342 // converts stuff. And trying to insert strip tags fails too. At
4343 // this point all real inputted tags have already been escaped,
4344 // so we don't have to worry about a user trying to input one of
4345 // these markers directly. We use a page and section attribute
4346 // to stop the language converter from converting these
4347 // important bits of data, but put the headline hint inside a
4348 // content block because the language converter is supposed to
4349 // be able to convert that piece of data.
4350 // Gets replaced with html in ParserOutput::getText
4351 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4352 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4353 if ( $editsectionContent !== null ) {
4354 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4355 } else {
4356 $editlink .= '/>';
4357 }
4358 } else {
4359 $editlink = '';
4360 }
4361 $head[$headlineCount] = Linker::makeHeadline( $level,
4362 $matches['attrib'][$headlineCount], $anchor, $headline,
4363 $editlink, $legacyAnchor );
4364
4365 $headlineCount++;
4366 }
4367
4368 $this->setOutputType( $oldType );
4369
4370 # Never ever show TOC if no headers
4371 if ( $numVisible < 1 ) {
4372 $enoughToc = false;
4373 }
4374
4375 if ( $enoughToc ) {
4376 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4377 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4378 }
4379 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4380 $this->mOutput->setTOCHTML( $toc );
4381 $toc = self::TOC_START . $toc . self::TOC_END;
4382 $this->mOutput->addModules( 'mediawiki.toc' );
4383 }
4384
4385 if ( $isMain ) {
4386 $this->mOutput->setSections( $tocraw );
4387 }
4388
4389 # split up and insert constructed headlines
4390 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4391 $i = 0;
4392
4393 // build an array of document sections
4394 $sections = [];
4395 foreach ( $blocks as $block ) {
4396 // $head is zero-based, sections aren't.
4397 if ( empty( $head[$i - 1] ) ) {
4398 $sections[$i] = $block;
4399 } else {
4400 $sections[$i] = $head[$i - 1] . $block;
4401 }
4402
4413 Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4414
4415 $i++;
4416 }
4417
4418 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4419 // append the TOC at the beginning
4420 // Top anchor now in skin
4421 $sections[0] = $sections[0] . $toc . "\n";
4422 }
4423
4424 $full .= implode( '', $sections );
4425
4426 if ( $this->mForceTocPosition ) {
4427 return str_replace( '<!--MWTOC-->', $toc, $full );
4428 } else {
4429 return $full;
4430 }
4431 }
4432
4444 public function preSaveTransform( $text, Title $title, User $user,
4445 ParserOptions $options, $clearState = true
4446 ) {
4447 if ( $clearState ) {
4448 $magicScopeVariable = $this->lock();
4449 }
4450 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4451 $this->setUser( $user );
4452
4453 // We still normalize line endings for backwards-compatibility
4454 // with other code that just calls PST, but this should already
4455 // be handled in TextContent subclasses
4456 $text = TextContent::normalizeLineEndings( $text );
4457
4458 if ( $options->getPreSaveTransform() ) {
4459 $text = $this->pstPass2( $text, $user );
4460 }
4461 $text = $this->mStripState->unstripBoth( $text );
4462
4463 $this->setUser( null ); # Reset
4464
4465 return $text;
4466 }
4467
4476 private function pstPass2( $text, $user ) {
4478
4479 # Note: This is the timestamp saved as hardcoded wikitext to
4480 # the database, we use $wgContLang here in order to give
4481 # everyone the same signature and use the default one rather
4482 # than the one selected in each user's preferences.
4483 # (see also bug 12815)
4484 $ts = $this->mOptions->getTimestamp();
4486 $ts = $timestamp->format( 'YmdHis' );
4487 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4488
4489 $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4490
4491 # Variable replacement
4492 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4493 $text = $this->replaceVariables( $text );
4494
4495 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4496 # which may corrupt this parser instance via its wfMessage()->text() call-
4497
4498 # Signatures
4499 $sigText = $this->getUserSig( $user );
4500 $text = strtr( $text, [
4501 '~~~~~' => $d,
4502 '~~~~' => "$sigText $d",
4503 '~~~' => $sigText
4504 ] );
4505
4506 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4507 $tc = '[' . Title::legalChars() . ']';
4508 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4509
4510 // [[ns:page (context)|]]
4511 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4512 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4513 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4514 // [[ns:page (context), context|]] (using either single or double-width comma)
4515 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4516 // [[|page]] (reverse pipe trick: add context from page title)
4517 $p2 = "/\[\[\\|($tc+)]]/";
4518
4519 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4520 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4521 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4522 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4523
4524 $t = $this->mTitle->getText();
4525 $m = [];
4526 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4527 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4528 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4529 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4530 } else {
4531 # if there's no context, don't bother duplicating the title
4532 $text = preg_replace( $p2, '[[\\1]]', $text );
4533 }
4534
4535 return $text;
4536 }
4537
4552 public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4554
4555 $username = $user->getName();
4556
4557 # If not given, retrieve from the user object.
4558 if ( $nickname === false ) {
4559 $nickname = $user->getOption( 'nickname' );
4560 }
4561
4562 if ( is_null( $fancySig ) ) {
4563 $fancySig = $user->getBoolOption( 'fancysig' );
4564 }
4565
4566 $nickname = $nickname == null ? $username : $nickname;
4567
4568 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4569 $nickname = $username;
4570 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4571 } elseif ( $fancySig !== false ) {
4572 # Sig. might contain markup; validate this
4573 if ( $this->validateSig( $nickname ) !== false ) {
4574 # Validated; clean up (if needed) and return it
4575 return $this->cleanSig( $nickname, true );
4576 } else {
4577 # Failed to validate; fall back to the default
4578 $nickname = $username;
4579 wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4580 }
4581 }
4582
4583 # Make sure nickname doesnt get a sig in a sig
4584 $nickname = self::cleanSigInSig( $nickname );
4585
4586 # If we're still here, make it a link to the user page
4587 $userText = wfEscapeWikiText( $username );
4588 $nickText = wfEscapeWikiText( $nickname );
4589 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4590
4591 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4592 ->title( $this->getTitle() )->text();
4593 }
4594
4601 public function validateSig( $text ) {
4602 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4603 }
4604
4615 public function cleanSig( $text, $parsing = false ) {
4616 if ( !$parsing ) {
4618 $magicScopeVariable = $this->lock();
4619 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4620 }
4621
4622 # Option to disable this feature
4623 if ( !$this->mOptions->getCleanSignatures() ) {
4624 return $text;
4625 }
4626
4627 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4628 # => Move this logic to braceSubstitution()
4629 $substWord = MagicWord::get( 'subst' );
4630 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4631 $substText = '{{' . $substWord->getSynonym( 0 );
4632
4633 $text = preg_replace( $substRegex, $substText, $text );
4634 $text = self::cleanSigInSig( $text );
4635 $dom = $this->preprocessToDom( $text );
4636 $frame = $this->getPreprocessor()->newFrame();
4637 $text = $frame->expand( $dom );
4638
4639 if ( !$parsing ) {
4640 $text = $this->mStripState->unstripBoth( $text );
4641 }
4642
4643 return $text;
4644 }
4645
4652 public static function cleanSigInSig( $text ) {
4653 $text = preg_replace( '/~{3,5}/', '', $text );
4654 return $text;
4655 }
4656
4666 public function startExternalParse( Title $title = null, ParserOptions $options,
4667 $outputType, $clearState = true
4668 ) {
4669 $this->startParse( $title, $options, $outputType, $clearState );
4670 }
4671
4678 private function startParse( Title $title = null, ParserOptions $options,
4679 $outputType, $clearState = true
4680 ) {
4681 $this->setTitle( $title );
4682 $this->mOptions = $options;
4683 $this->setOutputType( $outputType );
4684 if ( $clearState ) {
4685 $this->clearState();
4686 }
4687 }
4688
4697 public function transformMsg( $text, $options, $title = null ) {
4698 static $executing = false;
4699
4700 # Guard against infinite recursion
4701 if ( $executing ) {
4702 return $text;
4703 }
4704 $executing = true;
4705
4706 if ( !$title ) {
4708 $title = $wgTitle;
4709 }
4710
4711 $text = $this->preprocess( $text, $title, $options );
4712
4713 $executing = false;
4714 return $text;
4715 }
4716
4741 public function setHook( $tag, $callback ) {
4742 $tag = strtolower( $tag );
4743 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4744 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4745 }
4746 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4747 $this->mTagHooks[$tag] = $callback;
4748 if ( !in_array( $tag, $this->mStripList ) ) {
4749 $this->mStripList[] = $tag;
4750 }
4751
4752 return $oldVal;
4753 }
4754
4772 public function setTransparentTagHook( $tag, $callback ) {
4773 $tag = strtolower( $tag );
4774 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4775 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4776 }
4777 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4778 $this->mTransparentTagHooks[$tag] = $callback;
4779
4780 return $oldVal;
4781 }
4782
4786 public function clearTagHooks() {
4787 $this->mTagHooks = [];
4788 $this->mFunctionTagHooks = [];
4789 $this->mStripList = $this->mDefaultStripList;
4790 }
4791
4835 public function setFunctionHook( $id, $callback, $flags = 0 ) {
4837
4838 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4839 $this->mFunctionHooks[$id] = [ $callback, $flags ];
4840
4841 # Add to function cache
4842 $mw = MagicWord::get( $id );
4843 if ( !$mw ) {
4844 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4845 }
4846
4847 $synonyms = $mw->getSynonyms();
4848 $sensitive = intval( $mw->isCaseSensitive() );
4849
4850 foreach ( $synonyms as $syn ) {
4851 # Case
4852 if ( !$sensitive ) {
4853 $syn = $wgContLang->lc( $syn );
4854 }
4855 # Add leading hash
4856 if ( !( $flags & self::SFH_NO_HASH ) ) {
4857 $syn = '#' . $syn;
4858 }
4859 # Remove trailing colon
4860 if ( substr( $syn, -1, 1 ) === ':' ) {
4861 $syn = substr( $syn, 0, -1 );
4862 }
4863 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4864 }
4865 return $oldVal;
4866 }
4867
4873 public function getFunctionHooks() {
4874 return array_keys( $this->mFunctionHooks );
4875 }
4876
4887 public function setFunctionTagHook( $tag, $callback, $flags ) {
4888 $tag = strtolower( $tag );
4889 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4890 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4891 }
4892 $old = isset( $this->mFunctionTagHooks[$tag] ) ?
4893 $this->mFunctionTagHooks[$tag] : null;
4894 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4895
4896 if ( !in_array( $tag, $this->mStripList ) ) {
4897 $this->mStripList[] = $tag;
4898 }
4899
4900 return $old;
4901 }
4902
4910 public function replaceLinkHolders( &$text, $options = 0 ) {
4911 $this->mLinkHolders->replace( $text );
4912 }
4913
4921 public function replaceLinkHoldersText( $text ) {
4922 return $this->mLinkHolders->replaceText( $text );
4923 }
4924
4938 public function renderImageGallery( $text, $params ) {
4939
4940 $mode = false;
4941 if ( isset( $params['mode'] ) ) {
4942 $mode = $params['mode'];
4943 }
4944
4945 try {
4946 $ig = ImageGalleryBase::factory( $mode );
4947 } catch ( Exception $e ) {
4948 // If invalid type set, fallback to default.
4949 $ig = ImageGalleryBase::factory( false );
4950 }
4951
4952 $ig->setContextTitle( $this->mTitle );
4953 $ig->setShowBytes( false );
4954 $ig->setShowFilename( false );
4955 $ig->setParser( $this );
4956 $ig->setHideBadImages();
4957 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
4958
4959 if ( isset( $params['showfilename'] ) ) {
4960 $ig->setShowFilename( true );
4961 } else {
4962 $ig->setShowFilename( false );
4963 }
4964 if ( isset( $params['caption'] ) ) {
4965 $caption = $params['caption'];
4966 $caption = htmlspecialchars( $caption );
4967 $caption = $this->replaceInternalLinks( $caption );
4968 $ig->setCaptionHtml( $caption );
4969 }
4970 if ( isset( $params['perrow'] ) ) {
4971 $ig->setPerRow( $params['perrow'] );
4972 }
4973 if ( isset( $params['widths'] ) ) {
4974 $ig->setWidths( $params['widths'] );
4975 }
4976 if ( isset( $params['heights'] ) ) {
4977 $ig->setHeights( $params['heights'] );
4978 }
4979 $ig->setAdditionalOptions( $params );
4980
4981 // Avoid PHP 7.1 warning from passing $this by reference
4982 $parser = $this;
4983 Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
4984
4985 $lines = StringUtils::explode( "\n", $text );
4986 foreach ( $lines as $line ) {
4987 # match lines like these:
4988 # Image:someimage.jpg|This is some image
4989 $matches = [];
4990 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4991 # Skip empty lines
4992 if ( count( $matches ) == 0 ) {
4993 continue;
4994 }
4995
4996 if ( strpos( $matches[0], '%' ) !== false ) {
4997 $matches[1] = rawurldecode( $matches[1] );
4998 }
4999 $title = Title::newFromText( $matches[1], NS_FILE );
5000 if ( is_null( $title ) ) {
5001 # Bogus title. Ignore these so we don't bomb out later.
5002 continue;
5003 }
5004
5005 # We need to get what handler the file uses, to figure out parameters.
5006 # Note, a hook can overide the file name, and chose an entirely different
5007 # file (which potentially could be of a different type and have different handler).
5008 $options = [];
5009 $descQuery = false;
5010 Hooks::run( 'BeforeParserFetchFileAndTitle',
5011 [ $this, $title, &$options, &$descQuery ] );
5012 # Don't register it now, as ImageGallery does that later.
5013 $file = $this->fetchFileNoRegister( $title, $options );
5014 $handler = $file ? $file->getHandler() : false;
5015
5016 $paramMap = [
5017 'img_alt' => 'gallery-internal-alt',
5018 'img_link' => 'gallery-internal-link',
5019 ];
5020 if ( $handler ) {
5021 $paramMap = $paramMap + $handler->getParamMap();
5022 // We don't want people to specify per-image widths.
5023 // Additionally the width parameter would need special casing anyhow.
5024 unset( $paramMap['img_width'] );
5025 }
5026
5027 $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5028
5029 $label = '';
5030 $alt = '';
5031 $link = '';
5032 $handlerOptions = [];
5033 if ( isset( $matches[3] ) ) {
5034 // look for an |alt= definition while trying not to break existing
5035 // captions with multiple pipes (|) in it, until a more sensible grammar
5036 // is defined for images in galleries
5037
5038 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5039 // splitting on '|' is a bit odd, and different from makeImage.
5040 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5041 $parameterMatches = StringUtils::explode( '|', $matches[3] );
5042
5043 foreach ( $parameterMatches as $parameterMatch ) {
5044 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5045 if ( $magicName ) {
5046 $paramName = $paramMap[$magicName];
5047
5048 switch ( $paramName ) {
5049 case 'gallery-internal-alt':
5050 $alt = $this->stripAltText( $match, false );
5051 break;
5052 case 'gallery-internal-link':
5053 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5054 $chars = self::EXT_LINK_URL_CLASS;
5055 $addr = self::EXT_LINK_ADDR;
5056 $prots = $this->mUrlProtocols;
5057 // check to see if link matches an absolute url, if not then it must be a wiki link.
5058 if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
5059 $link = $linkValue;
5060 $this->mOutput->addExternalLink( $link );
5061 } else {
5062 $localLinkTitle = Title::newFromText( $linkValue );
5063 if ( $localLinkTitle !== null ) {
5064 $this->mOutput->addLink( $localLinkTitle );
5065 $link = $localLinkTitle->getLinkURL();
5066 }
5067 }
5068 break;
5069 default:
5070 // Must be a handler specific parameter.
5071 if ( $handler->validateParam( $paramName, $match ) ) {
5072 $handlerOptions[$paramName] = $match;
5073 } else {
5074 // Guess not, consider it as caption.
5075 wfDebug( "$parameterMatch failed parameter validation\n" );
5076 $label = '|' . $parameterMatch;
5077 }
5078 }
5079
5080 } else {
5081 // Last pipe wins.
5082 $label = '|' . $parameterMatch;
5083 }
5084 }
5085 // Remove the pipe.
5086 $label = substr( $label, 1 );
5087 }
5088
5089 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5090 }
5091 $html = $ig->toHTML();
5092 Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5093 return $html;
5094 }
5095
5100 public function getImageParams( $handler ) {
5101 if ( $handler ) {
5102 $handlerClass = get_class( $handler );
5103 } else {
5104 $handlerClass = '';
5105 }
5106 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5107 # Initialise static lists
5108 static $internalParamNames = [
5109 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5110 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5111 'bottom', 'text-bottom' ],
5112 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5113 'upright', 'border', 'link', 'alt', 'class' ],
5114 ];
5115 static $internalParamMap;
5116 if ( !$internalParamMap ) {
5117 $internalParamMap = [];
5118 foreach ( $internalParamNames as $type => $names ) {
5119 foreach ( $names as $name ) {
5120 $magicName = str_replace( '-', '_', "img_$name" );
5121 $internalParamMap[$magicName] = [ $type, $name ];
5122 }
5123 }
5124 }
5125
5126 # Add handler params
5127 $paramMap = $internalParamMap;
5128 if ( $handler ) {
5129 $handlerParamMap = $handler->getParamMap();
5130 foreach ( $handlerParamMap as $magic => $paramName ) {
5131 $paramMap[$magic] = [ 'handler', $paramName ];
5132 }
5133 }
5134 $this->mImageParams[$handlerClass] = $paramMap;
5135 $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5136 }
5137 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5138 }
5139
5148 public function makeImage( $title, $options, $holders = false ) {
5149 # Check if the options text is of the form "options|alt text"
5150 # Options are:
5151 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5152 # * left no resizing, just left align. label is used for alt= only
5153 # * right same, but right aligned
5154 # * none same, but not aligned
5155 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5156 # * center center the image
5157 # * frame Keep original image size, no magnify-button.
5158 # * framed Same as "frame"
5159 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5160 # * upright reduce width for upright images, rounded to full __0 px
5161 # * border draw a 1px border around the image
5162 # * alt Text for HTML alt attribute (defaults to empty)
5163 # * class Set a class for img node
5164 # * link Set the target of the image link. Can be external, interwiki, or local
5165 # vertical-align values (no % or length right now):
5166 # * baseline
5167 # * sub
5168 # * super
5169 # * top
5170 # * text-top
5171 # * middle
5172 # * bottom
5173 # * text-bottom
5174
5175 $parts = StringUtils::explode( "|", $options );
5176
5177 # Give extensions a chance to select the file revision for us
5178 $options = [];
5179 $descQuery = false;
5180 Hooks::run( 'BeforeParserFetchFileAndTitle',
5181 [ $this, $title, &$options, &$descQuery ] );
5182 # Fetch and register the file (file title may be different via hooks)
5183 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5184
5185 # Get parameter map
5186 $handler = $file ? $file->getHandler() : false;
5187
5188 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5189
5190 if ( !$file ) {
5191 $this->addTrackingCategory( 'broken-file-category' );
5192 }
5193
5194 # Process the input parameters
5195 $caption = '';
5196 $params = [ 'frame' => [], 'handler' => [],
5197 'horizAlign' => [], 'vertAlign' => [] ];
5198 $seenformat = false;
5199 foreach ( $parts as $part ) {
5200 $part = trim( $part );
5201 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5202 $validated = false;
5203 if ( isset( $paramMap[$magicName] ) ) {
5204 list( $type, $paramName ) = $paramMap[$magicName];
5205
5206 # Special case; width and height come in one variable together
5207 if ( $type === 'handler' && $paramName === 'width' ) {
5208 $parsedWidthParam = $this->parseWidthParam( $value );
5209 if ( isset( $parsedWidthParam['width'] ) ) {
5210 $width = $parsedWidthParam['width'];
5211 if ( $handler->validateParam( 'width', $width ) ) {
5212 $params[$type]['width'] = $width;
5213 $validated = true;
5214 }
5215 }
5216 if ( isset( $parsedWidthParam['height'] ) ) {
5217 $height = $parsedWidthParam['height'];
5218 if ( $handler->validateParam( 'height', $height ) ) {
5219 $params[$type]['height'] = $height;
5220 $validated = true;
5221 }
5222 }
5223 # else no validation -- bug 13436
5224 } else {
5225 if ( $type === 'handler' ) {
5226 # Validate handler parameter
5227 $validated = $handler->validateParam( $paramName, $value );
5228 } else {
5229 # Validate internal parameters
5230 switch ( $paramName ) {
5231 case 'manualthumb':
5232 case 'alt':
5233 case 'class':
5234 # @todo FIXME: Possibly check validity here for
5235 # manualthumb? downstream behavior seems odd with
5236 # missing manual thumbs.
5237 $validated = true;
5238 $value = $this->stripAltText( $value, $holders );
5239 break;
5240 case 'link':
5241 $chars = self::EXT_LINK_URL_CLASS;
5242 $addr = self::EXT_LINK_ADDR;
5243 $prots = $this->mUrlProtocols;
5244 if ( $value === '' ) {
5245 $paramName = 'no-link';
5246 $value = true;
5247 $validated = true;
5248 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5249 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5250 $paramName = 'link-url';
5251 $this->mOutput->addExternalLink( $value );
5252 if ( $this->mOptions->getExternalLinkTarget() ) {
5253 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5254 }
5255 $validated = true;
5256 }
5257 } else {
5258 $linkTitle = Title::newFromText( $value );
5259 if ( $linkTitle ) {
5260 $paramName = 'link-title';
5261 $value = $linkTitle;
5262 $this->mOutput->addLink( $linkTitle );
5263 $validated = true;
5264 }
5265 }
5266 break;
5267 case 'frameless':
5268 case 'framed':
5269 case 'thumbnail':
5270 // use first appearing option, discard others.
5271 $validated = ! $seenformat;
5272 $seenformat = true;
5273 break;
5274 default:
5275 # Most other things appear to be empty or numeric...
5276 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5277 }
5278 }
5279
5280 if ( $validated ) {
5281 $params[$type][$paramName] = $value;
5282 }
5283 }
5284 }
5285 if ( !$validated ) {
5286 $caption = $part;
5287 }
5288 }
5289
5290 # Process alignment parameters
5291 if ( $params['horizAlign'] ) {
5292 $params['frame']['align'] = key( $params['horizAlign'] );
5293 }
5294 if ( $params['vertAlign'] ) {
5295 $params['frame']['valign'] = key( $params['vertAlign'] );
5296 }
5297
5298 $params['frame']['caption'] = $caption;
5299
5300 # Will the image be presented in a frame, with the caption below?
5301 $imageIsFramed = isset( $params['frame']['frame'] )
5302 || isset( $params['frame']['framed'] )
5303 || isset( $params['frame']['thumbnail'] )
5304 || isset( $params['frame']['manualthumb'] );
5305
5306 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5307 # came to also set the caption, ordinary text after the image -- which
5308 # makes no sense, because that just repeats the text multiple times in
5309 # screen readers. It *also* came to set the title attribute.
5310 # Now that we have an alt attribute, we should not set the alt text to
5311 # equal the caption: that's worse than useless, it just repeats the
5312 # text. This is the framed/thumbnail case. If there's no caption, we
5313 # use the unnamed parameter for alt text as well, just for the time be-
5314 # ing, if the unnamed param is set and the alt param is not.
5315 # For the future, we need to figure out if we want to tweak this more,
5316 # e.g., introducing a title= parameter for the title; ignoring the un-
5317 # named parameter entirely for images without a caption; adding an ex-
5318 # plicit caption= parameter and preserving the old magic unnamed para-
5319 # meter for BC; ...
5320 if ( $imageIsFramed ) { # Framed image
5321 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5322 # No caption or alt text, add the filename as the alt text so
5323 # that screen readers at least get some description of the image
5324 $params['frame']['alt'] = $title->getText();
5325 }
5326 # Do not set $params['frame']['title'] because tooltips don't make sense
5327 # for framed images
5328 } else { # Inline image
5329 if ( !isset( $params['frame']['alt'] ) ) {
5330 # No alt text, use the "caption" for the alt text
5331 if ( $caption !== '' ) {
5332 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5333 } else {
5334 # No caption, fall back to using the filename for the
5335 # alt text
5336 $params['frame']['alt'] = $title->getText();
5337 }
5338 }
5339 # Use the "caption" for the tooltip text
5340 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5341 }
5342
5343 Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5344
5345 # Linker does the rest
5346 $time = isset( $options['time'] ) ? $options['time'] : false;
5347 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5348 $time, $descQuery, $this->mOptions->getThumbSize() );
5349
5350 # Give the handler a chance to modify the parser object
5351 if ( $handler ) {
5352 $handler->parserTransformHook( $this, $file );
5353 }
5354
5355 return $ret;
5356 }
5357
5363 protected function stripAltText( $caption, $holders ) {
5364 # Strip bad stuff out of the title (tooltip). We can't just use
5365 # replaceLinkHoldersText() here, because if this function is called
5366 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5367 if ( $holders ) {
5368 $tooltip = $holders->replaceText( $caption );
5369 } else {
5370 $tooltip = $this->replaceLinkHoldersText( $caption );
5371 }
5372
5373 # make sure there are no placeholders in thumbnail attributes
5374 # that are later expanded to html- so expand them now and
5375 # remove the tags
5376 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5377 $tooltip = Sanitizer::stripAllTags( $tooltip );
5378
5379 return $tooltip;
5380 }
5381
5387 public function disableCache() {
5388 wfDebug( "Parser output marked as uncacheable.\n" );
5389 if ( !$this->mOutput ) {
5390 throw new MWException( __METHOD__ .
5391 " can only be called when actually parsing something" );
5392 }
5393 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5394 }
5395
5404 public function attributeStripCallback( &$text, $frame = false ) {
5405 $text = $this->replaceVariables( $text, $frame );
5406 $text = $this->mStripState->unstripBoth( $text );
5407 return $text;
5408 }
5409
5415 public function getTags() {
5416 return array_merge(
5417 array_keys( $this->mTransparentTagHooks ),
5418 array_keys( $this->mTagHooks ),
5419 array_keys( $this->mFunctionTagHooks )
5420 );
5421 }
5422
5433 public function replaceTransparentTags( $text ) {
5434 $matches = [];
5435 $elements = array_keys( $this->mTransparentTagHooks );
5436 $text = self::extractTagsAndParams( $elements, $text, $matches );
5437 $replacements = [];
5438
5439 foreach ( $matches as $marker => $data ) {
5440 list( $element, $content, $params, $tag ) = $data;
5441 $tagName = strtolower( $element );
5442 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5443 $output = call_user_func_array(
5444 $this->mTransparentTagHooks[$tagName],
5445 [ $content, $params, $this ]
5446 );
5447 } else {
5448 $output = $tag;
5449 }
5450 $replacements[$marker] = $output;
5451 }
5452 return strtr( $text, $replacements );
5453 }
5454
5484 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5485 global $wgTitle; # not generally used but removes an ugly failure mode
5486
5487 $magicScopeVariable = $this->lock();
5488 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5489 $outText = '';
5490 $frame = $this->getPreprocessor()->newFrame();
5491
5492 # Process section extraction flags
5493 $flags = 0;
5494 $sectionParts = explode( '-', $sectionId );
5495 $sectionIndex = array_pop( $sectionParts );
5496 foreach ( $sectionParts as $part ) {
5497 if ( $part === 'T' ) {
5498 $flags |= self::PTD_FOR_INCLUSION;
5499 }
5500 }
5501
5502 # Check for empty input
5503 if ( strval( $text ) === '' ) {
5504 # Only sections 0 and T-0 exist in an empty document
5505 if ( $sectionIndex == 0 ) {
5506 if ( $mode === 'get' ) {
5507 return '';
5508 } else {
5509 return $newText;
5510 }
5511 } else {
5512 if ( $mode === 'get' ) {
5513 return $newText;
5514 } else {
5515 return $text;
5516 }
5517 }
5518 }
5519
5520 # Preprocess the text
5521 $root = $this->preprocessToDom( $text, $flags );
5522
5523 # <h> nodes indicate section breaks
5524 # They can only occur at the top level, so we can find them by iterating the root's children
5525 $node = $root->getFirstChild();
5526
5527 # Find the target section
5528 if ( $sectionIndex == 0 ) {
5529 # Section zero doesn't nest, level=big
5530 $targetLevel = 1000;
5531 } else {
5532 while ( $node ) {
5533 if ( $node->getName() === 'h' ) {
5534 $bits = $node->splitHeading();
5535 if ( $bits['i'] == $sectionIndex ) {
5536 $targetLevel = $bits['level'];
5537 break;
5538 }
5539 }
5540 if ( $mode === 'replace' ) {
5541 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5542 }
5543 $node = $node->getNextSibling();
5544 }
5545 }
5546
5547 if ( !$node ) {
5548 # Not found
5549 if ( $mode === 'get' ) {
5550 return $newText;
5551 } else {
5552 return $text;
5553 }
5554 }
5555
5556 # Find the end of the section, including nested sections
5557 do {
5558 if ( $node->getName() === 'h' ) {
5559 $bits = $node->splitHeading();
5560 $curLevel = $bits['level'];
5561 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5562 break;
5563 }
5564 }
5565 if ( $mode === 'get' ) {
5566 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5567 }
5568 $node = $node->getNextSibling();
5569 } while ( $node );
5570
5571 # Write out the remainder (in replace mode only)
5572 if ( $mode === 'replace' ) {
5573 # Output the replacement text
5574 # Add two newlines on -- trailing whitespace in $newText is conventionally
5575 # stripped by the editor, so we need both newlines to restore the paragraph gap
5576 # Only add trailing whitespace if there is newText
5577 if ( $newText != "" ) {
5578 $outText .= $newText . "\n\n";
5579 }
5580
5581 while ( $node ) {
5582 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5583 $node = $node->getNextSibling();
5584 }
5585 }
5586
5587 if ( is_string( $outText ) ) {
5588 # Re-insert stripped tags
5589 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5590 }
5591
5592 return $outText;
5593 }
5594
5609 public function getSection( $text, $sectionId, $defaultText = '' ) {
5610 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5611 }
5612
5625 public function replaceSection( $oldText, $sectionId, $newText ) {
5626 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5627 }
5628
5634 public function getRevisionId() {
5635 return $this->mRevisionId;
5636 }
5637
5644 public function getRevisionObject() {
5645 if ( !is_null( $this->mRevisionObject ) ) {
5646 return $this->mRevisionObject;
5647 }
5648 if ( is_null( $this->mRevisionId ) ) {
5649 return null;
5650 }
5651
5652 $rev = call_user_func(
5653 $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5654 );
5655
5656 # If the parse is for a new revision, then the callback should have
5657 # already been set to force the object and should match mRevisionId.
5658 # If not, try to fetch by mRevisionId for sanity.
5659 if ( $rev && $rev->getId() != $this->mRevisionId ) {
5660 $rev = Revision::newFromId( $this->mRevisionId );
5661 }
5662
5663 $this->mRevisionObject = $rev;
5664
5665 return $this->mRevisionObject;
5666 }
5667
5673 public function getRevisionTimestamp() {
5674 if ( is_null( $this->mRevisionTimestamp ) ) {
5676
5677 $revObject = $this->getRevisionObject();
5678 $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5679
5680 # The cryptic '' timezone parameter tells to use the site-default
5681 # timezone offset instead of the user settings.
5682 # Since this value will be saved into the parser cache, served
5683 # to other users, and potentially even used inside links and such,
5684 # it needs to be consistent for all visitors.
5685 $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5686
5687 }
5688 return $this->mRevisionTimestamp;
5689 }
5690
5696 public function getRevisionUser() {
5697 if ( is_null( $this->mRevisionUser ) ) {
5698 $revObject = $this->getRevisionObject();
5699
5700 # if this template is subst: the revision id will be blank,
5701 # so just use the current user's name
5702 if ( $revObject ) {
5703 $this->mRevisionUser = $revObject->getUserText();
5704 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5705 $this->mRevisionUser = $this->getUser()->getName();
5706 }
5707 }
5708 return $this->mRevisionUser;
5709 }
5710
5716 public function getRevisionSize() {
5717 if ( is_null( $this->mRevisionSize ) ) {
5718 $revObject = $this->getRevisionObject();
5719
5720 # if this variable is subst: the revision id will be blank,
5721 # so just use the parser input size, because the own substituation
5722 # will change the size.
5723 if ( $revObject ) {
5724 $this->mRevisionSize = $revObject->getSize();
5725 } else {
5726 $this->mRevisionSize = $this->mInputSize;
5727 }
5728 }
5729 return $this->mRevisionSize;
5730 }
5731
5737 public function setDefaultSort( $sort ) {
5738 $this->mDefaultSort = $sort;
5739 $this->mOutput->setProperty( 'defaultsort', $sort );
5740 }
5741
5752 public function getDefaultSort() {
5753 if ( $this->mDefaultSort !== false ) {
5754 return $this->mDefaultSort;
5755 } else {
5756 return '';
5757 }
5758 }
5759
5766 public function getCustomDefaultSort() {
5767 return $this->mDefaultSort;
5768 }
5769
5779 public function guessSectionNameFromWikiText( $text ) {
5780 # Strip out wikitext links(they break the anchor)
5781 $text = $this->stripSectionName( $text );
5782 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5783 return '#' . Sanitizer::escapeId( $text, 'noninitial' );
5784 }
5785
5794 public function guessLegacySectionNameFromWikiText( $text ) {
5795 # Strip out wikitext links(they break the anchor)
5796 $text = $this->stripSectionName( $text );
5797 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5798 return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
5799 }
5800
5815 public function stripSectionName( $text ) {
5816 # Strip internal link markup
5817 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5818 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5819
5820 # Strip external link markup
5821 # @todo FIXME: Not tolerant to blank link text
5822 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
5823 # on how many empty links there are on the page - need to figure that out.
5824 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5825
5826 # Parse wikitext quotes (italics & bold)
5827 $text = $this->doQuotes( $text );
5828
5829 # Strip HTML tags
5830 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
5831 return $text;
5832 }
5833
5844 public function testSrvus( $text, Title $title, ParserOptions $options,
5845 $outputType = self::OT_HTML
5846 ) {
5847 $magicScopeVariable = $this->lock();
5848 $this->startParse( $title, $options, $outputType, true );
5849
5850 $text = $this->replaceVariables( $text );
5851 $text = $this->mStripState->unstripBoth( $text );
5852 $text = Sanitizer::removeHTMLtags( $text );
5853 return $text;
5854 }
5855
5862 public function testPst( $text, Title $title, ParserOptions $options ) {
5863 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5864 }
5865
5872 public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5873 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5874 }
5875
5892 public function markerSkipCallback( $s, $callback ) {
5893 $i = 0;
5894 $out = '';
5895 while ( $i < strlen( $s ) ) {
5896 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5897 if ( $markerStart === false ) {
5898 $out .= call_user_func( $callback, substr( $s, $i ) );
5899 break;
5900 } else {
5901 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5902 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5903 if ( $markerEnd === false ) {
5904 $out .= substr( $s, $markerStart );
5905 break;
5906 } else {
5907 $markerEnd += strlen( self::MARKER_SUFFIX );
5908 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5909 $i = $markerEnd;
5910 }
5911 }
5912 }
5913 return $out;
5914 }
5915
5922 public function killMarkers( $text ) {
5923 return $this->mStripState->killMarkers( $text );
5924 }
5925
5942 public function serializeHalfParsedText( $text ) {
5943 $data = [
5944 'text' => $text,
5945 'version' => self::HALF_PARSED_VERSION,
5946 'stripState' => $this->mStripState->getSubState( $text ),
5947 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
5948 ];
5949 return $data;
5950 }
5951
5967 public function unserializeHalfParsedText( $data ) {
5968 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
5969 throw new MWException( __METHOD__ . ': invalid version' );
5970 }
5971
5972 # First, extract the strip state.
5973 $texts = [ $data['text'] ];
5974 $texts = $this->mStripState->merge( $data['stripState'], $texts );
5975
5976 # Now renumber links
5977 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
5978
5979 # Should be good to go.
5980 return $texts[0];
5981 }
5982
5992 public function isValidHalfParsedText( $data ) {
5993 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
5994 }
5995
6004 public function parseWidthParam( $value ) {
6005 $parsedWidthParam = [];
6006 if ( $value === '' ) {
6007 return $parsedWidthParam;
6008 }
6009 $m = [];
6010 # (bug 13500) In both cases (width/height and width only),
6011 # permit trailing "px" for backward compatibility.
6012 if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6013 $width = intval( $m[1] );
6014 $height = intval( $m[2] );
6015 $parsedWidthParam['width'] = $width;
6016 $parsedWidthParam['height'] = $height;
6017 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6018 $width = intval( $value );
6019 $parsedWidthParam['width'] = $width;
6020 }
6021 return $parsedWidthParam;
6022 }
6023
6033 protected function lock() {
6034 if ( $this->mInParse ) {
6035 throw new MWException( "Parser state cleared while parsing. "
6036 . "Did you call Parser::parse recursively?" );
6037 }
6038 $this->mInParse = true;
6039
6040 $recursiveCheck = new ScopedCallback( function() {
6041 $this->mInParse = false;
6042 } );
6043
6044 return $recursiveCheck;
6045 }
6046
6057 public static function stripOuterParagraph( $html ) {
6058 $m = [];
6059 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6060 if ( strpos( $m[1], '</p>' ) === false ) {
6061 $html = $m[1];
6062 }
6063 }
6064
6065 return $html;
6066 }
6067
6078 public function getFreshParser() {
6080 if ( $this->mInParse ) {
6081 return new $wgParserConf['class']( $wgParserConf );
6082 } else {
6083 return $this;
6084 }
6085 }
6086
6093 public function enableOOUI() {
6095 $this->mOutput->setEnableOOUI( true );
6096 }
6097}
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
$wgLanguageCode
Site language code.
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgServerName
Server name.
$wgExtraInterlanguageLinkPrefixes
List of additional interwiki prefixes that should be treated as interlanguage links (i....
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
$wgSitename
Name of the site.
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
$wgExperimentalHtmlIds
Should we allow a broader set of characters in id attributes, per HTML5? If not, use only HTML 4-comp...
$wgScriptPath
The path we should point to.
$wgTranscludeCacheExpiry
Expiry time for transcluded templates cached in transcache database table.
$wgParserConf
Parser configuration.
$wgMaxSigChars
Maximum number of Unicode characters in signature.
$wgEnableScaryTranscluding
Enable interwiki transcluding.
$wgStylePath
The URL path of the skins directory.
$wgServer
URL of the server.
$wgMaxTocLevel
Maximum indent level of toc.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfRandomString( $length=32)
Get a random string containing a number of pseudo-random hex characters.
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.
if(! $wgRequest->checkUrlExtension()) if(isset($_SERVER[ 'PATH_INFO']) &&$_SERVER[ 'PATH_INFO'] !='') if(! $wgEnableAPI) $wgTitle
Definition api.php:68
$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:131
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
static tocList( $toc, $lang=false)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition Linker.php:1645
static makeMediaLinkFile(Title $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:874
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1615
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:415
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:362
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1439
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:277
static tocIndent()
Add another level to the Table of Contents.
Definition Linker.php:1589
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:1722
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:934
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1701
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1600
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1633
MediaWiki exception.
static tidy( $text)
Interface with html tidy.
Definition MWTidy.php:46
static isEnabled()
Definition MWTidy.php:79
static getInstance( $ts=false)
Get a timestamp instance in GMT.
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Class for handling an array of magic words.
static getCacheTTL( $id)
Allow external reads of TTL array.
static getVariableIDs()
Get an array of parser variable IDs.
static & get( $id)
Factory: creates an object representing an ID.
static getDoubleUnderscoreArray()
Get a MagicWordArray of double-underscore entities.
static getSubstIDs()
Get an array of parser substitution modifier IDs.
Handles a simple LRU key/value map with a maximum number of entries.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static setupOOUI( $skinName='', $dir='ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
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:70
addTrackingCategory( $msg)
Definition Parser.php:4013
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:866
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:5673
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:2003
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3822
const TOC_END
Definition Parser.php:138
$mDefaultStripList
Definition Parser.php:147
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:5737
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6057
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:1967
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:697
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition Parser.php:2444
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:300
ParserOutput $mOutput
Definition Parser.php:176
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2026
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4652
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3691
$mFirstCall
Definition Parser.php:152
LinkRenderer $mLinkRenderer
Definition Parser.php:257
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:652
$mHighestExpansionDepth
Definition Parser.php:191
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:4938
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:5766
stripAltText( $caption, $holders)
Definition Parser.php:5363
getOptions()
Get the ParserOptions object.
Definition Parser.php:821
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4615
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:4444
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:4678
fetchScaryTemplateMaybeFromCache( $url)
Definition Parser.php:3729
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:5696
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3507
replaceExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:1828
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2914
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
Definition Parser.php:82
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:2411
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:2435
$mHeadings
Definition Parser.php:193
Title $mTitle
Definition Parser.php:213
uniqPrefix()
Accessor for mUniqPrefix.
Definition Parser.php:739
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2098
const PTD_FOR_INCLUSION
Definition Parser.php:106
$mTplDomCache
Definition Parser.php:193
$mGeneratedPPNodeCount
Definition Parser.php:191
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1430
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
Definition Parser.php:5967
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:2991
LinkHolderArray $mLinkHolders
Definition Parser.php:188
$mFunctionTagHooks
Definition Parser.php:145
$mRevisionId
Definition Parser.php:217
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5148
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
Definition Parser.php:5387
pstPass2( $text, $user)
Pre-save transform helper function.
Definition Parser.php:4476
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Definition Parser.php:4697
getRevisionSize()
Get the size of the revision.
Definition Parser.php:5716
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:5484
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:5815
$mDefaultSort
Definition Parser.php:192
$mTagHooks
Definition Parser.php:141
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:678
$mFunctionHooks
Definition Parser.php:143
$mShowToc
Definition Parser.php:195
setHook( $tag, $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4741
getImageParams( $handler)
Definition Parser.php:5100
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition Parser.php:5942
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:4033
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1326
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3441
$mRevisionSize
Definition Parser.php:220
doTableStuff( $text)
parse the wiki syntax used to render tables
Definition Parser.php:1050
getTitle()
Accessor for the Title object.
Definition Parser.php:767
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:1904
ParserOptions $mOptions
Definition Parser.php:208
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition Parser.php:5844
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:76
MagicWordArray $mSubstWords
Definition Parser.php:164
$mUrlProtocols
Definition Parser.php:166
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
Definition Parser.php:5433
MapCacheLRU null $currentRevisionCache
Definition Parser.php:243
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:920
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition Parser.php:2881
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:1925
setOutputType( $ot)
Set the output type.
Definition Parser.php:786
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:853
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2484
StripState $mStripState
Definition Parser.php:182
$mInputSize
Definition Parser.php:222
MagicWordArray $mVariables
Definition Parser.php:159
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1023
$mOutputType
Definition Parser.php:214
$mImageParamsMagicArray
Definition Parser.php:150
setUser( $user)
Set the current user.
Definition Parser.php:729
$mAutonumber
Definition Parser.php:177
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:5892
getTags()
Accessor.
Definition Parser.php:5415
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:3958
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3769
replaceInternalLinks( $s)
Process [[ ]] wikilinks.
Definition Parser.php:2085
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:235
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:609
$mRevisionUser
Definition Parser.php:219
string $mUniqPrefix
Deprecated accessor for the strip marker prefix.
Definition Parser.php:228
$mTplRedirCache
Definition Parser.php:193
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3666
$mFunctionSynonyms
Definition Parser.php:144
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5609
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1253
$mRevisionTimestamp
Definition Parser.php:218
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5625
interwikiTransclude( $title, $action)
Transclude an interwiki link.
Definition Parser.php:3710
parseWidthParam( $value)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6004
setTitle( $t)
Set the context title.
Definition Parser.php:749
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:3931
static getRandomString()
Get a random string.
Definition Parser.php:718
User $mUser
Definition Parser.php:200
bool $mInParse
Recursive call protection.
Definition Parser.php:249
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
Definition Parser.php:2457
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:5644
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2469
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3524
$mPPNodeCount
Definition Parser.php:191
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:5752
const MARKER_PREFIX
Definition Parser.php:134
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:4921
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:4873
doAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1623
$mMarkerIndex
Definition Parser.php:151
const EXT_LINK_ADDR
Definition Parser.php:95
$mExpensiveFunctionCount
Definition Parser.php:194
Preprocessor $mPreprocessor
Definition Parser.php:170
$mDoubleUnderscores
Definition Parser.php:193
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Definition Parser.php:3484
getOutput()
Get the ParserOutput object.
Definition Parser.php:812
$mVarCache
Definition Parser.php:148
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:831
$mImageParams
Definition Parser.php:149
Title( $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:777
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:635
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:5779
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4786
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:4910
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:5634
$mIncludeCount
Definition Parser.php:184
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4601
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:906
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead.
Definition Parser.php:5794
const EXT_LINK_URL_CLASS
Definition Parser.php:92
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:803
$mIncludeSizes
Definition Parser.php:191
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:288
fetchFile( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3655
setFunctionTagHook( $tag, $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:4887
setLinkID( $id)
Definition Parser.php:845
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:894
doQuotes( $text)
Helper function for doAllQuotes()
Definition Parser.php:1640
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3552
doHeadings( $text)
Parse headers and return html.
Definition Parser.php:1607
$mExtLinkBracketedRegex
Definition Parser.php:166
const EXT_IMAGE_REGEX
Definition Parser.php:98
$mRevIdForTs
Definition Parser.php:221
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:1036
static extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix=null)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition Parser.php:953
__construct( $conf=[])
Definition Parser.php:262
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:3945
testPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:5872
const SPACE_NOT_NL
Definition Parser.php:103
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:323
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:2869
$mForceTocPosition
Definition Parser.php:195
clearState()
Clear Parser state.
Definition Parser.php:343
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:5922
setTransparentTagHook( $tag, $callback)
As setHook(), but letting the contents be parsed.
Definition Parser.php:4772
static replaceUnusualEscapes( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:1953
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6093
nextLinkID()
Definition Parser.php:838
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:4552
setFunctionHook( $id, $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:4835
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3013
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5404
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
Definition Parser.php:2839
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
Definition Parser.php:5992
lock()
Lock the current instance of the parser.
Definition Parser.php:6033
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition Parser.php:2943
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:403
$mStripList
Definition Parser.php:146
testPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:5862
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:4666
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6078
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3342
SectionProfiler $mProfiler
Definition Parser.php:252
$mTransparentTagHooks
Definition Parser.php:142
getConverterLanguage()
Get the language object for language conversion.
Definition Parser.php:884
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3565
Variant of the Message class.
Definition Message.php:1260
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:59
Group all the pieces relevant to the context of a request into one instance.
setTitle(Title $title=null)
Set the Title object.
static newKnownCurrent(IDatabase $db, $pageId, $revId)
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:128
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:110
static cleanUrl( $url)
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static articles()
static images()
static edits()
static users()
static pages()
static numberingroup( $group)
Find the number of users in a given user group.
static activeUsers()
static capturePath(Title $title, IContextSource $context, LinkRenderer $linkRenderer=null)
Just like executePath() but will override global variables and execute the page in "inclusion" mode.
static getPage( $name)
Find the object with a given name and return it (or NULL)
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
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 normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Represents a title within MediaWiki.
Definition Title.php:36
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
Definition Xml.php:735
=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
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like please read the documentation for except in special pages derived from QueryPage It s a common pitfall for new developers to submit code containing SQL queries which examine huge numbers of rows Remember that COUNT * is(N), counting rows in atable is like counting beans in a bucket.------------------------------------------------------------------------ Replication------------------------------------------------------------------------The largest installation of MediaWiki, Wikimedia, uses a large set ofslave MySQL servers replicating writes made to a master MySQL server. Itis important to understand the issues associated with this setup if youwant to write code destined for Wikipedia.It 's often the case that the best algorithm to use for a given taskdepends on whether or not replication is in use. Due to our unabashedWikipedia-centrism, we often just use the replication-friendly version, but if you like, you can use wfGetLB() ->getServerCount() > 1 tocheck to see if replication is in use.===Lag===Lag primarily occurs when large write queries are sent to the master.Writes on the master are executed in parallel, but they are executed inserial when they are replicated to the slaves. The master writes thequery to the binlog when the transaction is committed. The slaves pollthe binlog and start executing the query as soon as it appears. They canservice reads while they are performing a write query, but will not readanything more from the binlog and thus will perform no more writes. Thismeans that if the write query runs for a long time, the slaves will lagbehind the master for the time it takes for the write query to complete.Lag can be exacerbated by high read load. MediaWiki 's load balancer willstop sending reads to a slave when it is lagged by more than 30 seconds.If the load ratios are set incorrectly, or if there is too much loadgenerally, this may lead to a slave permanently hovering around 30seconds lag.If all slaves are lagged by more than 30 seconds, MediaWiki will stopwriting to the database. All edits and other write operations will berefused, with an error returned to the user. This gives the slaves achance to catch up. Before we had this mechanism, the slaves wouldregularly lag by several minutes, making review of recent editsdifficult.In addition to this, MediaWiki attempts to ensure that the user seesevents occurring on the wiki in chronological order. A few seconds of lagcan be tolerated, as long as the user sees a consistent picture fromsubsequent requests. This is done by saving the master binlog positionin the session, and then at the start of each request, waiting for theslave to catch up to that position before doing any reads from it. Ifthis wait times out, reads are allowed anyway, but the request isconsidered to be in "lagged slave mode". Lagged slave mode can bechecked by calling wfGetLB() ->getLaggedSlaveMode(). The onlypractical consequence at present is a warning displayed in the pagefooter.===Lag avoidance===To avoid excessive lag, queries which write large numbers of rows shouldbe split up, generally to write one row at a time. Multi-row INSERT ...SELECT queries are the worst offenders should be avoided altogether.Instead do the select first and then the insert.===Working with lag===Despite our best efforts, it 's not practical to guarantee a low-lagenvironment. Lag will usually be less than one second, but mayoccasionally be up to 30 seconds. For scalability, it 's very importantto keep load on the master low, so simply sending all your queries tothe master is not the answer. So when you have a genuine need forup-to-date data, the following approach is advised:1) Do a quick query to the master for a sequence number or timestamp 2) Run the full query on the slave and check if it matches the data you gotfrom the master 3) If it doesn 't, run the full query on the masterTo avoid swamping the master every time the slaves lag, use of thisapproach should be kept to a minimum. In most cases you should just readfrom the slave and let the user deal with the delay.------------------------------------------------------------------------ Lock contention----------------------------------------