MediaWiki REL1_31
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 // phpcs:ignore 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
103 # Regular expression for a non-newline space
104 const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
105
106 # Flags for preprocessToDom
108
109 # Allowed values for $this->mOutputType
110 # Parameter to startExternalParse().
111 const OT_HTML = 1; # like parse()
112 const OT_WIKI = 2; # like preSaveTransform()
113 const OT_PREPROCESS = 3; # like preprocess()
114 const OT_MSG = 3;
115 const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
116
134 const MARKER_SUFFIX = "-QINU`\"'\x7f";
135 const MARKER_PREFIX = "\x7f'\"`UNIQ-";
136
137 # Markers used for wrapping the table of contents
138 const TOC_START = '<mw:toc>';
139 const TOC_END = '</mw:toc>';
140
141 # Persistent:
142 public $mTagHooks = [];
144 public $mFunctionHooks = [];
145 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
147 public $mStripList = [];
149 public $mVarCache = [];
150 public $mImageParams = [];
152 public $mMarkerIndex = 0;
153 public $mFirstCall = true;
154
155 # Initialised by initialiseVariables()
156
161
166 # Initialised in constructor
167 public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
168
169 # Initialized in getPreprocessor()
172
173 # Cleared with clearState():
177 public $mOutput;
179
184
190
191 public $mLinkID;
192 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
194 public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
195 public $mExpensiveFunctionCount; # number of expensive parser function calls
197
201 public $mUser; # User object; only used when doing pre-save transform
202
203 # Temporary
204 # These are variables reset at least once per parse regardless of $clearState
205
209 public $mOptions;
210
214 public $mTitle; # Title context, used for self-link rendering and similar things
215 public $mOutputType; # Output type, one of the OT_xxx constants
216 public $ot; # Shortcut alias, see setOutputType()
217 public $mRevisionObject; # The revision object of the specified revision ID
218 public $mRevisionId; # ID to display in {{REVISIONID}} tags
219 public $mRevisionTimestamp; # The timestamp of the specified revision ID
220 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
221 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
222 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
223 public $mInputSize = false; # For {{PAGESIZE}} on current page.
224
229 public $mUniqPrefix = self::MARKER_PREFIX;
230
237
245
250 public $mInParse = false;
251
253 protected $mProfiler;
254
258 protected $mLinkRenderer;
259
263 public function __construct( $conf = [] ) {
264 $this->mConf = $conf;
265 $this->mUrlProtocols = wfUrlProtocols();
266 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
267 self::EXT_LINK_ADDR .
268 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
269 if ( isset( $conf['preprocessorClass'] ) ) {
270 $this->mPreprocessorClass = $conf['preprocessorClass'];
271 } elseif ( defined( 'HPHP_VERSION' ) ) {
272 # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
273 $this->mPreprocessorClass = Preprocessor_Hash::class;
274 } elseif ( extension_loaded( 'domxml' ) ) {
275 # PECL extension that conflicts with the core DOM extension (T15770)
276 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
277 $this->mPreprocessorClass = Preprocessor_Hash::class;
278 } elseif ( extension_loaded( 'dom' ) ) {
279 $this->mPreprocessorClass = Preprocessor_DOM::class;
280 } else {
281 $this->mPreprocessorClass = Preprocessor_Hash::class;
282 }
283 wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
284 }
285
289 public function __destruct() {
290 if ( isset( $this->mLinkHolders ) ) {
291 unset( $this->mLinkHolders );
292 }
293 foreach ( $this as $name => $value ) {
294 unset( $this->$name );
295 }
296 }
297
301 public function __clone() {
302 $this->mInParse = false;
303
304 // T58226: When you create a reference "to" an object field, that
305 // makes the object field itself be a reference too (until the other
306 // reference goes out of scope). When cloning, any field that's a
307 // reference is copied as a reference in the new object. Both of these
308 // are defined PHP5 behaviors, as inconvenient as it is for us when old
309 // hooks from PHP4 days are passing fields by reference.
310 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
311 // Make a non-reference copy of the field, then rebind the field to
312 // reference the new copy.
313 $tmp = $this->$k;
314 $this->$k =& $tmp;
315 unset( $tmp );
316 }
317
318 Hooks::run( 'ParserCloned', [ $this ] );
319 }
320
324 public function firstCallInit() {
325 if ( !$this->mFirstCall ) {
326 return;
327 }
328 $this->mFirstCall = false;
329
331 CoreTagHooks::register( $this );
332 $this->initialiseVariables();
333
334 // Avoid PHP 7.1 warning from passing $this by reference
335 $parser = $this;
336 Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
337 }
338
344 public function clearState() {
345 if ( $this->mFirstCall ) {
346 $this->firstCallInit();
347 }
348 $this->mOutput = new ParserOutput;
349 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
350 $this->mAutonumber = 0;
351 $this->mIncludeCount = [];
352 $this->mLinkHolders = new LinkHolderArray( $this );
353 $this->mLinkID = 0;
354 $this->mRevisionObject = $this->mRevisionTimestamp =
355 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
356 $this->mVarCache = [];
357 $this->mUser = null;
358 $this->mLangLinkLanguages = [];
359 $this->currentRevisionCache = null;
360
361 $this->mStripState = new StripState( $this );
362
363 # Clear these on every parse, T6549
364 $this->mTplRedirCache = $this->mTplDomCache = [];
365
366 $this->mShowToc = true;
367 $this->mForceTocPosition = false;
368 $this->mIncludeSizes = [
369 'post-expand' => 0,
370 'arg' => 0,
371 ];
372 $this->mPPNodeCount = 0;
373 $this->mGeneratedPPNodeCount = 0;
374 $this->mHighestExpansionDepth = 0;
375 $this->mDefaultSort = false;
376 $this->mHeadings = [];
377 $this->mDoubleUnderscores = [];
378 $this->mExpensiveFunctionCount = 0;
379
380 # Fix cloning
381 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
382 $this->mPreprocessor = null;
383 }
384
385 $this->mProfiler = new SectionProfiler();
386
387 // Avoid PHP 7.1 warning from passing $this by reference
388 $parser = $this;
389 Hooks::run( 'ParserClearState', [ &$parser ] );
390 }
391
404 public function parse(
405 $text, Title $title, ParserOptions $options,
406 $linestart = true, $clearState = true, $revid = null
407 ) {
408 if ( $clearState ) {
409 // We use U+007F DELETE to construct strip markers, so we have to make
410 // sure that this character does not occur in the input text.
411 $text = strtr( $text, "\x7f", "?" );
412 $magicScopeVariable = $this->lock();
413 }
414 // Strip U+0000 NULL (T159174)
415 $text = str_replace( "\000", '', $text );
416
417 $this->startParse( $title, $options, self::OT_HTML, $clearState );
418
419 $this->currentRevisionCache = null;
420 $this->mInputSize = strlen( $text );
421 if ( $this->mOptions->getEnableLimitReport() ) {
422 $this->mOutput->resetParseStartTime();
423 }
424
425 $oldRevisionId = $this->mRevisionId;
426 $oldRevisionObject = $this->mRevisionObject;
427 $oldRevisionTimestamp = $this->mRevisionTimestamp;
428 $oldRevisionUser = $this->mRevisionUser;
429 $oldRevisionSize = $this->mRevisionSize;
430 if ( $revid !== null ) {
431 $this->mRevisionId = $revid;
432 $this->mRevisionObject = null;
433 $this->mRevisionTimestamp = null;
434 $this->mRevisionUser = null;
435 $this->mRevisionSize = null;
436 }
437
438 // Avoid PHP 7.1 warning from passing $this by reference
439 $parser = $this;
440 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
441 # No more strip!
442 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
443 $text = $this->internalParse( $text );
444 Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
445
446 $text = $this->internalParseHalfParsed( $text, true, $linestart );
447
455 if ( !( $options->getDisableTitleConversion()
456 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
457 || isset( $this->mDoubleUnderscores['notitleconvert'] )
458 || $this->mOutput->getDisplayTitle() !== false )
459 ) {
460 $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
461 if ( $convruletitle ) {
462 $this->mOutput->setTitleText( $convruletitle );
463 } else {
464 $titleText = $this->getConverterLanguage()->convertTitle( $title );
465 $this->mOutput->setTitleText( $titleText );
466 }
467 }
468
469 # Compute runtime adaptive expiry if set
470 $this->mOutput->finalizeAdaptiveCacheExpiry();
471
472 # Warn if too many heavyweight parser functions were used
473 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
474 $this->limitationWarn( 'expensive-parserfunction',
475 $this->mExpensiveFunctionCount,
476 $this->mOptions->getExpensiveParserFunctionLimit()
477 );
478 }
479
480 # Information on limits, for the benefit of users who try to skirt them
481 if ( $this->mOptions->getEnableLimitReport() ) {
482 $text .= $this->makeLimitReport();
483 }
484
485 # Wrap non-interface parser output in a <div> so it can be targeted
486 # with CSS (T37247)
487 $class = $this->mOptions->getWrapOutputClass();
488 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
489 $text = Html::rawElement( 'div', [ 'class' => $class ], $text );
490 }
491
492 $this->mOutput->setText( $text );
493
494 $this->mRevisionId = $oldRevisionId;
495 $this->mRevisionObject = $oldRevisionObject;
496 $this->mRevisionTimestamp = $oldRevisionTimestamp;
497 $this->mRevisionUser = $oldRevisionUser;
498 $this->mRevisionSize = $oldRevisionSize;
499 $this->mInputSize = false;
500 $this->currentRevisionCache = null;
501
502 return $this->mOutput;
503 }
504
511 protected function makeLimitReport() {
512 global $wgShowHostnames;
513
514 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
515
516 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
517 if ( $cpuTime !== null ) {
518 $this->mOutput->setLimitReportData( 'limitreport-cputime',
519 sprintf( "%.3f", $cpuTime )
520 );
521 }
522
523 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
524 $this->mOutput->setLimitReportData( 'limitreport-walltime',
525 sprintf( "%.3f", $wallTime )
526 );
527
528 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
529 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
530 );
531 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
532 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
533 );
534 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
535 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
536 );
537 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
538 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
539 );
540 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
541 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
542 );
543 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
544 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
545 );
546
547 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
548 $this->mOutput->setLimitReportData( $key, $value );
549 }
550
551 Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
552
553 $limitReport = "NewPP limit report\n";
554 if ( $wgShowHostnames ) {
555 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
556 }
557 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
558 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
559 $limitReport .= 'Dynamic content: ' .
560 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
561 "\n";
562
563 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
564 if ( Hooks::run( 'ParserLimitReportFormat',
565 [ $key, &$value, &$limitReport, false, false ]
566 ) ) {
567 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
568 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
569 ->inLanguage( 'en' )->useDatabase( false );
570 if ( !$valueMsg->exists() ) {
571 $valueMsg = new RawMessage( '$1' );
572 }
573 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
574 $valueMsg->params( $value );
575 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
576 }
577 }
578 }
579 // Since we're not really outputting HTML, decode the entities and
580 // then re-encode the things that need hiding inside HTML comments.
581 $limitReport = htmlspecialchars_decode( $limitReport );
582 // Run deprecated hook
583 Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
584
585 // Sanitize for comment. Note '‐' in the replacement is U+2010,
586 // which looks much like the problematic '-'.
587 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
588 $text = "\n<!-- \n$limitReport-->\n";
589
590 // Add on template profiling data in human/machine readable way
591 $dataByFunc = $this->mProfiler->getFunctionStats();
592 uasort( $dataByFunc, function ( $a, $b ) {
593 return $a['real'] < $b['real']; // descending order
594 } );
595 $profileReport = [];
596 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
597 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
598 $item['%real'], $item['real'], $item['calls'],
599 htmlspecialchars( $item['name'] ) );
600 }
601 $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
602 $text .= implode( "\n", $profileReport ) . "\n-->\n";
603
604 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
605
606 // Add other cache related metadata
607 if ( $wgShowHostnames ) {
608 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
609 }
610 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
611 $this->mOutput->getCacheTime() );
612 $this->mOutput->setLimitReportData( 'cachereport-ttl',
613 $this->mOutput->getCacheExpiry() );
614 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
615 $this->mOutput->hasDynamicContent() );
616
617 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
618 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
619 $this->mTitle->getPrefixedDBkey() );
620 }
621 return $text;
622 }
623
646 public function recursiveTagParse( $text, $frame = false ) {
647 // Avoid PHP 7.1 warning from passing $this by reference
648 $parser = $this;
649 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
650 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
651 $text = $this->internalParse( $text, false, $frame );
652 return $text;
653 }
654
672 public function recursiveTagParseFully( $text, $frame = false ) {
673 $text = $this->recursiveTagParse( $text, $frame );
674 $text = $this->internalParseHalfParsed( $text, false );
675 return $text;
676 }
677
689 public function preprocess( $text, Title $title = null,
690 ParserOptions $options, $revid = null, $frame = false
691 ) {
692 $magicScopeVariable = $this->lock();
693 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
694 if ( $revid !== null ) {
695 $this->mRevisionId = $revid;
696 }
697 // Avoid PHP 7.1 warning from passing $this by reference
698 $parser = $this;
699 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
700 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
701 $text = $this->replaceVariables( $text, $frame );
702 $text = $this->mStripState->unstripBoth( $text );
703 return $text;
704 }
705
715 public function recursivePreprocess( $text, $frame = false ) {
716 $text = $this->replaceVariables( $text, $frame );
717 $text = $this->mStripState->unstripBoth( $text );
718 return $text;
719 }
720
734 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
735 $msg = new RawMessage( $text );
736 $text = $msg->params( $params )->plain();
737
738 # Parser (re)initialisation
739 $magicScopeVariable = $this->lock();
740 $this->startParse( $title, $options, self::OT_PLAIN, true );
741
743 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
744 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
745 $text = $this->mStripState->unstripBoth( $text );
746 return $text;
747 }
748
755 public function setUser( $user ) {
756 $this->mUser = $user;
757 }
758
764 public function setTitle( $t ) {
765 if ( !$t ) {
766 $t = Title::newFromText( 'NO TITLE' );
767 }
768
769 if ( $t->hasFragment() ) {
770 # Strip the fragment to avoid various odd effects
771 $this->mTitle = $t->createFragmentTarget( '' );
772 } else {
773 $this->mTitle = $t;
774 }
775 }
776
782 public function getTitle() {
783 return $this->mTitle;
784 }
785
792 public function Title( $x = null ) {
793 return wfSetVar( $this->mTitle, $x );
794 }
795
801 public function setOutputType( $ot ) {
802 $this->mOutputType = $ot;
803 # Shortcut alias
804 $this->ot = [
805 'html' => $ot == self::OT_HTML,
806 'wiki' => $ot == self::OT_WIKI,
807 'pre' => $ot == self::OT_PREPROCESS,
808 'plain' => $ot == self::OT_PLAIN,
809 ];
810 }
811
818 public function OutputType( $x = null ) {
819 return wfSetVar( $this->mOutputType, $x );
820 }
821
827 public function getOutput() {
828 return $this->mOutput;
829 }
830
836 public function getOptions() {
837 return $this->mOptions;
838 }
839
846 public function Options( $x = null ) {
847 return wfSetVar( $this->mOptions, $x );
848 }
849
853 public function nextLinkID() {
854 return $this->mLinkID++;
855 }
856
860 public function setLinkID( $id ) {
861 $this->mLinkID = $id;
862 }
863
868 public function getFunctionLang() {
869 return $this->getTargetLanguage();
870 }
871
881 public function getTargetLanguage() {
882 $target = $this->mOptions->getTargetLanguage();
883
884 if ( $target !== null ) {
885 return $target;
886 } elseif ( $this->mOptions->getInterfaceMessage() ) {
887 return $this->mOptions->getUserLangObj();
888 } elseif ( is_null( $this->mTitle ) ) {
889 throw new MWException( __METHOD__ . ': $this->mTitle is null' );
890 }
891
892 return $this->mTitle->getPageLanguage();
893 }
894
899 public function getConverterLanguage() {
900 return $this->getTargetLanguage();
901 }
902
909 public function getUser() {
910 if ( !is_null( $this->mUser ) ) {
911 return $this->mUser;
912 }
913 return $this->mOptions->getUser();
914 }
915
921 public function getPreprocessor() {
922 if ( !isset( $this->mPreprocessor ) ) {
923 $class = $this->mPreprocessorClass;
924 $this->mPreprocessor = new $class( $this );
925 }
926 return $this->mPreprocessor;
927 }
928
935 public function getLinkRenderer() {
936 if ( !$this->mLinkRenderer ) {
937 $this->mLinkRenderer = MediaWikiServices::getInstance()
938 ->getLinkRendererFactory()->create();
939 $this->mLinkRenderer->setStubThreshold(
940 $this->getOptions()->getStubThreshold()
941 );
942 }
943
944 return $this->mLinkRenderer;
945 }
946
966 public static function extractTagsAndParams( $elements, $text, &$matches ) {
967 static $n = 1;
968 $stripped = '';
969 $matches = [];
970
971 $taglist = implode( '|', $elements );
972 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
973
974 while ( $text != '' ) {
975 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
976 $stripped .= $p[0];
977 if ( count( $p ) < 5 ) {
978 break;
979 }
980 if ( count( $p ) > 5 ) {
981 # comment
982 $element = $p[4];
983 $attributes = '';
984 $close = '';
985 $inside = $p[5];
986 } else {
987 # tag
988 $element = $p[1];
989 $attributes = $p[2];
990 $close = $p[3];
991 $inside = $p[4];
992 }
993
994 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
995 $stripped .= $marker;
996
997 if ( $close === '/>' ) {
998 # Empty element tag, <tag />
999 $content = null;
1000 $text = $inside;
1001 $tail = null;
1002 } else {
1003 if ( $element === '!--' ) {
1004 $end = '/(-->)/';
1005 } else {
1006 $end = "/(<\\/$element\\s*>)/i";
1007 }
1008 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1009 $content = $q[0];
1010 if ( count( $q ) < 3 ) {
1011 # No end tag -- let it run out to the end of the text.
1012 $tail = '';
1013 $text = '';
1014 } else {
1015 $tail = $q[1];
1016 $text = $q[2];
1017 }
1018 }
1019
1020 $matches[$marker] = [ $element,
1021 $content,
1022 Sanitizer::decodeTagAttributes( $attributes ),
1023 "<$element$attributes$close$content$tail" ];
1024 }
1025 return $stripped;
1026 }
1027
1033 public function getStripList() {
1034 return $this->mStripList;
1035 }
1036
1046 public function insertStripItem( $text ) {
1047 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1048 $this->mMarkerIndex++;
1049 $this->mStripState->addGeneral( $marker, $text );
1050 return $marker;
1051 }
1052
1060 public function doTableStuff( $text ) {
1061 $lines = StringUtils::explode( "\n", $text );
1062 $out = '';
1063 $td_history = []; # Is currently a td tag open?
1064 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1065 $tr_history = []; # Is currently a tr tag open?
1066 $tr_attributes = []; # history of tr attributes
1067 $has_opened_tr = []; # Did this table open a <tr> element?
1068 $indent_level = 0; # indent level of the table
1069
1070 foreach ( $lines as $outLine ) {
1071 $line = trim( $outLine );
1072
1073 if ( $line === '' ) { # empty line, go to next line
1074 $out .= $outLine . "\n";
1075 continue;
1076 }
1077
1078 $first_character = $line[0];
1079 $first_two = substr( $line, 0, 2 );
1080 $matches = [];
1081
1082 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1083 # First check if we are starting a new table
1084 $indent_level = strlen( $matches[1] );
1085
1086 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1087 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1088
1089 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1090 array_push( $td_history, false );
1091 array_push( $last_tag_history, '' );
1092 array_push( $tr_history, false );
1093 array_push( $tr_attributes, '' );
1094 array_push( $has_opened_tr, false );
1095 } elseif ( count( $td_history ) == 0 ) {
1096 # Don't do any of the following
1097 $out .= $outLine . "\n";
1098 continue;
1099 } elseif ( $first_two === '|}' ) {
1100 # We are ending a table
1101 $line = '</table>' . substr( $line, 2 );
1102 $last_tag = array_pop( $last_tag_history );
1103
1104 if ( !array_pop( $has_opened_tr ) ) {
1105 $line = "<tr><td></td></tr>{$line}";
1106 }
1107
1108 if ( array_pop( $tr_history ) ) {
1109 $line = "</tr>{$line}";
1110 }
1111
1112 if ( array_pop( $td_history ) ) {
1113 $line = "</{$last_tag}>{$line}";
1114 }
1115 array_pop( $tr_attributes );
1116 if ( $indent_level > 0 ) {
1117 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1118 } else {
1119 $outLine = $line;
1120 }
1121 } elseif ( $first_two === '|-' ) {
1122 # Now we have a table row
1123 $line = preg_replace( '#^\|-+#', '', $line );
1124
1125 # Whats after the tag is now only attributes
1126 $attributes = $this->mStripState->unstripBoth( $line );
1127 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1128 array_pop( $tr_attributes );
1129 array_push( $tr_attributes, $attributes );
1130
1131 $line = '';
1132 $last_tag = array_pop( $last_tag_history );
1133 array_pop( $has_opened_tr );
1134 array_push( $has_opened_tr, true );
1135
1136 if ( array_pop( $tr_history ) ) {
1137 $line = '</tr>';
1138 }
1139
1140 if ( array_pop( $td_history ) ) {
1141 $line = "</{$last_tag}>{$line}";
1142 }
1143
1144 $outLine = $line;
1145 array_push( $tr_history, false );
1146 array_push( $td_history, false );
1147 array_push( $last_tag_history, '' );
1148 } elseif ( $first_character === '|'
1149 || $first_character === '!'
1150 || $first_two === '|+'
1151 ) {
1152 # This might be cell elements, td, th or captions
1153 if ( $first_two === '|+' ) {
1154 $first_character = '+';
1155 $line = substr( $line, 2 );
1156 } else {
1157 $line = substr( $line, 1 );
1158 }
1159
1160 // Implies both are valid for table headings.
1161 if ( $first_character === '!' ) {
1162 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1163 }
1164
1165 # Split up multiple cells on the same line.
1166 # FIXME : This can result in improper nesting of tags processed
1167 # by earlier parser steps.
1168 $cells = explode( '||', $line );
1169
1170 $outLine = '';
1171
1172 # Loop through each table cell
1173 foreach ( $cells as $cell ) {
1174 $previous = '';
1175 if ( $first_character !== '+' ) {
1176 $tr_after = array_pop( $tr_attributes );
1177 if ( !array_pop( $tr_history ) ) {
1178 $previous = "<tr{$tr_after}>\n";
1179 }
1180 array_push( $tr_history, true );
1181 array_push( $tr_attributes, '' );
1182 array_pop( $has_opened_tr );
1183 array_push( $has_opened_tr, true );
1184 }
1185
1186 $last_tag = array_pop( $last_tag_history );
1187
1188 if ( array_pop( $td_history ) ) {
1189 $previous = "</{$last_tag}>\n{$previous}";
1190 }
1191
1192 if ( $first_character === '|' ) {
1193 $last_tag = 'td';
1194 } elseif ( $first_character === '!' ) {
1195 $last_tag = 'th';
1196 } elseif ( $first_character === '+' ) {
1197 $last_tag = 'caption';
1198 } else {
1199 $last_tag = '';
1200 }
1201
1202 array_push( $last_tag_history, $last_tag );
1203
1204 # A cell could contain both parameters and data
1205 $cell_data = explode( '|', $cell, 2 );
1206
1207 # T2553: Note that a '|' inside an invalid link should not
1208 # be mistaken as delimiting cell parameters
1209 # Bug T153140: Neither should language converter markup.
1210 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1211 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1212 } elseif ( count( $cell_data ) == 1 ) {
1213 // Whitespace in cells is trimmed
1214 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1215 } else {
1216 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1217 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1218 // Whitespace in cells is trimmed
1219 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1220 }
1221
1222 $outLine .= $cell;
1223 array_push( $td_history, true );
1224 }
1225 }
1226 $out .= $outLine . "\n";
1227 }
1228
1229 # Closing open td, tr && table
1230 while ( count( $td_history ) > 0 ) {
1231 if ( array_pop( $td_history ) ) {
1232 $out .= "</td>\n";
1233 }
1234 if ( array_pop( $tr_history ) ) {
1235 $out .= "</tr>\n";
1236 }
1237 if ( !array_pop( $has_opened_tr ) ) {
1238 $out .= "<tr><td></td></tr>\n";
1239 }
1240
1241 $out .= "</table>\n";
1242 }
1243
1244 # Remove trailing line-ending (b/c)
1245 if ( substr( $out, -1 ) === "\n" ) {
1246 $out = substr( $out, 0, -1 );
1247 }
1248
1249 # special case: don't return empty table
1250 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1251 $out = '';
1252 }
1253
1254 return $out;
1255 }
1256
1269 public function internalParse( $text, $isMain = true, $frame = false ) {
1270 $origText = $text;
1271
1272 // Avoid PHP 7.1 warning from passing $this by reference
1273 $parser = $this;
1274
1275 # Hook to suspend the parser in this state
1276 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1277 return $text;
1278 }
1279
1280 # if $frame is provided, then use $frame for replacing any variables
1281 if ( $frame ) {
1282 # use frame depth to infer how include/noinclude tags should be handled
1283 # depth=0 means this is the top-level document; otherwise it's an included document
1284 if ( !$frame->depth ) {
1285 $flag = 0;
1286 } else {
1287 $flag = self::PTD_FOR_INCLUSION;
1288 }
1289 $dom = $this->preprocessToDom( $text, $flag );
1290 $text = $frame->expand( $dom );
1291 } else {
1292 # if $frame is not provided, then use old-style replaceVariables
1293 $text = $this->replaceVariables( $text );
1294 }
1295
1296 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1297 $text = Sanitizer::removeHTMLtags(
1298 $text,
1299 [ $this, 'attributeStripCallback' ],
1300 false,
1301 array_keys( $this->mTransparentTagHooks ),
1302 [],
1303 [ $this, 'addTrackingCategory' ]
1304 );
1305 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1306
1307 # Tables need to come after variable replacement for things to work
1308 # properly; putting them before other transformations should keep
1309 # exciting things like link expansions from showing up in surprising
1310 # places.
1311 $text = $this->doTableStuff( $text );
1312
1313 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1314
1315 $text = $this->doDoubleUnderscore( $text );
1316
1317 $text = $this->doHeadings( $text );
1318 $text = $this->replaceInternalLinks( $text );
1319 $text = $this->doAllQuotes( $text );
1320 $text = $this->replaceExternalLinks( $text );
1321
1322 # replaceInternalLinks may sometimes leave behind
1323 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1324 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1325
1326 $text = $this->doMagicLinks( $text );
1327 $text = $this->formatHeadings( $text, $origText, $isMain );
1328
1329 return $text;
1330 }
1331
1341 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1342 $text = $this->mStripState->unstripGeneral( $text );
1343
1344 // Avoid PHP 7.1 warning from passing $this by reference
1345 $parser = $this;
1346
1347 if ( $isMain ) {
1348 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1349 }
1350
1351 # Clean up special characters, only run once, next-to-last before doBlockLevels
1352 $fixtags = [
1353 # French spaces, last one Guillemet-left
1354 # only if there is something before the space
1355 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1356 # french spaces, Guillemet-right
1357 '/(\\302\\253) /' => '\\1&#160;',
1358 '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, T13874.
1359 ];
1360 $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1361
1362 $text = $this->doBlockLevels( $text, $linestart );
1363
1364 $this->replaceLinkHolders( $text );
1365
1373 if ( !( $this->mOptions->getDisableContentConversion()
1374 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1375 ) {
1376 if ( !$this->mOptions->getInterfaceMessage() ) {
1377 # The position of the convert() call should not be changed. it
1378 # assumes that the links are all replaced and the only thing left
1379 # is the <nowiki> mark.
1380 $text = $this->getConverterLanguage()->convert( $text );
1381 }
1382 }
1383
1384 $text = $this->mStripState->unstripNoWiki( $text );
1385
1386 if ( $isMain ) {
1387 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1388 }
1389
1390 $text = $this->replaceTransparentTags( $text );
1391 $text = $this->mStripState->unstripGeneral( $text );
1392
1393 $text = Sanitizer::normalizeCharReferences( $text );
1394
1395 if ( MWTidy::isEnabled() ) {
1396 if ( $this->mOptions->getTidy() ) {
1397 $text = MWTidy::tidy( $text );
1398 }
1399 } else {
1400 # attempt to sanitize at least some nesting problems
1401 # (T4702 and quite a few others)
1402 $tidyregs = [
1403 # ''Something [http://www.cool.com cool''] -->
1404 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1405 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1406 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1407 # fix up an anchor inside another anchor, only
1408 # at least for a single single nested link (T5695)
1409 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1410 '\\1\\2</a>\\3</a>\\1\\4</a>',
1411 # fix div inside inline elements- doBlockLevels won't wrap a line which
1412 # contains a div, so fix it up here; replace
1413 # div with escaped text
1414 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1415 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1416 # remove empty italic or bold tag pairs, some
1417 # introduced by rules above
1418 '/<([bi])><\/\\1>/' => '',
1419 ];
1420
1421 $text = preg_replace(
1422 array_keys( $tidyregs ),
1423 array_values( $tidyregs ),
1424 $text );
1425 }
1426
1427 if ( $isMain ) {
1428 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1429 }
1430
1431 return $text;
1432 }
1433
1445 public function doMagicLinks( $text ) {
1447 $urlChar = self::EXT_LINK_URL_CLASS;
1448 $addr = self::EXT_LINK_ADDR;
1449 $space = self::SPACE_NOT_NL; # non-newline space
1450 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1451 $spaces = "$space++"; # possessive match of 1 or more spaces
1452 $text = preg_replace_callback(
1453 '!(?: # Start cases
1454 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1455 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1456 (\b # m[3]: Free external links
1457 (?i:$prots)
1458 ($addr$urlChar*) # m[4]: Post-protocol path
1459 ) |
1460 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1461 ([0-9]+)\b |
1462 \bISBN $spaces ( # m[6]: ISBN, capture number
1463 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1464 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1465 [0-9Xx] # check digit
1466 )\b
1467 )!xu", [ $this, 'magicLinkCallback' ], $text );
1468 return $text;
1469 }
1470
1476 public function magicLinkCallback( $m ) {
1477 if ( isset( $m[1] ) && $m[1] !== '' ) {
1478 # Skip anchor
1479 return $m[0];
1480 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1481 # Skip HTML element
1482 return $m[0];
1483 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1484 # Free external link
1485 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1486 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1487 # RFC or PMID
1488 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1489 if ( !$this->mOptions->getMagicRFCLinks() ) {
1490 return $m[0];
1491 }
1492 $keyword = 'RFC';
1493 $urlmsg = 'rfcurl';
1494 $cssClass = 'mw-magiclink-rfc';
1495 $trackingCat = 'magiclink-tracking-rfc';
1496 $id = $m[5];
1497 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1498 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1499 return $m[0];
1500 }
1501 $keyword = 'PMID';
1502 $urlmsg = 'pubmedurl';
1503 $cssClass = 'mw-magiclink-pmid';
1504 $trackingCat = 'magiclink-tracking-pmid';
1505 $id = $m[5];
1506 } else {
1507 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1508 substr( $m[0], 0, 20 ) . '"' );
1509 }
1510 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1511 $this->addTrackingCategory( $trackingCat );
1512 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1513 } elseif ( isset( $m[6] ) && $m[6] !== ''
1514 && $this->mOptions->getMagicISBNLinks()
1515 ) {
1516 # ISBN
1517 $isbn = $m[6];
1518 $space = self::SPACE_NOT_NL; # non-newline space
1519 $isbn = preg_replace( "/$space/", ' ', $isbn );
1520 $num = strtr( $isbn, [
1521 '-' => '',
1522 ' ' => '',
1523 'x' => 'X',
1524 ] );
1525 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1526 return $this->getLinkRenderer()->makeKnownLink(
1527 SpecialPage::getTitleFor( 'Booksources', $num ),
1528 "ISBN $isbn",
1529 [
1530 'class' => 'internal mw-magiclink-isbn',
1531 'title' => false // suppress title attribute
1532 ]
1533 );
1534 } else {
1535 return $m[0];
1536 }
1537 }
1538
1548 public function makeFreeExternalLink( $url, $numPostProto ) {
1549 $trail = '';
1550
1551 # The characters '<' and '>' (which were escaped by
1552 # removeHTMLtags()) should not be included in
1553 # URLs, per RFC 2396.
1554 # Make &nbsp; terminate a URL as well (bug T84937)
1555 $m2 = [];
1556 if ( preg_match(
1557 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1558 $url,
1559 $m2,
1560 PREG_OFFSET_CAPTURE
1561 ) ) {
1562 $trail = substr( $url, $m2[0][1] ) . $trail;
1563 $url = substr( $url, 0, $m2[0][1] );
1564 }
1565
1566 # Move trailing punctuation to $trail
1567 $sep = ',;\.:!?';
1568 # If there is no left bracket, then consider right brackets fair game too
1569 if ( strpos( $url, '(' ) === false ) {
1570 $sep .= ')';
1571 }
1572
1573 $urlRev = strrev( $url );
1574 $numSepChars = strspn( $urlRev, $sep );
1575 # Don't break a trailing HTML entity by moving the ; into $trail
1576 # This is in hot code, so use substr_compare to avoid having to
1577 # create a new string object for the comparison
1578 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1579 # more optimization: instead of running preg_match with a $
1580 # anchor, which can be slow, do the match on the reversed
1581 # string starting at the desired offset.
1582 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1583 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1584 $numSepChars--;
1585 }
1586 }
1587 if ( $numSepChars ) {
1588 $trail = substr( $url, -$numSepChars ) . $trail;
1589 $url = substr( $url, 0, -$numSepChars );
1590 }
1591
1592 # Verify that we still have a real URL after trail removal, and
1593 # not just lone protocol
1594 if ( strlen( $trail ) >= $numPostProto ) {
1595 return $url . $trail;
1596 }
1597
1598 $url = Sanitizer::cleanUrl( $url );
1599
1600 # Is this an external image?
1601 $text = $this->maybeMakeExternalImage( $url );
1602 if ( $text === false ) {
1603 # Not an image, make a link
1604 $text = Linker::makeExternalLink( $url,
1605 $this->getConverterLanguage()->markNoConversion( $url, true ),
1606 true, 'free',
1607 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1608 # Register it in the output object...
1609 $this->mOutput->addExternalLink( $url );
1610 }
1611 return $text . $trail;
1612 }
1613
1623 public function doHeadings( $text ) {
1624 for ( $i = 6; $i >= 1; --$i ) {
1625 $h = str_repeat( '=', $i );
1626 // Trim non-newline whitespace from headings
1627 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1628 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1629 }
1630 return $text;
1631 }
1632
1641 public function doAllQuotes( $text ) {
1642 $outtext = '';
1643 $lines = StringUtils::explode( "\n", $text );
1644 foreach ( $lines as $line ) {
1645 $outtext .= $this->doQuotes( $line ) . "\n";
1646 }
1647 $outtext = substr( $outtext, 0, -1 );
1648 return $outtext;
1649 }
1650
1658 public function doQuotes( $text ) {
1659 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1660 $countarr = count( $arr );
1661 if ( $countarr == 1 ) {
1662 return $text;
1663 }
1664
1665 // First, do some preliminary work. This may shift some apostrophes from
1666 // being mark-up to being text. It also counts the number of occurrences
1667 // of bold and italics mark-ups.
1668 $numbold = 0;
1669 $numitalics = 0;
1670 for ( $i = 1; $i < $countarr; $i += 2 ) {
1671 $thislen = strlen( $arr[$i] );
1672 // If there are ever four apostrophes, assume the first is supposed to
1673 // be text, and the remaining three constitute mark-up for bold text.
1674 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1675 if ( $thislen == 4 ) {
1676 $arr[$i - 1] .= "'";
1677 $arr[$i] = "'''";
1678 $thislen = 3;
1679 } elseif ( $thislen > 5 ) {
1680 // If there are more than 5 apostrophes in a row, assume they're all
1681 // text except for the last 5.
1682 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1683 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1684 $arr[$i] = "'''''";
1685 $thislen = 5;
1686 }
1687 // Count the number of occurrences of bold and italics mark-ups.
1688 if ( $thislen == 2 ) {
1689 $numitalics++;
1690 } elseif ( $thislen == 3 ) {
1691 $numbold++;
1692 } elseif ( $thislen == 5 ) {
1693 $numitalics++;
1694 $numbold++;
1695 }
1696 }
1697
1698 // If there is an odd number of both bold and italics, it is likely
1699 // that one of the bold ones was meant to be an apostrophe followed
1700 // by italics. Which one we cannot know for certain, but it is more
1701 // likely to be one that has a single-letter word before it.
1702 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1703 $firstsingleletterword = -1;
1704 $firstmultiletterword = -1;
1705 $firstspace = -1;
1706 for ( $i = 1; $i < $countarr; $i += 2 ) {
1707 if ( strlen( $arr[$i] ) == 3 ) {
1708 $x1 = substr( $arr[$i - 1], -1 );
1709 $x2 = substr( $arr[$i - 1], -2, 1 );
1710 if ( $x1 === ' ' ) {
1711 if ( $firstspace == -1 ) {
1712 $firstspace = $i;
1713 }
1714 } elseif ( $x2 === ' ' ) {
1715 $firstsingleletterword = $i;
1716 // if $firstsingleletterword is set, we don't
1717 // look at the other options, so we can bail early.
1718 break;
1719 } else {
1720 if ( $firstmultiletterword == -1 ) {
1721 $firstmultiletterword = $i;
1722 }
1723 }
1724 }
1725 }
1726
1727 // If there is a single-letter word, use it!
1728 if ( $firstsingleletterword > -1 ) {
1729 $arr[$firstsingleletterword] = "''";
1730 $arr[$firstsingleletterword - 1] .= "'";
1731 } elseif ( $firstmultiletterword > -1 ) {
1732 // If not, but there's a multi-letter word, use that one.
1733 $arr[$firstmultiletterword] = "''";
1734 $arr[$firstmultiletterword - 1] .= "'";
1735 } elseif ( $firstspace > -1 ) {
1736 // ... otherwise use the first one that has neither.
1737 // (notice that it is possible for all three to be -1 if, for example,
1738 // there is only one pentuple-apostrophe in the line)
1739 $arr[$firstspace] = "''";
1740 $arr[$firstspace - 1] .= "'";
1741 }
1742 }
1743
1744 // Now let's actually convert our apostrophic mush to HTML!
1745 $output = '';
1746 $buffer = '';
1747 $state = '';
1748 $i = 0;
1749 foreach ( $arr as $r ) {
1750 if ( ( $i % 2 ) == 0 ) {
1751 if ( $state === 'both' ) {
1752 $buffer .= $r;
1753 } else {
1754 $output .= $r;
1755 }
1756 } else {
1757 $thislen = strlen( $r );
1758 if ( $thislen == 2 ) {
1759 if ( $state === 'i' ) {
1760 $output .= '</i>';
1761 $state = '';
1762 } elseif ( $state === 'bi' ) {
1763 $output .= '</i>';
1764 $state = 'b';
1765 } elseif ( $state === 'ib' ) {
1766 $output .= '</b></i><b>';
1767 $state = 'b';
1768 } elseif ( $state === 'both' ) {
1769 $output .= '<b><i>' . $buffer . '</i>';
1770 $state = 'b';
1771 } else { // $state can be 'b' or ''
1772 $output .= '<i>';
1773 $state .= 'i';
1774 }
1775 } elseif ( $thislen == 3 ) {
1776 if ( $state === 'b' ) {
1777 $output .= '</b>';
1778 $state = '';
1779 } elseif ( $state === 'bi' ) {
1780 $output .= '</i></b><i>';
1781 $state = 'i';
1782 } elseif ( $state === 'ib' ) {
1783 $output .= '</b>';
1784 $state = 'i';
1785 } elseif ( $state === 'both' ) {
1786 $output .= '<i><b>' . $buffer . '</b>';
1787 $state = 'i';
1788 } else { // $state can be 'i' or ''
1789 $output .= '<b>';
1790 $state .= 'b';
1791 }
1792 } elseif ( $thislen == 5 ) {
1793 if ( $state === 'b' ) {
1794 $output .= '</b><i>';
1795 $state = 'i';
1796 } elseif ( $state === 'i' ) {
1797 $output .= '</i><b>';
1798 $state = 'b';
1799 } elseif ( $state === 'bi' ) {
1800 $output .= '</i></b>';
1801 $state = '';
1802 } elseif ( $state === 'ib' ) {
1803 $output .= '</b></i>';
1804 $state = '';
1805 } elseif ( $state === 'both' ) {
1806 $output .= '<i><b>' . $buffer . '</b></i>';
1807 $state = '';
1808 } else { // ($state == '')
1809 $buffer = '';
1810 $state = 'both';
1811 }
1812 }
1813 }
1814 $i++;
1815 }
1816 // Now close all remaining tags. Notice that the order is important.
1817 if ( $state === 'b' || $state === 'ib' ) {
1818 $output .= '</b>';
1819 }
1820 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1821 $output .= '</i>';
1822 }
1823 if ( $state === 'bi' ) {
1824 $output .= '</b>';
1825 }
1826 // There might be lonely ''''', so make sure we have a buffer
1827 if ( $state === 'both' && $buffer ) {
1828 $output .= '<b><i>' . $buffer . '</i></b>';
1829 }
1830 return $output;
1831 }
1832
1846 public function replaceExternalLinks( $text ) {
1847 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1848 if ( $bits === false ) {
1849 throw new MWException( "PCRE needs to be compiled with "
1850 . "--enable-unicode-properties in order for MediaWiki to function" );
1851 }
1852 $s = array_shift( $bits );
1853
1854 $i = 0;
1855 while ( $i < count( $bits ) ) {
1856 $url = $bits[$i++];
1857 $i++; // protocol
1858 $text = $bits[$i++];
1859 $trail = $bits[$i++];
1860
1861 # The characters '<' and '>' (which were escaped by
1862 # removeHTMLtags()) should not be included in
1863 # URLs, per RFC 2396.
1864 $m2 = [];
1865 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1866 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1867 $url = substr( $url, 0, $m2[0][1] );
1868 }
1869
1870 # If the link text is an image URL, replace it with an <img> tag
1871 # This happened by accident in the original parser, but some people used it extensively
1872 $img = $this->maybeMakeExternalImage( $text );
1873 if ( $img !== false ) {
1874 $text = $img;
1875 }
1876
1877 $dtrail = '';
1878
1879 # Set linktype for CSS
1880 $linktype = 'text';
1881
1882 # No link text, e.g. [http://domain.tld/some.link]
1883 if ( $text == '' ) {
1884 # Autonumber
1885 $langObj = $this->getTargetLanguage();
1886 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1887 $linktype = 'autonumber';
1888 } else {
1889 # Have link text, e.g. [http://domain.tld/some.link text]s
1890 # Check for trail
1891 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1892 }
1893
1894 $text = $this->getConverterLanguage()->markNoConversion( $text );
1895
1896 $url = Sanitizer::cleanUrl( $url );
1897
1898 # Use the encoded URL
1899 # This means that users can paste URLs directly into the text
1900 # Funny characters like ö aren't valid in URLs anyway
1901 # This was changed in August 2004
1902 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1903 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1904
1905 # Register link in the output object.
1906 $this->mOutput->addExternalLink( $url );
1907 }
1908
1909 return $s;
1910 }
1911
1921 public static function getExternalLinkRel( $url = false, $title = null ) {
1923 $ns = $title ? $title->getNamespace() : false;
1924 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1926 ) {
1927 return 'nofollow';
1928 }
1929 return null;
1930 }
1931
1942 public function getExternalLinkAttribs( $url ) {
1943 $attribs = [];
1944 $rel = self::getExternalLinkRel( $url, $this->mTitle );
1945
1946 $target = $this->mOptions->getExternalLinkTarget();
1947 if ( $target ) {
1948 $attribs['target'] = $target;
1949 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1950 // T133507. New windows can navigate parent cross-origin.
1951 // Including noreferrer due to lacking browser
1952 // support of noopener. Eventually noreferrer should be removed.
1953 if ( $rel !== '' ) {
1954 $rel .= ' ';
1955 }
1956 $rel .= 'noreferrer noopener';
1957 }
1958 }
1959 $attribs['rel'] = $rel;
1960 return $attribs;
1961 }
1962
1972 public static function normalizeLinkUrl( $url ) {
1973 # First, make sure unsafe characters are encoded
1974 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1975 function ( $m ) {
1976 return rawurlencode( $m[0] );
1977 },
1978 $url
1979 );
1980
1981 $ret = '';
1982 $end = strlen( $url );
1983
1984 # Fragment part - 'fragment'
1985 $start = strpos( $url, '#' );
1986 if ( $start !== false && $start < $end ) {
1987 $ret = self::normalizeUrlComponent(
1988 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1989 $end = $start;
1990 }
1991
1992 # Query part - 'query' minus &=+;
1993 $start = strpos( $url, '?' );
1994 if ( $start !== false && $start < $end ) {
1995 $ret = self::normalizeUrlComponent(
1996 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1997 $end = $start;
1998 }
1999
2000 # Scheme and path part - 'pchar'
2001 # (we assume no userinfo or encoded colons in the host)
2002 $ret = self::normalizeUrlComponent(
2003 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2004
2005 return $ret;
2006 }
2007
2008 private static function normalizeUrlComponent( $component, $unsafe ) {
2009 $callback = function ( $matches ) use ( $unsafe ) {
2010 $char = urldecode( $matches[0] );
2011 $ord = ord( $char );
2012 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2013 # Unescape it
2014 return $char;
2015 } else {
2016 # Leave it escaped, but use uppercase for a-f
2017 return strtoupper( $matches[0] );
2018 }
2019 };
2020 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2021 }
2022
2031 private function maybeMakeExternalImage( $url ) {
2032 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2033 $imagesexception = !empty( $imagesfrom );
2034 $text = false;
2035 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2036 if ( $imagesexception && is_array( $imagesfrom ) ) {
2037 $imagematch = false;
2038 foreach ( $imagesfrom as $match ) {
2039 if ( strpos( $url, $match ) === 0 ) {
2040 $imagematch = true;
2041 break;
2042 }
2043 }
2044 } elseif ( $imagesexception ) {
2045 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2046 } else {
2047 $imagematch = false;
2048 }
2049
2050 if ( $this->mOptions->getAllowExternalImages()
2051 || ( $imagesexception && $imagematch )
2052 ) {
2053 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2054 # Image found
2055 $text = Linker::makeExternalImage( $url );
2056 }
2057 }
2058 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2059 && preg_match( self::EXT_IMAGE_REGEX, $url )
2060 ) {
2061 $whitelist = explode(
2062 "\n",
2063 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2064 );
2065
2066 foreach ( $whitelist as $entry ) {
2067 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2068 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2069 continue;
2070 }
2071 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2072 # Image matches a whitelist entry
2073 $text = Linker::makeExternalImage( $url );
2074 break;
2075 }
2076 }
2077 }
2078 return $text;
2079 }
2080
2090 public function replaceInternalLinks( $s ) {
2091 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2092 return $s;
2093 }
2094
2103 public function replaceInternalLinks2( &$s ) {
2105
2106 static $tc = false, $e1, $e1_img;
2107 # the % is needed to support urlencoded titles as well
2108 if ( !$tc ) {
2109 $tc = Title::legalChars() . '#%';
2110 # Match a link having the form [[namespace:link|alternate]]trail
2111 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2112 # Match cases where there is no "]]", which might still be images
2113 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2114 }
2115
2116 $holders = new LinkHolderArray( $this );
2117
2118 # split the entire text string on occurrences of [[
2119 $a = StringUtils::explode( '[[', ' ' . $s );
2120 # get the first element (all text up to first [[), and remove the space we added
2121 $s = $a->current();
2122 $a->next();
2123 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2124 $s = substr( $s, 1 );
2125
2126 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2127 $e2 = null;
2128 if ( $useLinkPrefixExtension ) {
2129 # Match the end of a line for a word that's not followed by whitespace,
2130 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2131 global $wgContLang;
2132 $charset = $wgContLang->linkPrefixCharset();
2133 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2134 }
2135
2136 if ( is_null( $this->mTitle ) ) {
2137 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2138 }
2139 $nottalk = !$this->mTitle->isTalkPage();
2140
2141 if ( $useLinkPrefixExtension ) {
2142 $m = [];
2143 if ( preg_match( $e2, $s, $m ) ) {
2144 $first_prefix = $m[2];
2145 } else {
2146 $first_prefix = false;
2147 }
2148 } else {
2149 $prefix = '';
2150 }
2151
2152 $useSubpages = $this->areSubpagesAllowed();
2153
2154 # Loop for each link
2155 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2156 # Check for excessive memory usage
2157 if ( $holders->isBig() ) {
2158 # Too big
2159 # Do the existence check, replace the link holders and clear the array
2160 $holders->replace( $s );
2161 $holders->clear();
2162 }
2163
2164 if ( $useLinkPrefixExtension ) {
2165 if ( preg_match( $e2, $s, $m ) ) {
2166 $prefix = $m[2];
2167 $s = $m[1];
2168 } else {
2169 $prefix = '';
2170 }
2171 # first link
2172 if ( $first_prefix ) {
2173 $prefix = $first_prefix;
2174 $first_prefix = false;
2175 }
2176 }
2177
2178 $might_be_img = false;
2179
2180 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2181 $text = $m[2];
2182 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2183 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2184 # the real problem is with the $e1 regex
2185 # See T1500.
2186 # Still some problems for cases where the ] is meant to be outside punctuation,
2187 # and no image is in sight. See T4095.
2188 if ( $text !== ''
2189 && substr( $m[3], 0, 1 ) === ']'
2190 && strpos( $text, '[' ) !== false
2191 ) {
2192 $text .= ']'; # so that replaceExternalLinks($text) works later
2193 $m[3] = substr( $m[3], 1 );
2194 }
2195 # fix up urlencoded title texts
2196 if ( strpos( $m[1], '%' ) !== false ) {
2197 # Should anchors '#' also be rejected?
2198 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2199 }
2200 $trail = $m[3];
2201 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2202 # Invalid, but might be an image with a link in its caption
2203 $might_be_img = true;
2204 $text = $m[2];
2205 if ( strpos( $m[1], '%' ) !== false ) {
2206 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2207 }
2208 $trail = "";
2209 } else { # Invalid form; output directly
2210 $s .= $prefix . '[[' . $line;
2211 continue;
2212 }
2213
2214 $origLink = ltrim( $m[1], ' ' );
2215
2216 # Don't allow internal links to pages containing
2217 # PROTO: where PROTO is a valid URL protocol; these
2218 # should be external links.
2219 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2220 $s .= $prefix . '[[' . $line;
2221 continue;
2222 }
2223
2224 # Make subpage if necessary
2225 if ( $useSubpages ) {
2226 $link = $this->maybeDoSubpageLink( $origLink, $text );
2227 } else {
2228 $link = $origLink;
2229 }
2230
2231 // \x7f isn't a default legal title char, so most likely strip
2232 // markers will force us into the "invalid form" path above. But,
2233 // just in case, let's assert that xmlish tags aren't valid in
2234 // the title position.
2235 $unstrip = $this->mStripState->killMarkers( $link );
2236 $noMarkers = ( $unstrip === $link );
2237
2238 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2239 if ( $nt === null ) {
2240 $s .= $prefix . '[[' . $line;
2241 continue;
2242 }
2243
2244 $ns = $nt->getNamespace();
2245 $iw = $nt->getInterwiki();
2246
2247 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2248
2249 if ( $might_be_img ) { # if this is actually an invalid link
2250 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2251 $found = false;
2252 while ( true ) {
2253 # look at the next 'line' to see if we can close it there
2254 $a->next();
2255 $next_line = $a->current();
2256 if ( $next_line === false || $next_line === null ) {
2257 break;
2258 }
2259 $m = explode( ']]', $next_line, 3 );
2260 if ( count( $m ) == 3 ) {
2261 # the first ]] closes the inner link, the second the image
2262 $found = true;
2263 $text .= "[[{$m[0]}]]{$m[1]}";
2264 $trail = $m[2];
2265 break;
2266 } elseif ( count( $m ) == 2 ) {
2267 # if there's exactly one ]] that's fine, we'll keep looking
2268 $text .= "[[{$m[0]}]]{$m[1]}";
2269 } else {
2270 # if $next_line is invalid too, we need look no further
2271 $text .= '[[' . $next_line;
2272 break;
2273 }
2274 }
2275 if ( !$found ) {
2276 # we couldn't find the end of this imageLink, so output it raw
2277 # but don't ignore what might be perfectly normal links in the text we've examined
2278 $holders->merge( $this->replaceInternalLinks2( $text ) );
2279 $s .= "{$prefix}[[$link|$text";
2280 # note: no $trail, because without an end, there *is* no trail
2281 continue;
2282 }
2283 } else { # it's not an image, so output it raw
2284 $s .= "{$prefix}[[$link|$text";
2285 # note: no $trail, because without an end, there *is* no trail
2286 continue;
2287 }
2288 }
2289
2290 $wasblank = ( $text == '' );
2291 if ( $wasblank ) {
2292 $text = $link;
2293 if ( !$noforce ) {
2294 # Strip off leading ':'
2295 $text = substr( $text, 1 );
2296 }
2297 } else {
2298 # T6598 madness. Handle the quotes only if they come from the alternate part
2299 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2300 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2301 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2302 $text = $this->doQuotes( $text );
2303 }
2304
2305 # Link not escaped by : , create the various objects
2306 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2307 # Interwikis
2308 if (
2309 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2310 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2311 in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2312 )
2313 ) {
2314 # T26502: filter duplicates
2315 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2316 $this->mLangLinkLanguages[$iw] = true;
2317 $this->mOutput->addLanguageLink( $nt->getFullText() );
2318 }
2319
2323 $s = rtrim( $s . $prefix ) . $trail; # T175416
2324 continue;
2325 }
2326
2327 if ( $ns == NS_FILE ) {
2328 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2329 if ( $wasblank ) {
2330 # if no parameters were passed, $text
2331 # becomes something like "File:Foo.png",
2332 # which we don't want to pass on to the
2333 # image generator
2334 $text = '';
2335 } else {
2336 # recursively parse links inside the image caption
2337 # actually, this will parse them in any other parameters, too,
2338 # but it might be hard to fix that, and it doesn't matter ATM
2339 $text = $this->replaceExternalLinks( $text );
2340 $holders->merge( $this->replaceInternalLinks2( $text ) );
2341 }
2342 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2343 $s .= $prefix . $this->armorLinks(
2344 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2345 continue;
2346 }
2347 } elseif ( $ns == NS_CATEGORY ) {
2351 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2352
2353 if ( $wasblank ) {
2354 $sortkey = $this->getDefaultSort();
2355 } else {
2356 $sortkey = $text;
2357 }
2358 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2359 $sortkey = str_replace( "\n", '', $sortkey );
2360 $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2361 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2362
2363 continue;
2364 }
2365 }
2366
2367 # Self-link checking. For some languages, variants of the title are checked in
2368 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2369 # for linking to a different variant.
2370 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2371 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2372 continue;
2373 }
2374
2375 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2376 # @todo FIXME: Should do batch file existence checks, see comment below
2377 if ( $ns == NS_MEDIA ) {
2378 # Give extensions a chance to select the file revision for us
2379 $options = [];
2380 $descQuery = false;
2381 Hooks::run( 'BeforeParserFetchFileAndTitle',
2382 [ $this, $nt, &$options, &$descQuery ] );
2383 # Fetch and register the file (file title may be different via hooks)
2384 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2385 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2386 $s .= $prefix . $this->armorLinks(
2387 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2388 continue;
2389 }
2390
2391 # Some titles, such as valid special pages or files in foreign repos, should
2392 # be shown as bluelinks even though they're not included in the page table
2393 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2394 # batch file existence checks for NS_FILE and NS_MEDIA
2395 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2396 $this->mOutput->addLink( $nt );
2397 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2398 } else {
2399 # Links will be added to the output link list after checking
2400 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2401 }
2402 }
2403 return $holders;
2404 }
2405
2419 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2420 list( $inside, $trail ) = Linker::splitTrail( $trail );
2421
2422 if ( $text == '' ) {
2423 $text = htmlspecialchars( $nt->getPrefixedText() );
2424 }
2425
2426 $link = $this->getLinkRenderer()->makeKnownLink(
2427 $nt, new HtmlArmor( "$prefix$text$inside" )
2428 );
2429
2430 return $this->armorLinks( $link ) . $trail;
2431 }
2432
2443 public function armorLinks( $text ) {
2444 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2445 self::MARKER_PREFIX . "NOPARSE$1", $text );
2446 }
2447
2452 public function areSubpagesAllowed() {
2453 # Some namespaces don't allow subpages
2454 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2455 }
2456
2465 public function maybeDoSubpageLink( $target, &$text ) {
2466 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2467 }
2468
2477 public function doBlockLevels( $text, $linestart ) {
2478 return BlockLevelPass::doBlockLevels( $text, $linestart );
2479 }
2480
2492 public function getVariableValue( $index, $frame = false ) {
2495
2496 if ( is_null( $this->mTitle ) ) {
2497 // If no title set, bad things are going to happen
2498 // later. Title should always be set since this
2499 // should only be called in the middle of a parse
2500 // operation (but the unit-tests do funky stuff)
2501 throw new MWException( __METHOD__ . ' Should only be '
2502 . ' called while parsing (no title set)' );
2503 }
2504
2505 // Avoid PHP 7.1 warning from passing $this by reference
2506 $parser = $this;
2507
2512 if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) {
2513 if ( isset( $this->mVarCache[$index] ) ) {
2514 return $this->mVarCache[$index];
2515 }
2516 }
2517
2518 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2519 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2520
2521 $pageLang = $this->getFunctionLang();
2522
2523 switch ( $index ) {
2524 case '!':
2525 $value = '|';
2526 break;
2527 case 'currentmonth':
2528 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2529 break;
2530 case 'currentmonth1':
2531 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2532 break;
2533 case 'currentmonthname':
2534 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2535 break;
2536 case 'currentmonthnamegen':
2537 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2538 break;
2539 case 'currentmonthabbrev':
2540 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2541 break;
2542 case 'currentday':
2543 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2544 break;
2545 case 'currentday2':
2546 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2547 break;
2548 case 'localmonth':
2549 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2550 break;
2551 case 'localmonth1':
2552 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2553 break;
2554 case 'localmonthname':
2555 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2556 break;
2557 case 'localmonthnamegen':
2558 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2559 break;
2560 case 'localmonthabbrev':
2561 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2562 break;
2563 case 'localday':
2564 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2565 break;
2566 case 'localday2':
2567 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2568 break;
2569 case 'pagename':
2570 $value = wfEscapeWikiText( $this->mTitle->getText() );
2571 break;
2572 case 'pagenamee':
2573 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2574 break;
2575 case 'fullpagename':
2576 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2577 break;
2578 case 'fullpagenamee':
2579 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2580 break;
2581 case 'subpagename':
2582 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2583 break;
2584 case 'subpagenamee':
2585 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2586 break;
2587 case 'rootpagename':
2588 $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2589 break;
2590 case 'rootpagenamee':
2591 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2592 ' ',
2593 '_',
2594 $this->mTitle->getRootText()
2595 ) ) );
2596 break;
2597 case 'basepagename':
2598 $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2599 break;
2600 case 'basepagenamee':
2601 $value = wfEscapeWikiText( wfUrlencode( str_replace(
2602 ' ',
2603 '_',
2604 $this->mTitle->getBaseText()
2605 ) ) );
2606 break;
2607 case 'talkpagename':
2608 if ( $this->mTitle->canHaveTalkPage() ) {
2609 $talkPage = $this->mTitle->getTalkPage();
2610 $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2611 } else {
2612 $value = '';
2613 }
2614 break;
2615 case 'talkpagenamee':
2616 if ( $this->mTitle->canHaveTalkPage() ) {
2617 $talkPage = $this->mTitle->getTalkPage();
2618 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2619 } else {
2620 $value = '';
2621 }
2622 break;
2623 case 'subjectpagename':
2624 $subjPage = $this->mTitle->getSubjectPage();
2625 $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2626 break;
2627 case 'subjectpagenamee':
2628 $subjPage = $this->mTitle->getSubjectPage();
2629 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2630 break;
2631 case 'pageid': // requested in T25427
2632 $pageid = $this->getTitle()->getArticleID();
2633 if ( $pageid == 0 ) {
2634 # 0 means the page doesn't exist in the database,
2635 # which means the user is previewing a new page.
2636 # The vary-revision flag must be set, because the magic word
2637 # will have a different value once the page is saved.
2638 $this->mOutput->setFlag( 'vary-revision' );
2639 wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2640 }
2641 $value = $pageid ? $pageid : null;
2642 break;
2643 case 'revisionid':
2644 # Let the edit saving system know we should parse the page
2645 # *after* a revision ID has been assigned.
2646 $this->mOutput->setFlag( 'vary-revision-id' );
2647 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2648 $value = $this->mRevisionId;
2649 if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
2650 $value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
2651 $this->mOutput->setSpeculativeRevIdUsed( $value );
2652 }
2653 break;
2654 case 'revisionday':
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__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
2659 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
2660 break;
2661 case 'revisionday2':
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__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
2666 $value = substr( $this->getRevisionTimestamp(), 6, 2 );
2667 break;
2668 case 'revisionmonth':
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__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
2673 $value = substr( $this->getRevisionTimestamp(), 4, 2 );
2674 break;
2675 case 'revisionmonth1':
2676 # Let the edit saving system know we should parse the page
2677 # *after* a revision ID has been assigned. This is for null edits.
2678 $this->mOutput->setFlag( 'vary-revision' );
2679 wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
2680 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
2681 break;
2682 case 'revisionyear':
2683 # Let the edit saving system know we should parse the page
2684 # *after* a revision ID has been assigned. This is for null edits.
2685 $this->mOutput->setFlag( 'vary-revision' );
2686 wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
2687 $value = substr( $this->getRevisionTimestamp(), 0, 4 );
2688 break;
2689 case 'revisiontimestamp':
2690 # Let the edit saving system know we should parse the page
2691 # *after* a revision ID has been assigned. This is for null edits.
2692 $this->mOutput->setFlag( 'vary-revision' );
2693 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2694 $value = $this->getRevisionTimestamp();
2695 break;
2696 case 'revisionuser':
2697 # Let the edit saving system know we should parse the page
2698 # *after* a revision ID has been assigned for null edits.
2699 $this->mOutput->setFlag( 'vary-user' );
2700 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2701 $value = $this->getRevisionUser();
2702 break;
2703 case 'revisionsize':
2704 $value = $this->getRevisionSize();
2705 break;
2706 case 'namespace':
2707 $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2708 break;
2709 case 'namespacee':
2710 $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2711 break;
2712 case 'namespacenumber':
2713 $value = $this->mTitle->getNamespace();
2714 break;
2715 case 'talkspace':
2716 $value = $this->mTitle->canHaveTalkPage()
2717 ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2718 : '';
2719 break;
2720 case 'talkspacee':
2721 $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2722 break;
2723 case 'subjectspace':
2724 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2725 break;
2726 case 'subjectspacee':
2727 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2728 break;
2729 case 'currentdayname':
2730 $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2731 break;
2732 case 'currentyear':
2733 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2734 break;
2735 case 'currenttime':
2736 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2737 break;
2738 case 'currenthour':
2739 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2740 break;
2741 case 'currentweek':
2742 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2743 # int to remove the padding
2744 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2745 break;
2746 case 'currentdow':
2747 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2748 break;
2749 case 'localdayname':
2750 $value = $pageLang->getWeekdayName(
2751 (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2752 );
2753 break;
2754 case 'localyear':
2755 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2756 break;
2757 case 'localtime':
2758 $value = $pageLang->time(
2759 MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2760 false,
2761 false
2762 );
2763 break;
2764 case 'localhour':
2765 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2766 break;
2767 case 'localweek':
2768 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2769 # int to remove the padding
2770 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2771 break;
2772 case 'localdow':
2773 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2774 break;
2775 case 'numberofarticles':
2776 $value = $pageLang->formatNum( SiteStats::articles() );
2777 break;
2778 case 'numberoffiles':
2779 $value = $pageLang->formatNum( SiteStats::images() );
2780 break;
2781 case 'numberofusers':
2782 $value = $pageLang->formatNum( SiteStats::users() );
2783 break;
2784 case 'numberofactiveusers':
2785 $value = $pageLang->formatNum( SiteStats::activeUsers() );
2786 break;
2787 case 'numberofpages':
2788 $value = $pageLang->formatNum( SiteStats::pages() );
2789 break;
2790 case 'numberofadmins':
2791 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2792 break;
2793 case 'numberofedits':
2794 $value = $pageLang->formatNum( SiteStats::edits() );
2795 break;
2796 case 'currenttimestamp':
2797 $value = wfTimestamp( TS_MW, $ts );
2798 break;
2799 case 'localtimestamp':
2800 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2801 break;
2802 case 'currentversion':
2804 break;
2805 case 'articlepath':
2806 return $wgArticlePath;
2807 case 'sitename':
2808 return $wgSitename;
2809 case 'server':
2810 return $wgServer;
2811 case 'servername':
2812 return $wgServerName;
2813 case 'scriptpath':
2814 return $wgScriptPath;
2815 case 'stylepath':
2816 return $wgStylePath;
2817 case 'directionmark':
2818 return $pageLang->getDirMark();
2819 case 'contentlanguage':
2820 global $wgLanguageCode;
2821 return $wgLanguageCode;
2822 case 'pagelanguage':
2823 $value = $pageLang->getCode();
2824 break;
2825 case 'cascadingsources':
2827 break;
2828 default:
2829 $ret = null;
2830 Hooks::run(
2831 'ParserGetVariableValueSwitch',
2832 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2833 );
2834
2835 return $ret;
2836 }
2837
2838 if ( $index ) {
2839 $this->mVarCache[$index] = $value;
2840 }
2841
2842 return $value;
2843 }
2844
2850 public function initialiseVariables() {
2851 $variableIDs = MagicWord::getVariableIDs();
2852 $substIDs = MagicWord::getSubstIDs();
2853
2854 $this->mVariables = new MagicWordArray( $variableIDs );
2855 $this->mSubstWords = new MagicWordArray( $substIDs );
2856 }
2857
2880 public function preprocessToDom( $text, $flags = 0 ) {
2881 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2882 return $dom;
2883 }
2884
2892 public static function splitWhitespace( $s ) {
2893 $ltrimmed = ltrim( $s );
2894 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2895 $trimmed = rtrim( $ltrimmed );
2896 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
2897 if ( $diff > 0 ) {
2898 $w2 = substr( $ltrimmed, -$diff );
2899 } else {
2900 $w2 = '';
2901 }
2902 return [ $w1, $trimmed, $w2 ];
2903 }
2904
2925 public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2926 # Is there any text? Also, Prevent too big inclusions!
2927 $textSize = strlen( $text );
2928 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2929 return $text;
2930 }
2931
2932 if ( $frame === false ) {
2933 $frame = $this->getPreprocessor()->newFrame();
2934 } elseif ( !( $frame instanceof PPFrame ) ) {
2935 wfDebug( __METHOD__ . " called using plain parameters instead of "
2936 . "a PPFrame instance. Creating custom frame.\n" );
2937 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2938 }
2939
2940 $dom = $this->preprocessToDom( $text );
2941 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2942 $text = $frame->expand( $dom, $flags );
2943
2944 return $text;
2945 }
2946
2954 public static function createAssocArgs( $args ) {
2955 $assocArgs = [];
2956 $index = 1;
2957 foreach ( $args as $arg ) {
2958 $eqpos = strpos( $arg, '=' );
2959 if ( $eqpos === false ) {
2960 $assocArgs[$index++] = $arg;
2961 } else {
2962 $name = trim( substr( $arg, 0, $eqpos ) );
2963 $value = trim( substr( $arg, $eqpos + 1 ) );
2964 if ( $value === false ) {
2965 $value = '';
2966 }
2967 if ( $name !== false ) {
2968 $assocArgs[$name] = $value;
2969 }
2970 }
2971 }
2972
2973 return $assocArgs;
2974 }
2975
3002 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3003 # does no harm if $current and $max are present but are unnecessary for the message
3004 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3005 # only during preview, and that would split the parser cache unnecessarily.
3006 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3007 ->text();
3008 $this->mOutput->addWarning( $warning );
3009 $this->addTrackingCategory( "$limitationType-category" );
3010 }
3011
3024 public function braceSubstitution( $piece, $frame ) {
3025 // Flags
3026
3027 // $text has been filled
3028 $found = false;
3029 // wiki markup in $text should be escaped
3030 $nowiki = false;
3031 // $text is HTML, armour it against wikitext transformation
3032 $isHTML = false;
3033 // Force interwiki transclusion to be done in raw mode not rendered
3034 $forceRawInterwiki = false;
3035 // $text is a DOM node needing expansion in a child frame
3036 $isChildObj = false;
3037 // $text is a DOM node needing expansion in the current frame
3038 $isLocalObj = false;
3039
3040 # Title object, where $text came from
3041 $title = false;
3042
3043 # $part1 is the bit before the first |, and must contain only title characters.
3044 # Various prefixes will be stripped from it later.
3045 $titleWithSpaces = $frame->expand( $piece['title'] );
3046 $part1 = trim( $titleWithSpaces );
3047 $titleText = false;
3048
3049 # Original title text preserved for various purposes
3050 $originalTitle = $part1;
3051
3052 # $args is a list of argument nodes, starting from index 0, not including $part1
3053 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3054 # below won't work b/c this $args isn't an object
3055 $args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3056
3057 $profileSection = null; // profile templates
3058
3059 # SUBST
3060 if ( !$found ) {
3061 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3062
3063 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3064 # Decide whether to expand template or keep wikitext as-is.
3065 if ( $this->ot['wiki'] ) {
3066 if ( $substMatch === false ) {
3067 $literal = true; # literal when in PST with no prefix
3068 } else {
3069 $literal = false; # expand when in PST with subst: or safesubst:
3070 }
3071 } else {
3072 if ( $substMatch == 'subst' ) {
3073 $literal = true; # literal when not in PST with plain subst:
3074 } else {
3075 $literal = false; # expand when not in PST with safesubst: or no prefix
3076 }
3077 }
3078 if ( $literal ) {
3079 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3080 $isLocalObj = true;
3081 $found = true;
3082 }
3083 }
3084
3085 # Variables
3086 if ( !$found && $args->getLength() == 0 ) {
3087 $id = $this->mVariables->matchStartToEnd( $part1 );
3088 if ( $id !== false ) {
3089 $text = $this->getVariableValue( $id, $frame );
3090 if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3091 $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3092 }
3093 $found = true;
3094 }
3095 }
3096
3097 # MSG, MSGNW and RAW
3098 if ( !$found ) {
3099 # Check for MSGNW:
3100 $mwMsgnw = MagicWord::get( 'msgnw' );
3101 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3102 $nowiki = true;
3103 } else {
3104 # Remove obsolete MSG:
3105 $mwMsg = MagicWord::get( 'msg' );
3106 $mwMsg->matchStartAndRemove( $part1 );
3107 }
3108
3109 # Check for RAW:
3110 $mwRaw = MagicWord::get( 'raw' );
3111 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3112 $forceRawInterwiki = true;
3113 }
3114 }
3115
3116 # Parser functions
3117 if ( !$found ) {
3118 $colonPos = strpos( $part1, ':' );
3119 if ( $colonPos !== false ) {
3120 $func = substr( $part1, 0, $colonPos );
3121 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3122 $argsLength = $args->getLength();
3123 for ( $i = 0; $i < $argsLength; $i++ ) {
3124 $funcArgs[] = $args->item( $i );
3125 }
3126 try {
3127 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3128 } catch ( Exception $ex ) {
3129 throw $ex;
3130 }
3131
3132 // Extract any forwarded flags
3133 if ( isset( $result['title'] ) ) {
3134 $title = $result['title'];
3135 }
3136 if ( isset( $result['found'] ) ) {
3137 $found = $result['found'];
3138 }
3139 if ( array_key_exists( 'text', $result ) ) {
3140 // a string or null
3141 $text = $result['text'];
3142 }
3143 if ( isset( $result['nowiki'] ) ) {
3144 $nowiki = $result['nowiki'];
3145 }
3146 if ( isset( $result['isHTML'] ) ) {
3147 $isHTML = $result['isHTML'];
3148 }
3149 if ( isset( $result['forceRawInterwiki'] ) ) {
3150 $forceRawInterwiki = $result['forceRawInterwiki'];
3151 }
3152 if ( isset( $result['isChildObj'] ) ) {
3153 $isChildObj = $result['isChildObj'];
3154 }
3155 if ( isset( $result['isLocalObj'] ) ) {
3156 $isLocalObj = $result['isLocalObj'];
3157 }
3158 }
3159 }
3160
3161 # Finish mangling title and then check for loops.
3162 # Set $title to a Title object and $titleText to the PDBK
3163 if ( !$found ) {
3164 $ns = NS_TEMPLATE;
3165 # Split the title into page and subpage
3166 $subpage = '';
3167 $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3168 if ( $part1 !== $relative ) {
3169 $part1 = $relative;
3170 $ns = $this->mTitle->getNamespace();
3171 }
3172 $title = Title::newFromText( $part1, $ns );
3173 if ( $title ) {
3174 $titleText = $title->getPrefixedText();
3175 # Check for language variants if the template is not found
3176 if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3177 $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3178 }
3179 # Do recursion depth check
3180 $limit = $this->mOptions->getMaxTemplateDepth();
3181 if ( $frame->depth >= $limit ) {
3182 $found = true;
3183 $text = '<span class="error">'
3184 . wfMessage( 'parser-template-recursion-depth-warning' )
3185 ->numParams( $limit )->inContentLanguage()->text()
3186 . '</span>';
3187 }
3188 }
3189 }
3190
3191 # Load from database
3192 if ( !$found && $title ) {
3193 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3194 if ( !$title->isExternal() ) {
3195 if ( $title->isSpecialPage()
3196 && $this->mOptions->getAllowSpecialInclusion()
3197 && $this->ot['html']
3198 ) {
3199 $specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3200 // Pass the template arguments as URL parameters.
3201 // "uselang" will have no effect since the Language object
3202 // is forced to the one defined in ParserOptions.
3203 $pageArgs = [];
3204 $argsLength = $args->getLength();
3205 for ( $i = 0; $i < $argsLength; $i++ ) {
3206 $bits = $args->item( $i )->splitArg();
3207 if ( strval( $bits['index'] ) === '' ) {
3208 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3209 $value = trim( $frame->expand( $bits['value'] ) );
3210 $pageArgs[$name] = $value;
3211 }
3212 }
3213
3214 // Create a new context to execute the special page
3216 $context->setTitle( $title );
3217 $context->setRequest( new FauxRequest( $pageArgs ) );
3218 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3219 $context->setUser( $this->getUser() );
3220 } else {
3221 // If this page is cached, then we better not be per user.
3222 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3223 }
3224 $context->setLanguage( $this->mOptions->getUserLangObj() );
3226 $title, $context, $this->getLinkRenderer() );
3227 if ( $ret ) {
3228 $text = $context->getOutput()->getHTML();
3229 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3230 $found = true;
3231 $isHTML = true;
3232 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3233 $this->mOutput->updateRuntimeAdaptiveExpiry(
3234 $specialPage->maxIncludeCacheTime()
3235 );
3236 }
3237 }
3238 } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3239 $found = false; # access denied
3240 wfDebug( __METHOD__ . ": template inclusion denied for " .
3241 $title->getPrefixedDBkey() . "\n" );
3242 } else {
3243 list( $text, $title ) = $this->getTemplateDom( $title );
3244 if ( $text !== false ) {
3245 $found = true;
3246 $isChildObj = true;
3247 }
3248 }
3249
3250 # If the title is valid but undisplayable, make a link to it
3251 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3252 $text = "[[:$titleText]]";
3253 $found = true;
3254 }
3255 } elseif ( $title->isTrans() ) {
3256 # Interwiki transclusion
3257 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3258 $text = $this->interwikiTransclude( $title, 'render' );
3259 $isHTML = true;
3260 } else {
3261 $text = $this->interwikiTransclude( $title, 'raw' );
3262 # Preprocess it like a template
3263 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3264 $isChildObj = true;
3265 }
3266 $found = true;
3267 }
3268
3269 # Do infinite loop check
3270 # This has to be done after redirect resolution to avoid infinite loops via redirects
3271 if ( !$frame->loopCheck( $title ) ) {
3272 $found = true;
3273 $text = '<span class="error">'
3274 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3275 . '</span>';
3276 $this->addTrackingCategory( 'template-loop-category' );
3277 $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3278 wfEscapeWikiText( $titleText ) )->text() );
3279 wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3280 }
3281 }
3282
3283 # If we haven't found text to substitute by now, we're done
3284 # Recover the source wikitext and return it
3285 if ( !$found ) {
3286 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3287 if ( $profileSection ) {
3288 $this->mProfiler->scopedProfileOut( $profileSection );
3289 }
3290 return [ 'object' => $text ];
3291 }
3292
3293 # Expand DOM-style return values in a child frame
3294 if ( $isChildObj ) {
3295 # Clean up argument array
3296 $newFrame = $frame->newChild( $args, $title );
3297
3298 if ( $nowiki ) {
3299 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3300 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3301 # Expansion is eligible for the empty-frame cache
3302 $text = $newFrame->cachedExpand( $titleText, $text );
3303 } else {
3304 # Uncached expansion
3305 $text = $newFrame->expand( $text );
3306 }
3307 }
3308 if ( $isLocalObj && $nowiki ) {
3309 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3310 $isLocalObj = false;
3311 }
3312
3313 if ( $profileSection ) {
3314 $this->mProfiler->scopedProfileOut( $profileSection );
3315 }
3316
3317 # Replace raw HTML by a placeholder
3318 if ( $isHTML ) {
3319 $text = $this->insertStripItem( $text );
3320 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3321 # Escape nowiki-style return values
3322 $text = wfEscapeWikiText( $text );
3323 } elseif ( is_string( $text )
3324 && !$piece['lineStart']
3325 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3326 ) {
3327 # T2529: if the template begins with a table or block-level
3328 # element, it should be treated as beginning a new line.
3329 # This behavior is somewhat controversial.
3330 $text = "\n" . $text;
3331 }
3332
3333 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3334 # Error, oversize inclusion
3335 if ( $titleText !== false ) {
3336 # Make a working, properly escaped link if possible (T25588)
3337 $text = "[[:$titleText]]";
3338 } else {
3339 # This will probably not be a working link, but at least it may
3340 # provide some hint of where the problem is
3341 preg_replace( '/^:/', '', $originalTitle );
3342 $text = "[[:$originalTitle]]";
3343 }
3344 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3345 . 'post-expand include size too large -->' );
3346 $this->limitationWarn( 'post-expand-template-inclusion' );
3347 }
3348
3349 if ( $isLocalObj ) {
3350 $ret = [ 'object' => $text ];
3351 } else {
3352 $ret = [ 'text' => $text ];
3353 }
3354
3355 return $ret;
3356 }
3357
3377 public function callParserFunction( $frame, $function, array $args = [] ) {
3378 global $wgContLang;
3379
3380 # Case sensitive functions
3381 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3382 $function = $this->mFunctionSynonyms[1][$function];
3383 } else {
3384 # Case insensitive functions
3385 $function = $wgContLang->lc( $function );
3386 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3387 $function = $this->mFunctionSynonyms[0][$function];
3388 } else {
3389 return [ 'found' => false ];
3390 }
3391 }
3392
3393 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3394
3395 // Avoid PHP 7.1 warning from passing $this by reference
3396 $parser = $this;
3397
3398 $allArgs = [ &$parser ];
3399 if ( $flags & self::SFH_OBJECT_ARGS ) {
3400 # Convert arguments to PPNodes and collect for appending to $allArgs
3401 $funcArgs = [];
3402 foreach ( $args as $k => $v ) {
3403 if ( $v instanceof PPNode || $k === 0 ) {
3404 $funcArgs[] = $v;
3405 } else {
3406 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3407 }
3408 }
3409
3410 # Add a frame parameter, and pass the arguments as an array
3411 $allArgs[] = $frame;
3412 $allArgs[] = $funcArgs;
3413 } else {
3414 # Convert arguments to plain text and append to $allArgs
3415 foreach ( $args as $k => $v ) {
3416 if ( $v instanceof PPNode ) {
3417 $allArgs[] = trim( $frame->expand( $v ) );
3418 } elseif ( is_int( $k ) && $k >= 0 ) {
3419 $allArgs[] = trim( $v );
3420 } else {
3421 $allArgs[] = trim( "$k=$v" );
3422 }
3423 }
3424 }
3425
3426 $result = call_user_func_array( $callback, $allArgs );
3427
3428 # The interface for function hooks allows them to return a wikitext
3429 # string or an array containing the string and any flags. This mungs
3430 # things around to match what this method should return.
3431 if ( !is_array( $result ) ) {
3432 $result = [
3433 'found' => true,
3434 'text' => $result,
3435 ];
3436 } else {
3437 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3438 $result['text'] = $result[0];
3439 }
3440 unset( $result[0] );
3441 $result += [
3442 'found' => true,
3443 ];
3444 }
3445
3446 $noparse = true;
3447 $preprocessFlags = 0;
3448 if ( isset( $result['noparse'] ) ) {
3449 $noparse = $result['noparse'];
3450 }
3451 if ( isset( $result['preprocessFlags'] ) ) {
3452 $preprocessFlags = $result['preprocessFlags'];
3453 }
3454
3455 if ( !$noparse ) {
3456 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3457 $result['isChildObj'] = true;
3458 }
3459
3460 return $result;
3461 }
3462
3471 public function getTemplateDom( $title ) {
3472 $cacheTitle = $title;
3473 $titleText = $title->getPrefixedDBkey();
3474
3475 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3476 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3477 $title = Title::makeTitle( $ns, $dbk );
3478 $titleText = $title->getPrefixedDBkey();
3479 }
3480 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3481 return [ $this->mTplDomCache[$titleText], $title ];
3482 }
3483
3484 # Cache miss, go to the database
3485 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3486
3487 if ( $text === false ) {
3488 $this->mTplDomCache[$titleText] = false;
3489 return [ false, $title ];
3490 }
3491
3492 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3493 $this->mTplDomCache[$titleText] = $dom;
3494
3495 if ( !$title->equals( $cacheTitle ) ) {
3496 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3497 [ $title->getNamespace(), $title->getDBkey() ];
3498 }
3499
3500 return [ $dom, $title ];
3501 }
3502
3514 public function fetchCurrentRevisionOfTitle( $title ) {
3515 $cacheKey = $title->getPrefixedDBkey();
3516 if ( !$this->currentRevisionCache ) {
3517 $this->currentRevisionCache = new MapCacheLRU( 100 );
3518 }
3519 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3520 $this->currentRevisionCache->set( $cacheKey,
3521 // Defaults to Parser::statelessFetchRevision()
3522 call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3523 );
3524 }
3525 return $this->currentRevisionCache->get( $cacheKey );
3526 }
3527
3537 public static function statelessFetchRevision( Title $title, $parser = false ) {
3538 $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
3539
3540 return $rev;
3541 }
3542
3548 public function fetchTemplateAndTitle( $title ) {
3549 // Defaults to Parser::statelessFetchTemplate()
3550 $templateCb = $this->mOptions->getTemplateCallback();
3551 $stuff = call_user_func( $templateCb, $title, $this );
3552 // We use U+007F DELETE to distinguish strip markers from regular text.
3553 $text = $stuff['text'];
3554 if ( is_string( $stuff['text'] ) ) {
3555 $text = strtr( $text, "\x7f", "?" );
3556 }
3557 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3558 if ( isset( $stuff['deps'] ) ) {
3559 foreach ( $stuff['deps'] as $dep ) {
3560 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3561 if ( $dep['title']->equals( $this->getTitle() ) ) {
3562 // If we transclude ourselves, the final result
3563 // will change based on the new version of the page
3564 $this->mOutput->setFlag( 'vary-revision' );
3565 }
3566 }
3567 }
3568 return [ $text, $finalTitle ];
3569 }
3570
3576 public function fetchTemplate( $title ) {
3577 return $this->fetchTemplateAndTitle( $title )[0];
3578 }
3579
3589 public static function statelessFetchTemplate( $title, $parser = false ) {
3590 $text = $skip = false;
3591 $finalTitle = $title;
3592 $deps = [];
3593
3594 # Loop to fetch the article, with up to 1 redirect
3595 // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
3596 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3597 # Give extensions a chance to select the revision instead
3598 $id = false; # Assume current
3599 Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3600 [ $parser, $title, &$skip, &$id ] );
3601
3602 if ( $skip ) {
3603 $text = false;
3604 $deps[] = [
3605 'title' => $title,
3606 'page_id' => $title->getArticleID(),
3607 'rev_id' => null
3608 ];
3609 break;
3610 }
3611 # Get the revision
3612 if ( $id ) {
3613 $rev = Revision::newFromId( $id );
3614 } elseif ( $parser ) {
3615 $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3616 } else {
3617 $rev = Revision::newFromTitle( $title );
3618 }
3619 $rev_id = $rev ? $rev->getId() : 0;
3620 # If there is no current revision, there is no page
3621 if ( $id === false && !$rev ) {
3622 $linkCache = LinkCache::singleton();
3623 $linkCache->addBadLinkObj( $title );
3624 }
3625
3626 $deps[] = [
3627 'title' => $title,
3628 'page_id' => $title->getArticleID(),
3629 'rev_id' => $rev_id ];
3630 if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3631 # We fetched a rev from a different title; register it too...
3632 $deps[] = [
3633 'title' => $rev->getTitle(),
3634 'page_id' => $rev->getPage(),
3635 'rev_id' => $rev_id ];
3636 }
3637
3638 if ( $rev ) {
3639 $content = $rev->getContent();
3640 $text = $content ? $content->getWikitextForTransclusion() : null;
3641
3642 Hooks::run( 'ParserFetchTemplate',
3643 [ $parser, $title, $rev, &$text, &$deps ] );
3644
3645 if ( $text === false || $text === null ) {
3646 $text = false;
3647 break;
3648 }
3649 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3650 global $wgContLang;
3651 $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3652 if ( !$message->exists() ) {
3653 $text = false;
3654 break;
3655 }
3656 $content = $message->content();
3657 $text = $message->plain();
3658 } else {
3659 break;
3660 }
3661 if ( !$content ) {
3662 break;
3663 }
3664 # Redirect?
3665 $finalTitle = $title;
3666 $title = $content->getRedirectTarget();
3667 }
3668 return [
3669 'text' => $text,
3670 'finalTitle' => $finalTitle,
3671 'deps' => $deps ];
3672 }
3673
3681 public function fetchFile( $title, $options = [] ) {
3682 return $this->fetchFileAndTitle( $title, $options )[0];
3683 }
3684
3692 public function fetchFileAndTitle( $title, $options = [] ) {
3693 $file = $this->fetchFileNoRegister( $title, $options );
3694
3695 $time = $file ? $file->getTimestamp() : false;
3696 $sha1 = $file ? $file->getSha1() : false;
3697 # Register the file as a dependency...
3698 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3699 if ( $file && !$title->equals( $file->getTitle() ) ) {
3700 # Update fetched file title
3701 $title = $file->getTitle();
3702 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3703 }
3704 return [ $file, $title ];
3705 }
3706
3717 protected function fetchFileNoRegister( $title, $options = [] ) {
3718 if ( isset( $options['broken'] ) ) {
3719 $file = false; // broken thumbnail forced by hook
3720 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3721 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3722 } else { // get by (name,timestamp)
3723 $file = wfFindFile( $title, $options );
3724 }
3725 return $file;
3726 }
3727
3736 public function interwikiTransclude( $title, $action ) {
3738
3740 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3741 }
3742
3743 $url = $title->getFullURL( [ 'action' => $action ] );
3744
3745 if ( strlen( $url ) > 255 ) {
3746 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3747 }
3748 return $this->fetchScaryTemplateMaybeFromCache( $url );
3749 }
3750
3755 public function fetchScaryTemplateMaybeFromCache( $url ) {
3757 $dbr = wfGetDB( DB_REPLICA );
3758 $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3759 $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3760 [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
3761 if ( $obj ) {
3762 return $obj->tc_contents;
3763 }
3764
3765 $req = MWHttpRequest::factory( $url, [], __METHOD__ );
3766 $status = $req->execute(); // Status object
3767 if ( $status->isOK() ) {
3768 $text = $req->getContent();
3769 } elseif ( $req->getStatus() != 200 ) {
3770 // Though we failed to fetch the content, this status is useless.
3771 return wfMessage( 'scarytranscludefailed-httpstatus' )
3772 ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3773 } else {
3774 return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3775 }
3776
3777 $dbw = wfGetDB( DB_MASTER );
3778 $dbw->replace( 'transcache', [ 'tc_url' ], [
3779 'tc_url' => $url,
3780 'tc_time' => $dbw->timestamp( time() ),
3781 'tc_contents' => $text
3782 ] );
3783 return $text;
3784 }
3785
3795 public function argSubstitution( $piece, $frame ) {
3796 $error = false;
3797 $parts = $piece['parts'];
3798 $nameWithSpaces = $frame->expand( $piece['title'] );
3799 $argName = trim( $nameWithSpaces );
3800 $object = false;
3801 $text = $frame->getArgument( $argName );
3802 if ( $text === false && $parts->getLength() > 0
3803 && ( $this->ot['html']
3804 || $this->ot['pre']
3805 || ( $this->ot['wiki'] && $frame->isTemplate() )
3806 )
3807 ) {
3808 # No match in frame, use the supplied default
3809 $object = $parts->item( 0 )->getChildren();
3810 }
3811 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3812 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3813 $this->limitationWarn( 'post-expand-template-argument' );
3814 }
3815
3816 if ( $text === false && $object === false ) {
3817 # No match anywhere
3818 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3819 }
3820 if ( $error !== false ) {
3821 $text .= $error;
3822 }
3823 if ( $object !== false ) {
3824 $ret = [ 'object' => $object ];
3825 } else {
3826 $ret = [ 'text' => $text ];
3827 }
3828
3829 return $ret;
3830 }
3831
3847 public function extensionSubstitution( $params, $frame ) {
3848 static $errorStr = '<span class="error">';
3849 static $errorLen = 20;
3850
3851 $name = $frame->expand( $params['name'] );
3852 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3853 // Probably expansion depth or node count exceeded. Just punt the
3854 // error up.
3855 return $name;
3856 }
3857
3858 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3859 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3860 // See above
3861 return $attrText;
3862 }
3863
3864 // We can't safely check if the expansion for $content resulted in an
3865 // error, because the content could happen to be the error string
3866 // (T149622).
3867 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3868
3869 $marker = self::MARKER_PREFIX . "-$name-"
3870 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3871
3872 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3873 ( $this->ot['html'] || $this->ot['pre'] );
3874 if ( $isFunctionTag ) {
3875 $markerType = 'none';
3876 } else {
3877 $markerType = 'general';
3878 }
3879 if ( $this->ot['html'] || $isFunctionTag ) {
3880 $name = strtolower( $name );
3881 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3882 if ( isset( $params['attributes'] ) ) {
3883 $attributes = $attributes + $params['attributes'];
3884 }
3885
3886 if ( isset( $this->mTagHooks[$name] ) ) {
3887 $output = call_user_func_array( $this->mTagHooks[$name],
3888 [ $content, $attributes, $this, $frame ] );
3889 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3890 list( $callback, ) = $this->mFunctionTagHooks[$name];
3891
3892 // Avoid PHP 7.1 warning from passing $this by reference
3893 $parser = $this;
3894 $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
3895 } else {
3896 $output = '<span class="error">Invalid tag extension name: ' .
3897 htmlspecialchars( $name ) . '</span>';
3898 }
3899
3900 if ( is_array( $output ) ) {
3901 // Extract flags
3902 $flags = $output;
3903 $output = $flags[0];
3904 if ( isset( $flags['markerType'] ) ) {
3905 $markerType = $flags['markerType'];
3906 }
3907 }
3908 } else {
3909 if ( is_null( $attrText ) ) {
3910 $attrText = '';
3911 }
3912 if ( isset( $params['attributes'] ) ) {
3913 foreach ( $params['attributes'] as $attrName => $attrValue ) {
3914 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3915 htmlspecialchars( $attrValue ) . '"';
3916 }
3917 }
3918 if ( $content === null ) {
3919 $output = "<$name$attrText/>";
3920 } else {
3921 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3922 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3923 // See above
3924 return $close;
3925 }
3926 $output = "<$name$attrText>$content$close";
3927 }
3928 }
3929
3930 if ( $markerType === 'none' ) {
3931 return $output;
3932 } elseif ( $markerType === 'nowiki' ) {
3933 $this->mStripState->addNoWiki( $marker, $output );
3934 } elseif ( $markerType === 'general' ) {
3935 $this->mStripState->addGeneral( $marker, $output );
3936 } else {
3937 throw new MWException( __METHOD__ . ': invalid marker type' );
3938 }
3939 return $marker;
3940 }
3941
3949 public function incrementIncludeSize( $type, $size ) {
3950 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3951 return false;
3952 } else {
3953 $this->mIncludeSizes[$type] += $size;
3954 return true;
3955 }
3956 }
3957
3964 $this->mExpensiveFunctionCount++;
3965 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3966 }
3967
3976 public function doDoubleUnderscore( $text ) {
3977 # The position of __TOC__ needs to be recorded
3978 $mw = MagicWord::get( 'toc' );
3979 if ( $mw->match( $text ) ) {
3980 $this->mShowToc = true;
3981 $this->mForceTocPosition = true;
3982
3983 # Set a placeholder. At the end we'll fill it in with the TOC.
3984 $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
3985
3986 # Only keep the first one.
3987 $text = $mw->replace( '', $text );
3988 }
3989
3990 # Now match and remove the rest of them
3992 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
3993
3994 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
3995 $this->mOutput->mNoGallery = true;
3996 }
3997 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
3998 $this->mShowToc = false;
3999 }
4000 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4001 && $this->mTitle->getNamespace() == NS_CATEGORY
4002 ) {
4003 $this->addTrackingCategory( 'hidden-category-category' );
4004 }
4005 # (T10068) Allow control over whether robots index a page.
4006 # __INDEX__ always overrides __NOINDEX__, see T16899
4007 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4008 $this->mOutput->setIndexPolicy( 'noindex' );
4009 $this->addTrackingCategory( 'noindex-category' );
4010 }
4011 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4012 $this->mOutput->setIndexPolicy( 'index' );
4013 $this->addTrackingCategory( 'index-category' );
4014 }
4015
4016 # Cache all double underscores in the database
4017 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4018 $this->mOutput->setProperty( $key, '' );
4019 }
4020
4021 return $text;
4022 }
4023
4029 public function addTrackingCategory( $msg ) {
4030 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4031 }
4032
4049 public function formatHeadings( $text, $origText, $isMain = true ) {
4050 global $wgMaxTocLevel;
4051
4052 # Inhibit editsection links if requested in the page
4053 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4054 $maybeShowEditLink = false;
4055 } else {
4056 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4057 }
4058
4059 # Get all headlines for numbering them and adding funky stuff like [edit]
4060 # links - this is for later, but we need the number of headlines right now
4061 # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
4062 # be trimmed here since whitespace in HTML headings is significant.
4063 $matches = [];
4064 $numMatches = preg_match_all(
4065 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4066 $text,
4067 $matches
4068 );
4069
4070 # if there are fewer than 4 headlines in the article, do not show TOC
4071 # unless it's been explicitly enabled.
4072 $enoughToc = $this->mShowToc &&
4073 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4074
4075 # Allow user to stipulate that a page should have a "new section"
4076 # link added via __NEWSECTIONLINK__
4077 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4078 $this->mOutput->setNewSection( true );
4079 }
4080
4081 # Allow user to remove the "new section"
4082 # link via __NONEWSECTIONLINK__
4083 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4084 $this->mOutput->hideNewSection( true );
4085 }
4086
4087 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4088 # override above conditions and always show TOC above first header
4089 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4090 $this->mShowToc = true;
4091 $enoughToc = true;
4092 }
4093
4094 # headline counter
4095 $headlineCount = 0;
4096 $numVisible = 0;
4097
4098 # Ugh .. the TOC should have neat indentation levels which can be
4099 # passed to the skin functions. These are determined here
4100 $toc = '';
4101 $full = '';
4102 $head = [];
4103 $sublevelCount = [];
4104 $levelCount = [];
4105 $level = 0;
4106 $prevlevel = 0;
4107 $toclevel = 0;
4108 $prevtoclevel = 0;
4109 $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4110 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4111 $oldType = $this->mOutputType;
4112 $this->setOutputType( self::OT_WIKI );
4113 $frame = $this->getPreprocessor()->newFrame();
4114 $root = $this->preprocessToDom( $origText );
4115 $node = $root->getFirstChild();
4116 $byteOffset = 0;
4117 $tocraw = [];
4118 $refers = [];
4119
4120 $headlines = $numMatches !== false ? $matches[3] : [];
4121
4122 foreach ( $headlines as $headline ) {
4123 $isTemplate = false;
4124 $titleText = false;
4125 $sectionIndex = false;
4126 $numbering = '';
4127 $markerMatches = [];
4128 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4129 $serial = $markerMatches[1];
4130 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4131 $isTemplate = ( $titleText != $baseTitleText );
4132 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4133 }
4134
4135 if ( $toclevel ) {
4136 $prevlevel = $level;
4137 }
4138 $level = $matches[1][$headlineCount];
4139
4140 if ( $level > $prevlevel ) {
4141 # Increase TOC level
4142 $toclevel++;
4143 $sublevelCount[$toclevel] = 0;
4144 if ( $toclevel < $wgMaxTocLevel ) {
4145 $prevtoclevel = $toclevel;
4146 $toc .= Linker::tocIndent();
4147 $numVisible++;
4148 }
4149 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4150 # Decrease TOC level, find level to jump to
4151
4152 for ( $i = $toclevel; $i > 0; $i-- ) {
4153 if ( $levelCount[$i] == $level ) {
4154 # Found last matching level
4155 $toclevel = $i;
4156 break;
4157 } elseif ( $levelCount[$i] < $level ) {
4158 # Found first matching level below current level
4159 $toclevel = $i + 1;
4160 break;
4161 }
4162 }
4163 if ( $i == 0 ) {
4164 $toclevel = 1;
4165 }
4166 if ( $toclevel < $wgMaxTocLevel ) {
4167 if ( $prevtoclevel < $wgMaxTocLevel ) {
4168 # Unindent only if the previous toc level was shown :p
4169 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4170 $prevtoclevel = $toclevel;
4171 } else {
4172 $toc .= Linker::tocLineEnd();
4173 }
4174 }
4175 } else {
4176 # No change in level, end TOC line
4177 if ( $toclevel < $wgMaxTocLevel ) {
4178 $toc .= Linker::tocLineEnd();
4179 }
4180 }
4181
4182 $levelCount[$toclevel] = $level;
4183
4184 # count number of headlines for each level
4185 $sublevelCount[$toclevel]++;
4186 $dot = 0;
4187 for ( $i = 1; $i <= $toclevel; $i++ ) {
4188 if ( !empty( $sublevelCount[$i] ) ) {
4189 if ( $dot ) {
4190 $numbering .= '.';
4191 }
4192 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4193 $dot = 1;
4194 }
4195 }
4196
4197 # The safe header is a version of the header text safe to use for links
4198
4199 # Remove link placeholders by the link text.
4200 # <!--LINK number-->
4201 # turns into
4202 # link text with suffix
4203 # Do this before unstrip since link text can contain strip markers
4204 $safeHeadline = $this->replaceLinkHoldersText( $headline );
4205
4206 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4207 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4208
4209 # Strip out HTML (first regex removes any tag not allowed)
4210 # Allowed tags are:
4211 # * <sup> and <sub> (T10393)
4212 # * <i> (T28375)
4213 # * <b> (r105284)
4214 # * <bdi> (T74884)
4215 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4216 # * <s> and <strike> (T35715)
4217 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4218 # to allow setting directionality in toc items.
4219 $tocline = preg_replace(
4220 [
4221 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4222 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4223 ],
4224 [ '', '<$1>' ],
4225 $safeHeadline
4226 );
4227
4228 # Strip '<span></span>', which is the result from the above if
4229 # <span id="foo"></span> is used to produce an additional anchor
4230 # for a section.
4231 $tocline = str_replace( '<span></span>', '', $tocline );
4232
4233 $tocline = trim( $tocline );
4234
4235 # For the anchor, strip out HTML-y stuff period
4236 $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4237 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4238
4239 # Save headline for section edit hint before it's escaped
4240 $headlineHint = $safeHeadline;
4241
4242 # Decode HTML entities
4243 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4244
4245 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4246
4247 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4248 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4249 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4250 if ( $fallbackHeadline === $safeHeadline ) {
4251 # No reason to have both (in fact, we can't)
4252 $fallbackHeadline = false;
4253 }
4254
4255 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4256 # @todo FIXME: We may be changing them depending on the current locale.
4257 $arrayKey = strtolower( $safeHeadline );
4258 if ( $fallbackHeadline === false ) {
4259 $fallbackArrayKey = false;
4260 } else {
4261 $fallbackArrayKey = strtolower( $fallbackHeadline );
4262 }
4263
4264 # Create the anchor for linking from the TOC to the section
4265 $anchor = $safeHeadline;
4266 $fallbackAnchor = $fallbackHeadline;
4267 if ( isset( $refers[$arrayKey] ) ) {
4268 // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
4269 for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4270 $anchor .= "_$i";
4271 $linkAnchor .= "_$i";
4272 $refers["${arrayKey}_$i"] = true;
4273 } else {
4274 $refers[$arrayKey] = true;
4275 }
4276 if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4277 // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
4278 for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4279 $fallbackAnchor .= "_$i";
4280 $refers["${fallbackArrayKey}_$i"] = true;
4281 } else {
4282 $refers[$fallbackArrayKey] = true;
4283 }
4284
4285 # Don't number the heading if it is the only one (looks silly)
4286 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4287 # the two are different if the line contains a link
4288 $headline = Html::element(
4289 'span',
4290 [ 'class' => 'mw-headline-number' ],
4291 $numbering
4292 ) . ' ' . $headline;
4293 }
4294
4295 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4296 $toc .= Linker::tocLine( $linkAnchor, $tocline,
4297 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4298 }
4299
4300 # Add the section to the section tree
4301 # Find the DOM node for this header
4302 $noOffset = ( $isTemplate || $sectionIndex === false );
4303 while ( $node && !$noOffset ) {
4304 if ( $node->getName() === 'h' ) {
4305 $bits = $node->splitHeading();
4306 if ( $bits['i'] == $sectionIndex ) {
4307 break;
4308 }
4309 }
4310 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4311 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4312 $node = $node->getNextSibling();
4313 }
4314 $tocraw[] = [
4315 'toclevel' => $toclevel,
4316 'level' => $level,
4317 'line' => $tocline,
4318 'number' => $numbering,
4319 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4320 'fromtitle' => $titleText,
4321 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4322 'anchor' => $anchor,
4323 ];
4324
4325 # give headline the correct <h#> tag
4326 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4327 // Output edit section links as markers with styles that can be customized by skins
4328 if ( $isTemplate ) {
4329 # Put a T flag in the section identifier, to indicate to extractSections()
4330 # that sections inside <includeonly> should be counted.
4331 $editsectionPage = $titleText;
4332 $editsectionSection = "T-$sectionIndex";
4333 $editsectionContent = null;
4334 } else {
4335 $editsectionPage = $this->mTitle->getPrefixedText();
4336 $editsectionSection = $sectionIndex;
4337 $editsectionContent = $headlineHint;
4338 }
4339 // We use a bit of pesudo-xml for editsection markers. The
4340 // language converter is run later on. Using a UNIQ style marker
4341 // leads to the converter screwing up the tokens when it
4342 // converts stuff. And trying to insert strip tags fails too. At
4343 // this point all real inputted tags have already been escaped,
4344 // so we don't have to worry about a user trying to input one of
4345 // these markers directly. We use a page and section attribute
4346 // to stop the language converter from converting these
4347 // important bits of data, but put the headline hint inside a
4348 // content block because the language converter is supposed to
4349 // be able to convert that piece of data.
4350 // Gets replaced with html in ParserOutput::getText
4351 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4352 $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4353 if ( $editsectionContent !== null ) {
4354 $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4355 } else {
4356 $editlink .= '/>';
4357 }
4358 } else {
4359 $editlink = '';
4360 }
4361 $head[$headlineCount] = Linker::makeHeadline( $level,
4362 $matches['attrib'][$headlineCount], $anchor, $headline,
4363 $editlink, $fallbackAnchor );
4364
4365 $headlineCount++;
4366 }
4367
4368 $this->setOutputType( $oldType );
4369
4370 # Never ever show TOC if no headers
4371 if ( $numVisible < 1 ) {
4372 $enoughToc = false;
4373 }
4374
4375 if ( $enoughToc ) {
4376 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4377 $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4378 }
4379 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4380 $this->mOutput->setTOCHTML( $toc );
4381 $toc = self::TOC_START . $toc . self::TOC_END;
4382 }
4383
4384 if ( $isMain ) {
4385 $this->mOutput->setSections( $tocraw );
4386 }
4387
4388 # split up and insert constructed headlines
4389 $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4390 $i = 0;
4391
4392 // build an array of document sections
4393 $sections = [];
4394 foreach ( $blocks as $block ) {
4395 // $head is zero-based, sections aren't.
4396 if ( empty( $head[$i - 1] ) ) {
4397 $sections[$i] = $block;
4398 } else {
4399 $sections[$i] = $head[$i - 1] . $block;
4400 }
4401
4412 Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4413
4414 $i++;
4415 }
4416
4417 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4418 // append the TOC at the beginning
4419 // Top anchor now in skin
4420 $sections[0] = $sections[0] . $toc . "\n";
4421 }
4422
4423 $full .= implode( '', $sections );
4424
4425 if ( $this->mForceTocPosition ) {
4426 return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4427 } else {
4428 return $full;
4429 }
4430 }
4431
4443 public function preSaveTransform( $text, Title $title, User $user,
4444 ParserOptions $options, $clearState = true
4445 ) {
4446 if ( $clearState ) {
4447 $magicScopeVariable = $this->lock();
4448 }
4449 $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4450 $this->setUser( $user );
4451
4452 // Strip U+0000 NULL (T159174)
4453 $text = str_replace( "\000", '', $text );
4454
4455 // We still normalize line endings for backwards-compatibility
4456 // with other code that just calls PST, but this should already
4457 // be handled in TextContent subclasses
4458 $text = TextContent::normalizeLineEndings( $text );
4459
4460 if ( $options->getPreSaveTransform() ) {
4461 $text = $this->pstPass2( $text, $user );
4462 }
4463 $text = $this->mStripState->unstripBoth( $text );
4464
4465 $this->setUser( null ); # Reset
4466
4467 return $text;
4468 }
4469
4478 private function pstPass2( $text, $user ) {
4479 global $wgContLang;
4480
4481 # Note: This is the timestamp saved as hardcoded wikitext to
4482 # the database, we use $wgContLang here in order to give
4483 # everyone the same signature and use the default one rather
4484 # than the one selected in each user's preferences.
4485 # (see also T14815)
4486 $ts = $this->mOptions->getTimestamp();
4487 $timestamp = MWTimestamp::getLocalInstance( $ts );
4488 $ts = $timestamp->format( 'YmdHis' );
4489 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4490
4491 $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4492
4493 # Variable replacement
4494 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4495 $text = $this->replaceVariables( $text );
4496
4497 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4498 # which may corrupt this parser instance via its wfMessage()->text() call-
4499
4500 # Signatures
4501 if ( strpos( $text, '~~~' ) !== false ) {
4502 $sigText = $this->getUserSig( $user );
4503 $text = strtr( $text, [
4504 '~~~~~' => $d,
4505 '~~~~' => "$sigText $d",
4506 '~~~' => $sigText
4507 ] );
4508 # The main two signature forms used above are time-sensitive
4509 $this->mOutput->setFlag( 'user-signature' );
4510 }
4511
4512 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4513 $tc = '[' . Title::legalChars() . ']';
4514 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4515
4516 // [[ns:page (context)|]]
4517 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4518 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4519 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4520 // [[ns:page (context), context|]] (using either single or double-width comma)
4521 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4522 // [[|page]] (reverse pipe trick: add context from page title)
4523 $p2 = "/\[\[\\|($tc+)]]/";
4524
4525 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4526 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4527 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4528 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4529
4530 $t = $this->mTitle->getText();
4531 $m = [];
4532 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4533 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4534 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4535 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4536 } else {
4537 # if there's no context, don't bother duplicating the title
4538 $text = preg_replace( $p2, '[[\\1]]', $text );
4539 }
4540
4541 return $text;
4542 }
4543
4558 public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4559 global $wgMaxSigChars;
4560
4561 $username = $user->getName();
4562
4563 # If not given, retrieve from the user object.
4564 if ( $nickname === false ) {
4565 $nickname = $user->getOption( 'nickname' );
4566 }
4567
4568 if ( is_null( $fancySig ) ) {
4569 $fancySig = $user->getBoolOption( 'fancysig' );
4570 }
4571
4572 $nickname = $nickname == null ? $username : $nickname;
4573
4574 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4575 $nickname = $username;
4576 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4577 } elseif ( $fancySig !== false ) {
4578 # Sig. might contain markup; validate this
4579 if ( $this->validateSig( $nickname ) !== false ) {
4580 # Validated; clean up (if needed) and return it
4581 return $this->cleanSig( $nickname, true );
4582 } else {
4583 # Failed to validate; fall back to the default
4584 $nickname = $username;
4585 wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4586 }
4587 }
4588
4589 # Make sure nickname doesnt get a sig in a sig
4590 $nickname = self::cleanSigInSig( $nickname );
4591
4592 # If we're still here, make it a link to the user page
4593 $userText = wfEscapeWikiText( $username );
4594 $nickText = wfEscapeWikiText( $nickname );
4595 $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4596
4597 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4598 ->title( $this->getTitle() )->text();
4599 }
4600
4607 public function validateSig( $text ) {
4608 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4609 }
4610
4621 public function cleanSig( $text, $parsing = false ) {
4622 if ( !$parsing ) {
4623 global $wgTitle;
4624 $magicScopeVariable = $this->lock();
4625 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4626 }
4627
4628 # Option to disable this feature
4629 if ( !$this->mOptions->getCleanSignatures() ) {
4630 return $text;
4631 }
4632
4633 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4634 # => Move this logic to braceSubstitution()
4635 $substWord = MagicWord::get( 'subst' );
4636 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4637 $substText = '{{' . $substWord->getSynonym( 0 );
4638
4639 $text = preg_replace( $substRegex, $substText, $text );
4640 $text = self::cleanSigInSig( $text );
4641 $dom = $this->preprocessToDom( $text );
4642 $frame = $this->getPreprocessor()->newFrame();
4643 $text = $frame->expand( $dom );
4644
4645 if ( !$parsing ) {
4646 $text = $this->mStripState->unstripBoth( $text );
4647 }
4648
4649 return $text;
4650 }
4651
4658 public static function cleanSigInSig( $text ) {
4659 $text = preg_replace( '/~{3,5}/', '', $text );
4660 return $text;
4661 }
4662
4672 public function startExternalParse( Title $title = null, ParserOptions $options,
4673 $outputType, $clearState = true
4674 ) {
4675 $this->startParse( $title, $options, $outputType, $clearState );
4676 }
4677
4684 private function startParse( Title $title = null, ParserOptions $options,
4685 $outputType, $clearState = true
4686 ) {
4687 $this->setTitle( $title );
4688 $this->mOptions = $options;
4689 $this->setOutputType( $outputType );
4690 if ( $clearState ) {
4691 $this->clearState();
4692 }
4693 }
4694
4703 public function transformMsg( $text, $options, $title = null ) {
4704 static $executing = false;
4705
4706 # Guard against infinite recursion
4707 if ( $executing ) {
4708 return $text;
4709 }
4710 $executing = true;
4711
4712 if ( !$title ) {
4713 global $wgTitle;
4714 $title = $wgTitle;
4715 }
4716
4717 $text = $this->preprocess( $text, $title, $options );
4718
4719 $executing = false;
4720 return $text;
4721 }
4722
4747 public function setHook( $tag, callable $callback ) {
4748 $tag = strtolower( $tag );
4749 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4750 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4751 }
4752 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4753 $this->mTagHooks[$tag] = $callback;
4754 if ( !in_array( $tag, $this->mStripList ) ) {
4755 $this->mStripList[] = $tag;
4756 }
4757
4758 return $oldVal;
4759 }
4760
4778 public function setTransparentTagHook( $tag, callable $callback ) {
4779 $tag = strtolower( $tag );
4780 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4781 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4782 }
4783 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4784 $this->mTransparentTagHooks[$tag] = $callback;
4785
4786 return $oldVal;
4787 }
4788
4792 public function clearTagHooks() {
4793 $this->mTagHooks = [];
4794 $this->mFunctionTagHooks = [];
4795 $this->mStripList = $this->mDefaultStripList;
4796 }
4797
4841 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4842 global $wgContLang;
4843
4844 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4845 $this->mFunctionHooks[$id] = [ $callback, $flags ];
4846
4847 # Add to function cache
4848 $mw = MagicWord::get( $id );
4849 if ( !$mw ) {
4850 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4851 }
4852
4853 $synonyms = $mw->getSynonyms();
4854 $sensitive = intval( $mw->isCaseSensitive() );
4855
4856 foreach ( $synonyms as $syn ) {
4857 # Case
4858 if ( !$sensitive ) {
4859 $syn = $wgContLang->lc( $syn );
4860 }
4861 # Add leading hash
4862 if ( !( $flags & self::SFH_NO_HASH ) ) {
4863 $syn = '#' . $syn;
4864 }
4865 # Remove trailing colon
4866 if ( substr( $syn, -1, 1 ) === ':' ) {
4867 $syn = substr( $syn, 0, -1 );
4868 }
4869 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4870 }
4871 return $oldVal;
4872 }
4873
4879 public function getFunctionHooks() {
4880 return array_keys( $this->mFunctionHooks );
4881 }
4882
4893 public function setFunctionTagHook( $tag, callable $callback, $flags ) {
4894 $tag = strtolower( $tag );
4895 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4896 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4897 }
4898 $old = isset( $this->mFunctionTagHooks[$tag] ) ?
4899 $this->mFunctionTagHooks[$tag] : null;
4900 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4901
4902 if ( !in_array( $tag, $this->mStripList ) ) {
4903 $this->mStripList[] = $tag;
4904 }
4905
4906 return $old;
4907 }
4908
4916 public function replaceLinkHolders( &$text, $options = 0 ) {
4917 $this->mLinkHolders->replace( $text );
4918 }
4919
4927 public function replaceLinkHoldersText( $text ) {
4928 return $this->mLinkHolders->replaceText( $text );
4929 }
4930
4944 public function renderImageGallery( $text, $params ) {
4945 $mode = false;
4946 if ( isset( $params['mode'] ) ) {
4947 $mode = $params['mode'];
4948 }
4949
4950 try {
4951 $ig = ImageGalleryBase::factory( $mode );
4952 } catch ( Exception $e ) {
4953 // If invalid type set, fallback to default.
4954 $ig = ImageGalleryBase::factory( false );
4955 }
4956
4957 $ig->setContextTitle( $this->mTitle );
4958 $ig->setShowBytes( false );
4959 $ig->setShowDimensions( false );
4960 $ig->setShowFilename( false );
4961 $ig->setParser( $this );
4962 $ig->setHideBadImages();
4963 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
4964
4965 if ( isset( $params['showfilename'] ) ) {
4966 $ig->setShowFilename( true );
4967 } else {
4968 $ig->setShowFilename( false );
4969 }
4970 if ( isset( $params['caption'] ) ) {
4971 $caption = $params['caption'];
4972 $caption = htmlspecialchars( $caption );
4973 $caption = $this->replaceInternalLinks( $caption );
4974 $ig->setCaptionHtml( $caption );
4975 }
4976 if ( isset( $params['perrow'] ) ) {
4977 $ig->setPerRow( $params['perrow'] );
4978 }
4979 if ( isset( $params['widths'] ) ) {
4980 $ig->setWidths( $params['widths'] );
4981 }
4982 if ( isset( $params['heights'] ) ) {
4983 $ig->setHeights( $params['heights'] );
4984 }
4985 $ig->setAdditionalOptions( $params );
4986
4987 // Avoid PHP 7.1 warning from passing $this by reference
4988 $parser = $this;
4989 Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
4990
4991 $lines = StringUtils::explode( "\n", $text );
4992 foreach ( $lines as $line ) {
4993 # match lines like these:
4994 # Image:someimage.jpg|This is some image
4995 $matches = [];
4996 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
4997 # Skip empty lines
4998 if ( count( $matches ) == 0 ) {
4999 continue;
5000 }
5001
5002 if ( strpos( $matches[0], '%' ) !== false ) {
5003 $matches[1] = rawurldecode( $matches[1] );
5004 }
5005 $title = Title::newFromText( $matches[1], NS_FILE );
5006 if ( is_null( $title ) ) {
5007 # Bogus title. Ignore these so we don't bomb out later.
5008 continue;
5009 }
5010
5011 # We need to get what handler the file uses, to figure out parameters.
5012 # Note, a hook can overide the file name, and chose an entirely different
5013 # file (which potentially could be of a different type and have different handler).
5014 $options = [];
5015 $descQuery = false;
5016 Hooks::run( 'BeforeParserFetchFileAndTitle',
5017 [ $this, $title, &$options, &$descQuery ] );
5018 # Don't register it now, as TraditionalImageGallery does that later.
5019 $file = $this->fetchFileNoRegister( $title, $options );
5020 $handler = $file ? $file->getHandler() : false;
5021
5022 $paramMap = [
5023 'img_alt' => 'gallery-internal-alt',
5024 'img_link' => 'gallery-internal-link',
5025 ];
5026 if ( $handler ) {
5027 $paramMap = $paramMap + $handler->getParamMap();
5028 // We don't want people to specify per-image widths.
5029 // Additionally the width parameter would need special casing anyhow.
5030 unset( $paramMap['img_width'] );
5031 }
5032
5033 $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5034
5035 $label = '';
5036 $alt = '';
5037 $link = '';
5038 $handlerOptions = [];
5039 if ( isset( $matches[3] ) ) {
5040 // look for an |alt= definition while trying not to break existing
5041 // captions with multiple pipes (|) in it, until a more sensible grammar
5042 // is defined for images in galleries
5043
5044 // FIXME: Doing recursiveTagParse at this stage, and the trim before
5045 // splitting on '|' is a bit odd, and different from makeImage.
5046 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5047 // Protect LanguageConverter markup
5048 $parameterMatches = StringUtils::delimiterExplode(
5049 '-{', '}-', '|', $matches[3], true /* nested */
5050 );
5051
5052 foreach ( $parameterMatches as $parameterMatch ) {
5053 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5054 if ( $magicName ) {
5055 $paramName = $paramMap[$magicName];
5056
5057 switch ( $paramName ) {
5058 case 'gallery-internal-alt':
5059 $alt = $this->stripAltText( $match, false );
5060 break;
5061 case 'gallery-internal-link':
5062 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5063 $chars = self::EXT_LINK_URL_CLASS;
5064 $addr = self::EXT_LINK_ADDR;
5065 $prots = $this->mUrlProtocols;
5066 // check to see if link matches an absolute url, if not then it must be a wiki link.
5067 if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5068 // Result of LanguageConverter::markNoConversion
5069 // invoked on an external link.
5070 $linkValue = substr( $linkValue, 4, -2 );
5071 }
5072 if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
5073 $link = $linkValue;
5074 $this->mOutput->addExternalLink( $link );
5075 } else {
5076 $localLinkTitle = Title::newFromText( $linkValue );
5077 if ( $localLinkTitle !== null ) {
5078 $this->mOutput->addLink( $localLinkTitle );
5079 $link = $localLinkTitle->getLinkURL();
5080 }
5081 }
5082 break;
5083 default:
5084 // Must be a handler specific parameter.
5085 if ( $handler->validateParam( $paramName, $match ) ) {
5086 $handlerOptions[$paramName] = $match;
5087 } else {
5088 // Guess not, consider it as caption.
5089 wfDebug( "$parameterMatch failed parameter validation\n" );
5090 $label = '|' . $parameterMatch;
5091 }
5092 }
5093
5094 } else {
5095 // Last pipe wins.
5096 $label = '|' . $parameterMatch;
5097 }
5098 }
5099 // Remove the pipe.
5100 $label = substr( $label, 1 );
5101 }
5102
5103 $ig->add( $title, $label, $alt, $link, $handlerOptions );
5104 }
5105 $html = $ig->toHTML();
5106 Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5107 return $html;
5108 }
5109
5114 public function getImageParams( $handler ) {
5115 if ( $handler ) {
5116 $handlerClass = get_class( $handler );
5117 } else {
5118 $handlerClass = '';
5119 }
5120 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5121 # Initialise static lists
5122 static $internalParamNames = [
5123 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5124 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5125 'bottom', 'text-bottom' ],
5126 'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5127 'upright', 'border', 'link', 'alt', 'class' ],
5128 ];
5129 static $internalParamMap;
5130 if ( !$internalParamMap ) {
5131 $internalParamMap = [];
5132 foreach ( $internalParamNames as $type => $names ) {
5133 foreach ( $names as $name ) {
5134 // For grep: img_left, img_right, img_center, img_none,
5135 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5136 // img_bottom, img_text_bottom,
5137 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5138 // img_border, img_link, img_alt, img_class
5139 $magicName = str_replace( '-', '_', "img_$name" );
5140 $internalParamMap[$magicName] = [ $type, $name ];
5141 }
5142 }
5143 }
5144
5145 # Add handler params
5146 $paramMap = $internalParamMap;
5147 if ( $handler ) {
5148 $handlerParamMap = $handler->getParamMap();
5149 foreach ( $handlerParamMap as $magic => $paramName ) {
5150 $paramMap[$magic] = [ 'handler', $paramName ];
5151 }
5152 }
5153 $this->mImageParams[$handlerClass] = $paramMap;
5154 $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5155 }
5156 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5157 }
5158
5167 public function makeImage( $title, $options, $holders = false ) {
5168 # Check if the options text is of the form "options|alt text"
5169 # Options are:
5170 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5171 # * left no resizing, just left align. label is used for alt= only
5172 # * right same, but right aligned
5173 # * none same, but not aligned
5174 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5175 # * center center the image
5176 # * frame Keep original image size, no magnify-button.
5177 # * framed Same as "frame"
5178 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5179 # * upright reduce width for upright images, rounded to full __0 px
5180 # * border draw a 1px border around the image
5181 # * alt Text for HTML alt attribute (defaults to empty)
5182 # * class Set a class for img node
5183 # * link Set the target of the image link. Can be external, interwiki, or local
5184 # vertical-align values (no % or length right now):
5185 # * baseline
5186 # * sub
5187 # * super
5188 # * top
5189 # * text-top
5190 # * middle
5191 # * bottom
5192 # * text-bottom
5193
5194 # Protect LanguageConverter markup when splitting into parts
5196 '-{', '}-', '|', $options, true /* allow nesting */
5197 );
5198
5199 # Give extensions a chance to select the file revision for us
5200 $options = [];
5201 $descQuery = false;
5202 Hooks::run( 'BeforeParserFetchFileAndTitle',
5203 [ $this, $title, &$options, &$descQuery ] );
5204 # Fetch and register the file (file title may be different via hooks)
5205 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5206
5207 # Get parameter map
5208 $handler = $file ? $file->getHandler() : false;
5209
5210 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5211
5212 if ( !$file ) {
5213 $this->addTrackingCategory( 'broken-file-category' );
5214 }
5215
5216 # Process the input parameters
5217 $caption = '';
5218 $params = [ 'frame' => [], 'handler' => [],
5219 'horizAlign' => [], 'vertAlign' => [] ];
5220 $seenformat = false;
5221 foreach ( $parts as $part ) {
5222 $part = trim( $part );
5223 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5224 $validated = false;
5225 if ( isset( $paramMap[$magicName] ) ) {
5226 list( $type, $paramName ) = $paramMap[$magicName];
5227
5228 # Special case; width and height come in one variable together
5229 if ( $type === 'handler' && $paramName === 'width' ) {
5230 $parsedWidthParam = self::parseWidthParam( $value );
5231 if ( isset( $parsedWidthParam['width'] ) ) {
5232 $width = $parsedWidthParam['width'];
5233 if ( $handler->validateParam( 'width', $width ) ) {
5234 $params[$type]['width'] = $width;
5235 $validated = true;
5236 }
5237 }
5238 if ( isset( $parsedWidthParam['height'] ) ) {
5239 $height = $parsedWidthParam['height'];
5240 if ( $handler->validateParam( 'height', $height ) ) {
5241 $params[$type]['height'] = $height;
5242 $validated = true;
5243 }
5244 }
5245 # else no validation -- T15436
5246 } else {
5247 if ( $type === 'handler' ) {
5248 # Validate handler parameter
5249 $validated = $handler->validateParam( $paramName, $value );
5250 } else {
5251 # Validate internal parameters
5252 switch ( $paramName ) {
5253 case 'manualthumb':
5254 case 'alt':
5255 case 'class':
5256 # @todo FIXME: Possibly check validity here for
5257 # manualthumb? downstream behavior seems odd with
5258 # missing manual thumbs.
5259 $validated = true;
5260 $value = $this->stripAltText( $value, $holders );
5261 break;
5262 case 'link':
5263 $chars = self::EXT_LINK_URL_CLASS;
5264 $addr = self::EXT_LINK_ADDR;
5265 $prots = $this->mUrlProtocols;
5266 if ( $value === '' ) {
5267 $paramName = 'no-link';
5268 $value = true;
5269 $validated = true;
5270 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5271 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5272 $paramName = 'link-url';
5273 $this->mOutput->addExternalLink( $value );
5274 if ( $this->mOptions->getExternalLinkTarget() ) {
5275 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5276 }
5277 $validated = true;
5278 }
5279 } else {
5280 $linkTitle = Title::newFromText( $value );
5281 if ( $linkTitle ) {
5282 $paramName = 'link-title';
5283 $value = $linkTitle;
5284 $this->mOutput->addLink( $linkTitle );
5285 $validated = true;
5286 }
5287 }
5288 break;
5289 case 'frameless':
5290 case 'framed':
5291 case 'thumbnail':
5292 // use first appearing option, discard others.
5293 $validated = !$seenformat;
5294 $seenformat = true;
5295 break;
5296 default:
5297 # Most other things appear to be empty or numeric...
5298 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5299 }
5300 }
5301
5302 if ( $validated ) {
5303 $params[$type][$paramName] = $value;
5304 }
5305 }
5306 }
5307 if ( !$validated ) {
5308 $caption = $part;
5309 }
5310 }
5311
5312 # Process alignment parameters
5313 if ( $params['horizAlign'] ) {
5314 $params['frame']['align'] = key( $params['horizAlign'] );
5315 }
5316 if ( $params['vertAlign'] ) {
5317 $params['frame']['valign'] = key( $params['vertAlign'] );
5318 }
5319
5320 $params['frame']['caption'] = $caption;
5321
5322 # Will the image be presented in a frame, with the caption below?
5323 $imageIsFramed = isset( $params['frame']['frame'] )
5324 || isset( $params['frame']['framed'] )
5325 || isset( $params['frame']['thumbnail'] )
5326 || isset( $params['frame']['manualthumb'] );
5327
5328 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5329 # came to also set the caption, ordinary text after the image -- which
5330 # makes no sense, because that just repeats the text multiple times in
5331 # screen readers. It *also* came to set the title attribute.
5332 # Now that we have an alt attribute, we should not set the alt text to
5333 # equal the caption: that's worse than useless, it just repeats the
5334 # text. This is the framed/thumbnail case. If there's no caption, we
5335 # use the unnamed parameter for alt text as well, just for the time be-
5336 # ing, if the unnamed param is set and the alt param is not.
5337 # For the future, we need to figure out if we want to tweak this more,
5338 # e.g., introducing a title= parameter for the title; ignoring the un-
5339 # named parameter entirely for images without a caption; adding an ex-
5340 # plicit caption= parameter and preserving the old magic unnamed para-
5341 # meter for BC; ...
5342 if ( $imageIsFramed ) { # Framed image
5343 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5344 # No caption or alt text, add the filename as the alt text so
5345 # that screen readers at least get some description of the image
5346 $params['frame']['alt'] = $title->getText();
5347 }
5348 # Do not set $params['frame']['title'] because tooltips don't make sense
5349 # for framed images
5350 } else { # Inline image
5351 if ( !isset( $params['frame']['alt'] ) ) {
5352 # No alt text, use the "caption" for the alt text
5353 if ( $caption !== '' ) {
5354 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5355 } else {
5356 # No caption, fall back to using the filename for the
5357 # alt text
5358 $params['frame']['alt'] = $title->getText();
5359 }
5360 }
5361 # Use the "caption" for the tooltip text
5362 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5363 }
5364
5365 Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5366
5367 # Linker does the rest
5368 $time = isset( $options['time'] ) ? $options['time'] : false;
5369 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5370 $time, $descQuery, $this->mOptions->getThumbSize() );
5371
5372 # Give the handler a chance to modify the parser object
5373 if ( $handler ) {
5374 $handler->parserTransformHook( $this, $file );
5375 }
5376
5377 return $ret;
5378 }
5379
5385 protected function stripAltText( $caption, $holders ) {
5386 # Strip bad stuff out of the title (tooltip). We can't just use
5387 # replaceLinkHoldersText() here, because if this function is called
5388 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5389 if ( $holders ) {
5390 $tooltip = $holders->replaceText( $caption );
5391 } else {
5392 $tooltip = $this->replaceLinkHoldersText( $caption );
5393 }
5394
5395 # make sure there are no placeholders in thumbnail attributes
5396 # that are later expanded to html- so expand them now and
5397 # remove the tags
5398 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5399 $tooltip = Sanitizer::stripAllTags( $tooltip );
5400
5401 return $tooltip;
5402 }
5403
5409 public function disableCache() {
5410 wfDebug( "Parser output marked as uncacheable.\n" );
5411 if ( !$this->mOutput ) {
5412 throw new MWException( __METHOD__ .
5413 " can only be called when actually parsing something" );
5414 }
5415 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5416 }
5417
5426 public function attributeStripCallback( &$text, $frame = false ) {
5427 $text = $this->replaceVariables( $text, $frame );
5428 $text = $this->mStripState->unstripBoth( $text );
5429 return $text;
5430 }
5431
5437 public function getTags() {
5438 return array_merge(
5439 array_keys( $this->mTransparentTagHooks ),
5440 array_keys( $this->mTagHooks ),
5441 array_keys( $this->mFunctionTagHooks )
5442 );
5443 }
5444
5455 public function replaceTransparentTags( $text ) {
5456 $matches = [];
5457 $elements = array_keys( $this->mTransparentTagHooks );
5458 $text = self::extractTagsAndParams( $elements, $text, $matches );
5459 $replacements = [];
5460
5461 foreach ( $matches as $marker => $data ) {
5462 list( $element, $content, $params, $tag ) = $data;
5463 $tagName = strtolower( $element );
5464 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5465 $output = call_user_func_array(
5466 $this->mTransparentTagHooks[$tagName],
5467 [ $content, $params, $this ]
5468 );
5469 } else {
5470 $output = $tag;
5471 }
5472 $replacements[$marker] = $output;
5473 }
5474 return strtr( $text, $replacements );
5475 }
5476
5506 private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5507 global $wgTitle; # not generally used but removes an ugly failure mode
5508
5509 $magicScopeVariable = $this->lock();
5510 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5511 $outText = '';
5512 $frame = $this->getPreprocessor()->newFrame();
5513
5514 # Process section extraction flags
5515 $flags = 0;
5516 $sectionParts = explode( '-', $sectionId );
5517 $sectionIndex = array_pop( $sectionParts );
5518 foreach ( $sectionParts as $part ) {
5519 if ( $part === 'T' ) {
5520 $flags |= self::PTD_FOR_INCLUSION;
5521 }
5522 }
5523
5524 # Check for empty input
5525 if ( strval( $text ) === '' ) {
5526 # Only sections 0 and T-0 exist in an empty document
5527 if ( $sectionIndex == 0 ) {
5528 if ( $mode === 'get' ) {
5529 return '';
5530 } else {
5531 return $newText;
5532 }
5533 } else {
5534 if ( $mode === 'get' ) {
5535 return $newText;
5536 } else {
5537 return $text;
5538 }
5539 }
5540 }
5541
5542 # Preprocess the text
5543 $root = $this->preprocessToDom( $text, $flags );
5544
5545 # <h> nodes indicate section breaks
5546 # They can only occur at the top level, so we can find them by iterating the root's children
5547 $node = $root->getFirstChild();
5548
5549 # Find the target section
5550 if ( $sectionIndex == 0 ) {
5551 # Section zero doesn't nest, level=big
5552 $targetLevel = 1000;
5553 } else {
5554 while ( $node ) {
5555 if ( $node->getName() === 'h' ) {
5556 $bits = $node->splitHeading();
5557 if ( $bits['i'] == $sectionIndex ) {
5558 $targetLevel = $bits['level'];
5559 break;
5560 }
5561 }
5562 if ( $mode === 'replace' ) {
5563 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5564 }
5565 $node = $node->getNextSibling();
5566 }
5567 }
5568
5569 if ( !$node ) {
5570 # Not found
5571 if ( $mode === 'get' ) {
5572 return $newText;
5573 } else {
5574 return $text;
5575 }
5576 }
5577
5578 # Find the end of the section, including nested sections
5579 do {
5580 if ( $node->getName() === 'h' ) {
5581 $bits = $node->splitHeading();
5582 $curLevel = $bits['level'];
5583 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5584 break;
5585 }
5586 }
5587 if ( $mode === 'get' ) {
5588 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5589 }
5590 $node = $node->getNextSibling();
5591 } while ( $node );
5592
5593 # Write out the remainder (in replace mode only)
5594 if ( $mode === 'replace' ) {
5595 # Output the replacement text
5596 # Add two newlines on -- trailing whitespace in $newText is conventionally
5597 # stripped by the editor, so we need both newlines to restore the paragraph gap
5598 # Only add trailing whitespace if there is newText
5599 if ( $newText != "" ) {
5600 $outText .= $newText . "\n\n";
5601 }
5602
5603 while ( $node ) {
5604 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5605 $node = $node->getNextSibling();
5606 }
5607 }
5608
5609 if ( is_string( $outText ) ) {
5610 # Re-insert stripped tags
5611 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5612 }
5613
5614 return $outText;
5615 }
5616
5631 public function getSection( $text, $sectionId, $defaultText = '' ) {
5632 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5633 }
5634
5647 public function replaceSection( $oldText, $sectionId, $newText ) {
5648 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5649 }
5650
5656 public function getRevisionId() {
5657 return $this->mRevisionId;
5658 }
5659
5666 public function getRevisionObject() {
5667 if ( !is_null( $this->mRevisionObject ) ) {
5668 return $this->mRevisionObject;
5669 }
5670 if ( is_null( $this->mRevisionId ) ) {
5671 return null;
5672 }
5673
5674 $rev = call_user_func(
5675 $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5676 );
5677
5678 # If the parse is for a new revision, then the callback should have
5679 # already been set to force the object and should match mRevisionId.
5680 # If not, try to fetch by mRevisionId for sanity.
5681 if ( $rev && $rev->getId() != $this->mRevisionId ) {
5682 $rev = Revision::newFromId( $this->mRevisionId );
5683 }
5684
5685 $this->mRevisionObject = $rev;
5686
5687 return $this->mRevisionObject;
5688 }
5689
5695 public function getRevisionTimestamp() {
5696 if ( is_null( $this->mRevisionTimestamp ) ) {
5697 global $wgContLang;
5698
5699 $revObject = $this->getRevisionObject();
5700 $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5701
5702 # The cryptic '' timezone parameter tells to use the site-default
5703 # timezone offset instead of the user settings.
5704 # Since this value will be saved into the parser cache, served
5705 # to other users, and potentially even used inside links and such,
5706 # it needs to be consistent for all visitors.
5707 $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5708
5709 }
5710 return $this->mRevisionTimestamp;
5711 }
5712
5718 public function getRevisionUser() {
5719 if ( is_null( $this->mRevisionUser ) ) {
5720 $revObject = $this->getRevisionObject();
5721
5722 # if this template is subst: the revision id will be blank,
5723 # so just use the current user's name
5724 if ( $revObject ) {
5725 $this->mRevisionUser = $revObject->getUserText();
5726 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5727 $this->mRevisionUser = $this->getUser()->getName();
5728 }
5729 }
5730 return $this->mRevisionUser;
5731 }
5732
5738 public function getRevisionSize() {
5739 if ( is_null( $this->mRevisionSize ) ) {
5740 $revObject = $this->getRevisionObject();
5741
5742 # if this variable is subst: the revision id will be blank,
5743 # so just use the parser input size, because the own substituation
5744 # will change the size.
5745 if ( $revObject ) {
5746 $this->mRevisionSize = $revObject->getSize();
5747 } else {
5748 $this->mRevisionSize = $this->mInputSize;
5749 }
5750 }
5751 return $this->mRevisionSize;
5752 }
5753
5759 public function setDefaultSort( $sort ) {
5760 $this->mDefaultSort = $sort;
5761 $this->mOutput->setProperty( 'defaultsort', $sort );
5762 }
5763
5774 public function getDefaultSort() {
5775 if ( $this->mDefaultSort !== false ) {
5776 return $this->mDefaultSort;
5777 } else {
5778 return '';
5779 }
5780 }
5781
5788 public function getCustomDefaultSort() {
5789 return $this->mDefaultSort;
5790 }
5791
5792 private static function getSectionNameFromStrippedText( $text ) {
5793 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5794 $text = Sanitizer::decodeCharReferences( $text );
5795 $text = self::normalizeSectionName( $text );
5796 return $text;
5797 }
5798
5799 private static function makeAnchor( $sectionName ) {
5800 return '#' . Sanitizer::escapeIdForLink( $sectionName );
5801 }
5802
5803 private static function makeLegacyAnchor( $sectionName ) {
5804 global $wgFragmentMode;
5805 if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
5806 // ForAttribute() and ForLink() are the same for legacy encoding
5807 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
5808 } else {
5809 $id = Sanitizer::escapeIdForLink( $sectionName );
5810 }
5811
5812 return "#$id";
5813 }
5814
5823 public function guessSectionNameFromWikiText( $text ) {
5824 # Strip out wikitext links(they break the anchor)
5825 $text = $this->stripSectionName( $text );
5826 $sectionName = self::getSectionNameFromStrippedText( $text );
5827 return self::makeAnchor( $sectionName );
5828 }
5829
5839 public function guessLegacySectionNameFromWikiText( $text ) {
5840 # Strip out wikitext links(they break the anchor)
5841 $text = $this->stripSectionName( $text );
5842 $sectionName = self::getSectionNameFromStrippedText( $text );
5843 return self::makeLegacyAnchor( $sectionName );
5844 }
5845
5851 public static function guessSectionNameFromStrippedText( $text ) {
5852 $sectionName = self::getSectionNameFromStrippedText( $text );
5853 return self::makeAnchor( $sectionName );
5854 }
5855
5862 private static function normalizeSectionName( $text ) {
5863 # T90902: ensure the same normalization is applied for IDs as to links
5864 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
5865 try {
5866
5867 $parts = $titleParser->splitTitleString( "#$text" );
5868 } catch ( MalformedTitleException $ex ) {
5869 return $text;
5870 }
5871 return $parts['fragment'];
5872 }
5873
5888 public function stripSectionName( $text ) {
5889 # Strip internal link markup
5890 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5891 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5892
5893 # Strip external link markup
5894 # @todo FIXME: Not tolerant to blank link text
5895 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
5896 # on how many empty links there are on the page - need to figure that out.
5897 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5898
5899 # Parse wikitext quotes (italics & bold)
5900 $text = $this->doQuotes( $text );
5901
5902 # Strip HTML tags
5903 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
5904 return $text;
5905 }
5906
5917 public function testSrvus( $text, Title $title, ParserOptions $options,
5918 $outputType = self::OT_HTML
5919 ) {
5920 $magicScopeVariable = $this->lock();
5921 $this->startParse( $title, $options, $outputType, true );
5922
5923 $text = $this->replaceVariables( $text );
5924 $text = $this->mStripState->unstripBoth( $text );
5925 $text = Sanitizer::removeHTMLtags( $text );
5926 return $text;
5927 }
5928
5935 public function testPst( $text, Title $title, ParserOptions $options ) {
5936 return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5937 }
5938
5945 public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5946 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5947 }
5948
5965 public function markerSkipCallback( $s, $callback ) {
5966 $i = 0;
5967 $out = '';
5968 while ( $i < strlen( $s ) ) {
5969 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5970 if ( $markerStart === false ) {
5971 $out .= call_user_func( $callback, substr( $s, $i ) );
5972 break;
5973 } else {
5974 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5975 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5976 if ( $markerEnd === false ) {
5977 $out .= substr( $s, $markerStart );
5978 break;
5979 } else {
5980 $markerEnd += strlen( self::MARKER_SUFFIX );
5981 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5982 $i = $markerEnd;
5983 }
5984 }
5985 }
5986 return $out;
5987 }
5988
5995 public function killMarkers( $text ) {
5996 return $this->mStripState->killMarkers( $text );
5997 }
5998
6016 public function serializeHalfParsedText( $text ) {
6017 wfDeprecated( __METHOD__, '1.31' );
6018 $data = [
6019 'text' => $text,
6020 'version' => self::HALF_PARSED_VERSION,
6021 'stripState' => $this->mStripState->getSubState( $text ),
6022 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6023 ];
6024 return $data;
6025 }
6026
6043 public function unserializeHalfParsedText( $data ) {
6044 wfDeprecated( __METHOD__, '1.31' );
6045 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6046 throw new MWException( __METHOD__ . ': invalid version' );
6047 }
6048
6049 # First, extract the strip state.
6050 $texts = [ $data['text'] ];
6051 $texts = $this->mStripState->merge( $data['stripState'], $texts );
6052
6053 # Now renumber links
6054 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6055
6056 # Should be good to go.
6057 return $texts[0];
6058 }
6059
6070 public function isValidHalfParsedText( $data ) {
6071 wfDeprecated( __METHOD__, '1.31' );
6072 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6073 }
6074
6084 public static function parseWidthParam( $value, $parseHeight = true ) {
6085 $parsedWidthParam = [];
6086 if ( $value === '' ) {
6087 return $parsedWidthParam;
6088 }
6089 $m = [];
6090 # (T15500) In both cases (width/height and width only),
6091 # permit trailing "px" for backward compatibility.
6092 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6093 $width = intval( $m[1] );
6094 $height = intval( $m[2] );
6095 $parsedWidthParam['width'] = $width;
6096 $parsedWidthParam['height'] = $height;
6097 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6098 $width = intval( $value );
6099 $parsedWidthParam['width'] = $width;
6100 }
6101 return $parsedWidthParam;
6102 }
6103
6113 protected function lock() {
6114 if ( $this->mInParse ) {
6115 throw new MWException( "Parser state cleared while parsing. "
6116 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6117 }
6118
6119 // Save the backtrace when locking, so that if some code tries locking again,
6120 // we can print the lock owner's backtrace for easier debugging
6121 $e = new Exception;
6122 $this->mInParse = $e->getTraceAsString();
6123
6124 $recursiveCheck = new ScopedCallback( function () {
6125 $this->mInParse = false;
6126 } );
6127
6128 return $recursiveCheck;
6129 }
6130
6141 public static function stripOuterParagraph( $html ) {
6142 $m = [];
6143 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6144 if ( strpos( $m[1], '</p>' ) === false ) {
6145 $html = $m[1];
6146 }
6147 }
6148
6149 return $html;
6150 }
6151
6162 public function getFreshParser() {
6163 global $wgParserConf;
6164 if ( $this->mInParse ) {
6165 return new $wgParserConf['class']( $wgParserConf );
6166 } else {
6167 return $this;
6168 }
6169 }
6170
6177 public function enableOOUI() {
6178 OutputPage::setupOOUI();
6179 $this->mOutput->setEnableOOUI( true );
6180 }
6181}
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,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(! $wgRequest->checkUrlExtension()) if(isset($_SERVER[ 'PATH_INFO']) &&$_SERVER[ 'PATH_INFO'] !='') if(! $wgEnableAPI) $wgTitle
Definition api.php:68
$line
Definition cdb.php:59
if( $line===false) $args
Definition cdb.php:64
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
static register( $parser)
static register( $parser)
WebRequest clone which takes values from a provided array.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition Hooks.php: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:1581
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:1545
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:1369
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:1519
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:1665
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:1642
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition Linker.php:1530
static tocLineEnd()
End a Table Of Contents line.
Definition Linker.php:1569
MediaWiki exception.
static tidy( $text)
Interface with html tidy.
Definition MWTidy.php:46
static isEnabled()
Definition MWTidy.php:58
Class for handling an array of magic words.
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.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Handles a simple LRU key/value map with a maximum number of entries.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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:4029
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:881
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:5695
static normalizeUrlComponent( $component, $unsafe)
Definition Parser.php:2008
bool string $mInParse
Recursive call protection.
Definition Parser.php:250
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3847
const TOC_END
Definition Parser.php:139
$mDefaultStripList
Definition Parser.php:148
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition Parser.php:5759
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6141
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:1972
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:734
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition Parser.php:2452
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:301
ParserOutput $mOutput
Definition Parser.php:177
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition Parser.php:2031
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4658
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3717
$mFirstCall
Definition Parser.php:153
LinkRenderer $mLinkRenderer
Definition Parser.php:258
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:689
$mHighestExpansionDepth
Definition Parser.php:192
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:4944
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition Parser.php:5788
static getSectionNameFromStrippedText( $text)
Definition Parser.php:5792
stripAltText( $caption, $holders)
Definition Parser.php:5385
getOptions()
Get the ParserOptions object.
Definition Parser.php:836
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4621
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:4443
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition Parser.php:4684
fetchScaryTemplateMaybeFromCache( $url)
Definition Parser.php:3755
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:5718
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition Parser.php:3537
replaceExternalLinks( $text)
Replace external links (REL)
Definition Parser.php:1846
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2925
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:2419
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:2443
$mHeadings
Definition Parser.php:194
Title $mTitle
Definition Parser.php:214
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition Parser.php:2103
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition Parser.php:4893
static makeLegacyAnchor( $sectionName)
Definition Parser.php:5803
const PTD_FOR_INCLUSION
Definition Parser.php:107
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:5851
$mTplDomCache
Definition Parser.php:194
$mGeneratedPPNodeCount
Definition Parser.php:192
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition Parser.php:1445
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
Definition Parser.php:6043
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:3002
LinkHolderArray $mLinkHolders
Definition Parser.php:189
$mFunctionTagHooks
Definition Parser.php:146
$mRevisionId
Definition Parser.php:218
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5167
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
Definition Parser.php:5409
pstPass2( $text, $user)
Pre-save transform helper function.
Definition Parser.php:4478
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Definition Parser.php:4703
getRevisionSize()
Get the size of the revision.
Definition Parser.php:5738
const TOC_START
Definition Parser.php:138
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition Parser.php:5506
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:5888
$mDefaultSort
Definition Parser.php:193
$mTagHooks
Definition Parser.php:142
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition Parser.php:5862
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition Parser.php:1548
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:715
$mFunctionHooks
Definition Parser.php:144
$mShowToc
Definition Parser.php:196
getImageParams( $handler)
Definition Parser.php:5114
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition Parser.php:6016
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:4049
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition Parser.php:1341
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:4841
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3471
$mRevisionSize
Definition Parser.php:221
doTableStuff( $text)
parse the wiki syntax used to render tables
Definition Parser.php:1060
getTitle()
Accessor for the Title object.
Definition Parser.php:782
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:1921
ParserOptions $mOptions
Definition Parser.php:209
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition Parser.php:5917
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition Parser.php:76
MagicWordArray $mSubstWords
Definition Parser.php:165
$mUrlProtocols
Definition Parser.php:167
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
Definition Parser.php:5455
MapCacheLRU null $currentRevisionCache
Definition Parser.php:244
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:935
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition Parser.php:2892
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:1942
setOutputType( $ot)
Set the output type.
Definition Parser.php:801
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition Parser.php:868
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition Parser.php:2492
StripState $mStripState
Definition Parser.php:183
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition Parser.php:511
$mInputSize
Definition Parser.php:223
MagicWordArray $mVariables
Definition Parser.php:160
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1033
$mOutputType
Definition Parser.php:215
$mImageParamsMagicArray
Definition Parser.php:151
setUser( $user)
Set the current user.
Definition Parser.php:755
$mAutonumber
Definition Parser.php:178
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:5965
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4747
getTags()
Accessor.
Definition Parser.php:5437
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition Parser.php:3976
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3795
replaceInternalLinks( $s)
Process [[ ]] wikilinks.
Definition Parser.php:2090
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition Parser.php:236
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:646
$mRevisionUser
Definition Parser.php:220
string $mUniqPrefix
Deprecated accessor for the strip marker prefix.
Definition Parser.php:229
$mTplRedirCache
Definition Parser.php:194
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3692
$mFunctionSynonyms
Definition Parser.php:145
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5631
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1269
$mRevisionTimestamp
Definition Parser.php:219
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:966
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5647
interwikiTransclude( $title, $action)
Transclude an interwiki link.
Definition Parser.php:3736
setTitle( $t)
Set the context title.
Definition Parser.php:764
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition Parser.php:3949
User $mUser
Definition Parser.php:201
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
Definition Parser.php:2465
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition Parser.php:5666
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition Parser.php:2477
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3548
$mPPNodeCount
Definition Parser.php:192
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition Parser.php:5774
const MARKER_PREFIX
Definition Parser.php:135
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition Parser.php:4927
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:4879
doAllQuotes( $text)
Replace single quotes with HTML markup.
Definition Parser.php:1641
$mMarkerIndex
Definition Parser.php:152
const EXT_LINK_ADDR
Definition Parser.php:97
$mExpensiveFunctionCount
Definition Parser.php:195
Preprocessor $mPreprocessor
Definition Parser.php:171
$mDoubleUnderscores
Definition Parser.php:194
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Definition Parser.php:3514
getOutput()
Get the ParserOutput object.
Definition Parser.php:827
$mVarCache
Definition Parser.php:149
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition Parser.php:846
$mImageParams
Definition Parser.php:150
Title( $x=null)
Accessor/mutator for the Title object.
Definition Parser.php:792
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:672
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:5823
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4792
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:4916
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:5656
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6084
$mIncludeCount
Definition Parser.php:185
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4607
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:921
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:5839
const EXT_LINK_URL_CLASS
Definition Parser.php:94
OutputType( $x=null)
Accessor/mutator for the output type.
Definition Parser.php:818
magicLinkCallback( $m)
Definition Parser.php:1476
$mIncludeSizes
Definition Parser.php:192
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:289
fetchFile( $title, $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3681
setLinkID( $id)
Definition Parser.php:860
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition Parser.php:909
doQuotes( $text)
Helper function for doAllQuotes()
Definition Parser.php:1658
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3576
doHeadings( $text)
Parse headers and return html.
Definition Parser.php:1623
$mExtLinkBracketedRegex
Definition Parser.php:167
const EXT_IMAGE_REGEX
Definition Parser.php:100
$mRevIdForTs
Definition Parser.php:222
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:1046
__construct( $conf=[])
Definition Parser.php:263
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
Definition Parser.php:4778
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition Parser.php:3963
testPreprocess( $text, Title $title, ParserOptions $options)
Definition Parser.php:5945
static makeAnchor( $sectionName)
Definition Parser.php:5799
const SPACE_NOT_NL
Definition Parser.php:104
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition Parser.php:324
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition Parser.php:2880
$mForceTocPosition
Definition Parser.php:196
clearState()
Clear Parser state.
Definition Parser.php:344
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:5995
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6177
nextLinkID()
Definition Parser.php:853
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:4558
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:3024
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5426
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
Definition Parser.php:2850
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
Definition Parser.php:6070
lock()
Lock the current instance of the parser.
Definition Parser.php:6113
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition Parser.php:2954
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:404
$mStripList
Definition Parser.php:147
testPst( $text, Title $title, ParserOptions $options)
Definition Parser.php:5935
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:4672
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition Parser.php:6162
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition Parser.php:3377
SectionProfiler $mProfiler
Definition Parser.php:253
$mTransparentTagHooks
Definition Parser.php:143
getConverterLanguage()
Get the language object for language conversion.
Definition Parser.php:899
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3589
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.
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static articles()
static images()
static edits()
Definition SiteStats.php:94
static users()
static pages()
static numberingroup( $group)
Find the number of users in a given user group.
static activeUsers()
static 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 getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
static delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <....
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
static delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
Represents a title within MediaWiki.
Definition Title.php:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
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:990
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead $parser
Definition hooks.txt:2603
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1795
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:2255
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). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1051
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:1777
either a plain
Definition hooks.txt:2056
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:2001
null means default in associative array form
Definition hooks.txt:1996
usually copyright or history_copyright This message must be in HTML not wikitext if the section is included from a template to be included in the link
Definition hooks.txt:3023
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:2811
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
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
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:2006
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:2001
if the prop value should be in the metadata multi language array format
Definition hooks.txt:1656
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:2005
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:864
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:2013
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3021
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:785
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:2014
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:903
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:1777
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:2176
!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:195
const SFH_NO_HASH
Definition Defines.php:207
const SFH_OBJECT_ARGS
Definition Defines.php:208
const NS_FILE
Definition Defines.php:80
const NS_TEMPLATE
Definition Defines.php:84
const NS_SPECIAL
Definition Defines.php:63
const OT_PLAIN
Definition Defines.php:198
const OT_PREPROCESS
Definition Defines.php:196
const OT_HTML
Definition Defines.php:194
const NS_MEDIA
Definition Defines.php:62
const NS_CATEGORY
Definition Defines.php:88
const OT_MSG
Definition Defines.php:197
$wgArticlePath
Definition img_auth.php:45
const NO_TEMPLATES
const RECOVER_ORIG
const NO_ARGS
const STRIP_COMMENTS
There are three types of nodes:
controlled by $wgMainCacheType controlled by $wgParserCacheType controlled by $wgMessageCacheType If you set CACHE_NONE to one of the three control variable
Definition memcached.txt:87
$buffer
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition postgres.txt:30
$sort
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
$lines
Definition router.php:61
$params