MediaWiki REL1_30
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 T21052
92 # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
93 # uses to replace invalid HTML characters.
94 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
95 # Simplified expression to match an IPv4 or IPv6 address, or
96 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
97 const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
98 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
99 // @codingStandardsIgnoreStart Generic.Files.LineLength
100 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
101 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
102 // @codingStandardsIgnoreEnd
103
104 # Regular expression for a non-newline space
105 const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
106
107 # Flags for preprocessToDom
109
110 # Allowed values for $this->mOutputType
111 # Parameter to startExternalParse().
112 const OT_HTML = 1; # like parse()
113 const OT_WIKI = 2; # like preSaveTransform()
114 const OT_PREPROCESS = 3; # like preprocess()
115 const OT_MSG = 3;
116 const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
117
135 const MARKER_SUFFIX = "-QINU`\"'\x7f";
136 const MARKER_PREFIX = "\x7f'\"`UNIQ-";
137
138 # Markers used for wrapping the table of contents
139 const TOC_START = '<mw:toc>';
140 const TOC_END = '</mw:toc>';
141
142 # Persistent:
143 public $mTagHooks = [];
145 public $mFunctionHooks = [];
146 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
148 public $mStripList = [];
150 public $mVarCache = [];
151 public $mImageParams = [];
153 public $mMarkerIndex = 0;
154 public $mFirstCall = true;
155
156 # Initialised by initialiseVariables()
157
162
167 # Initialised in constructor
168 public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
169
170 # Initialized in getPreprocessor()
173
174 # Cleared with clearState():
178 public $mOutput;
180
185
191
192 public $mLinkID;
193 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
195 public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
196 public $mExpensiveFunctionCount; # number of expensive parser function calls
198
202 public $mUser; # User object; only used when doing pre-save transform
203
204 # Temporary
205 # These are variables reset at least once per parse regardless of $clearState
206
210 public $mOptions;
211
215 public $mTitle; # Title context, used for self-link rendering and similar things
216 public $mOutputType; # Output type, one of the OT_xxx constants
217 public $ot; # Shortcut alias, see setOutputType()
218 public $mRevisionObject; # The revision object of the specified revision ID
219 public $mRevisionId; # ID to display in {{REVISIONID}} tags
220 public $mRevisionTimestamp; # The timestamp of the specified revision ID
221 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
222 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
223 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
224 public $mInputSize = false; # For {{PAGESIZE}} on current page.
225
230 public $mUniqPrefix = self::MARKER_PREFIX;
231
238
246
251 public $mInParse = false;
252
254 protected $mProfiler;
255
259 protected $mLinkRenderer;
260
264 public function __construct( $conf = [] ) {
265 $this->mConf = $conf;
266 $this->mUrlProtocols = wfUrlProtocols();
267 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
268 self::EXT_LINK_ADDR .
269 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
270 if ( isset( $conf['preprocessorClass'] ) ) {
271 $this->mPreprocessorClass = $conf['preprocessorClass'];
272 } elseif ( defined( 'HPHP_VERSION' ) ) {
273 # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
274 $this->mPreprocessorClass = 'Preprocessor_Hash';
275 } elseif ( extension_loaded( 'domxml' ) ) {
276 # PECL extension that conflicts with the core DOM extension (T15770)
277 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
278 $this->mPreprocessorClass = 'Preprocessor_Hash';
279 } elseif ( extension_loaded( 'dom' ) ) {
280 $this->mPreprocessorClass = 'Preprocessor_DOM';
281 } else {
282 $this->mPreprocessorClass = 'Preprocessor_Hash';
283 }
284 wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
285 }
286
290 public function __destruct() {
291 if ( isset( $this->mLinkHolders ) ) {
292 unset( $this->mLinkHolders );
293 }
294 foreach ( $this as $name => $value ) {
295 unset( $this->$name );
296 }
297 }
298
302 public function __clone() {
303 $this->mInParse = false;
304
305 // T58226: When you create a reference "to" an object field, that
306 // makes the object field itself be a reference too (until the other
307 // reference goes out of scope). When cloning, any field that's a
308 // reference is copied as a reference in the new object. Both of these
309 // are defined PHP5 behaviors, as inconvenient as it is for us when old
310 // hooks from PHP4 days are passing fields by reference.
311 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
312 // Make a non-reference copy of the field, then rebind the field to
313 // reference the new copy.
314 $tmp = $this->$k;
315 $this->$k =& $tmp;
316 unset( $tmp );
317 }
318
319 Hooks::run( 'ParserCloned', [ $this ] );
320 }
321
325 public function firstCallInit() {
326 if ( !$this->mFirstCall ) {
327 return;
328 }
329 $this->mFirstCall = false;
330
332 CoreTagHooks::register( $this );
333 $this->initialiseVariables();
334
335 // Avoid PHP 7.1 warning from passing $this by reference
336 $parser = $this;
337 Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
338 }
339
345 public function clearState() {
346 if ( $this->mFirstCall ) {
347 $this->firstCallInit();
348 }
349 $this->mOutput = new ParserOutput;
350 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
351 $this->mAutonumber = 0;
352 $this->mIncludeCount = [];
353 $this->mLinkHolders = new LinkHolderArray( $this );
354 $this->mLinkID = 0;
355 $this->mRevisionObject = $this->mRevisionTimestamp =
356 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
357 $this->mVarCache = [];
358 $this->mUser = null;
359 $this->mLangLinkLanguages = [];
360 $this->currentRevisionCache = null;
361
362 $this->mStripState = new StripState;
363
364 # Clear these on every parse, T6549
365 $this->mTplRedirCache = $this->mTplDomCache = [];
366
367 $this->mShowToc = true;
368 $this->mForceTocPosition = false;
369 $this->mIncludeSizes = [
370 'post-expand' => 0,
371 'arg' => 0,
372 ];
373 $this->mPPNodeCount = 0;
374 $this->mGeneratedPPNodeCount = 0;
375 $this->mHighestExpansionDepth = 0;
376 $this->mDefaultSort = false;
377 $this->mHeadings = [];
378 $this->mDoubleUnderscores = [];
379 $this->mExpensiveFunctionCount = 0;
380
381 # Fix cloning
382 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
383 $this->mPreprocessor = null;
384 }
385
386 $this->mProfiler = new SectionProfiler();
387
388 // Avoid PHP 7.1 warning from passing $this by reference
389 $parser = $this;
390 Hooks::run( 'ParserClearState', [ &$parser ] );
391 }
392
405 public function parse(
406 $text, Title $title, ParserOptions $options,
407 $linestart = true, $clearState = true, $revid = null
408 ) {
414 global $wgShowHostnames;
415
416 if ( $clearState ) {
417 // We use U+007F DELETE to construct strip markers, so we have to make
418 // sure that this character does not occur in the input text.
419 $text = strtr( $text, "\x7f", "?" );
420 $magicScopeVariable = $this->lock();
421 }
422 // Strip U+0000 NULL (T159174)
423 $text = str_replace( "\000", '', $text );
424
425 $this->startParse( $title, $options, self::OT_HTML, $clearState );
426
427 $this->currentRevisionCache = null;
428 $this->mInputSize = strlen( $text );
429 if ( $this->mOptions->getEnableLimitReport() ) {
430 $this->mOutput->resetParseStartTime();
431 }
432
433 $oldRevisionId = $this->mRevisionId;
434 $oldRevisionObject = $this->mRevisionObject;
435 $oldRevisionTimestamp = $this->mRevisionTimestamp;
436 $oldRevisionUser = $this->mRevisionUser;
437 $oldRevisionSize = $this->mRevisionSize;
438 if ( $revid !== null ) {
439 $this->mRevisionId = $revid;
440 $this->mRevisionObject = null;
441 $this->mRevisionTimestamp = null;
442 $this->mRevisionUser = null;
443 $this->mRevisionSize = null;
444 }
445
446 // Avoid PHP 7.1 warning from passing $this by reference
447 $parser = $this;
448 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
449 # No more strip!
450 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
451 $text = $this->internalParse( $text );
452 Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
453
454 $text = $this->internalParseHalfParsed( $text, true, $linestart );
455
463 if ( !( $options->getDisableTitleConversion()
464 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
465 || isset( $this->mDoubleUnderscores['notitleconvert'] )
466 || $this->mOutput->getDisplayTitle() !== false )
467 ) {
468 $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
469 if ( $convruletitle ) {
470 $this->mOutput->setTitleText( $convruletitle );
471 } else {
472 $titleText = $this->getConverterLanguage()->convertTitle( $title );
473 $this->mOutput->setTitleText( $titleText );
474 }
475 }
476
477 # Done parsing! Compute runtime adaptive expiry if set
478 $this->mOutput->finalizeAdaptiveCacheExpiry();
479
480 # Warn if too many heavyweight parser functions were used
481 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
482 $this->limitationWarn( 'expensive-parserfunction',
483 $this->mExpensiveFunctionCount,
484 $this->mOptions->getExpensiveParserFunctionLimit()
485 );
486 }
487
488 # Information on include size limits, for the benefit of users who try to skirt them
489 if ( $this->mOptions->getEnableLimitReport() ) {
490 $max = $this->mOptions->getMaxIncludeSize();
491
492 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
493 if ( $cpuTime !== null ) {
494 $this->mOutput->setLimitReportData( 'limitreport-cputime',
495 sprintf( "%.3f", $cpuTime )
496 );
497 }
498
499 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
500 $this->mOutput->setLimitReportData( 'limitreport-walltime',
501 sprintf( "%.3f", $wallTime )
502 );
503
504 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
505 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
506 );
507 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
508 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
509 );
510 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
511 [ $this->mIncludeSizes['post-expand'], $max ]
512 );
513 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
514 [ $this->mIncludeSizes['arg'], $max ]
515 );
516 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
517 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
518 );
519 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
520 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
521 );
522 Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
523
524 $limitReport = "NewPP limit report\n";
525 if ( $wgShowHostnames ) {
526 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
527 }
528 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
529 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
530 $limitReport .= 'Dynamic content: ' .
531 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
532 "\n";
533
534 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
535 if ( Hooks::run( 'ParserLimitReportFormat',
536 [ $key, &$value, &$limitReport, false, false ]
537 ) ) {
538 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
539 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
540 ->inLanguage( 'en' )->useDatabase( false );
541 if ( !$valueMsg->exists() ) {
542 $valueMsg = new RawMessage( '$1' );
543 }
544 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
545 $valueMsg->params( $value );
546 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
547 }
548 }
549 }
550 // Since we're not really outputting HTML, decode the entities and
551 // then re-encode the things that need hiding inside HTML comments.
552 $limitReport = htmlspecialchars_decode( $limitReport );
553 // Run deprecated hook
554 Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
555
556 // Sanitize for comment. Note '‐' in the replacement is U+2010,
557 // which looks much like the problematic '-'.
558 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
559 $text .= "\n<!-- \n$limitReport-->\n";
560
561 // Add on template profiling data in human/machine readable way
562 $dataByFunc = $this->mProfiler->getFunctionStats();
563 uasort( $dataByFunc, function ( $a, $b ) {
564 return $a['real'] < $b['real']; // descending order
565 } );
566 $profileReport = [];
567 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
568 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
569 $item['%real'], $item['real'], $item['calls'],
570 htmlspecialchars( $item['name'] ) );
571 }
572 $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
573 $text .= implode( "\n", $profileReport ) . "\n-->\n";
574
575 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
576
577 // Add other cache related metadata
578 if ( $wgShowHostnames ) {
579 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
580 }
581 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
582 $this->mOutput->getCacheTime() );
583 $this->mOutput->setLimitReportData( 'cachereport-ttl',
584 $this->mOutput->getCacheExpiry() );
585 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
586 $this->mOutput->hasDynamicContent() );
587
588 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
589 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
590 $this->mTitle->getPrefixedDBkey() );
591 }
592 }
593
594 # Wrap non-interface parser output in a <div> so it can be targeted
595 # with CSS (T37247)
596 $class = $this->mOptions->getWrapOutputClass();
597 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
598 $text = Html::rawElement( 'div', [ 'class' => $class ], $text );
599 }
600
601 $this->mOutput->setText( $text );
602
603 $this->mRevisionId = $oldRevisionId;
604 $this->mRevisionObject = $oldRevisionObject;
605 $this->mRevisionTimestamp = $oldRevisionTimestamp;
606 $this->mRevisionUser = $oldRevisionUser;
607 $this->mRevisionSize = $oldRevisionSize;
608 $this->mInputSize = false;
609 $this->currentRevisionCache = null;
610
611 return $this->mOutput;
612 }
613
636 public function recursiveTagParse( $text, $frame = false ) {
637 // Avoid PHP 7.1 warning from passing $this by reference
638 $parser = $this;
639 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
640 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
641 $text = $this->internalParse( $text, false, $frame );
642 return $text;
643 }
644
662 public function recursiveTagParseFully( $text, $frame = false ) {
663 $text = $this->recursiveTagParse( $text, $frame );
664 $text = $this->internalParseHalfParsed( $text, false );
665 return $text;
666 }
667
679 public function preprocess( $text, Title $title = null,
680 ParserOptions $options, $revid = null, $frame = false
681 ) {
682 $magicScopeVariable = $this->lock();
683 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
684 if ( $revid !== null ) {
685 $this->mRevisionId = $revid;
686 }
687 // Avoid PHP 7.1 warning from passing $this by reference
688 $parser = $this;
689 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
690 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
691 $text = $this->replaceVariables( $text, $frame );
692 $text = $this->mStripState->unstripBoth( $text );
693 return $text;
694 }
695
705 public function recursivePreprocess( $text, $frame = false ) {
706 $text = $this->replaceVariables( $text, $frame );
707 $text = $this->mStripState->unstripBoth( $text );
708 return $text;
709 }
710
724 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
725 $msg = new RawMessage( $text );
726 $text = $msg->params( $params )->plain();
727
728 # Parser (re)initialisation
729 $magicScopeVariable = $this->lock();
730 $this->startParse( $title, $options, self::OT_PLAIN, true );
731
733 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
734 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
735 $text = $this->mStripState->unstripBoth( $text );
736 return $text;
737 }
738
745 public function setUser( $user ) {
746 $this->mUser = $user;
747 }
748
754 public function setTitle( $t ) {
755 if ( !$t ) {
756 $t = Title::newFromText( 'NO TITLE' );
757 }
758
759 if ( $t->hasFragment() ) {
760 # Strip the fragment to avoid various odd effects
761 $this->mTitle = $t->createFragmentTarget( '' );
762 } else {
763 $this->mTitle = $t;
764 }
765 }
766
772 public function getTitle() {
773 return $this->mTitle;
774 }
775
782 public function Title( $x = null ) {
783 return wfSetVar( $this->mTitle, $x );
784 }
785
791 public function setOutputType( $ot ) {
792 $this->mOutputType = $ot;
793 # Shortcut alias
794 $this->ot = [
795 'html' => $ot == self::OT_HTML,
796 'wiki' => $ot == self::OT_WIKI,
797 'pre' => $ot == self::OT_PREPROCESS,
798 'plain' => $ot == self::OT_PLAIN,
799 ];
800 }
801
808 public function OutputType( $x = null ) {
809 return wfSetVar( $this->mOutputType, $x );
810 }
811
817 public function getOutput() {
818 return $this->mOutput;
819 }
820
826 public function getOptions() {
827 return $this->mOptions;
828 }
829
836 public function Options( $x = null ) {
837 return wfSetVar( $this->mOptions, $x );
838 }
839
843 public function nextLinkID() {
844 return $this->mLinkID++;
845 }
846
850 public function setLinkID( $id ) {
851 $this->mLinkID = $id;
852 }
853
858 public function getFunctionLang() {
859 return $this->getTargetLanguage();
860 }
861
871 public function getTargetLanguage() {
872 $target = $this->mOptions->getTargetLanguage();
873
874 if ( $target !== null ) {
875 return $target;
876 } elseif ( $this->mOptions->getInterfaceMessage() ) {
877 return $this->mOptions->getUserLangObj();
878 } elseif ( is_null( $this->mTitle ) ) {
879 throw new MWException( __METHOD__ . ': $this->mTitle is null' );
880 }
881
882 return $this->mTitle->getPageLanguage();
883 }
884
889 public function getConverterLanguage() {
890 return $this->getTargetLanguage();
891 }
892
899 public function getUser() {
900 if ( !is_null( $this->mUser ) ) {
901 return $this->mUser;
902 }
903 return $this->mOptions->getUser();
904 }
905
911 public function getPreprocessor() {
912 if ( !isset( $this->mPreprocessor ) ) {
913 $class = $this->mPreprocessorClass;
914 $this->mPreprocessor = new $class( $this );
915 }
916 return $this->mPreprocessor;
917 }
918
925 public function getLinkRenderer() {
926 if ( !$this->mLinkRenderer ) {
927 $this->mLinkRenderer = MediaWikiServices::getInstance()
928 ->getLinkRendererFactory()->create();
929 $this->mLinkRenderer->setStubThreshold(
930 $this->getOptions()->getStubThreshold()
931 );
932 }
933
934 return $this->mLinkRenderer;
935 }
936
956 public static function extractTagsAndParams( $elements, $text, &$matches ) {
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 $lines = StringUtils::explode( "\n", $text );
1052 $out = '';
1053 $td_history = []; # Is currently a td tag open?
1054 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1055 $tr_history = []; # Is currently a tr tag open?
1056 $tr_attributes = []; # history of tr attributes
1057 $has_opened_tr = []; # Did this table open a <tr> element?
1058 $indent_level = 0; # indent level of the table
1059
1060 foreach ( $lines as $outLine ) {
1061 $line = trim( $outLine );
1062
1063 if ( $line === '' ) { # empty line, go to next line
1064 $out .= $outLine . "\n";
1065 continue;
1066 }
1067
1068 $first_character = $line[0];
1069 $first_two = substr( $line, 0, 2 );
1070 $matches = [];
1071
1072 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1073 # First check if we are starting a new table
1074 $indent_level = strlen( $matches[1] );
1075
1076 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1077 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1078
1079 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1080 array_push( $td_history, false );
1081 array_push( $last_tag_history, '' );
1082 array_push( $tr_history, false );
1083 array_push( $tr_attributes, '' );
1084 array_push( $has_opened_tr, false );
1085 } elseif ( count( $td_history ) == 0 ) {
1086 # Don't do any of the following
1087 $out .= $outLine . "\n";
1088 continue;
1089 } elseif ( $first_two === '|}' ) {
1090 # We are ending a table
1091 $line = '</table>' . substr( $line, 2 );
1092 $last_tag = array_pop( $last_tag_history );
1093
1094 if ( !array_pop( $has_opened_tr ) ) {
1095 $line = "<tr><td></td></tr>{$line}";
1096 }
1097
1098 if ( array_pop( $tr_history ) ) {
1099 $line = "</tr>{$line}";
1100 }
1101
1102 if ( array_pop( $td_history ) ) {
1103 $line = "</{$last_tag}>{$line}";
1104 }
1105 array_pop( $tr_attributes );
1106 $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1107 } elseif ( $first_two === '|-' ) {
1108 # Now we have a table row
1109 $line = preg_replace( '#^\|-+#', '', $line );
1110
1111 # Whats after the tag is now only attributes
1112 $attributes = $this->mStripState->unstripBoth( $line );
1113 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1114 array_pop( $tr_attributes );
1115 array_push( $tr_attributes, $attributes );
1116
1117 $line = '';
1118 $last_tag = array_pop( $last_tag_history );
1119 array_pop( $has_opened_tr );
1120 array_push( $has_opened_tr, true );
1121
1122 if ( array_pop( $tr_history ) ) {
1123 $line = '</tr>';
1124 }
1125
1126 if ( array_pop( $td_history ) ) {
1127 $line = "</{$last_tag}>{$line}";
1128 }
1129
1130 $outLine = $line;
1131 array_push( $tr_history, false );
1132 array_push( $td_history, false );
1133 array_push( $last_tag_history, '' );
1134 } elseif ( $first_character === '|'
1135 || $first_character === '!'
1136 || $first_two === '|+'
1137 ) {
1138 # This might be cell elements, td, th or captions
1139 if ( $first_two === '|+' ) {
1140 $first_character = '+';
1141 $line = substr( $line, 2 );
1142 } else {
1143 $line = substr( $line, 1 );
1144 }
1145
1146 // Implies both are valid for table headings.
1147 if ( $first_character === '!' ) {
1148 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1149 }
1150
1151 # Split up multiple cells on the same line.
1152 # FIXME : This can result in improper nesting of tags processed
1153 # by earlier parser steps.
1154 $cells = explode( '||', $line );
1155
1156 $outLine = '';
1157
1158 # Loop through each table cell
1159 foreach ( $cells as $cell ) {
1160 $previous = '';
1161 if ( $first_character !== '+' ) {
1162 $tr_after = array_pop( $tr_attributes );
1163 if ( !array_pop( $tr_history ) ) {
1164 $previous = "<tr{$tr_after}>\n";
1165 }
1166 array_push( $tr_history, true );
1167 array_push( $tr_attributes, '' );
1168 array_pop( $has_opened_tr );
1169 array_push( $has_opened_tr, true );
1170 }
1171
1172 $last_tag = array_pop( $last_tag_history );
1173
1174 if ( array_pop( $td_history ) ) {
1175 $previous = "</{$last_tag}>\n{$previous}";
1176 }
1177
1178 if ( $first_character === '|' ) {
1179 $last_tag = 'td';
1180 } elseif ( $first_character === '!' ) {
1181 $last_tag = 'th';
1182 } elseif ( $first_character === '+' ) {
1183 $last_tag = 'caption';
1184 } else {
1185 $last_tag = '';
1186 }
1187
1188 array_push( $last_tag_history, $last_tag );
1189
1190 # A cell could contain both parameters and data
1191 $cell_data = explode( '|', $cell, 2 );
1192
1193 # T2553: Note that a '|' inside an invalid link should not
1194 # be mistaken as delimiting cell parameters
1195 # Bug T153140: Neither should language converter markup.
1196 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
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 $origText = $text;
1255
1256 // Avoid PHP 7.1 warning from passing $this by reference
1257 $parser = $this;
1258
1259 # Hook to suspend the parser in this state
1260 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1261 return $text;
1262 }
1263
1264 # if $frame is provided, then use $frame for replacing any variables
1265 if ( $frame ) {
1266 # use frame depth to infer how include/noinclude tags should be handled
1267 # depth=0 means this is the top-level document; otherwise it's an included document
1268 if ( !$frame->depth ) {
1269 $flag = 0;
1270 } else {
1271 $flag = self::PTD_FOR_INCLUSION;
1272 }
1273 $dom = $this->preprocessToDom( $text, $flag );
1274 $text = $frame->expand( $dom );
1275 } else {
1276 # if $frame is not provided, then use old-style replaceVariables
1277 $text = $this->replaceVariables( $text );
1278 }
1279
1280 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1281 $text = Sanitizer::removeHTMLtags(
1282 $text,
1283 [ $this, 'attributeStripCallback' ],
1284 false,
1285 array_keys( $this->mTransparentTagHooks ),
1286 [],
1287 [ $this, 'addTrackingCategory' ]
1288 );
1289 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1290
1291 # Tables need to come after variable replacement for things to work
1292 # properly; putting them before other transformations should keep
1293 # exciting things like link expansions from showing up in surprising
1294 # places.
1295 $text = $this->doTableStuff( $text );
1296
1297 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1298
1299 $text = $this->doDoubleUnderscore( $text );
1300
1301 $text = $this->doHeadings( $text );
1302 $text = $this->replaceInternalLinks( $text );
1303 $text = $this->doAllQuotes( $text );
1304 $text = $this->replaceExternalLinks( $text );
1305
1306 # replaceInternalLinks may sometimes leave behind
1307 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1308 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1309
1310 $text = $this->doMagicLinks( $text );
1311 $text = $this->formatHeadings( $text, $origText, $isMain );
1312
1313 return $text;
1314 }
1315
1325 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1326 $text = $this->mStripState->unstripGeneral( $text );
1327
1328 // Avoid PHP 7.1 warning from passing $this by reference
1329 $parser = $this;
1330
1331 if ( $isMain ) {
1332 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1333 }
1334
1335 # Clean up special characters, only run once, next-to-last before doBlockLevels
1336 $fixtags = [
1337 # French spaces, last one Guillemet-left
1338 # only if there is something before the space
1339 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1340 # french spaces, Guillemet-right
1341 '/(\\302\\253) /' => '\\1&#160;',
1342 '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, T13874.
1343 ];
1344 $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1345
1346 $text = $this->doBlockLevels( $text, $linestart );
1347
1348 $this->replaceLinkHolders( $text );
1349
1357 if ( !( $this->mOptions->getDisableContentConversion()
1358 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1359 ) {
1360 if ( !$this->mOptions->getInterfaceMessage() ) {
1361 # The position of the convert() call should not be changed. it
1362 # assumes that the links are all replaced and the only thing left
1363 # is the <nowiki> mark.
1364 $text = $this->getConverterLanguage()->convert( $text );
1365 }
1366 }
1367
1368 $text = $this->mStripState->unstripNoWiki( $text );
1369
1370 if ( $isMain ) {
1371 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1372 }
1373
1374 $text = $this->replaceTransparentTags( $text );
1375 $text = $this->mStripState->unstripGeneral( $text );
1376
1377 $text = Sanitizer::normalizeCharReferences( $text );
1378
1379 if ( MWTidy::isEnabled() ) {
1380 if ( $this->mOptions->getTidy() ) {
1381 $text = MWTidy::tidy( $text );
1382 }
1383 } else {
1384 # attempt to sanitize at least some nesting problems
1385 # (T4702 and quite a few others)
1386 $tidyregs = [
1387 # ''Something [http://www.cool.com cool''] -->
1388 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1389 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1390 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1391 # fix up an anchor inside another anchor, only
1392 # at least for a single single nested link (T5695)
1393 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1394 '\\1\\2</a>\\3</a>\\1\\4</a>',
1395 # fix div inside inline elements- doBlockLevels won't wrap a line which
1396 # contains a div, so fix it up here; replace
1397 # div with escaped text
1398 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1399 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1400 # remove empty italic or bold tag pairs, some
1401 # introduced by rules above
1402 '/<([bi])><\/\\1>/' => '',
1403 ];
1404
1405 $text = preg_replace(
1406 array_keys( $tidyregs ),
1407 array_values( $tidyregs ),
1408 $text );
1409 }
1410
1411 if ( $isMain ) {
1412 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1413 }
1414
1415 return $text;
1416 }
1417
1429 public function doMagicLinks( $text ) {
1431 $urlChar = self::EXT_LINK_URL_CLASS;
1432 $addr = self::EXT_LINK_ADDR;
1433 $space = self::SPACE_NOT_NL; # non-newline space
1434 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1435 $spaces = "$space++"; # possessive match of 1 or more spaces
1436 $text = preg_replace_callback(
1437 '!(?: # Start cases
1438 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1439 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1440 (\b # m[3]: Free external links
1441 (?i:$prots)
1442 ($addr$urlChar*) # m[4]: Post-protocol path
1443 ) |
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 // (T15227: ''''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 // (T15227: ''''''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 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1830 if ( $bits === false ) {
1831 throw new MWException( "PCRE needs to be compiled with "
1832 . "--enable-unicode-properties in order for MediaWiki to function" );
1833 }
1834 $s = array_shift( $bits );
1835
1836 $i = 0;
1837 while ( $i < count( $bits ) ) {
1838 $url = $bits[$i++];
1839 $i++; // protocol
1840 $text = $bits[$i++];
1841 $trail = $bits[$i++];
1842
1843 # The characters '<' and '>' (which were escaped by
1844 # removeHTMLtags()) should not be included in
1845 # URLs, per RFC 2396.
1846 $m2 = [];
1847 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1848 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1849 $url = substr( $url, 0, $m2[0][1] );
1850 }
1851
1852 # If the link text is an image URL, replace it with an <img> tag
1853 # This happened by accident in the original parser, but some people used it extensively
1854 $img = $this->maybeMakeExternalImage( $text );
1855 if ( $img !== false ) {
1856 $text = $img;
1857 }
1858
1859 $dtrail = '';
1860
1861 # Set linktype for CSS - if URL==text, link is essentially free
1862 $linktype = ( $text === $url ) ? 'free' : 'text';
1863
1864 # No link text, e.g. [http://domain.tld/some.link]
1865 if ( $text == '' ) {
1866 # Autonumber
1867 $langObj = $this->getTargetLanguage();
1868 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1869 $linktype = 'autonumber';
1870 } else {
1871 # Have link text, e.g. [http://domain.tld/some.link text]s
1872 # Check for trail
1873 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1874 }
1875
1876 $text = $this->getConverterLanguage()->markNoConversion( $text );
1877
1878 $url = Sanitizer::cleanUrl( $url );
1879
1880 # Use the encoded URL
1881 # This means that users can paste URLs directly into the text
1882 # Funny characters like ö aren't valid in URLs anyway
1883 # This was changed in August 2004
1884 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1885 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1886
1887 # Register link in the output object.
1888 $this->mOutput->addExternalLink( $url );
1889 }
1890
1891 return $s;
1892 }
1893
1903 public static function getExternalLinkRel( $url = false, $title = null ) {
1905 $ns = $title ? $title->getNamespace() : false;
1906 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1908 ) {
1909 return 'nofollow';
1910 }
1911 return null;
1912 }
1913
1924 public function getExternalLinkAttribs( $url ) {
1925 $attribs = [];
1926 $rel = self::getExternalLinkRel( $url, $this->mTitle );
1927
1928 $target = $this->mOptions->getExternalLinkTarget();
1929 if ( $target ) {
1930 $attribs['target'] = $target;
1931 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1932 // T133507. New windows can navigate parent cross-origin.
1933 // Including noreferrer due to lacking browser
1934 // support of noopener. Eventually noreferrer should be removed.
1935 if ( $rel !== '' ) {
1936 $rel .= ' ';
1937 }
1938 $rel .= 'noreferrer noopener';
1939 }
1940 }
1941 $attribs['rel'] = $rel;
1942 return $attribs;
1943 }
1944
1954 public static function normalizeLinkUrl( $url ) {
1955 # First, make sure unsafe characters are encoded
1956 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1957 function ( $m ) {
1958 return rawurlencode( $m[0] );
1959 },
1960 $url
1961 );
1962
1963 $ret = '';
1964 $end = strlen( $url );
1965
1966 # Fragment part - 'fragment'
1967 $start = strpos( $url, '#' );
1968 if ( $start !== false && $start < $end ) {
1969 $ret = self::normalizeUrlComponent(
1970 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1971 $end = $start;
1972 }
1973
1974 # Query part - 'query' minus &=+;
1975 $start = strpos( $url, '?' );
1976 if ( $start !== false && $start < $end ) {
1977 $ret = self::normalizeUrlComponent(
1978 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1979 $end = $start;
1980 }
1981
1982 # Scheme and path part - 'pchar'
1983 # (we assume no userinfo or encoded colons in the host)
1984 $ret = self::normalizeUrlComponent(
1985 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1986
1987 return $ret;
1988 }
1989
1990 private static function normalizeUrlComponent( $component, $unsafe ) {
1991 $callback = function ( $matches ) use ( $unsafe ) {
1992 $char = urldecode( $matches[0] );
1993 $ord = ord( $char );
1994 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1995 # Unescape it
1996 return $char;
1997 } else {
1998 # Leave it escaped, but use uppercase for a-f
1999 return strtoupper( $matches[0] );
2000 }
2001 };
2002 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2003 }
2004
2013 private function maybeMakeExternalImage( $url ) {
2014 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2015 $imagesexception = !empty( $imagesfrom );
2016 $text = false;
2017 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2018 if ( $imagesexception && is_array( $imagesfrom ) ) {
2019 $imagematch = false;
2020 foreach ( $imagesfrom as $match ) {
2021 if ( strpos( $url, $match ) === 0 ) {
2022 $imagematch = true;
2023 break;
2024 }
2025 }
2026 } elseif ( $imagesexception ) {
2027 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2028 } else {
2029 $imagematch = false;
2030 }
2031
2032 if ( $this->mOptions->getAllowExternalImages()
2033 || ( $imagesexception && $imagematch )
2034 ) {
2035 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2036 # Image found
2037 $text = Linker::makeExternalImage( $url );
2038 }
2039 }
2040 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2041 && preg_match( self::EXT_IMAGE_REGEX, $url )
2042 ) {
2043 $whitelist = explode(
2044 "\n",
2045 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2046 );
2047
2048 foreach ( $whitelist as $entry ) {
2049 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2050 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2051 continue;
2052 }
2053 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2054 # Image matches a whitelist entry
2055 $text = Linker::makeExternalImage( $url );
2056 break;
2057 }
2058 }
2059 }
2060 return $text;
2061 }
2062
2072 public function replaceInternalLinks( $s ) {
2073 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2074 return $s;
2075 }
2076
2085 public function replaceInternalLinks2( &$s ) {
2087
2088 static $tc = false, $e1, $e1_img;
2089 # the % is needed to support urlencoded titles as well
2090 if ( !$tc ) {
2091 $tc = Title::legalChars() . '#%';
2092 # Match a link having the form [[namespace:link|alternate]]trail
2093 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2094 # Match cases where there is no "]]", which might still be images
2095 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2096 }
2097
2098 $holders = new LinkHolderArray( $this );
2099
2100 # split the entire text string on occurrences of [[
2101 $a = StringUtils::explode( '[[', ' ' . $s );
2102 # get the first element (all text up to first [[), and remove the space we added
2103 $s = $a->current();
2104 $a->next();
2105 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2106 $s = substr( $s, 1 );
2107
2108 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2109 $e2 = null;
2110 if ( $useLinkPrefixExtension ) {
2111 # Match the end of a line for a word that's not followed by whitespace,
2112 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2113 global $wgContLang;
2114 $charset = $wgContLang->linkPrefixCharset();
2115 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2116 }
2117
2118 if ( is_null( $this->mTitle ) ) {
2119 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2120 }
2121 $nottalk = !$this->mTitle->isTalkPage();
2122
2123 if ( $useLinkPrefixExtension ) {
2124 $m = [];
2125 if ( preg_match( $e2, $s, $m ) ) {
2126 $first_prefix = $m[2];
2127 } else {
2128 $first_prefix = false;
2129 }
2130 } else {
2131 $prefix = '';
2132 }
2133
2134 $useSubpages = $this->areSubpagesAllowed();
2135
2136 // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2137 # Loop for each link
2138 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2139 // @codingStandardsIgnoreEnd
2140
2141 # Check for excessive memory usage
2142 if ( $holders->isBig() ) {
2143 # Too big
2144 # Do the existence check, replace the link holders and clear the array
2145 $holders->replace( $s );
2146 $holders->clear();
2147 }
2148
2149 if ( $useLinkPrefixExtension ) {
2150 if ( preg_match( $e2, $s, $m ) ) {
2151 $prefix = $m[2];
2152 $s = $m[1];
2153 } else {
2154 $prefix = '';
2155 }
2156 # first link
2157 if ( $first_prefix ) {
2158 $prefix = $first_prefix;
2159 $first_prefix = false;
2160 }
2161 }
2162
2163 $might_be_img = false;
2164
2165 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2166 $text = $m[2];
2167 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2168 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2169 # the real problem is with the $e1 regex
2170 # See T1500.
2171 # Still some problems for cases where the ] is meant to be outside punctuation,
2172 # and no image is in sight. See T4095.
2173 if ( $text !== ''
2174 && substr( $m[3], 0, 1 ) === ']'
2175 && strpos( $text, '[' ) !== false
2176 ) {
2177 $text .= ']'; # so that replaceExternalLinks($text) works later
2178 $m[3] = substr( $m[3], 1 );
2179 }
2180 # fix up urlencoded title texts
2181 if ( strpos( $m[1], '%' ) !== false ) {
2182 # Should anchors '#' also be rejected?
2183 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2184 }
2185 $trail = $m[3];
2186 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2187 # Invalid, but might be an image with a link in its caption
2188 $might_be_img = true;
2189 $text = $m[2];
2190 if ( strpos( $m[1], '%' ) !== false ) {
2191 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2192 }
2193 $trail = "";
2194 } else { # Invalid form; output directly
2195 $s .= $prefix . '[[' . $line;
2196 continue;
2197 }
2198
2199 $origLink = ltrim( $m[1], ' ' );
2200
2201 # Don't allow internal links to pages containing
2202 # PROTO: where PROTO is a valid URL protocol; these
2203 # should be external links.
2204 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2205 $s .= $prefix . '[[' . $line;
2206 continue;
2207 }
2208
2209 # Make subpage if necessary
2210 if ( $useSubpages ) {
2211 $link = $this->maybeDoSubpageLink( $origLink, $text );
2212 } else {
2213 $link = $origLink;
2214 }
2215
2216 $unstrip = $this->mStripState->unstripNoWiki( $link );
2217 $nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2218 if ( $nt === null ) {
2219 $s .= $prefix . '[[' . $line;
2220 continue;
2221 }
2222
2223 $ns = $nt->getNamespace();
2224 $iw = $nt->getInterwiki();
2225
2226 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2227
2228 if ( $might_be_img ) { # if this is actually an invalid link
2229 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2230 $found = false;
2231 while ( true ) {
2232 # look at the next 'line' to see if we can close it there
2233 $a->next();
2234 $next_line = $a->current();
2235 if ( $next_line === false || $next_line === null ) {
2236 break;
2237 }
2238 $m = explode( ']]', $next_line, 3 );
2239 if ( count( $m ) == 3 ) {
2240 # the first ]] closes the inner link, the second the image
2241 $found = true;
2242 $text .= "[[{$m[0]}]]{$m[1]}";
2243 $trail = $m[2];
2244 break;
2245 } elseif ( count( $m ) == 2 ) {
2246 # if there's exactly one ]] that's fine, we'll keep looking
2247 $text .= "[[{$m[0]}]]{$m[1]}";
2248 } else {
2249 # if $next_line is invalid too, we need look no further
2250 $text .= '[[' . $next_line;
2251 break;
2252 }
2253 }
2254 if ( !$found ) {
2255 # we couldn't find the end of this imageLink, so output it raw
2256 # but don't ignore what might be perfectly normal links in the text we've examined
2257 $holders->merge( $this->replaceInternalLinks2( $text ) );
2258 $s .= "{$prefix}[[$link|$text";
2259 # note: no $trail, because without an end, there *is* no trail
2260 continue;
2261 }
2262 } else { # it's not an image, so output it raw
2263 $s .= "{$prefix}[[$link|$text";
2264 # note: no $trail, because without an end, there *is* no trail
2265 continue;
2266 }
2267 }
2268
2269 $wasblank = ( $text == '' );
2270 if ( $wasblank ) {
2271 $text = $link;
2272 if ( !$noforce ) {
2273 # Strip off leading ':'
2274 $text = substr( $text, 1 );
2275 }
2276 } else {
2277 # T6598 madness. Handle the quotes only if they come from the alternate part
2278 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2279 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2280 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2281 $text = $this->doQuotes( $text );
2282 }
2283
2284 # Link not escaped by : , create the various objects
2285 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2286 # Interwikis
2287 if (
2288 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2289 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2290 in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2291 )
2292 ) {
2293 # T26502: filter duplicates
2294 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2295 $this->mLangLinkLanguages[$iw] = true;
2296 $this->mOutput->addLanguageLink( $nt->getFullText() );
2297 }
2298
2299 $s = rtrim( $s . $prefix );
2300 $s .= trim( $trail, "\n" ) == '' ? '' : $prefix . $trail;
2301 continue;
2302 }
2303
2304 if ( $ns == NS_FILE ) {
2305 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2306 if ( $wasblank ) {
2307 # if no parameters were passed, $text
2308 # becomes something like "File:Foo.png",
2309 # which we don't want to pass on to the
2310 # image generator
2311 $text = '';
2312 } else {
2313 # recursively parse links inside the image caption
2314 # actually, this will parse them in any other parameters, too,
2315 # but it might be hard to fix that, and it doesn't matter ATM
2316 $text = $this->replaceExternalLinks( $text );
2317 $holders->merge( $this->replaceInternalLinks2( $text ) );
2318 }
2319 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2320 $s .= $prefix . $this->armorLinks(
2321 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2322 continue;
2323 }
2324 } elseif ( $ns == NS_CATEGORY ) {
2325 $s = rtrim( $s . "\n" ); # T2087
2326
2327 if ( $wasblank ) {
2328 $sortkey = $this->getDefaultSort();
2329 } else {
2330 $sortkey = $text;
2331 }
2332 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2333 $sortkey = str_replace( "\n", '', $sortkey );
2334 $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2335 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2336
2340 $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2341
2342 continue;
2343 }
2344 }
2345
2346 # Self-link checking. For some languages, variants of the title are checked in
2347 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2348 # for linking to a different variant.
2349 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2350 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2351 continue;
2352 }
2353
2354 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2355 # @todo FIXME: Should do batch file existence checks, see comment below
2356 if ( $ns == NS_MEDIA ) {
2357 # Give extensions a chance to select the file revision for us
2358 $options = [];
2359 $descQuery = false;
2360 Hooks::run( 'BeforeParserFetchFileAndTitle',
2361 [ $this, $nt, &$options, &$descQuery ] );
2362 # Fetch and register the file (file title may be different via hooks)
2363 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2364 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2365 $s .= $prefix . $this->armorLinks(
2366 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2367 continue;
2368 }
2369
2370 # Some titles, such as valid special pages or files in foreign repos, should
2371 # be shown as bluelinks even though they're not included in the page table
2372 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2373 # batch file existence checks for NS_FILE and NS_MEDIA
2374 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2375 $this->mOutput->addLink( $nt );
2376 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2377 } else {
2378 # Links will be added to the output link list after checking
2379 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2380 }
2381 }
2382 return $holders;
2383 }
2384
2398 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2399 list( $inside, $trail ) = Linker::splitTrail( $trail );
2400
2401 if ( $text == '' ) {
2402 $text = htmlspecialchars( $nt->getPrefixedText() );
2403 }
2404
2405 $link = $this->getLinkRenderer()->makeKnownLink(
2406 $nt, new HtmlArmor( "$prefix$text$inside" )
2407 );
2408
2409 return $this->armorLinks( $link ) . $trail;
2410 }
2411
2422 public function armorLinks( $text ) {
2423 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2424 self::MARKER_PREFIX . "NOPARSE$1", $text );
2425 }
2426
2431 public function areSubpagesAllowed() {
2432 # Some namespaces don't allow subpages
2433 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2434 }
2435
2444 public function maybeDoSubpageLink( $target, &$text ) {
2445 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2446 }
2447
2456 public function doBlockLevels( $text, $linestart ) {
2457 return BlockLevelPass::doBlockLevels( $text, $linestart );
2458 }
2459
2471 public function getVariableValue( $index, $frame = false ) {
2474
2475 if ( is_null( $this->mTitle ) ) {
2476 // If no title set, bad things are going to happen
2477 // later. Title should always be set since this
2478 // should only be called in the middle of a parse
2479 // operation (but the unit-tests do funky stuff)
2480 throw new MWException( __METHOD__ . ' Should only be '
2481 . ' called while parsing (no title set)' );
2482 }
2483
2484 // Avoid PHP 7.1 warning from passing $this by reference
2485 $parser = $this;
2486
2491 if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) {
2492 if ( isset( $this->mVarCache[$index] ) ) {
2493 return $this->mVarCache[$index];
2494 }
2495 }
2496
2497 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2498 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2499
2500 $pageLang = $this->getFunctionLang();
2501
2502 switch ( $index ) {
2503 case '!':
2504 $value = '|';
2505 break;
2506 case 'currentmonth':
2507 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2508 break;
2509 case 'currentmonth1':
2510 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2511 break;
2512 case 'currentmonthname':
2513 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2514 break;
2515 case 'currentmonthnamegen':
2516 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2517 break;
2518 case 'currentmonthabbrev':
2519 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2520 break;
2521 case 'currentday':
2522 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2523 break;
2524 case 'currentday2':
2525 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2526 break;
2527 case 'localmonth':
2528 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2529 break;
2530 case 'localmonth1':
2531 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2532 break;
2533 case 'localmonthname':
2534 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2535 break;
2536 case 'localmonthnamegen':
2537 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2538 break;
2539 case 'localmonthabbrev':
2540 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2541 break;
2542 case 'localday':
2543 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2544 break;
2545 case 'localday2':
2546 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2547 break;
2548 case 'pagename':
2549 $value = wfEscapeWikiText( $this->mTitle->getText() );
2550 break;
2551 case 'pagenamee':
2552 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2553 break;
2554 case 'fullpagename':
2555 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2556 break;
2557 case 'fullpagenamee':
2558 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2559 break;
2560 case 'subpagename':
2561 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2562 break;
2563 case 'subpagenamee':
2564 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2565 break;
2566 case 'rootpagename':
2567 $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2568 break;
2569 case 'rootpagenamee':
2570 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2571 ' ',
2572 '_',
2573 $this->mTitle->getRootText()
2574 ) ) );
2575 break;
2576 case 'basepagename':
2577 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2578 break;
2579 case 'basepagenamee':
2580 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2581 ' ',
2582 '_',
2583 $this->mTitle->getBaseText()
2584 ) ) );
2585 break;
2586 case 'talkpagename':
2587 if ( $this->mTitle->canHaveTalkPage() ) {
2588 $talkPage = $this->mTitle->getTalkPage();
2589 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2590 } else {
2591 $value = '';
2592 }
2593 break;
2594 case 'talkpagenamee':
2595 if ( $this->mTitle->canHaveTalkPage() ) {
2596 $talkPage = $this->mTitle->getTalkPage();
2597 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2598 } else {
2599 $value = '';
2600 }
2601 break;
2602 case 'subjectpagename':
2603 $subjPage = $this->mTitle->getSubjectPage();
2604 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2605 break;
2606 case 'subjectpagenamee':
2607 $subjPage = $this->mTitle->getSubjectPage();
2608 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2609 break;
2610 case 'pageid': // requested in T25427
2611 $pageid = $this->getTitle()->getArticleID();
2612 if ( $pageid == 0 ) {
2613 # 0 means the page doesn't exist in the database,
2614 # which means the user is previewing a new page.
2615 # The vary-revision flag must be set, because the magic word
2616 # will have a different value once the page is saved.
2617 $this->mOutput->setFlag( 'vary-revision' );
2618 wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2619 }
2620 $value = $pageid ? $pageid : null;
2621 break;
2622 case 'revisionid':
2623 # Let the edit saving system know we should parse the page
2624 # *after* a revision ID has been assigned.
2625 $this->mOutput->setFlag( 'vary-revision-id' );
2626 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2627 $value = $this->mRevisionId;
2628 if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
2629 $value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
2630 $this->mOutput->setSpeculativeRevIdUsed( $value );
2631 }
2632 break;
2633 case 'revisionday':
2634 # Let the edit saving system know we should parse the page
2635 # *after* a revision ID has been assigned. This is for null edits.
2636 $this->mOutput->setFlag( 'vary-revision' );
2637 wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2638 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2639 break;
2640 case 'revisionday2':
2641 # Let the edit saving system know we should parse the page
2642 # *after* a revision ID has been assigned. This is for null edits.
2643 $this->mOutput->setFlag( 'vary-revision' );
2644 wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2645 $value = substr( $this->getRevisionTimestamp(), 6, 2 );
2646 break;
2647 case 'revisionmonth':
2648 # Let the edit saving system know we should parse the page
2649 # *after* a revision ID has been assigned. This is for null edits.
2650 $this->mOutput->setFlag( 'vary-revision' );
2651 wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2652 $value = substr( $this->getRevisionTimestamp(), 4, 2 );
2653 break;
2654 case 'revisionmonth1':
2655 # Let the edit saving system know we should parse the page
2656 # *after* a revision ID has been assigned. This is for null edits.
2657 $this->mOutput->setFlag( 'vary-revision' );
2658 wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2659 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2660 break;
2661 case 'revisionyear':
2662 # Let the edit saving system know we should parse the page
2663 # *after* a revision ID has been assigned. This is for null edits.
2664 $this->mOutput->setFlag( 'vary-revision' );
2665 wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2666 $value = substr( $this->getRevisionTimestamp(), 0, 4 );
2667 break;
2668 case 'revisiontimestamp':
2669 # Let the edit saving system know we should parse the page
2670 # *after* a revision ID has been assigned. This is for null edits.
2671 $this->mOutput->setFlag( 'vary-revision' );
2672 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2673 $value = $this->getRevisionTimestamp();
2674 break;
2675 case 'revisionuser':
2676 # Let the edit saving system know we should parse the page
2677 # *after* a revision ID has been assigned for null edits.
2678 $this->mOutput->setFlag( 'vary-user' );
2679 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2680 $value = $this->getRevisionUser();
2681 break;
2682 case 'revisionsize':
2683 $value = $this->getRevisionSize();
2684 break;
2685 case 'namespace':
2686 $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2687 break;
2688 case 'namespacee':
2689 $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2690 break;
2691 case 'namespacenumber':
2692 $value = $this->mTitle->getNamespace();
2693 break;
2694 case 'talkspace':
2695 $value = $this->mTitle->canHaveTalkPage()
2696 ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2697 : '';
2698 break;
2699 case 'talkspacee':
2700 $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2701 break;
2702 case 'subjectspace':
2703 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2704 break;
2705 case 'subjectspacee':
2706 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2707 break;
2708 case 'currentdayname':
2709 $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2710 break;
2711 case 'currentyear':
2712 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2713 break;
2714 case 'currenttime':
2715 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2716 break;
2717 case 'currenthour':
2718 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2719 break;
2720 case 'currentweek':
2721 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2722 # int to remove the padding
2723 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2724 break;
2725 case 'currentdow':
2726 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2727 break;
2728 case 'localdayname':
2729 $value = $pageLang->getWeekdayName(
2730 (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2731 );
2732 break;
2733 case 'localyear':
2734 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2735 break;
2736 case 'localtime':
2737 $value = $pageLang->time(
2738 MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2739 false,
2740 false
2741 );
2742 break;
2743 case 'localhour':
2744 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2745 break;
2746 case 'localweek':
2747 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2748 # int to remove the padding
2749 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2750 break;
2751 case 'localdow':
2752 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2753 break;
2754 case 'numberofarticles':
2755 $value = $pageLang->formatNum( SiteStats::articles() );
2756 break;
2757 case 'numberoffiles':
2758 $value = $pageLang->formatNum( SiteStats::images() );
2759 break;
2760 case 'numberofusers':
2761 $value = $pageLang->formatNum( SiteStats::users() );
2762 break;
2763 case 'numberofactiveusers':
2764 $value = $pageLang->formatNum( SiteStats::activeUsers() );
2765 break;
2766 case 'numberofpages':
2767 $value = $pageLang->formatNum( SiteStats::pages() );
2768 break;
2769 case 'numberofadmins':
2770 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2771 break;
2772 case 'numberofedits':
2773 $value = $pageLang->formatNum( SiteStats::edits() );
2774 break;
2775 case 'currenttimestamp':
2776 $value = wfTimestamp( TS_MW, $ts );
2777 break;
2778 case 'localtimestamp':
2779 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2780 break;
2781 case 'currentversion':
2783 break;
2784 case 'articlepath':
2785 return $wgArticlePath;
2786 case 'sitename':
2787 return $wgSitename;
2788 case 'server':
2789 return $wgServer;
2790 case 'servername':
2791 return $wgServerName;
2792 case 'scriptpath':
2793 return $wgScriptPath;
2794 case 'stylepath':
2795 return $wgStylePath;
2796 case 'directionmark':
2797 return $pageLang->getDirMark();
2798 case 'contentlanguage':
2799 global $wgLanguageCode;
2800 return $wgLanguageCode;
2801 case 'pagelanguage':
2802 $value = $pageLang->getCode();
2803 break;
2804 case 'cascadingsources':
2806 break;
2807 default:
2808 $ret = null;
2809 Hooks::run(
2810 'ParserGetVariableValueSwitch',
2811 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2812 );
2813
2814 return $ret;
2815 }
2816
2817 if ( $index ) {
2818 $this->mVarCache[$index] = $value;
2819 }
2820
2821 return $value;
2822 }
2823
2829 public function initialiseVariables() {
2830 $variableIDs = MagicWord::getVariableIDs();
2831 $substIDs = MagicWord::getSubstIDs();
2832
2833 $this->mVariables = new MagicWordArray( $variableIDs );
2834 $this->mSubstWords = new MagicWordArray( $substIDs );
2835 }
2836
2859 public function preprocessToDom( $text, $flags = 0 ) {
2860 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2861 return $dom;
2862 }
2863
2871 public static function splitWhitespace( $s ) {
2872 $ltrimmed = ltrim( $s );
2873 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2874 $trimmed = rtrim( $ltrimmed );
2875 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
2876 if ( $diff > 0 ) {
2877 $w2 = substr( $ltrimmed, -$diff );
2878 } else {
2879 $w2 = '';
2880 }
2881 return [ $w1, $trimmed, $w2 ];
2882 }
2883
2904 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2905 # Is there any text? Also, Prevent too big inclusions!
2906 $textSize = strlen( $text );
2907 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2908 return $text;
2909 }
2910
2911 if ( $frame === false ) {
2912 $frame = $this->getPreprocessor()->newFrame();
2913 } elseif ( !( $frame instanceof PPFrame ) ) {
2914 wfDebug( __METHOD__ . " called using plain parameters instead of "
2915 . "a PPFrame instance. Creating custom frame.\n" );
2916 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2917 }
2918
2919 $dom = $this->preprocessToDom( $text );
2920 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2921 $text = $frame->expand( $dom, $flags );
2922
2923 return $text;
2924 }
2925
2933 public static function createAssocArgs( $args ) {
2934 $assocArgs = [];
2935 $index = 1;
2936 foreach ( $args as $arg ) {
2937 $eqpos = strpos( $arg, '=' );
2938 if ( $eqpos === false ) {
2939 $assocArgs[$index++] = $arg;
2940 } else {
2941 $name = trim( substr( $arg, 0, $eqpos ) );
2942 $value = trim( substr( $arg, $eqpos + 1 ) );
2943 if ( $value === false ) {
2944 $value = '';
2945 }
2946 if ( $name !== false ) {
2947 $assocArgs[$name] = $value;
2948 }
2949 }
2950 }
2951
2952 return $assocArgs;
2953 }
2954
2981 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2982 # does no harm if $current and $max are present but are unnecessary for the message
2983 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2984 # only during preview, and that would split the parser cache unnecessarily.
2985 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2986 ->text();
2987 $this->mOutput->addWarning( $warning );
2988 $this->addTrackingCategory( "$limitationType-category" );
2989 }
2990
3003 public function braceSubstitution( $piece, $frame ) {
3004 // Flags
3005
3006 // $text has been filled
3007 $found = false;
3008 // wiki markup in $text should be escaped
3009 $nowiki = false;
3010 // $text is HTML, armour it against wikitext transformation
3011 $isHTML = false;
3012 // Force interwiki transclusion to be done in raw mode not rendered
3013 $forceRawInterwiki = false;
3014 // $text is a DOM node needing expansion in a child frame
3015 $isChildObj = false;
3016 // $text is a DOM node needing expansion in the current frame
3017 $isLocalObj = false;
3018
3019 # Title object, where $text came from
3020 $title = false;
3021
3022 # $part1 is the bit before the first |, and must contain only title characters.
3023 # Various prefixes will be stripped from it later.
3024 $titleWithSpaces = $frame->expand( $piece['title'] );
3025 $part1 = trim( $titleWithSpaces );
3026 $titleText = false;
3027
3028 # Original title text preserved for various purposes
3029 $originalTitle = $part1;
3030
3031 # $args is a list of argument nodes, starting from index 0, not including $part1
3032 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3033 # below won't work b/c this $args isn't an object
3034 $args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3035
3036 $profileSection = null; // profile templates
3037
3038 # SUBST
3039 if ( !$found ) {
3040 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3041
3042 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3043 # Decide whether to expand template or keep wikitext as-is.
3044 if ( $this->ot['wiki'] ) {
3045 if ( $substMatch === false ) {
3046 $literal = true; # literal when in PST with no prefix
3047 } else {
3048 $literal = false; # expand when in PST with subst: or safesubst:
3049 }
3050 } else {
3051 if ( $substMatch == 'subst' ) {
3052 $literal = true; # literal when not in PST with plain subst:
3053 } else {
3054 $literal = false; # expand when not in PST with safesubst: or no prefix
3055 }
3056 }
3057 if ( $literal ) {
3058 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3059 $isLocalObj = true;
3060 $found = true;
3061 }
3062 }
3063
3064 # Variables
3065 if ( !$found && $args->getLength() == 0 ) {
3066 $id = $this->mVariables->matchStartToEnd( $part1 );
3067 if ( $id !== false ) {
3068 $text = $this->getVariableValue( $id, $frame );
3069 if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3070 $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3071 }
3072 $found = true;
3073 }
3074 }
3075
3076 # MSG, MSGNW and RAW
3077 if ( !$found ) {
3078 # Check for MSGNW:
3079 $mwMsgnw = MagicWord::get( 'msgnw' );
3080 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3081 $nowiki = true;
3082 } else {
3083 # Remove obsolete MSG:
3084 $mwMsg = MagicWord::get( 'msg' );
3085 $mwMsg->matchStartAndRemove( $part1 );
3086 }
3087
3088 # Check for RAW:
3089 $mwRaw = MagicWord::get( 'raw' );
3090 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3091 $forceRawInterwiki = true;
3092 }
3093 }
3094
3095 # Parser functions
3096 if ( !$found ) {
3097 $colonPos = strpos( $part1, ':' );
3098 if ( $colonPos !== false ) {
3099 $func = substr( $part1, 0, $colonPos );
3100 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3101 $argsLength = $args->getLength();
3102 for ( $i = 0; $i < $argsLength; $i++ ) {
3103 $funcArgs[] = $args->item( $i );
3104 }
3105 try {
3106 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3107 } catch ( Exception $ex ) {
3108 throw $ex;
3109 }
3110
3111 # The interface for parser functions allows for extracting
3112 # flags into the local scope. Extract any forwarded flags
3113 # here.
3114 extract( $result );
3115 }
3116 }
3117
3118 # Finish mangling title and then check for loops.
3119 # Set $title to a Title object and $titleText to the PDBK
3120 if ( !$found ) {
3121 $ns = NS_TEMPLATE;
3122 # Split the title into page and subpage
3123 $subpage = '';
3124 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3125 if ( $part1 !== $relative ) {
3126 $part1 = $relative;
3127 $ns = $this->mTitle->getNamespace();
3128 }
3129 $title = Title::newFromText( $part1, $ns );
3130 if ( $title ) {
3131 $titleText = $title->getPrefixedText();
3132 # Check for language variants if the template is not found
3133 if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3134 $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3135 }
3136 # Do recursion depth check
3137 $limit = $this->mOptions->getMaxTemplateDepth();
3138 if ( $frame->depth >= $limit ) {
3139 $found = true;
3140 $text = '<span class="error">'
3141 . wfMessage( 'parser-template-recursion-depth-warning' )
3142 ->numParams( $limit )->inContentLanguage()->text()
3143 . '</span>';
3144 }
3145 }
3146 }
3147
3148 # Load from database
3149 if ( !$found && $title ) {
3150 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3151 if ( !$title->isExternal() ) {
3152 if ( $title->isSpecialPage()
3153 && $this->mOptions->getAllowSpecialInclusion()
3154 && $this->ot['html']
3155 ) {
3156 $specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3157 // Pass the template arguments as URL parameters.
3158 // "uselang" will have no effect since the Language object
3159 // is forced to the one defined in ParserOptions.
3160 $pageArgs = [];
3161 $argsLength = $args->getLength();
3162 for ( $i = 0; $i < $argsLength; $i++ ) {
3163 $bits = $args->item( $i )->splitArg();
3164 if ( strval( $bits['index'] ) === '' ) {
3165 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3166 $value = trim( $frame->expand( $bits['value'] ) );
3167 $pageArgs[$name] = $value;
3168 }
3169 }
3170
3171 // Create a new context to execute the special page
3173 $context->setTitle( $title );
3174 $context->setRequest( new FauxRequest( $pageArgs ) );
3175 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3176 $context->setUser( $this->getUser() );
3177 } else {
3178 // If this page is cached, then we better not be per user.
3179 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3180 }
3181 $context->setLanguage( $this->mOptions->getUserLangObj() );
3183 $title, $context, $this->getLinkRenderer() );
3184 if ( $ret ) {
3185 $text = $context->getOutput()->getHTML();
3186 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3187 $found = true;
3188 $isHTML = true;
3189 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3190 $this->mOutput->updateRuntimeAdaptiveExpiry(
3191 $specialPage->maxIncludeCacheTime()
3192 );
3193 }
3194 }
3195 } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3196 $found = false; # access denied
3197 wfDebug( __METHOD__ . ": template inclusion denied for " .
3198 $title->getPrefixedDBkey() . "\n" );
3199 } else {
3200 list( $text, $title ) = $this->getTemplateDom( $title );
3201 if ( $text !== false ) {
3202 $found = true;
3203 $isChildObj = true;
3204 }
3205 }
3206
3207 # If the title is valid but undisplayable, make a link to it
3208 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3209 $text = "[[:$titleText]]";
3210 $found = true;
3211 }
3212 } elseif ( $title->isTrans() ) {
3213 # Interwiki transclusion
3214 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3215 $text = $this->interwikiTransclude( $title, 'render' );
3216 $isHTML = true;
3217 } else {
3218 $text = $this->interwikiTransclude( $title, 'raw' );
3219 # Preprocess it like a template
3220 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3221 $isChildObj = true;
3222 }
3223 $found = true;
3224 }
3225
3226 # Do infinite loop check
3227 # This has to be done after redirect resolution to avoid infinite loops via redirects
3228 if ( !$frame->loopCheck( $title ) ) {
3229 $found = true;
3230 $text = '<span class="error">'
3231 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3232 . '</span>';
3233 $this->addTrackingCategory( 'template-loop-category' );
3234 $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3235 wfEscapeWikiText( $titleText ) )->text() );
3236 wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3237 }
3238 }
3239
3240 # If we haven't found text to substitute by now, we're done
3241 # Recover the source wikitext and return it
3242 if ( !$found ) {
3243 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3244 if ( $profileSection ) {
3245 $this->mProfiler->scopedProfileOut( $profileSection );
3246 }
3247 return [ 'object' => $text ];
3248 }
3249
3250 # Expand DOM-style return values in a child frame
3251 if ( $isChildObj ) {
3252 # Clean up argument array
3253 $newFrame = $frame->newChild( $args, $title );
3254
3255 if ( $nowiki ) {
3256 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3257 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3258 # Expansion is eligible for the empty-frame cache
3259 $text = $newFrame->cachedExpand( $titleText, $text );
3260 } else {
3261 # Uncached expansion
3262 $text = $newFrame->expand( $text );
3263 }
3264 }
3265 if ( $isLocalObj && $nowiki ) {
3266 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3267 $isLocalObj = false;
3268 }
3269
3270 if ( $profileSection ) {
3271 $this->mProfiler->scopedProfileOut( $profileSection );
3272 }
3273
3274 # Replace raw HTML by a placeholder
3275 if ( $isHTML ) {
3276 $text = $this->insertStripItem( $text );
3277 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3278 # Escape nowiki-style return values
3279 $text = wfEscapeWikiText( $text );
3280 } elseif ( is_string( $text )
3281 && !$piece['lineStart']
3282 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3283 ) {
3284 # T2529: if the template begins with a table or block-level
3285 # element, it should be treated as beginning a new line.
3286 # This behavior is somewhat controversial.
3287 $text = "\n" . $text;
3288 }
3289
3290 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3291 # Error, oversize inclusion
3292 if ( $titleText !== false ) {
3293 # Make a working, properly escaped link if possible (T25588)
3294 $text = "[[:$titleText]]";
3295 } else {
3296 # This will probably not be a working link, but at least it may
3297 # provide some hint of where the problem is
3298 preg_replace( '/^:/', '', $originalTitle );
3299 $text = "[[:$originalTitle]]";
3300 }
3301 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3302 . 'post-expand include size too large -->' );
3303 $this->limitationWarn( 'post-expand-template-inclusion' );
3304 }
3305
3306 if ( $isLocalObj ) {
3307 $ret = [ 'object' => $text ];
3308 } else {
3309 $ret = [ 'text' => $text ];
3310 }
3311
3312 return $ret;
3313 }
3314
3334 public function callParserFunction( $frame, $function, array $args = [] ) {
3335 global $wgContLang;
3336
3337 # Case sensitive functions
3338 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3339 $function = $this->mFunctionSynonyms[1][$function];
3340 } else {
3341 # Case insensitive functions
3342 $function = $wgContLang->lc( $function );
3343 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3344 $function = $this->mFunctionSynonyms[0][$function];
3345 } else {
3346 return [ 'found' => false ];
3347 }
3348 }
3349
3350 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3351
3352 // Avoid PHP 7.1 warning from passing $this by reference
3353 $parser = $this;
3354
3355 $allArgs = [ &$parser ];
3356 if ( $flags & self::SFH_OBJECT_ARGS ) {
3357 # Convert arguments to PPNodes and collect for appending to $allArgs
3358 $funcArgs = [];
3359 foreach ( $args as $k => $v ) {
3360 if ( $v instanceof PPNode || $k === 0 ) {
3361 $funcArgs[] = $v;
3362 } else {
3363 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3364 }
3365 }
3366
3367 # Add a frame parameter, and pass the arguments as an array
3368 $allArgs[] = $frame;
3369 $allArgs[] = $funcArgs;
3370 } else {
3371 # Convert arguments to plain text and append to $allArgs
3372 foreach ( $args as $k => $v ) {
3373 if ( $v instanceof PPNode ) {
3374 $allArgs[] = trim( $frame->expand( $v ) );
3375 } elseif ( is_int( $k ) && $k >= 0 ) {
3376 $allArgs[] = trim( $v );
3377 } else {
3378 $allArgs[] = trim( "$k=$v" );
3379 }
3380 }
3381 }
3382
3383 $result = call_user_func_array( $callback, $allArgs );
3384
3385 # The interface for function hooks allows them to return a wikitext
3386 # string or an array containing the string and any flags. This mungs
3387 # things around to match what this method should return.
3388 if ( !is_array( $result ) ) {
3389 $result = [
3390 'found' => true,
3391 'text' => $result,
3392 ];
3393 } else {
3394 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3395 $result['text'] = $result[0];
3396 }
3397 unset( $result[0] );
3398 $result += [
3399 'found' => true,
3400 ];
3401 }
3402
3403 $noparse = true;
3404 $preprocessFlags = 0;
3405 if ( isset( $result['noparse'] ) ) {
3406 $noparse = $result['noparse'];
3407 }
3408 if ( isset( $result['preprocessFlags'] ) ) {
3409 $preprocessFlags = $result['preprocessFlags'];
3410 }
3411
3412 if ( !$noparse ) {
3413 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3414 $result['isChildObj'] = true;
3415 }
3416
3417 return $result;
3418 }
3419
3428 public function getTemplateDom( $title ) {
3429 $cacheTitle = $title;
3430 $titleText = $title->getPrefixedDBkey();
3431
3432 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3433 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3434 $title = Title::makeTitle( $ns, $dbk );
3435 $titleText = $title->getPrefixedDBkey();
3436 }
3437 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3438 return [ $this->mTplDomCache[$titleText], $title ];
3439 }
3440
3441 # Cache miss, go to the database
3442 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3443
3444 if ( $text === false ) {
3445 $this->mTplDomCache[$titleText] = false;
3446 return [ false, $title ];
3447 }
3448
3449 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3450 $this->mTplDomCache[$titleText] = $dom;
3451
3452 if ( !$title->equals( $cacheTitle ) ) {
3453 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3454 [ $title->getNamespace(), $title->getDBkey() ];
3455 }
3456
3457 return [ $dom, $title ];
3458 }
3459
3471 public function fetchCurrentRevisionOfTitle( $title ) {
3472 $cacheKey = $title->getPrefixedDBkey();
3473 if ( !$this->currentRevisionCache ) {
3474 $this->currentRevisionCache = new MapCacheLRU( 100 );
3475 }
3476 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3477 $this->currentRevisionCache->set( $cacheKey,
3478 // Defaults to Parser::statelessFetchRevision()
3479 call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3480 );
3481 }
3482 return $this->currentRevisionCache->get( $cacheKey );
3483 }
3484
3494 public static function statelessFetchRevision( Title $title, $parser = false ) {
3495 $pageId = $title->getArticleID();
3496 $revId = $title->getLatestRevID();
3497
3498 $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
3499 if ( $rev ) {
3500 $rev->setTitle( $title );
3501 }
3502
3503 return $rev;
3504 }
3505
3511 public function fetchTemplateAndTitle( $title ) {
3512 // Defaults to Parser::statelessFetchTemplate()
3513 $templateCb = $this->mOptions->getTemplateCallback();
3514 $stuff = call_user_func( $templateCb, $title, $this );
3515 // We use U+007F DELETE to distinguish strip markers from regular text.
3516 $text = $stuff['text'];
3517 if ( is_string( $stuff['text'] ) ) {
3518 $text = strtr( $text, "\x7f", "?" );
3519 }
3520 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3521 if ( isset( $stuff['deps'] ) ) {
3522 foreach ( $stuff['deps'] as $dep ) {
3523 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3524 if ( $dep['title']->equals( $this->getTitle() ) ) {
3525 // If we transclude ourselves, the final result
3526 // will change based on the new version of the page
3527 $this->mOutput->setFlag( 'vary-revision' );
3528 }
3529 }
3530 }
3531 return [ $text, $finalTitle ];
3532 }
3533
3539 public function fetchTemplate( $title ) {
3540 return $this->fetchTemplateAndTitle( $title )[0];
3541 }
3542
3552 public static function statelessFetchTemplate( $title, $parser = false ) {
3553 $text = $skip = false;
3554 $finalTitle = $title;
3555 $deps = [];
3556
3557 # Loop to fetch the article, with up to 1 redirect
3558 // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3559 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3560 // @codingStandardsIgnoreEnd
3561 # Give extensions a chance to select the revision instead
3562 $id = false; # Assume current
3563 Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3564 [ $parser, $title, &$skip, &$id ] );
3565
3566 if ( $skip ) {
3567 $text = false;
3568 $deps[] = [
3569 'title' => $title,
3570 'page_id' => $title->getArticleID(),
3571 'rev_id' => null
3572 ];
3573 break;
3574 }
3575 # Get the revision
3576 if ( $id ) {
3577 $rev = Revision::newFromId( $id );
3578 } elseif ( $parser ) {
3579 $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3580 } else {
3581 $rev = Revision::newFromTitle( $title );
3582 }
3583 $rev_id = $rev ? $rev->getId() : 0;
3584 # If there is no current revision, there is no page
3585 if ( $id === false && !$rev ) {
3586 $linkCache = LinkCache::singleton();
3587 $linkCache->addBadLinkObj( $title );
3588 }
3589
3590 $deps[] = [
3591 'title' => $title,
3592 'page_id' => $title->getArticleID(),
3593 'rev_id' => $rev_id ];
3594 if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3595 # We fetched a rev from a different title; register it too...
3596 $deps[] = [
3597 'title' => $rev->getTitle(),
3598 'page_id' => $rev->getPage(),
3599 'rev_id' => $rev_id ];
3600 }
3601
3602 if ( $rev ) {
3603 $content = $rev->getContent();
3604 $text = $content ? $content->getWikitextForTransclusion() : null;
3605
3606 Hooks::run( 'ParserFetchTemplate',
3607 [ $parser, $title, $rev, &$text, &$deps ] );
3608
3609 if ( $text === false || $text === null ) {
3610 $text = false;
3611 break;
3612 }
3613 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3614 global $wgContLang;
3615 $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3616 if ( !$message->exists() ) {
3617 $text = false;
3618 break;
3619 }
3620 $content = $message->content();
3621 $text = $message->plain();
3622 } else {
3623 break;
3624 }
3625 if ( !$content ) {
3626 break;
3627 }
3628 # Redirect?
3629 $finalTitle = $title;
3630 $title = $content->getRedirectTarget();
3631 }
3632 return [
3633 'text' => $text,
3634 'finalTitle' => $finalTitle,
3635 'deps' => $deps ];
3636 }
3637
3645 public function fetchFile( $title, $options = [] ) {
3646 return $this->fetchFileAndTitle( $title, $options )[0];
3647 }
3648
3656 public function fetchFileAndTitle( $title, $options = [] ) {
3657 $file = $this->fetchFileNoRegister( $title, $options );
3658
3659 $time = $file ? $file->getTimestamp() : false;
3660 $sha1 = $file ? $file->getSha1() : false;
3661 # Register the file as a dependency...
3662 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3663 if ( $file && !$title->equals( $file->getTitle() ) ) {
3664 # Update fetched file title
3665 $title = $file->getTitle();
3666 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3667 }
3668 return [ $file, $title ];
3669 }
3670
3681 protected function fetchFileNoRegister( $title, $options = [] ) {
3682 if ( isset( $options['broken'] ) ) {
3683 $file = false; // broken thumbnail forced by hook
3684 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3685 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3686 } else { // get by (name,timestamp)
3687 $file = wfFindFile( $title, $options );
3688 }
3689 return $file;
3690 }
3691
3700 public function interwikiTransclude( $title, $action ) {
3702
3704 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3705 }
3706
3707 $url = $title->getFullURL( [ 'action' => $action ] );
3708
3709 if ( strlen( $url ) > 255 ) {
3710 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3711 }
3712 return $this->fetchScaryTemplateMaybeFromCache( $url );
3713 }
3714
3719 public function fetchScaryTemplateMaybeFromCache( $url ) {
3721 $dbr = wfGetDB( DB_REPLICA );
3722 $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3723 $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3724 [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
3725 if ( $obj ) {
3726 return $obj->tc_contents;
3727 }
3728
3729 $req = MWHttpRequest::factory( $url, [], __METHOD__ );
3730 $status = $req->execute(); // Status object
3731 if ( $status->isOK() ) {
3732 $text = $req->getContent();
3733 } elseif ( $req->getStatus() != 200 ) {
3734 // Though we failed to fetch the content, this status is useless.
3735 return wfMessage( 'scarytranscludefailed-httpstatus' )
3736 ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3737 } else {
3738 return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3739 }
3740
3741 $dbw = wfGetDB( DB_MASTER );
3742 $dbw->replace( 'transcache', [ 'tc_url' ], [
3743 'tc_url' => $url,
3744 'tc_time' => $dbw->timestamp( time() ),
3745 'tc_contents' => $text
3746 ] );
3747 return $text;
3748 }
3749
3759 public function argSubstitution( $piece, $frame ) {
3760 $error = false;
3761 $parts = $piece['parts'];
3762 $nameWithSpaces = $frame->expand( $piece['title'] );
3763 $argName = trim( $nameWithSpaces );
3764 $object = false;
3765 $text = $frame->getArgument( $argName );
3766 if ( $text === false && $parts->getLength() > 0
3767 && ( $this->ot['html']
3768 || $this->ot['pre']
3769 || ( $this->ot['wiki'] && $frame->isTemplate() )
3770 )
3771 ) {
3772 # No match in frame, use the supplied default
3773 $object = $parts->item( 0 )->getChildren();
3774 }
3775 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3776 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3777 $this->limitationWarn( 'post-expand-template-argument' );
3778 }
3779
3780 if ( $text === false && $object === false ) {
3781 # No match anywhere
3782 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3783 }
3784 if ( $error !== false ) {
3785 $text .= $error;
3786 }
3787 if ( $object !== false ) {
3788 $ret = [ 'object' => $object ];
3789 } else {
3790 $ret = [ 'text' => $text ];
3791 }
3792
3793 return $ret;
3794 }
3795
3811 public function extensionSubstitution( $params, $frame ) {
3812 static $errorStr = '<span class="error">';
3813 static $errorLen = 20;
3814
3815 $name = $frame->expand( $params['name'] );
3816 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3817 // Probably expansion depth or node count exceeded. Just punt the
3818 // error up.
3819 return $name;
3820 }
3821
3822 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3823 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3824 // See above
3825 return $attrText;
3826 }
3827
3828 // We can't safely check if the expansion for $content resulted in an
3829 // error, because the content could happen to be the error string
3830 // (T149622).
3831 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3832
3833 $marker = self::MARKER_PREFIX . "-$name-"
3834 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3835
3836 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3837 ( $this->ot['html'] || $this->ot['pre'] );
3838 if ( $isFunctionTag ) {
3839 $markerType = 'none';
3840 } else {
3841 $markerType = 'general';
3842 }
3843 if ( $this->ot['html'] || $isFunctionTag ) {
3844 $name = strtolower( $name );
3845 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3846 if ( isset( $params['attributes'] ) ) {
3847 $attributes = $attributes + $params['attributes'];
3848 }
3849
3850 if ( isset( $this->mTagHooks[$name] ) ) {
3851 $output = call_user_func_array( $this->mTagHooks[$name],
3852 [ $content, $attributes, $this, $frame ] );
3853 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3854 list( $callback, ) = $this->mFunctionTagHooks[$name];
3855
3856 // Avoid PHP 7.1 warning from passing $this by reference
3857 $parser = $this;
3858 $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
3859 } else {
3860 $output = '<span class="error">Invalid tag extension name: ' .
3861 htmlspecialchars( $name ) . '</span>';
3862 }
3863
3864 if ( is_array( $output ) ) {
3865 # Extract flags to local scope (to override $markerType)
3866 $flags = $output;
3867 $output = $flags[0];
3868 unset( $flags[0] );
3869 extract( $flags );
3870 }
3871 } else {
3872 if ( is_null( $attrText ) ) {
3873 $attrText = '';
3874 }
3875 if ( isset( $params['attributes'] ) ) {
3876 foreach ( $params['attributes'] as $attrName => $attrValue ) {
3877 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3878 htmlspecialchars( $attrValue ) . '"';
3879 }
3880 }
3881 if ( $content === null ) {
3882 $output = "<$name$attrText/>";
3883 } else {
3884 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3885 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3886 // See above
3887 return $close;
3888 }
3889 $output = "<$name$attrText>$content$close";
3890 }
3891 }
3892
3893 if ( $markerType === 'none' ) {
3894 return $output;
3895 } elseif ( $markerType === 'nowiki' ) {
3896 $this->mStripState->addNoWiki( $marker, $output );
3897 } elseif ( $markerType === 'general' ) {
3898 $this->mStripState->addGeneral( $marker, $output );
3899 } else {
3900 throw new MWException( __METHOD__ . ': invalid marker type' );
3901 }
3902 return $marker;
3903 }
3904
3912 public function incrementIncludeSize( $type, $size ) {
3913 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3914 return false;
3915 } else {
3916 $this->mIncludeSizes[$type] += $size;
3917 return true;
3918 }
3919 }
3920
3927 $this->mExpensiveFunctionCount++;
3928 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3929 }
3930
3939 public function doDoubleUnderscore( $text ) {
3940 # The position of __TOC__ needs to be recorded
3941 $mw = MagicWord::get( 'toc' );
3942 if ( $mw->match( $text ) ) {
3943 $this->mShowToc = true;
3944 $this->mForceTocPosition = true;
3945
3946 # Set a placeholder. At the end we'll fill it in with the TOC.
3947 $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
3948
3949 # Only keep the first one.
3950 $text = $mw->replace( '', $text );
3951 }
3952
3953 # Now match and remove the rest of them
3955 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3956
3957 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3958 $this->mOutput->mNoGallery = true;
3959 }
3960 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3961 $this->mShowToc = false;
3962 }
3963 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
3964 && $this->mTitle->getNamespace() == NS_CATEGORY
3965 ) {
3966 $this->addTrackingCategory( 'hidden-category-category' );
3967 }
3968 # (T10068) Allow control over whether robots index a page.
3969 # __INDEX__ always overrides __NOINDEX__, see T16899
3970 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
3971 $this->mOutput->setIndexPolicy( 'noindex' );
3972 $this->addTrackingCategory( 'noindex-category' );
3973 }
3974 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
3975 $this->mOutput->setIndexPolicy( 'index' );
3976 $this->addTrackingCategory( 'index-category' );
3977 }
3978
3979 # Cache all double underscores in the database
3980 foreach ( $this->mDoubleUnderscores as $key => $val ) {
3981 $this->mOutput->setProperty( $key, '' );
3982 }
3983
3984 return $text;
3985 }
3986
3992 public function addTrackingCategory( $msg ) {
3993 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
3994 }
3995
4012 public function formatHeadings( $text, $origText, $isMain = true ) {
4013 global $wgMaxTocLevel;
4014
4015 # Inhibit editsection links if requested in the page
4016 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4017 $maybeShowEditLink = $showEditLink = false;
4018 } else {
4019 $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4020 $showEditLink = $this->mOptions->getEditSection();
4021 }
4022 if ( $showEditLink ) {
4023 $this->mOutput->setEditSectionTokens( true );
4024 }
4025
4026 # Get all headlines for numbering them and adding funky stuff like [edit]
4027 # links - this is for later, but we need the number of headlines right now
4028 $matches = [];
4029 $numMatches = preg_match_all(
4030 '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4031 $text,
4032 $matches
4033 );
4034
4035 # if there are fewer than 4 headlines in the article, do not show TOC
4036 # unless it's been explicitly enabled.
4037 $enoughToc = $this->mShowToc &&
4038 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4039
4040 # Allow user to stipulate that a page should have a "new section"
4041 # link added via __NEWSECTIONLINK__
4042 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4043 $this->mOutput->setNewSection( true );
4044 }
4045
4046 # Allow user to remove the "new section"
4047 # link via __NONEWSECTIONLINK__
4048 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4049 $this->mOutput->hideNewSection( true );
4050 }
4051
4052 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4053 # override above conditions and always show TOC above first header
4054 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4055 $this->mShowToc = true;
4056 $enoughToc = true;
4057 }
4058
4059 # headline counter
4060 $headlineCount = 0;
4061 $numVisible = 0;
4062
4063 # Ugh .. the TOC should have neat indentation levels which can be
4064 # passed to the skin functions. These are determined here
4065 $toc = '';
4066 $full = '';
4067 $head = [];
4068 $sublevelCount = [];
4069 $levelCount = [];
4070 $level = 0;
4071 $prevlevel = 0;
4072 $toclevel = 0;
4073 $prevtoclevel = 0;
4074 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4075 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4076 $oldType = $this->mOutputType;
4077 $this->setOutputType( self::OT_WIKI );
4078 $frame = $this->getPreprocessor()->newFrame();
4079 $root = $this->preprocessToDom( $origText );
4080 $node = $root->getFirstChild();
4081 $byteOffset = 0;
4082 $tocraw = [];
4083 $refers = [];
4084
4085 $headlines = $numMatches !== false ? $matches[3] : [];
4086
4087 foreach ( $headlines as $headline ) {
4088 $isTemplate = false;
4089 $titleText = false;
4090 $sectionIndex = false;
4091 $numbering = '';
4092 $markerMatches = [];
4093 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4094 $serial = $markerMatches[1];
4095 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4096 $isTemplate = ( $titleText != $baseTitleText );
4097 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4098 }
4099
4100 if ( $toclevel ) {
4101 $prevlevel = $level;
4102 }
4103 $level = $matches[1][$headlineCount];
4104
4105 if ( $level > $prevlevel ) {
4106 # Increase TOC level
4107 $toclevel++;
4108 $sublevelCount[$toclevel] = 0;
4109 if ( $toclevel < $wgMaxTocLevel ) {
4110 $prevtoclevel = $toclevel;
4111 $toc .= Linker::tocIndent();
4112 $numVisible++;
4113 }
4114 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4115 # Decrease TOC level, find level to jump to
4116
4117 for ( $i = $toclevel; $i > 0; $i-- ) {
4118 if ( $levelCount[$i] == $level ) {
4119 # Found last matching level
4120 $toclevel = $i;
4121 break;
4122 } elseif ( $levelCount[$i] < $level ) {
4123 # Found first matching level below current level
4124 $toclevel = $i + 1;
4125 break;
4126 }
4127 }
4128 if ( $i == 0 ) {
4129 $toclevel = 1;
4130 }
4131 if ( $toclevel < $wgMaxTocLevel ) {
4132 if ( $prevtoclevel < $wgMaxTocLevel ) {
4133 # Unindent only if the previous toc level was shown :p
4134 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4135 $prevtoclevel = $toclevel;
4136 } else {
4137 $toc .= Linker::tocLineEnd();
4138 }
4139 }
4140 } else {
4141 # No change in level, end TOC line
4142 if ( $toclevel < $wgMaxTocLevel ) {
4143 $toc .= Linker::tocLineEnd();
4144 }
4145 }
4146
4147 $levelCount[$toclevel] = $level;
4148
4149 # count number of headlines for each level
4150 $sublevelCount[$toclevel]++;
4151 $dot = 0;
4152 for ( $i = 1; $i <= $toclevel; $i++ ) {
4153 if ( !empty( $sublevelCount[$i] ) ) {
4154 if ( $dot ) {
4155 $numbering .= '.';
4156 }
4157 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4158 $dot = 1;
4159 }
4160 }
4161
4162 # The safe header is a version of the header text safe to use for links
4163
4164 # Remove link placeholders by the link text.
4165 # <!--LINK number-->
4166 # turns into
4167 # link text with suffix
4168 # Do this before unstrip since link text can contain strip markers
4169 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4170
4171 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4172 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4173
4174 # Strip out HTML (first regex removes any tag not allowed)
4175 # Allowed tags are:
4176 # * <sup> and <sub> (T10393)
4177 # * <i> (T28375)
4178 # * <b> (r105284)
4179 # * <bdi> (T74884)
4180 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4181 # * <s> and <strike> (T35715)
4182 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4183 # to allow setting directionality in toc items.
4184 $tocline = preg_replace(
4185 [
4186 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4187 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4188 ],
4189 [ '', '<$1>' ],
4190 $safeHeadline
4191 );
4192
4193 # Strip '<span></span>', which is the result from the above if
4194 # <span id="foo"></span> is used to produce an additional anchor
4195 # for a section.
4196 $tocline = str_replace( '<span></span>', '', $tocline );
4197
4198 $tocline = trim( $tocline );
4199
4200 # For the anchor, strip out HTML-y stuff period
4201 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4202 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4203
4204 # Save headline for section edit hint before it's escaped
4205 $headlineHint = $safeHeadline;
4206
4207 # Decode HTML entities
4208 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4209 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4210 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4211 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4212 if ( $fallbackHeadline === $safeHeadline ) {
4213 # No reason to have both (in fact, we can't)
4214 $fallbackHeadline = false;
4215 }
4216
4217 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4218 # @todo FIXME: We may be changing them depending on the current locale.
4219 $arrayKey = strtolower( $safeHeadline );
4220 if ( $fallbackHeadline === false ) {
4221 $fallbackArrayKey = false;
4222 } else {
4223 $fallbackArrayKey = strtolower( $fallbackHeadline );
4224 }
4225
4226 # Create the anchor for linking from the TOC to the section
4227 $anchor = $safeHeadline;
4228 $fallbackAnchor = $fallbackHeadline;
4229 if ( isset( $refers[$arrayKey] ) ) {
4230 // @codingStandardsIgnoreStart
4231 for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4232 // @codingStandardsIgnoreEnd
4233 $anchor .= "_$i";
4234 $linkAnchor .= "_$i";
4235 $refers["${arrayKey}_$i"] = true;
4236 } else {
4237 $refers[$arrayKey] = true;
4238 }
4239 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4240 // @codingStandardsIgnoreStart
4241 for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4242 // @codingStandardsIgnoreEnd
4243 $fallbackAnchor .= "_$i";
4244 $refers["${fallbackArrayKey}_$i"] = true;
4245 } else {
4246 $refers[$fallbackArrayKey] = true;
4247 }
4248
4249 # Don't number the heading if it is the only one (looks silly)
4250 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4251 # the two are different if the line contains a link
4252 $headline = Html::element(
4253 'span',
4254 [ 'class' => 'mw-headline-number' ],
4255 $numbering
4256 ) . ' ' . $headline;
4257 }
4258
4259 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4260 $toc .= Linker::tocLine( $linkAnchor, $tocline,
4261 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4262 }
4263
4264 # Add the section to the section tree
4265 # Find the DOM node for this header
4266 $noOffset = ( $isTemplate || $sectionIndex === false );
4267 while ( $node && !$noOffset ) {
4268 if ( $node->getName() === 'h' ) {
4269 $bits = $node->splitHeading();
4270 if ( $bits['i'] == $sectionIndex ) {
4271 break;
4272 }
4273 }
4274 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4275 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4276 $node = $node->getNextSibling();
4277 }
4278 $tocraw[] = [
4279 'toclevel' => $toclevel,
4280 'level' => $level,
4281 'line' => $tocline,
4282 'number' => $numbering,
4283 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4284 'fromtitle' => $titleText,
4285 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4286 'anchor' => $anchor,
4287 ];
4288
4289 # give headline the correct <h#> tag
4290 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4291 // Output edit section links as markers with styles that can be customized by skins
4292 if ( $isTemplate ) {
4293 # Put a T flag in the section identifier, to indicate to extractSections()
4294 # that sections inside <includeonly> should be counted.
4295 $editsectionPage = $titleText;
4296 $editsectionSection = "T-$sectionIndex";
4297 $editsectionContent = null;
4298 } else {
4299 $editsectionPage = $this->mTitle->getPrefixedText();
4300 $editsectionSection = $sectionIndex;
4301 $editsectionContent = $headlineHint;
4302 }
4303 // We use a bit of pesudo-xml for editsection markers. The
4304 // language converter is run later on. Using a UNIQ style marker
4305 // leads to the converter screwing up the tokens when it
4306 // converts stuff. And trying to insert strip tags fails too. At
4307 // this point all real inputted tags have already been escaped,
4308 // so we don't have to worry about a user trying to input one of
4309 // these markers directly. We use a page and section attribute
4310 // to stop the language converter from converting these
4311 // important bits of data, but put the headline hint inside a
4312 // content block because the language converter is supposed to
4313 // be able to convert that piece of data.
4314 // Gets replaced with html in ParserOutput::getText
4315 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4316 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4317 if ( $editsectionContent !== null ) {
4318 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4319 } else {
4320 $editlink .= '/>';
4321 }
4322 } else {
4323 $editlink = '';
4324 }
4325 $head[$headlineCount] = Linker::makeHeadline( $level,
4326 $matches['attrib'][$headlineCount], $anchor, $headline,
4327 $editlink, $fallbackAnchor );
4328
4329 $headlineCount++;
4330 }
4331
4332 $this->setOutputType( $oldType );
4333
4334 # Never ever show TOC if no headers
4335 if ( $numVisible < 1 ) {
4336 $enoughToc = false;
4337 }
4338
4339 if ( $enoughToc ) {
4340 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4341 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4342 }
4343 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4344 $this->mOutput->setTOCHTML( $toc );
4345 $toc = self::TOC_START . $toc . self::TOC_END;
4346 }
4347
4348 if ( $isMain ) {
4349 $this->mOutput->setSections( $tocraw );
4350 }
4351
4352 # split up and insert constructed headlines
4353 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4354 $i = 0;
4355
4356 // build an array of document sections
4357 $sections = [];
4358 foreach ( $blocks as $block ) {
4359 // $head is zero-based, sections aren't.
4360 if ( empty( $head[$i - 1] ) ) {
4361 $sections[$i] = $block;
4362 } else {
4363 $sections[$i] = $head[$i - 1] . $block;
4364 }
4365
4376 Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4377
4378 $i++;
4379 }
4380
4381 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4382 // append the TOC at the beginning
4383 // Top anchor now in skin
4384 $sections[0] = $sections[0] . $toc . "\n";
4385 }
4386
4387 $full .= implode( '', $sections );
4388
4389 if ( $this->mForceTocPosition ) {
4390 return str_replace( '<!--MWTOC-->', $toc, $full );
4391 } else {
4392 return $full;
4393 }
4394 }
4395
4407 public function preSaveTransform( $text, Title $title, User $user,
4408 ParserOptions $options, $clearState = true
4409 ) {
4410 if ( $clearState ) {
4411 $magicScopeVariable = $this->lock();
4412 }
4413 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4414 $this->setUser( $user );
4415
4416 // Strip U+0000 NULL (T159174)
4417 $text = str_replace( "\000", '', $text );
4418
4419 // We still normalize line endings for backwards-compatibility
4420 // with other code that just calls PST, but this should already
4421 // be handled in TextContent subclasses
4422 $text = TextContent::normalizeLineEndings( $text );
4423
4424 if ( $options->getPreSaveTransform() ) {
4425 $text = $this->pstPass2( $text, $user );
4426 }
4427 $text = $this->mStripState->unstripBoth( $text );
4428
4429 $this->setUser( null ); # Reset
4430
4431 return $text;
4432 }
4433
4442 private function pstPass2( $text, $user ) {
4443 global $wgContLang;
4444
4445 # Note: This is the timestamp saved as hardcoded wikitext to
4446 # the database, we use $wgContLang here in order to give
4447 # everyone the same signature and use the default one rather
4448 # than the one selected in each user's preferences.
4449 # (see also T14815)
4450 $ts = $this->mOptions->getTimestamp();
4451 $timestamp = MWTimestamp::getLocalInstance( $ts );
4452 $ts = $timestamp->format( 'YmdHis' );
4453 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4454
4455 $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4456
4457 # Variable replacement
4458 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4459 $text = $this->replaceVariables( $text );
4460
4461 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4462 # which may corrupt this parser instance via its wfMessage()->text() call-
4463
4464 # Signatures
4465 if ( strpos( $text, '~~~' ) !== false ) {
4466 $sigText = $this->getUserSig( $user );
4467 $text = strtr( $text, [
4468 '~~~~~' => $d,
4469 '~~~~' => "$sigText $d",
4470 '~~~' => $sigText
4471 ] );
4472 # The main two signature forms used above are time-sensitive
4473 $this->mOutput->setFlag( 'user-signature' );
4474 }
4475
4476 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4477 $tc = '[' . Title::legalChars() . ']';
4478 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4479
4480 // [[ns:page (context)|]]
4481 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4482 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4483 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4484 // [[ns:page (context), context|]] (using either single or double-width comma)
4485 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4486 // [[|page]] (reverse pipe trick: add context from page title)
4487 $p2 = "/\[\[\\|($tc+)]]/";
4488
4489 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4490 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4491 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4492 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4493
4494 $t = $this->mTitle->getText();
4495 $m = [];
4496 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4497 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4498 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4499 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4500 } else {
4501 # if there's no context, don't bother duplicating the title
4502 $text = preg_replace( $p2, '[[\\1]]', $text );
4503 }
4504
4505 return $text;
4506 }
4507
4522 public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4523 global $wgMaxSigChars;
4524
4525 $username = $user->getName();
4526
4527 # If not given, retrieve from the user object.
4528 if ( $nickname === false ) {
4529 $nickname = $user->getOption( 'nickname' );
4530 }
4531
4532 if ( is_null( $fancySig ) ) {
4533 $fancySig = $user->getBoolOption( 'fancysig' );
4534 }
4535
4536 $nickname = $nickname == null ? $username : $nickname;
4537
4538 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4539 $nickname = $username;
4540 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4541 } elseif ( $fancySig !== false ) {
4542 # Sig. might contain markup; validate this
4543 if ( $this->validateSig( $nickname ) !== false ) {
4544 # Validated; clean up (if needed) and return it
4545 return $this->cleanSig( $nickname, true );
4546 } else {
4547 # Failed to validate; fall back to the default
4548 $nickname = $username;
4549 wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4550 }
4551 }
4552
4553 # Make sure nickname doesnt get a sig in a sig
4554 $nickname = self::cleanSigInSig( $nickname );
4555
4556 # If we're still here, make it a link to the user page
4557 $userText = wfEscapeWikiText( $username );
4558 $nickText = wfEscapeWikiText( $nickname );
4559 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4560
4561 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4562 ->title( $this->getTitle() )->text();
4563 }
4564
4571 public function validateSig( $text ) {
4572 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4573 }
4574
4585 public function cleanSig( $text, $parsing = false ) {
4586 if ( !$parsing ) {
4587 global $wgTitle;
4588 $magicScopeVariable = $this->lock();
4589 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4590 }
4591
4592 # Option to disable this feature
4593 if ( !$this->mOptions->getCleanSignatures() ) {
4594 return $text;
4595 }
4596
4597 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4598 # => Move this logic to braceSubstitution()
4599 $substWord = MagicWord::get( 'subst' );
4600 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4601 $substText = '{{' . $substWord->getSynonym( 0 );
4602
4603 $text = preg_replace( $substRegex, $substText, $text );
4604 $text = self::cleanSigInSig( $text );
4605 $dom = $this->preprocessToDom( $text );
4606 $frame = $this->getPreprocessor()->newFrame();
4607 $text = $frame->expand( $dom );
4608
4609 if ( !$parsing ) {
4610 $text = $this->mStripState->unstripBoth( $text );
4611 }
4612
4613 return $text;
4614 }
4615
4622 public static function cleanSigInSig( $text ) {
4623 $text = preg_replace( '/~{3,5}/', '', $text );
4624 return $text;
4625 }
4626
4636 public function startExternalParse( Title $title = null, ParserOptions $options,
4637 $outputType, $clearState = true
4638 ) {
4639 $this->startParse( $title, $options, $outputType, $clearState );
4640 }
4641
4648 private function startParse( Title $title = null, ParserOptions $options,
4649 $outputType, $clearState = true
4650 ) {
4651 $this->setTitle( $title );
4652 $this->mOptions = $options;
4653 $this->setOutputType( $outputType );
4654 if ( $clearState ) {
4655 $this->clearState();
4656 }
4657 }
4658
4667 public function transformMsg( $text, $options, $title = null ) {
4668 static $executing = false;
4669
4670 # Guard against infinite recursion
4671 if ( $executing ) {
4672 return $text;
4673 }
4674 $executing = true;
4675
4676 if ( !$title ) {
4677 global $wgTitle;
4678 $title = $wgTitle;
4679 }
4680
4681 $text = $this->preprocess( $text, $title, $options );
4682
4683 $executing = false;
4684 return $text;
4685 }
4686
4711 public function setHook( $tag, callable $callback ) {
4712 $tag = strtolower( $tag );
4713 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4714 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4715 }
4716 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4717 $this->mTagHooks[$tag] = $callback;
4718 if ( !in_array( $tag, $this->mStripList ) ) {
4719 $this->mStripList[] = $tag;
4720 }
4721
4722 return $oldVal;
4723 }
4724
4742 public function setTransparentTagHook( $tag, callable $callback ) {
4743 $tag = strtolower( $tag );
4744 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4745 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4746 }
4747 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4748 $this->mTransparentTagHooks[$tag] = $callback;
4749
4750 return $oldVal;
4751 }
4752
4756 public function clearTagHooks() {
4757 $this->mTagHooks = [];
4758 $this->mFunctionTagHooks = [];
4759 $this->mStripList = $this->mDefaultStripList;
4760 }
4761
4805 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4806 global $wgContLang;
4807
4808 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4809 $this->mFunctionHooks[$id] = [ $callback, $flags ];
4810
4811 # Add to function cache
4812 $mw = MagicWord::get( $id );
4813 if ( !$mw ) {
4814 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4815 }
4816
4817 $synonyms = $mw->getSynonyms();
4818 $sensitive = intval( $mw->isCaseSensitive() );
4819
4820 foreach ( $synonyms as $syn ) {
4821 # Case
4822 if ( !$sensitive ) {
4823 $syn = $wgContLang->lc( $syn );
4824 }
4825 # Add leading hash
4826 if ( !( $flags & self::SFH_NO_HASH ) ) {
4827 $syn = '#' . $syn;
4828 }
4829 # Remove trailing colon
4830 if ( substr( $syn, -1, 1 ) === ':' ) {
4831 $syn = substr( $syn, 0, -1 );
4832 }
4833 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4834 }
4835 return $oldVal;
4836 }
4837
4843 public function getFunctionHooks() {
4844 return array_keys( $this->mFunctionHooks );
4845 }
4846
4857 public function setFunctionTagHook( $tag, callable $callback, $flags ) {
4858 $tag = strtolower( $tag );
4859 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4860 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4861 }
4862 $old = isset( $this->mFunctionTagHooks[$tag] ) ?
4863 $this->mFunctionTagHooks[$tag] : null;
4864 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4865
4866 if ( !in_array( $tag, $this->mStripList ) ) {
4867 $this->mStripList[] = $tag;
4868 }
4869
4870 return $old;
4871 }
4872
4880 public function replaceLinkHolders( &$text, $options = 0 ) {
4881 $this->mLinkHolders->replace( $text );
4882 }
4883
4891 public function replaceLinkHoldersText( $text ) {
4892 return $this->mLinkHolders->replaceText( $text );
4893 }
4894
4908 public function renderImageGallery( $text, $params ) {
4909 $mode = false;
4910 if ( isset( $params['mode'] ) ) {
4911 $mode = $params['mode'];
4912 }
4913
4914 try {
4915 $ig = ImageGalleryBase::factory( $mode );
4916 } catch ( Exception $e ) {
4917 // If invalid type set, fallback to default.
4918 $ig = ImageGalleryBase::factory( false );
4919 }
4920
4921 $ig->setContextTitle( $this->mTitle );
4922 $ig->setShowBytes( false );
4923 $ig->setShowDimensions( false );
4924 $ig->setShowFilename( false );
4925 $ig->setParser( $this );
4926 $ig->setHideBadImages();
4927 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
4928
4929 if ( isset( $params['showfilename'] ) ) {
4930 $ig->setShowFilename( true );
4931 } else {
4932 $ig->setShowFilename( false );
4933 }
4934 if ( isset( $params['caption'] ) ) {
4935 $caption = $params['caption'];
4936 $caption = htmlspecialchars( $caption );
4937 $caption = $this->replaceInternalLinks( $caption );
4938 $ig->setCaptionHtml( $caption );
4939 }
4940 if ( isset( $params['perrow'] ) ) {
4941 $ig->setPerRow( $params['perrow'] );
4942 }
4943 if ( isset( $params['widths'] ) ) {
4944 $ig->setWidths( $params['widths'] );
4945 }
4946 if ( isset( $params['heights'] ) ) {
4947 $ig->setHeights( $params['heights'] );
4948 }
4949 $ig->setAdditionalOptions( $params );
4950
4951 // Avoid PHP 7.1 warning from passing $this by reference
4952 $parser = $this;
4953 Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
4954
4955 $lines = StringUtils::explode( "\n", $text );
4956 foreach ( $lines as $line ) {
4957 # match lines like these:
4958 # Image:someimage.jpg|This is some image
4959 $matches = [];
4960 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4961 # Skip empty lines
4962 if ( count( $matches ) == 0 ) {
4963 continue;
4964 }
4965
4966 if ( strpos( $matches[0], '%' ) !== false ) {
4967 $matches[1] = rawurldecode( $matches[1] );
4968 }
4969 $title = Title::newFromText( $matches[1], NS_FILE );
4970 if ( is_null( $title ) ) {
4971 # Bogus title. Ignore these so we don't bomb out later.
4972 continue;
4973 }
4974
4975 # We need to get what handler the file uses, to figure out parameters.
4976 # Note, a hook can overide the file name, and chose an entirely different
4977 # file (which potentially could be of a different type and have different handler).
4978 $options = [];
4979 $descQuery = false;
4980 Hooks::run( 'BeforeParserFetchFileAndTitle',
4981 [ $this, $title, &$options, &$descQuery ] );
4982 # Don't register it now, as TraditionalImageGallery does that later.
4983 $file = $this->fetchFileNoRegister( $title, $options );
4984 $handler = $file ? $file->getHandler() : false;
4985
4986 $paramMap = [
4987 'img_alt' => 'gallery-internal-alt',
4988 'img_link' => 'gallery-internal-link',
4989 ];
4990 if ( $handler ) {
4991 $paramMap = $paramMap + $handler->getParamMap();
4992 // We don't want people to specify per-image widths.
4993 // Additionally the width parameter would need special casing anyhow.
4994 unset( $paramMap['img_width'] );
4995 }
4996
4997 $mwArray = new MagicWordArray( array_keys( $paramMap ) );
4998
4999 $label = '';
5000 $alt = '';
5001 $link = '';
5002 $handlerOptions = [];
5003 if ( isset( $matches[3] ) ) {
5004 // look for an |alt= definition while trying not to break existing
5005 // captions with multiple pipes (|) in it, until a more sensible grammar
5006 // is defined for images in galleries
5007
5008 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5009 // splitting on '|' is a bit odd, and different from makeImage.
5010 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5011 // Protect LanguageConverter markup
5012 $parameterMatches = StringUtils::delimiterExplode(
5013 '-{', '}-', '|', $matches[3], true /* nested */
5014 );
5015
5016 foreach ( $parameterMatches as $parameterMatch ) {
5017 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5018 if ( $magicName ) {
5019 $paramName = $paramMap[$magicName];
5020
5021 switch ( $paramName ) {
5022 case 'gallery-internal-alt':
5023 $alt = $this->stripAltText( $match, false );
5024 break;
5025 case 'gallery-internal-link':
5026 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5027 $chars = self::EXT_LINK_URL_CLASS;
5028 $addr = self::EXT_LINK_ADDR;
5029 $prots = $this->mUrlProtocols;
5030 // check to see if link matches an absolute url, if not then it must be a wiki link.
5031 if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5032 // Result of LanguageConverter::markNoConversion
5033 // invoked on an external link.
5034 $linkValue = substr( $linkValue, 4, -2 );
5035 }
5036 if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
5037 $link = $linkValue;
5038 $this->mOutput->addExternalLink( $link );
5039 } else {
5040 $localLinkTitle = Title::newFromText( $linkValue );
5041 if ( $localLinkTitle !== null ) {
5042 $this->mOutput->addLink( $localLinkTitle );
5043 $link = $localLinkTitle->getLinkURL();
5044 }
5045 }
5046 break;
5047 default:
5048 // Must be a handler specific parameter.
5049 if ( $handler->validateParam( $paramName, $match ) ) {
5050 $handlerOptions[$paramName] = $match;
5051 } else {
5052 // Guess not, consider it as caption.
5053 wfDebug( "$parameterMatch failed parameter validation\n" );
5054 $label = '|' . $parameterMatch;
5055 }
5056 }
5057
5058 } else {
5059 // Last pipe wins.
5060 $label = '|' . $parameterMatch;
5061 }
5062 }
5063 // Remove the pipe.
5064 $label = substr( $label, 1 );
5065 }
5066
5067 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5068 }
5069 $html = $ig->toHTML();
5070 Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5071 return $html;
5072 }
5073
5078 public function getImageParams( $handler ) {
5079 if ( $handler ) {
5080 $handlerClass = get_class( $handler );
5081 } else {
5082 $handlerClass = '';
5083 }
5084 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5085 # Initialise static lists
5086 static $internalParamNames = [
5087 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5088 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5089 'bottom', 'text-bottom' ],
5090 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5091 'upright', 'border', 'link', 'alt', 'class' ],
5092 ];
5093 static $internalParamMap;
5094 if ( !$internalParamMap ) {
5095 $internalParamMap = [];
5096 foreach ( $internalParamNames as $type => $names ) {
5097 foreach ( $names as $name ) {
5098 // For grep: img_left, img_right, img_center, img_none,
5099 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5100 // img_bottom, img_text_bottom,
5101 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5102 // img_border, img_link, img_alt, img_class
5103 $magicName = str_replace( '-', '_', "img_$name" );
5104 $internalParamMap[$magicName] = [ $type, $name ];
5105 }
5106 }
5107 }
5108
5109 # Add handler params
5110 $paramMap = $internalParamMap;
5111 if ( $handler ) {
5112 $handlerParamMap = $handler->getParamMap();
5113 foreach ( $handlerParamMap as $magic => $paramName ) {
5114 $paramMap[$magic] = [ 'handler', $paramName ];
5115 }
5116 }
5117 $this->mImageParams[$handlerClass] = $paramMap;
5118 $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5119 }
5120 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5121 }
5122
5131 public function makeImage( $title, $options, $holders = false ) {
5132 # Check if the options text is of the form "options|alt text"
5133 # Options are:
5134 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5135 # * left no resizing, just left align. label is used for alt= only
5136 # * right same, but right aligned
5137 # * none same, but not aligned
5138 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5139 # * center center the image
5140 # * frame Keep original image size, no magnify-button.
5141 # * framed Same as "frame"
5142 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5143 # * upright reduce width for upright images, rounded to full __0 px
5144 # * border draw a 1px border around the image
5145 # * alt Text for HTML alt attribute (defaults to empty)
5146 # * class Set a class for img node
5147 # * link Set the target of the image link. Can be external, interwiki, or local
5148 # vertical-align values (no % or length right now):
5149 # * baseline
5150 # * sub
5151 # * super
5152 # * top
5153 # * text-top
5154 # * middle
5155 # * bottom
5156 # * text-bottom
5157
5158 # Protect LanguageConverter markup when splitting into parts
5160 '-{', '}-', '|', $options, true /* allow nesting */
5161 );
5162
5163 # Give extensions a chance to select the file revision for us
5164 $options = [];
5165 $descQuery = false;
5166 Hooks::run( 'BeforeParserFetchFileAndTitle',
5167 [ $this, $title, &$options, &$descQuery ] );
5168 # Fetch and register the file (file title may be different via hooks)
5169 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5170
5171 # Get parameter map
5172 $handler = $file ? $file->getHandler() : false;
5173
5174 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5175
5176 if ( !$file ) {
5177 $this->addTrackingCategory( 'broken-file-category' );
5178 }
5179
5180 # Process the input parameters
5181 $caption = '';
5182 $params = [ 'frame' => [], 'handler' => [],
5183 'horizAlign' => [], 'vertAlign' => [] ];
5184 $seenformat = false;
5185 foreach ( $parts as $part ) {
5186 $part = trim( $part );
5187 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5188 $validated = false;
5189 if ( isset( $paramMap[$magicName] ) ) {
5190 list( $type, $paramName ) = $paramMap[$magicName];
5191
5192 # Special case; width and height come in one variable together
5193 if ( $type === 'handler' && $paramName === 'width' ) {
5194 $parsedWidthParam = $this->parseWidthParam( $value );
5195 if ( isset( $parsedWidthParam['width'] ) ) {
5196 $width = $parsedWidthParam['width'];
5197 if ( $handler->validateParam( 'width', $width ) ) {
5198 $params[$type]['width'] = $width;
5199 $validated = true;
5200 }
5201 }
5202 if ( isset( $parsedWidthParam['height'] ) ) {
5203 $height = $parsedWidthParam['height'];
5204 if ( $handler->validateParam( 'height', $height ) ) {
5205 $params[$type]['height'] = $height;
5206 $validated = true;
5207 }
5208 }
5209 # else no validation -- T15436
5210 } else {
5211 if ( $type === 'handler' ) {
5212 # Validate handler parameter
5213 $validated = $handler->validateParam( $paramName, $value );
5214 } else {
5215 # Validate internal parameters
5216 switch ( $paramName ) {
5217 case 'manualthumb':
5218 case 'alt':
5219 case 'class':
5220 # @todo FIXME: Possibly check validity here for
5221 # manualthumb? downstream behavior seems odd with
5222 # missing manual thumbs.
5223 $validated = true;
5224 $value = $this->stripAltText( $value, $holders );
5225 break;
5226 case 'link':
5227 $chars = self::EXT_LINK_URL_CLASS;
5228 $addr = self::EXT_LINK_ADDR;
5229 $prots = $this->mUrlProtocols;
5230 if ( $value === '' ) {
5231 $paramName = 'no-link';
5232 $value = true;
5233 $validated = true;
5234 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5235 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5236 $paramName = 'link-url';
5237 $this->mOutput->addExternalLink( $value );
5238 if ( $this->mOptions->getExternalLinkTarget() ) {
5239 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5240 }
5241 $validated = true;
5242 }
5243 } else {
5244 $linkTitle = Title::newFromText( $value );
5245 if ( $linkTitle ) {
5246 $paramName = 'link-title';
5247 $value = $linkTitle;
5248 $this->mOutput->addLink( $linkTitle );
5249 $validated = true;
5250 }
5251 }
5252 break;
5253 case 'frameless':
5254 case 'framed':
5255 case 'thumbnail':
5256 // use first appearing option, discard others.
5257 $validated = !$seenformat;
5258 $seenformat = true;
5259 break;
5260 default:
5261 # Most other things appear to be empty or numeric...
5262 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5263 }
5264 }
5265
5266 if ( $validated ) {
5267 $params[$type][$paramName] = $value;
5268 }
5269 }
5270 }
5271 if ( !$validated ) {
5272 $caption = $part;
5273 }
5274 }
5275
5276 # Process alignment parameters
5277 if ( $params['horizAlign'] ) {
5278 $params['frame']['align'] = key( $params['horizAlign'] );
5279 }
5280 if ( $params['vertAlign'] ) {
5281 $params['frame']['valign'] = key( $params['vertAlign'] );
5282 }
5283
5284 $params['frame']['caption'] = $caption;
5285
5286 # Will the image be presented in a frame, with the caption below?
5287 $imageIsFramed = isset( $params['frame']['frame'] )
5288 || isset( $params['frame']['framed'] )
5289 || isset( $params['frame']['thumbnail'] )
5290 || isset( $params['frame']['manualthumb'] );
5291
5292 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5293 # came to also set the caption, ordinary text after the image -- which
5294 # makes no sense, because that just repeats the text multiple times in
5295 # screen readers. It *also* came to set the title attribute.
5296 # Now that we have an alt attribute, we should not set the alt text to
5297 # equal the caption: that's worse than useless, it just repeats the
5298 # text. This is the framed/thumbnail case. If there's no caption, we
5299 # use the unnamed parameter for alt text as well, just for the time be-
5300 # ing, if the unnamed param is set and the alt param is not.
5301 # For the future, we need to figure out if we want to tweak this more,
5302 # e.g., introducing a title= parameter for the title; ignoring the un-
5303 # named parameter entirely for images without a caption; adding an ex-
5304 # plicit caption= parameter and preserving the old magic unnamed para-
5305 # meter for BC; ...
5306 if ( $imageIsFramed ) { # Framed image
5307 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5308 # No caption or alt text, add the filename as the alt text so
5309 # that screen readers at least get some description of the image
5310 $params['frame']['alt'] = $title->getText();
5311 }
5312 # Do not set $params['frame']['title'] because tooltips don't make sense
5313 # for framed images
5314 } else { # Inline image
5315 if ( !isset( $params['frame']['alt'] ) ) {
5316 # No alt text, use the "caption" for the alt text
5317 if ( $caption !== '' ) {
5318 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5319 } else {
5320 # No caption, fall back to using the filename for the
5321 # alt text
5322 $params['frame']['alt'] = $title->getText();
5323 }
5324 }
5325 # Use the "caption" for the tooltip text
5326 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5327 }
5328
5329 Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5330
5331 # Linker does the rest
5332 $time = isset( $options['time'] ) ? $options['time'] : false;
5333 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5334 $time, $descQuery, $this->mOptions->getThumbSize() );
5335
5336 # Give the handler a chance to modify the parser object
5337 if ( $handler ) {
5338 $handler->parserTransformHook( $this, $file );
5339 }
5340
5341 return $ret;
5342 }
5343
5349 protected function stripAltText( $caption, $holders ) {
5350 # Strip bad stuff out of the title (tooltip). We can't just use
5351 # replaceLinkHoldersText() here, because if this function is called
5352 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5353 if ( $holders ) {
5354 $tooltip = $holders->replaceText( $caption );
5355 } else {
5356 $tooltip = $this->replaceLinkHoldersText( $caption );
5357 }
5358
5359 # make sure there are no placeholders in thumbnail attributes
5360 # that are later expanded to html- so expand them now and
5361 # remove the tags
5362 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5363 $tooltip = Sanitizer::stripAllTags( $tooltip );
5364
5365 return $tooltip;
5366 }
5367
5373 public function disableCache() {
5374 wfDebug( "Parser output marked as uncacheable.\n" );
5375 if ( !$this->mOutput ) {
5376 throw new MWException( __METHOD__ .
5377 " can only be called when actually parsing something" );
5378 }
5379 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5380 }
5381
5390 public function attributeStripCallback( &$text, $frame = false ) {
5391 $text = $this->replaceVariables( $text, $frame );
5392 $text = $this->mStripState->unstripBoth( $text );
5393 return $text;
5394 }
5395
5401 public function getTags() {
5402 return array_merge(
5403 array_keys( $this->mTransparentTagHooks ),
5404 array_keys( $this->mTagHooks ),
5405 array_keys( $this->mFunctionTagHooks )
5406 );
5407 }
5408
5419 public function replaceTransparentTags( $text ) {
5420 $matches = [];
5421 $elements = array_keys( $this->mTransparentTagHooks );
5422 $text = self::extractTagsAndParams( $elements, $text, $matches );
5423 $replacements = [];
5424
5425 foreach ( $matches as $marker => $data ) {
5426 list( $element, $content, $params, $tag ) = $data;
5427 $tagName = strtolower( $element );
5428 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5429 $output = call_user_func_array(
5430 $this->mTransparentTagHooks[$tagName],
5431 [ $content, $params, $this ]
5432 );
5433 } else {
5434 $output = $tag;
5435 }
5436 $replacements[$marker] = $output;
5437 }
5438 return strtr( $text, $replacements );
5439 }
5440
5470 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5471 global $wgTitle; # not generally used but removes an ugly failure mode
5472
5473 $magicScopeVariable = $this->lock();
5474 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5475 $outText = '';
5476 $frame = $this->getPreprocessor()->newFrame();
5477
5478 # Process section extraction flags
5479 $flags = 0;
5480 $sectionParts = explode( '-', $sectionId );
5481 $sectionIndex = array_pop( $sectionParts );
5482 foreach ( $sectionParts as $part ) {
5483 if ( $part === 'T' ) {
5484 $flags |= self::PTD_FOR_INCLUSION;
5485 }
5486 }
5487
5488 # Check for empty input
5489 if ( strval( $text ) === '' ) {
5490 # Only sections 0 and T-0 exist in an empty document
5491 if ( $sectionIndex == 0 ) {
5492 if ( $mode === 'get' ) {
5493 return '';
5494 } else {
5495 return $newText;
5496 }
5497 } else {
5498 if ( $mode === 'get' ) {
5499 return $newText;
5500 } else {
5501 return $text;
5502 }
5503 }
5504 }
5505
5506 # Preprocess the text
5507 $root = $this->preprocessToDom( $text, $flags );
5508
5509 # <h> nodes indicate section breaks
5510 # They can only occur at the top level, so we can find them by iterating the root's children
5511 $node = $root->getFirstChild();
5512
5513 # Find the target section
5514 if ( $sectionIndex == 0 ) {
5515 # Section zero doesn't nest, level=big
5516 $targetLevel = 1000;
5517 } else {
5518 while ( $node ) {
5519 if ( $node->getName() === 'h' ) {
5520 $bits = $node->splitHeading();
5521 if ( $bits['i'] == $sectionIndex ) {
5522 $targetLevel = $bits['level'];
5523 break;
5524 }
5525 }
5526 if ( $mode === 'replace' ) {
5527 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5528 }
5529 $node = $node->getNextSibling();
5530 }
5531 }
5532
5533 if ( !$node ) {
5534 # Not found
5535 if ( $mode === 'get' ) {
5536 return $newText;
5537 } else {
5538 return $text;
5539 }
5540 }
5541
5542 # Find the end of the section, including nested sections
5543 do {
5544 if ( $node->getName() === 'h' ) {
5545 $bits = $node->splitHeading();
5546 $curLevel = $bits['level'];
5547 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5548 break;
5549 }
5550 }
5551 if ( $mode === 'get' ) {
5552 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5553 }
5554 $node = $node->getNextSibling();
5555 } while ( $node );
5556
5557 # Write out the remainder (in replace mode only)
5558 if ( $mode === 'replace' ) {
5559 # Output the replacement text
5560 # Add two newlines on -- trailing whitespace in $newText is conventionally
5561 # stripped by the editor, so we need both newlines to restore the paragraph gap
5562 # Only add trailing whitespace if there is newText
5563 if ( $newText != "" ) {
5564 $outText .= $newText . "\n\n";
5565 }
5566
5567 while ( $node ) {
5568 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5569 $node = $node->getNextSibling();
5570 }
5571 }
5572
5573 if ( is_string( $outText ) ) {
5574 # Re-insert stripped tags
5575 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5576 }
5577
5578 return $outText;
5579 }
5580
5595 public function getSection( $text, $sectionId, $defaultText = '' ) {
5596 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5597 }
5598
5611 public function replaceSection( $oldText, $sectionId, $newText ) {
5612 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5613 }
5614
5620 public function getRevisionId() {
5621 return $this->mRevisionId;
5622 }
5623
5630 public function getRevisionObject() {
5631 if ( !is_null( $this->mRevisionObject ) ) {
5632 return $this->mRevisionObject;
5633 }
5634 if ( is_null( $this->mRevisionId ) ) {
5635 return null;
5636 }
5637
5638 $rev = call_user_func(
5639 $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5640 );
5641
5642 # If the parse is for a new revision, then the callback should have
5643 # already been set to force the object and should match mRevisionId.
5644 # If not, try to fetch by mRevisionId for sanity.
5645 if ( $rev && $rev->getId() != $this->mRevisionId ) {
5646 $rev = Revision::newFromId( $this->mRevisionId );
5647 }
5648
5649 $this->mRevisionObject = $rev;
5650
5651 return $this->mRevisionObject;
5652 }
5653
5659 public function getRevisionTimestamp() {
5660 if ( is_null( $this->mRevisionTimestamp ) ) {
5661 global $wgContLang;
5662
5663 $revObject = $this->getRevisionObject();
5664 $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5665
5666 # The cryptic '' timezone parameter tells to use the site-default
5667 # timezone offset instead of the user settings.
5668 # Since this value will be saved into the parser cache, served
5669 # to other users, and potentially even used inside links and such,
5670 # it needs to be consistent for all visitors.
5671 $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5672
5673 }
5674 return $this->mRevisionTimestamp;
5675 }
5676
5682 public function getRevisionUser() {
5683 if ( is_null( $this->mRevisionUser ) ) {
5684 $revObject = $this->getRevisionObject();
5685
5686 # if this template is subst: the revision id will be blank,
5687 # so just use the current user's name
5688 if ( $revObject ) {
5689 $this->mRevisionUser = $revObject->getUserText();
5690 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5691 $this->mRevisionUser = $this->getUser()->getName();
5692 }
5693 }
5694 return $this->mRevisionUser;
5695 }
5696
5702 public function getRevisionSize() {
5703 if ( is_null( $this->mRevisionSize ) ) {
5704 $revObject = $this->getRevisionObject();
5705
5706 # if this variable is subst: the revision id will be blank,
5707 # so just use the parser input size, because the own substituation
5708 # will change the size.
5709 if ( $revObject ) {
5710 $this->mRevisionSize = $revObject->getSize();
5711 } else {
5712 $this->mRevisionSize = $this->mInputSize;
5713 }
5714 }
5715 return $this->mRevisionSize;
5716 }
5717
5723 public function setDefaultSort( $sort ) {
5724 $this->mDefaultSort = $sort;
5725 $this->mOutput->setProperty( 'defaultsort', $sort );
5726 }
5727
5738 public function getDefaultSort() {
5739 if ( $this->mDefaultSort !== false ) {
5740 return $this->mDefaultSort;
5741 } else {
5742 return '';
5743 }
5744 }
5745
5752 public function getCustomDefaultSort() {
5753 return $this->mDefaultSort;
5754 }
5755
5765 public function guessSectionNameFromWikiText( $text ) {
5766 # Strip out wikitext links(they break the anchor)
5767 $text = $this->stripSectionName( $text );
5768 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5769 $text = Sanitizer::decodeCharReferences( $text );
5770 return '#' . Sanitizer::escapeIdForLink( $text );
5771 }
5772
5782 public function guessLegacySectionNameFromWikiText( $text ) {
5783 global $wgFragmentMode;
5784
5785 # Strip out wikitext links(they break the anchor)
5786 $text = $this->stripSectionName( $text );
5787 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5788 $text = Sanitizer::decodeCharReferences( $text );
5789
5790 if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
5791 // ForAttribute() and ForLink() are the same for legacy encoding
5792 $id = Sanitizer::escapeIdForAttribute( $text, Sanitizer::ID_FALLBACK );
5793 } else {
5794 $id = Sanitizer::escapeIdForLink( $text );
5795 }
5796
5797 return "#$id";
5798 }
5799
5814 public function stripSectionName( $text ) {
5815 # Strip internal link markup
5816 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5817 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5818
5819 # Strip external link markup
5820 # @todo FIXME: Not tolerant to blank link text
5821 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
5822 # on how many empty links there are on the page - need to figure that out.
5823 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5824
5825 # Parse wikitext quotes (italics & bold)
5826 $text = $this->doQuotes( $text );
5827
5828 # Strip HTML tags
5829 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
5830 return $text;
5831 }
5832
5843 public function testSrvus( $text, Title $title, ParserOptions $options,
5844 $outputType = self::OT_HTML
5845 ) {
5846 $magicScopeVariable = $this->lock();
5847 $this->startParse( $title, $options, $outputType, true );
5848
5849 $text = $this->replaceVariables( $text );
5850 $text = $this->mStripState->unstripBoth( $text );
5851 $text = Sanitizer::removeHTMLtags( $text );
5852 return $text;
5853 }
5854
5861 public function testPst( $text, Title $title, ParserOptions $options ) {
5862 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5863 }
5864
5871 public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5872 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5873 }
5874
5891 public function markerSkipCallback( $s, $callback ) {
5892 $i = 0;
5893 $out = '';
5894 while ( $i < strlen( $s ) ) {
5895 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5896 if ( $markerStart === false ) {
5897 $out .= call_user_func( $callback, substr( $s, $i ) );
5898 break;
5899 } else {
5900 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5901 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5902 if ( $markerEnd === false ) {
5903 $out .= substr( $s, $markerStart );
5904 break;
5905 } else {
5906 $markerEnd += strlen( self::MARKER_SUFFIX );
5907 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5908 $i = $markerEnd;
5909 }
5910 }
5911 }
5912 return $out;
5913 }
5914
5921 public function killMarkers( $text ) {
5922 return $this->mStripState->killMarkers( $text );
5923 }
5924
5941 public function serializeHalfParsedText( $text ) {
5942 $data = [
5943 'text' => $text,
5944 'version' => self::HALF_PARSED_VERSION,
5945 'stripState' => $this->mStripState->getSubState( $text ),
5946 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
5947 ];
5948 return $data;
5949 }
5950
5966 public function unserializeHalfParsedText( $data ) {
5967 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
5968 throw new MWException( __METHOD__ . ': invalid version' );
5969 }
5970
5971 # First, extract the strip state.
5972 $texts = [ $data['text'] ];
5973 $texts = $this->mStripState->merge( $data['stripState'], $texts );
5974
5975 # Now renumber links
5976 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
5977
5978 # Should be good to go.
5979 return $texts[0];
5980 }
5981
5991 public function isValidHalfParsedText( $data ) {
5992 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
5993 }
5994
6003 public function parseWidthParam( $value ) {
6004 $parsedWidthParam = [];
6005 if ( $value === '' ) {
6006 return $parsedWidthParam;
6007 }
6008 $m = [];
6009 # (T15500) In both cases (width/height and width only),
6010 # permit trailing "px" for backward compatibility.
6011 if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6012 $width = intval( $m[1] );
6013 $height = intval( $m[2] );
6014 $parsedWidthParam['width'] = $width;
6015 $parsedWidthParam['height'] = $height;
6016 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6017 $width = intval( $value );
6018 $parsedWidthParam['width'] = $width;
6019 }
6020 return $parsedWidthParam;
6021 }
6022
6032 protected function lock() {
6033 if ( $this->mInParse ) {
6034 throw new MWException( "Parser state cleared while parsing. "
6035 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6036 }
6037
6038 // Save the backtrace when locking, so that if some code tries locking again,
6039 // we can print the lock owner's backtrace for easier debugging
6040 $e = new Exception;
6041 $this->mInParse = $e->getTraceAsString();
6042
6043 $recursiveCheck = new ScopedCallback( function () {
6044 $this->mInParse = false;
6045 } );
6046
6047 return $recursiveCheck;
6048 }
6049
6060 public static function stripOuterParagraph( $html ) {
6061 $m = [];
6062 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6063 if ( strpos( $m[1], '</p>' ) === false ) {
6064 $html = $m[1];
6065 }
6066 }
6067
6068 return $html;
6069 }
6070
6081 public function getFreshParser() {
6082 global $wgParserConf;
6083 if ( $this->mInParse ) {
6084 return new $wgParserConf['class']( $wgParserConf );
6085 } else {
6086 return $this;
6087 }
6088 }
6089
6096 public function enableOOUI() {
6098 $this->mOutput->setEnableOOUI( true );
6099 }
6100}
If you want to remove the page from your watchlist later
we sometimes make exceptions for this Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally NO WARRANTY BECAUSE THE PROGRAM IS LICENSED FREE OF THERE IS NO WARRANTY FOR THE TO THE EXTENT PERMITTED BY APPLICABLE LAW EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND OR OTHER PARTIES PROVIDE THE PROGRAM AS IS WITHOUT WARRANTY OF ANY EITHER EXPRESSED OR BUT NOT LIMITED THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU SHOULD THE PROGRAM PROVE YOU ASSUME THE COST OF ALL NECESSARY REPAIR OR CORRECTION IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT OR ANY OTHER PARTY WHO MAY MODIFY AND OR REDISTRIBUTE THE PROGRAM AS PERMITTED BE LIABLE TO YOU FOR INCLUDING ANY INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new and you want it to be of the greatest possible use to the public
Definition COPYING.txt:285
$wgLanguageCode
Site language code.
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgServerName
Server name.
$wgExtraInterlanguageLinkPrefixes
List of additional interwiki prefixes that should be treated as interlanguage links (i....
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
$wgSitename
Name of the site.
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
$wgScriptPath
The path we should point to.
$wgFragmentMode
How should section IDs be encoded? This array can contain 1 or 2 elements, each of them can be one of...
$wgTranscludeCacheExpiry
Expiry time for transcluded templates cached in 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,...
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,...
if(! $wgRequest->checkUrlExtension()) if(isset($_SERVER[ 'PATH_INFO']) &&$_SERVER[ 'PATH_INFO'] !='') if(! $wgEnableAPI) $wgTitle
Definition api.php:68
$line
Definition cdb.php:58
if( $line===false) $args
Definition cdb.php:63
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:203
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:1571
static makeMediaLinkFile(Title $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition Linker.php:783
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition Linker.php:1535
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:324
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition Linker.php:271
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition Linker.php:1359
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition Linker.php:186
static tocIndent()
Add another level to the Table of Contents.
Definition Linker.php:1509
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:1650
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:843
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition Linker.php:1627
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1520
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1559
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='default', $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:3992
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:871
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:5659
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:1990
bool string $mInParse
Recursive call protection.
Definition Parser.php:251
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3811
const TOC_END
Definition Parser.php:140
$mDefaultStripList
Definition Parser.php:149
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:5723
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6060
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:1954
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:724
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition Parser.php:2431
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:302
ParserOutput $mOutput
Definition Parser.php:178
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2013
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4622
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3681
$mFirstCall
Definition Parser.php:154
LinkRenderer $mLinkRenderer
Definition Parser.php:259
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:679
$mHighestExpansionDepth
Definition Parser.php:193
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:4908
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:5752
stripAltText( $caption, $holders)
Definition Parser.php:5349
getOptions()
Get the ParserOptions object.
Definition Parser.php:826
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4585
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:4407
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:4648
fetchScaryTemplateMaybeFromCache( $url)
Definition Parser.php:3719
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:5682
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3494
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:2904
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:2398
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:2422
$mHeadings
Definition Parser.php:195
Title $mTitle
Definition Parser.php:215
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2085
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:4857
const PTD_FOR_INCLUSION
Definition Parser.php:108
$mTplDomCache
Definition Parser.php:195
$mGeneratedPPNodeCount
Definition Parser.php:193
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1429
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
Definition Parser.php:5966
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:2981
LinkHolderArray $mLinkHolders
Definition Parser.php:190
$mFunctionTagHooks
Definition Parser.php:147
$mRevisionId
Definition Parser.php:219
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5131
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
Definition Parser.php:5373
pstPass2( $text, $user)
Pre-save transform helper function.
Definition Parser.php:4442
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Definition Parser.php:4667
getRevisionSize()
Get the size of the revision.
Definition Parser.php:5702
const TOC_START
Definition Parser.php:139
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition Parser.php:5470
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:5814
$mDefaultSort
Definition Parser.php:194
$mTagHooks
Definition Parser.php:143
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition Parser.php:1532
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:705
$mFunctionHooks
Definition Parser.php:145
$mShowToc
Definition Parser.php:197
getImageParams( $handler)
Definition Parser.php:5078
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition Parser.php:5941
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:4012
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1325
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:4805
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3428
$mRevisionSize
Definition Parser.php:222
doTableStuff( $text)
parse the wiki syntax used to render tables
Definition Parser.php:1050
getTitle()
Accessor for the Title object.
Definition Parser.php:772
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:1903
ParserOptions $mOptions
Definition Parser.php:210
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition Parser.php:5843
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:76
MagicWordArray $mSubstWords
Definition Parser.php:166
$mUrlProtocols
Definition Parser.php:168
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
Definition Parser.php:5419
MapCacheLRU null $currentRevisionCache
Definition Parser.php:245
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:925
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition Parser.php:2871
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:1924
setOutputType( $ot)
Set the output type.
Definition Parser.php:791
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:858
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2471
StripState $mStripState
Definition Parser.php:184
$mInputSize
Definition Parser.php:224
MagicWordArray $mVariables
Definition Parser.php:161
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1023
$mOutputType
Definition Parser.php:216
$mImageParamsMagicArray
Definition Parser.php:152
setUser( $user)
Set the current user.
Definition Parser.php:745
$mAutonumber
Definition Parser.php:179
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:5891
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4711
getTags()
Accessor.
Definition Parser.php:5401
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:3939
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3759
replaceInternalLinks( $s)
Process [[ ]] wikilinks.
Definition Parser.php:2072
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:237
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:636
$mRevisionUser
Definition Parser.php:221
string $mUniqPrefix
Deprecated accessor for the strip marker prefix.
Definition Parser.php:230
$mTplRedirCache
Definition Parser.php:195
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3656
$mFunctionSynonyms
Definition Parser.php:146
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5595
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:220
static extractTagsAndParams( $elements, $text, &$matches)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition Parser.php:956
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5611
interwikiTransclude( $title, $action)
Transclude an interwiki link.
Definition Parser.php:3700
parseWidthParam( $value)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6003
setTitle( $t)
Set the context title.
Definition Parser.php:754
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:3912
User $mUser
Definition Parser.php:202
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
Definition Parser.php:2444
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:5630
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2456
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3511
$mPPNodeCount
Definition Parser.php:193
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:5738
const MARKER_PREFIX
Definition Parser.php:136
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:4891
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:4843
doAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1623
$mMarkerIndex
Definition Parser.php:153
const EXT_LINK_ADDR
Definition Parser.php:97
$mExpensiveFunctionCount
Definition Parser.php:196
Preprocessor $mPreprocessor
Definition Parser.php:172
$mDoubleUnderscores
Definition Parser.php:195
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Definition Parser.php:3471
getOutput()
Get the ParserOutput object.
Definition Parser.php:817
$mVarCache
Definition Parser.php:150
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:836
$mImageParams
Definition Parser.php:151
Title( $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:782
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:662
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:5765
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4756
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:4880
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:5620
$mIncludeCount
Definition Parser.php:186
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4571
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:911
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:5782
const EXT_LINK_URL_CLASS
Definition Parser.php:94
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:808
magicLinkCallback( $m)
Definition Parser.php:1460
$mIncludeSizes
Definition Parser.php:193
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:290
fetchFile( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3645
setLinkID( $id)
Definition Parser.php:850
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:899
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:3539
doHeadings( $text)
Parse headers and return html.
Definition Parser.php:1607
$mExtLinkBracketedRegex
Definition Parser.php:168
const EXT_IMAGE_REGEX
Definition Parser.php:100
$mRevIdForTs
Definition Parser.php:223
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
__construct( $conf=[])
Definition Parser.php:264
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
Definition Parser.php:4742
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:3926
testPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:5871
const SPACE_NOT_NL
Definition Parser.php:105
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:325
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:2859
$mForceTocPosition
Definition Parser.php:197
clearState()
Clear Parser state.
Definition Parser.php:345
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:5921
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6096
nextLinkID()
Definition Parser.php:843
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:4522
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3003
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5390
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
Definition Parser.php:2829
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
Definition Parser.php:5991
lock()
Lock the current instance of the parser.
Definition Parser.php:6032
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition Parser.php:2933
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:405
$mStripList
Definition Parser.php:148
testPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:5861
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:4636
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6081
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3334
SectionProfiler $mProfiler
Definition Parser.php:254
$mTransparentTagHooks
Definition Parser.php:144
getConverterLanguage()
Get the language object for language conversion.
Definition Parser.php:889
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3552
Variant of the Message class.
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:59
Group all the pieces relevant to the context of a request into one instance.
static newKnownCurrent(IDatabase $db, $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:134
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:116
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 delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
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:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
if(! $regexes) $dbr
Definition cleanup.php:94
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition deferred.txt:5
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global then executing the whole list after the page is displayed We don t do anything smart like collating updates to the same table or such because the list is almost always going to have just one item on if so it s not worth the trouble Since there is a job queue in the jobs table
Definition deferred.txt:16
I won t presume to tell you how to I m just describing the methods I chose to use for myself If you do choose to follow these it will probably be easier for you to collaborate with others on the but if you want to contribute without by all means do which work well I also use K &R brace matching style I know that s a religious issue for so if you want to use a style that puts opening braces on the next line
Definition design.txt:80
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling output() to send it all. It could be easily changed to send incrementally if that becomes useful
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition design.txt:26
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of normal(non-web) applications -- they might conflict with distributors' policies
this hook is for auditing only $req
Definition hooks.txt:988
namespace being checked & $result
Definition hooks.txt:2293
do that in ParserLimitReportFormat instead $parser
Definition hooks.txt:2572
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1778
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2225
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc next in line in page history
Definition hooks.txt:1760
either a plain
Definition hooks.txt:2026
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1971
null means default in associative array form
Definition hooks.txt:1966
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:962
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:1976
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped broken
Definition hooks.txt:1971
if the prop value should be in the metadata multi language array format
Definition hooks.txt:1646
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:1975
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:862
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition hooks.txt:1983
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:2989
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:783
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:1984
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1049
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:901
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1760
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2146
!hooks source !endhooks !test Non existent language !input< source lang="doesnotexist"> foobar</source > ! result< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > foobar</pre ></div > !end !test No language specified ! wikitext< source > foo</source > ! html< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > foo</pre ></div > !end !test No language specified(no wellformed xml) !! config !! wikitext< source > bar</source > !! html< div class
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php For a description of the see design txt $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition globals.txt:64
const OT_WIKI
Definition Defines.php:186
const SFH_NO_HASH
Definition Defines.php:198
const SFH_OBJECT_ARGS
Definition Defines.php:199
const NS_FILE
Definition Defines.php:71
const NS_TEMPLATE
Definition Defines.php:75
const NS_SPECIAL
Definition Defines.php:54
const OT_PLAIN
Definition Defines.php:189
const OT_PREPROCESS
Definition Defines.php:187
const OT_HTML
Definition Defines.php:185
const NS_MEDIA
Definition Defines.php:53
const NS_CATEGORY
Definition Defines.php:79</