Merge "Parser: Hard deprecate getConverterLanguage" into REL1_34
[lhc/web/wiklou.git] / includes / parser / Parser.php
1 <?php
2 /**
3 * PHP parser that converts wiki markup to HTML.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Parser
22 */
23 use MediaWiki\BadFileLookup;
24 use MediaWiki\Config\ServiceOptions;
25 use MediaWiki\Linker\LinkRenderer;
26 use MediaWiki\Linker\LinkRendererFactory;
27 use MediaWiki\Linker\LinkTarget;
28 use MediaWiki\MediaWikiServices;
29 use MediaWiki\Special\SpecialPageFactory;
30 use Psr\Log\NullLogger;
31 use Wikimedia\ScopedCallback;
32 use Psr\Log\LoggerInterface;
33
34 /**
35 * @defgroup Parser Parser
36 */
37
38 /**
39 * PHP Parser - Processes wiki markup (which uses a more user-friendly
40 * syntax, such as "[[link]]" for making links), and provides a one-way
41 * transformation of that wiki markup it into (X)HTML output / markup
42 * (which in turn the browser understands, and can display).
43 *
44 * There are seven main entry points into the Parser class:
45 *
46 * - Parser::parse()
47 * produces HTML output
48 * - Parser::preSaveTransform()
49 * produces altered wiki markup
50 * - Parser::preprocess()
51 * removes HTML comments and expands templates
52 * - Parser::cleanSig() and Parser::cleanSigInSig()
53 * cleans a signature before saving it to preferences
54 * - Parser::getSection()
55 * return the content of a section from an article for section editing
56 * - Parser::replaceSection()
57 * replaces a section by number inside an article
58 * - Parser::getPreloadText()
59 * removes <noinclude> sections and <includeonly> tags
60 *
61 * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
62 *
63 * @par Settings:
64 * $wgNamespacesWithSubpages
65 *
66 * @par Settings only within ParserOptions:
67 * $wgAllowExternalImages
68 * $wgAllowSpecialInclusion
69 * $wgInterwikiMagic
70 * $wgMaxArticleSize
71 *
72 * @ingroup Parser
73 */
74 class Parser {
75 /**
76 * Update this version number when the ParserOutput format
77 * changes in an incompatible way, so the parser cache
78 * can automatically discard old data.
79 */
80 const VERSION = '1.6.4';
81
82 /**
83 * Update this version number when the output of serialiseHalfParsedText()
84 * changes in an incompatible way
85 */
86 const HALF_PARSED_VERSION = 2;
87
88 # Flags for Parser::setFunctionHook
89 const SFH_NO_HASH = 1;
90 const SFH_OBJECT_ARGS = 2;
91
92 # Constants needed for external link processing
93 # Everything except bracket, space, or control characters
94 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
95 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
96 # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
97 # uses to replace invalid HTML characters.
98 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
99 # Simplified expression to match an IPv4 or IPv6 address, or
100 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
101 const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
102 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
103 // phpcs:ignore Generic.Files.LineLength
104 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
105 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
106
107 # Regular expression for a non-newline space
108 const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
109
110 # Flags for preprocessToDom
111 const PTD_FOR_INCLUSION = 1;
112
113 # Allowed values for $this->mOutputType
114 # Parameter to startExternalParse().
115 const OT_HTML = 1; # like parse()
116 const OT_WIKI = 2; # like preSaveTransform()
117 const OT_PREPROCESS = 3; # like preprocess()
118 const OT_MSG = 3;
119 const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
120
121 /**
122 * @var string Prefix and suffix for temporary replacement strings
123 * for the multipass parser.
124 *
125 * \x7f should never appear in input as it's disallowed in XML.
126 * Using it at the front also gives us a little extra robustness
127 * since it shouldn't match when butted up against identifier-like
128 * string constructs.
129 *
130 * Must not consist of all title characters, or else it will change
131 * the behavior of <nowiki> in a link.
132 *
133 * Must have a character that needs escaping in attributes, otherwise
134 * someone could put a strip marker in an attribute, to get around
135 * escaping quote marks, and break out of the attribute. Thus we add
136 * `'".
137 */
138 const MARKER_SUFFIX = "-QINU`\"'\x7f";
139 const MARKER_PREFIX = "\x7f'\"`UNIQ-";
140
141 # Markers used for wrapping the table of contents
142 const TOC_START = '<mw:toc>';
143 const TOC_END = '</mw:toc>';
144
145 /** @var int Assume that no output will later be saved this many seconds after parsing */
146 const MAX_TTS = 900;
147
148 # Persistent:
149 public $mTagHooks = [];
150 public $mTransparentTagHooks = [];
151 public $mFunctionHooks = [];
152 public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
153 public $mFunctionTagHooks = [];
154 public $mStripList = [];
155 public $mDefaultStripList = [];
156 public $mVarCache = [];
157 public $mImageParams = [];
158 public $mImageParamsMagicArray = [];
159 public $mMarkerIndex = 0;
160 /**
161 * @var bool Whether firstCallInit still needs to be called
162 */
163 public $mFirstCall = true;
164
165 # Initialised by initializeVariables()
166
167 /**
168 * @var MagicWordArray
169 */
170 public $mVariables;
171
172 /**
173 * @var MagicWordArray
174 */
175 public $mSubstWords;
176
177 /**
178 * @deprecated since 1.34, there should be no need to use this
179 * @var array
180 */
181 public $mConf;
182
183 # Initialised in constructor
184 public $mExtLinkBracketedRegex, $mUrlProtocols;
185
186 # Initialized in getPreprocessor()
187 /** @var Preprocessor */
188 public $mPreprocessor;
189
190 # Cleared with clearState():
191 /**
192 * @var ParserOutput
193 */
194 public $mOutput;
195 public $mAutonumber;
196
197 /**
198 * @var StripState
199 */
200 public $mStripState;
201
202 public $mIncludeCount;
203 /**
204 * @var LinkHolderArray
205 */
206 public $mLinkHolders;
207
208 public $mLinkID;
209 public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
210 public $mDefaultSort;
211 public $mTplRedirCache, $mHeadings, $mDoubleUnderscores;
212 public $mExpensiveFunctionCount; # number of expensive parser function calls
213 public $mShowToc, $mForceTocPosition;
214 /** @var array */
215 public $mTplDomCache;
216
217 /**
218 * @var User
219 */
220 public $mUser; # User object; only used when doing pre-save transform
221
222 # Temporary
223 # These are variables reset at least once per parse regardless of $clearState
224
225 /**
226 * @var ParserOptions
227 */
228 public $mOptions;
229
230 /**
231 * Since 1.34, leaving `mTitle` uninitialized or setting `mTitle` to
232 * `null` is deprecated.
233 *
234 * @internal
235 * @var Title|null
236 */
237 public $mTitle; # Title context, used for self-link rendering and similar things
238 public $mOutputType; # Output type, one of the OT_xxx constants
239 public $ot; # Shortcut alias, see setOutputType()
240 public $mRevisionObject; # The revision object of the specified revision ID
241 public $mRevisionId; # ID to display in {{REVISIONID}} tags
242 public $mRevisionTimestamp; # The timestamp of the specified revision ID
243 public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
244 public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
245 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
246 public $mInputSize = false; # For {{PAGESIZE}} on current page.
247
248 /**
249 * @var array Array with the language name of each language link (i.e. the
250 * interwiki prefix) in the key, value arbitrary. Used to avoid sending
251 * duplicate language links to the ParserOutput.
252 */
253 public $mLangLinkLanguages;
254
255 /**
256 * @var MapCacheLRU|null
257 * @since 1.24
258 *
259 * A cache of the current revisions of titles. Keys are $title->getPrefixedDbKey()
260 */
261 public $currentRevisionCache;
262
263 /**
264 * @var bool|string Recursive call protection.
265 * This variable should be treated as if it were private.
266 */
267 public $mInParse = false;
268
269 /** @var SectionProfiler */
270 protected $mProfiler;
271
272 /**
273 * @var LinkRenderer
274 */
275 protected $mLinkRenderer;
276
277 /** @var MagicWordFactory */
278 private $magicWordFactory;
279
280 /** @var Language */
281 private $contLang;
282
283 /** @var ParserFactory */
284 private $factory;
285
286 /** @var SpecialPageFactory */
287 private $specialPageFactory;
288
289 /**
290 * This is called $svcOptions instead of $options like elsewhere to avoid confusion with
291 * $mOptions, which is public and widely used, and also with the local variable $options used
292 * for ParserOptions throughout this file.
293 *
294 * @var ServiceOptions
295 */
296 private $svcOptions;
297
298 /** @var LinkRendererFactory */
299 private $linkRendererFactory;
300
301 /** @var NamespaceInfo */
302 private $nsInfo;
303
304 /** @var LoggerInterface */
305 private $logger;
306
307 /** @var BadFileLookup */
308 private $badFileLookup;
309
310 /**
311 * TODO Make this a const when HHVM support is dropped (T192166)
312 *
313 * @var array
314 * @since 1.33
315 */
316 public static $constructorOptions = [
317 // See $wgParserConf documentation
318 'class',
319 'preprocessorClass',
320 // See documentation for the corresponding config options
321 'ArticlePath',
322 'EnableScaryTranscluding',
323 'ExtraInterlanguageLinkPrefixes',
324 'FragmentMode',
325 'LanguageCode',
326 'MaxSigChars',
327 'MaxTocLevel',
328 'MiserMode',
329 'ScriptPath',
330 'Server',
331 'ServerName',
332 'ShowHostnames',
333 'Sitename',
334 'StylePath',
335 'TranscludeCacheExpiry',
336 ];
337
338 /**
339 * Constructing parsers directly is deprecated! Use a ParserFactory.
340 *
341 * @param ServiceOptions|null $svcOptions
342 * @param MagicWordFactory|null $magicWordFactory
343 * @param Language|null $contLang Content language
344 * @param ParserFactory|null $factory
345 * @param string|null $urlProtocols As returned from wfUrlProtocols()
346 * @param SpecialPageFactory|null $spFactory
347 * @param LinkRendererFactory|null $linkRendererFactory
348 * @param NamespaceInfo|null $nsInfo
349 * @param LoggerInterface|null $logger
350 * @param BadFileLookup|null $badFileLookup
351 */
352 public function __construct(
353 $svcOptions = null,
354 MagicWordFactory $magicWordFactory = null,
355 Language $contLang = null,
356 ParserFactory $factory = null,
357 $urlProtocols = null,
358 SpecialPageFactory $spFactory = null,
359 $linkRendererFactory = null,
360 $nsInfo = null,
361 $logger = null,
362 BadFileLookup $badFileLookup = null
363 ) {
364 if ( !$svcOptions || is_array( $svcOptions ) ) {
365 // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is
366 // Config, and the eighth is LinkRendererFactory.
367 $this->mConf = (array)$svcOptions;
368 if ( empty( $this->mConf['class'] ) ) {
369 $this->mConf['class'] = self::class;
370 }
371 if ( empty( $this->mConf['preprocessorClass'] ) ) {
372 $this->mConf['preprocessorClass'] = self::getDefaultPreprocessorClass();
373 }
374 $this->svcOptions = new ServiceOptions( self::$constructorOptions,
375 $this->mConf, func_num_args() > 6
376 ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
377 );
378 $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null;
379 $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null;
380 } else {
381 // New calling convention
382 $svcOptions->assertRequiredOptions( self::$constructorOptions );
383 // $this->mConf is public, so we'll keep those two options there as well for
384 // compatibility until it's removed
385 $this->mConf = [
386 'class' => $svcOptions->get( 'class' ),
387 'preprocessorClass' => $svcOptions->get( 'preprocessorClass' ),
388 ];
389 $this->svcOptions = $svcOptions;
390 }
391
392 $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
393 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
394 self::EXT_LINK_ADDR .
395 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
396
397 $this->magicWordFactory = $magicWordFactory ??
398 MediaWikiServices::getInstance()->getMagicWordFactory();
399
400 $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
401
402 $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory();
403 $this->specialPageFactory = $spFactory ??
404 MediaWikiServices::getInstance()->getSpecialPageFactory();
405 $this->linkRendererFactory = $linkRendererFactory ??
406 MediaWikiServices::getInstance()->getLinkRendererFactory();
407 $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
408 $this->logger = $logger ?: new NullLogger();
409 $this->badFileLookup = $badFileLookup ??
410 MediaWikiServices::getInstance()->getBadFileLookup();
411 }
412
413 /**
414 * Reduce memory usage to reduce the impact of circular references
415 */
416 public function __destruct() {
417 if ( isset( $this->mLinkHolders ) ) {
418 // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
419 unset( $this->mLinkHolders );
420 }
421 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
422 foreach ( $this as $name => $value ) {
423 unset( $this->$name );
424 }
425 }
426
427 /**
428 * Allow extensions to clean up when the parser is cloned
429 */
430 public function __clone() {
431 $this->mInParse = false;
432
433 // T58226: When you create a reference "to" an object field, that
434 // makes the object field itself be a reference too (until the other
435 // reference goes out of scope). When cloning, any field that's a
436 // reference is copied as a reference in the new object. Both of these
437 // are defined PHP5 behaviors, as inconvenient as it is for us when old
438 // hooks from PHP4 days are passing fields by reference.
439 foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
440 // Make a non-reference copy of the field, then rebind the field to
441 // reference the new copy.
442 $tmp = $this->$k;
443 $this->$k =& $tmp;
444 unset( $tmp );
445 }
446
447 Hooks::run( 'ParserCloned', [ $this ] );
448 }
449
450 /**
451 * Which class should we use for the preprocessor if not otherwise specified?
452 *
453 * @since 1.34
454 * @deprecated since 1.34, removing configurability of preprocessor
455 * @return string
456 */
457 public static function getDefaultPreprocessorClass() {
458 return Preprocessor_Hash::class;
459 }
460
461 /**
462 * Do various kinds of initialisation on the first call of the parser
463 */
464 public function firstCallInit() {
465 if ( !$this->mFirstCall ) {
466 return;
467 }
468 $this->mFirstCall = false;
469
470 CoreParserFunctions::register( $this );
471 CoreTagHooks::register( $this );
472 $this->initializeVariables();
473
474 // Avoid PHP 7.1 warning from passing $this by reference
475 $parser = $this;
476 Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
477 }
478
479 /**
480 * Clear Parser state
481 *
482 * @private
483 */
484 public function clearState() {
485 $this->firstCallInit();
486 $this->resetOutput();
487 $this->mAutonumber = 0;
488 $this->mIncludeCount = [];
489 $this->mLinkHolders = new LinkHolderArray( $this );
490 $this->mLinkID = 0;
491 $this->mRevisionObject = $this->mRevisionTimestamp =
492 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
493 $this->mVarCache = [];
494 $this->mUser = null;
495 $this->mLangLinkLanguages = [];
496 $this->currentRevisionCache = null;
497
498 $this->mStripState = new StripState( $this );
499
500 # Clear these on every parse, T6549
501 $this->mTplRedirCache = $this->mTplDomCache = [];
502
503 $this->mShowToc = true;
504 $this->mForceTocPosition = false;
505 $this->mIncludeSizes = [
506 'post-expand' => 0,
507 'arg' => 0,
508 ];
509 $this->mPPNodeCount = 0;
510 $this->mGeneratedPPNodeCount = 0;
511 $this->mHighestExpansionDepth = 0;
512 $this->mDefaultSort = false;
513 $this->mHeadings = [];
514 $this->mDoubleUnderscores = [];
515 $this->mExpensiveFunctionCount = 0;
516
517 # Fix cloning
518 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
519 $this->mPreprocessor = null;
520 }
521
522 $this->mProfiler = new SectionProfiler();
523
524 // Avoid PHP 7.1 warning from passing $this by reference
525 $parser = $this;
526 Hooks::run( 'ParserClearState', [ &$parser ] );
527 }
528
529 /**
530 * Reset the ParserOutput
531 */
532 public function resetOutput() {
533 $this->mOutput = new ParserOutput;
534 $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
535 }
536
537 /**
538 * Convert wikitext to HTML
539 * Do not call this function recursively.
540 *
541 * @param string $text Text we want to parse
542 * @param-taint $text escapes_htmlnoent
543 * @param Title $title
544 * @param ParserOptions $options
545 * @param bool $linestart
546 * @param bool $clearState
547 * @param int|null $revid ID of the revision being rendered. This is used to render
548 * REVISION* magic words. 0 means that any current revision will be used. Null means
549 * that {{REVISIONID}}/{{REVISIONUSER}} will be empty and {{REVISIONTIMESTAMP}} will
550 * use the current timestamp.
551 * @return ParserOutput A ParserOutput
552 * @return-taint escaped
553 */
554 public function parse(
555 $text, Title $title, ParserOptions $options,
556 $linestart = true, $clearState = true, $revid = null
557 ) {
558 if ( $clearState ) {
559 // We use U+007F DELETE to construct strip markers, so we have to make
560 // sure that this character does not occur in the input text.
561 $text = strtr( $text, "\x7f", "?" );
562 $magicScopeVariable = $this->lock();
563 }
564 // Strip U+0000 NULL (T159174)
565 $text = str_replace( "\000", '', $text );
566
567 $this->startParse( $title, $options, self::OT_HTML, $clearState );
568
569 $this->currentRevisionCache = null;
570 $this->mInputSize = strlen( $text );
571 if ( $this->mOptions->getEnableLimitReport() ) {
572 $this->mOutput->resetParseStartTime();
573 }
574
575 $oldRevisionId = $this->mRevisionId;
576 $oldRevisionObject = $this->mRevisionObject;
577 $oldRevisionTimestamp = $this->mRevisionTimestamp;
578 $oldRevisionUser = $this->mRevisionUser;
579 $oldRevisionSize = $this->mRevisionSize;
580 if ( $revid !== null ) {
581 $this->mRevisionId = $revid;
582 $this->mRevisionObject = null;
583 $this->mRevisionTimestamp = null;
584 $this->mRevisionUser = null;
585 $this->mRevisionSize = null;
586 }
587
588 // Avoid PHP 7.1 warning from passing $this by reference
589 $parser = $this;
590 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
591 # No more strip!
592 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
593 $text = $this->internalParse( $text );
594 Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
595
596 $text = $this->internalParseHalfParsed( $text, true, $linestart );
597
598 /**
599 * A converted title will be provided in the output object if title and
600 * content conversion are enabled, the article text does not contain
601 * a conversion-suppressing double-underscore tag, and no
602 * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
603 * automatic link conversion.
604 */
605 if ( !( $options->getDisableTitleConversion()
606 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
607 || isset( $this->mDoubleUnderscores['notitleconvert'] )
608 || $this->mOutput->getDisplayTitle() !== false )
609 ) {
610 $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
611 if ( $convruletitle ) {
612 $this->mOutput->setTitleText( $convruletitle );
613 } else {
614 $titleText = $this->getTargetLanguage()->convertTitle( $title );
615 $this->mOutput->setTitleText( $titleText );
616 }
617 }
618
619 # Compute runtime adaptive expiry if set
620 $this->mOutput->finalizeAdaptiveCacheExpiry();
621
622 # Warn if too many heavyweight parser functions were used
623 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
624 $this->limitationWarn( 'expensive-parserfunction',
625 $this->mExpensiveFunctionCount,
626 $this->mOptions->getExpensiveParserFunctionLimit()
627 );
628 }
629
630 # Information on limits, for the benefit of users who try to skirt them
631 if ( $this->mOptions->getEnableLimitReport() ) {
632 $text .= $this->makeLimitReport();
633 }
634
635 # Wrap non-interface parser output in a <div> so it can be targeted
636 # with CSS (T37247)
637 $class = $this->mOptions->getWrapOutputClass();
638 if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
639 $this->mOutput->addWrapperDivClass( $class );
640 }
641
642 $this->mOutput->setText( $text );
643
644 $this->mRevisionId = $oldRevisionId;
645 $this->mRevisionObject = $oldRevisionObject;
646 $this->mRevisionTimestamp = $oldRevisionTimestamp;
647 $this->mRevisionUser = $oldRevisionUser;
648 $this->mRevisionSize = $oldRevisionSize;
649 $this->mInputSize = false;
650 $this->currentRevisionCache = null;
651
652 return $this->mOutput;
653 }
654
655 /**
656 * Set the limit report data in the current ParserOutput, and return the
657 * limit report HTML comment.
658 *
659 * @return string
660 */
661 protected function makeLimitReport() {
662 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
663
664 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
665 if ( $cpuTime !== null ) {
666 $this->mOutput->setLimitReportData( 'limitreport-cputime',
667 sprintf( "%.3f", $cpuTime )
668 );
669 }
670
671 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
672 $this->mOutput->setLimitReportData( 'limitreport-walltime',
673 sprintf( "%.3f", $wallTime )
674 );
675
676 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
677 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
678 );
679 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
680 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
681 );
682 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
683 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
684 );
685 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
686 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
687 );
688 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
689 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
690 );
691 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
692 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
693 );
694
695 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
696 $this->mOutput->setLimitReportData( $key, $value );
697 }
698
699 Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
700
701 $limitReport = "NewPP limit report\n";
702 if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
703 $limitReport .= 'Parsed by ' . wfHostname() . "\n";
704 }
705 $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
706 $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
707 $limitReport .= 'Dynamic content: ' .
708 ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
709 "\n";
710 $limitReport .= 'Complications: [' . implode( ', ', $this->mOutput->getAllFlags() ) . "]\n";
711
712 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
713 if ( Hooks::run( 'ParserLimitReportFormat',
714 [ $key, &$value, &$limitReport, false, false ]
715 ) ) {
716 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
717 $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
718 ->inLanguage( 'en' )->useDatabase( false );
719 if ( !$valueMsg->exists() ) {
720 $valueMsg = new RawMessage( '$1' );
721 }
722 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
723 $valueMsg->params( $value );
724 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
725 }
726 }
727 }
728 // Since we're not really outputting HTML, decode the entities and
729 // then re-encode the things that need hiding inside HTML comments.
730 $limitReport = htmlspecialchars_decode( $limitReport );
731
732 // Sanitize for comment. Note '‐' in the replacement is U+2010,
733 // which looks much like the problematic '-'.
734 $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
735 $text = "\n<!-- \n$limitReport-->\n";
736
737 // Add on template profiling data in human/machine readable way
738 $dataByFunc = $this->mProfiler->getFunctionStats();
739 uasort( $dataByFunc, function ( $a, $b ) {
740 return $b['real'] <=> $a['real']; // descending order
741 } );
742 $profileReport = [];
743 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
744 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
745 $item['%real'], $item['real'], $item['calls'],
746 htmlspecialchars( $item['name'] ) );
747 }
748 $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
749 $text .= implode( "\n", $profileReport ) . "\n-->\n";
750
751 $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
752
753 // Add other cache related metadata
754 if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
755 $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
756 }
757 $this->mOutput->setLimitReportData( 'cachereport-timestamp',
758 $this->mOutput->getCacheTime() );
759 $this->mOutput->setLimitReportData( 'cachereport-ttl',
760 $this->mOutput->getCacheExpiry() );
761 $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
762 $this->mOutput->hasDynamicContent() );
763
764 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
765 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
766 $this->mTitle->getPrefixedDBkey() );
767 }
768 return $text;
769 }
770
771 /**
772 * Half-parse wikitext to half-parsed HTML. This recursive parser entry point
773 * can be called from an extension tag hook.
774 *
775 * The output of this function IS NOT SAFE PARSED HTML; it is "half-parsed"
776 * instead, which means that lists and links have not been fully parsed yet,
777 * and strip markers are still present.
778 *
779 * Use recursiveTagParseFully() to fully parse wikitext to output-safe HTML.
780 *
781 * Use this function if you're a parser tag hook and you want to parse
782 * wikitext before or after applying additional transformations, and you
783 * intend to *return the result as hook output*, which will cause it to go
784 * through the rest of parsing process automatically.
785 *
786 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
787 * $text are not expanded
788 *
789 * @param string $text Text extension wants to have parsed
790 * @param-taint $text escapes_htmlnoent
791 * @param bool|PPFrame $frame The frame to use for expanding any template variables
792 * @return string UNSAFE half-parsed HTML
793 * @return-taint escaped
794 */
795 public function recursiveTagParse( $text, $frame = false ) {
796 // Avoid PHP 7.1 warning from passing $this by reference
797 $parser = $this;
798 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
799 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
800 $text = $this->internalParse( $text, false, $frame );
801 return $text;
802 }
803
804 /**
805 * Fully parse wikitext to fully parsed HTML. This recursive parser entry
806 * point can be called from an extension tag hook.
807 *
808 * The output of this function is fully-parsed HTML that is safe for output.
809 * If you're a parser tag hook, you might want to use recursiveTagParse()
810 * instead.
811 *
812 * If $frame is not provided, then template variables (e.g., {{{1}}}) within
813 * $text are not expanded
814 *
815 * @since 1.25
816 *
817 * @param string $text Text extension wants to have parsed
818 * @param-taint $text escapes_htmlnoent
819 * @param bool|PPFrame $frame The frame to use for expanding any template variables
820 * @return string Fully parsed HTML
821 * @return-taint escaped
822 */
823 public function recursiveTagParseFully( $text, $frame = false ) {
824 $text = $this->recursiveTagParse( $text, $frame );
825 $text = $this->internalParseHalfParsed( $text, false );
826 return $text;
827 }
828
829 /**
830 * Expand templates and variables in the text, producing valid, static wikitext.
831 * Also removes comments.
832 * Do not call this function recursively.
833 * @param string $text
834 * @param Title|null $title
835 * @param ParserOptions $options
836 * @param int|null $revid
837 * @param bool|PPFrame $frame
838 * @return mixed|string
839 */
840 public function preprocess( $text, Title $title = null,
841 ParserOptions $options, $revid = null, $frame = false
842 ) {
843 $magicScopeVariable = $this->lock();
844 $this->startParse( $title, $options, self::OT_PREPROCESS, true );
845 if ( $revid !== null ) {
846 $this->mRevisionId = $revid;
847 }
848 // Avoid PHP 7.1 warning from passing $this by reference
849 $parser = $this;
850 Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
851 Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
852 $text = $this->replaceVariables( $text, $frame );
853 $text = $this->mStripState->unstripBoth( $text );
854 return $text;
855 }
856
857 /**
858 * Recursive parser entry point that can be called from an extension tag
859 * hook.
860 *
861 * @param string $text Text to be expanded
862 * @param bool|PPFrame $frame The frame to use for expanding any template variables
863 * @return string
864 * @since 1.19
865 */
866 public function recursivePreprocess( $text, $frame = false ) {
867 $text = $this->replaceVariables( $text, $frame );
868 $text = $this->mStripState->unstripBoth( $text );
869 return $text;
870 }
871
872 /**
873 * Process the wikitext for the "?preload=" feature. (T7210)
874 *
875 * "<noinclude>", "<includeonly>" etc. are parsed as for template
876 * transclusion, comments, templates, arguments, tags hooks and parser
877 * functions are untouched.
878 *
879 * @param string $text
880 * @param Title $title
881 * @param ParserOptions $options
882 * @param array $params
883 * @return string
884 */
885 public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
886 $msg = new RawMessage( $text );
887 $text = $msg->params( $params )->plain();
888
889 # Parser (re)initialisation
890 $magicScopeVariable = $this->lock();
891 $this->startParse( $title, $options, self::OT_PLAIN, true );
892
893 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
894 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
895 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
896 $text = $this->mStripState->unstripBoth( $text );
897 return $text;
898 }
899
900 /**
901 * Set the current user.
902 * Should only be used when doing pre-save transform.
903 *
904 * @param User|null $user User object or null (to reset)
905 */
906 public function setUser( $user ) {
907 $this->mUser = $user;
908 }
909
910 /**
911 * Set the context title
912 *
913 * @param Title|null $t
914 */
915 public function setTitle( Title $t = null ) {
916 if ( !$t ) {
917 $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
918 }
919
920 if ( $t->hasFragment() ) {
921 # Strip the fragment to avoid various odd effects
922 $this->mTitle = $t->createFragmentTarget( '' );
923 } else {
924 $this->mTitle = $t;
925 }
926 }
927
928 /**
929 * Accessor for the Title object
930 *
931 * Since 1.34, leaving `mTitle` uninitialized as `null` is deprecated.
932 *
933 * @return Title|null
934 */
935 public function getTitle() : ?Title {
936 if ( $this->mTitle === null ) {
937 wfDeprecated( 'Parser title should never be null', '1.34' );
938 }
939 return $this->mTitle;
940 }
941
942 /**
943 * Accessor/mutator for the Title object
944 *
945 * @param Title|null $x Title object or null to just get the current one
946 * @return Title|null
947 */
948 public function Title( Title $x = null ) : ?Title {
949 return wfSetVar( $this->mTitle, $x );
950 }
951
952 /**
953 * Set the output type
954 *
955 * @param int $ot New value
956 */
957 public function setOutputType( $ot ) {
958 $this->mOutputType = $ot;
959 # Shortcut alias
960 $this->ot = [
961 'html' => $ot == self::OT_HTML,
962 'wiki' => $ot == self::OT_WIKI,
963 'pre' => $ot == self::OT_PREPROCESS,
964 'plain' => $ot == self::OT_PLAIN,
965 ];
966 }
967
968 /**
969 * Accessor/mutator for the output type
970 *
971 * @param int|null $x New value or null to just get the current one
972 * @return int
973 */
974 public function OutputType( $x = null ) {
975 return wfSetVar( $this->mOutputType, $x );
976 }
977
978 /**
979 * Get the ParserOutput object
980 *
981 * @return ParserOutput
982 */
983 public function getOutput() {
984 return $this->mOutput;
985 }
986
987 /**
988 * Get the ParserOptions object
989 *
990 * @return ParserOptions
991 */
992 public function getOptions() {
993 return $this->mOptions;
994 }
995
996 /**
997 * Accessor/mutator for the ParserOptions object
998 *
999 * @param ParserOptions|null $x New value or null to just get the current one
1000 * @return ParserOptions Current ParserOptions object
1001 */
1002 public function Options( $x = null ) {
1003 return wfSetVar( $this->mOptions, $x );
1004 }
1005
1006 /**
1007 * @return int
1008 */
1009 public function nextLinkID() {
1010 return $this->mLinkID++;
1011 }
1012
1013 /**
1014 * @param int $id
1015 */
1016 public function setLinkID( $id ) {
1017 $this->mLinkID = $id;
1018 }
1019
1020 /**
1021 * Get a language object for use in parser functions such as {{FORMATNUM:}}
1022 * @return Language
1023 */
1024 public function getFunctionLang() {
1025 return $this->getTargetLanguage();
1026 }
1027
1028 /**
1029 * Get the target language for the content being parsed. This is usually the
1030 * language that the content is in.
1031 *
1032 * @since 1.19
1033 *
1034 * @throws MWException
1035 * @return Language
1036 */
1037 public function getTargetLanguage() {
1038 $target = $this->mOptions->getTargetLanguage();
1039
1040 if ( $target !== null ) {
1041 return $target;
1042 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1043 return $this->mOptions->getUserLangObj();
1044 } elseif ( is_null( $this->mTitle ) ) {
1045 throw new MWException( __METHOD__ . ': $this->mTitle is null' );
1046 }
1047
1048 return $this->mTitle->getPageLanguage();
1049 }
1050
1051 /**
1052 * Get the language object for language conversion
1053 * @deprecated since 1.32, just use getTargetLanguage()
1054 * @return Language
1055 */
1056 public function getConverterLanguage() {
1057 wfDeprecated( __METHOD__, '1.32' );
1058 return $this->getTargetLanguage();
1059 }
1060
1061 /**
1062 * Get a User object either from $this->mUser, if set, or from the
1063 * ParserOptions object otherwise
1064 *
1065 * @return User
1066 */
1067 public function getUser() {
1068 if ( !is_null( $this->mUser ) ) {
1069 return $this->mUser;
1070 }
1071 return $this->mOptions->getUser();
1072 }
1073
1074 /**
1075 * Get a preprocessor object
1076 *
1077 * @return Preprocessor
1078 */
1079 public function getPreprocessor() {
1080 if ( !isset( $this->mPreprocessor ) ) {
1081 $class = $this->svcOptions->get( 'preprocessorClass' );
1082 $this->mPreprocessor = new $class( $this );
1083 }
1084 return $this->mPreprocessor;
1085 }
1086
1087 /**
1088 * Get a LinkRenderer instance to make links with
1089 *
1090 * @since 1.28
1091 * @return LinkRenderer
1092 */
1093 public function getLinkRenderer() {
1094 // XXX We make the LinkRenderer with current options and then cache it forever
1095 if ( !$this->mLinkRenderer ) {
1096 $this->mLinkRenderer = $this->linkRendererFactory->create();
1097 $this->mLinkRenderer->setStubThreshold(
1098 $this->getOptions()->getStubThreshold()
1099 );
1100 }
1101
1102 return $this->mLinkRenderer;
1103 }
1104
1105 /**
1106 * Get the MagicWordFactory that this Parser is using
1107 *
1108 * @since 1.32
1109 * @return MagicWordFactory
1110 */
1111 public function getMagicWordFactory() {
1112 return $this->magicWordFactory;
1113 }
1114
1115 /**
1116 * Get the content language that this Parser is using
1117 *
1118 * @since 1.32
1119 * @return Language
1120 */
1121 public function getContentLanguage() {
1122 return $this->contLang;
1123 }
1124
1125 /**
1126 * Replaces all occurrences of HTML-style comments and the given tags
1127 * in the text with a random marker and returns the next text. The output
1128 * parameter $matches will be an associative array filled with data in
1129 * the form:
1130 *
1131 * @code
1132 * 'UNIQ-xxxxx' => [
1133 * 'element',
1134 * 'tag content',
1135 * [ 'param' => 'x' ],
1136 * '<element param="x">tag content</element>' ]
1137 * @endcode
1138 *
1139 * @param array $elements List of element names. Comments are always extracted.
1140 * @param string $text Source text string.
1141 * @param array &$matches Out parameter, Array: extracted tags
1142 * @return string Stripped text
1143 */
1144 public static function extractTagsAndParams( $elements, $text, &$matches ) {
1145 static $n = 1;
1146 $stripped = '';
1147 $matches = [];
1148
1149 $taglist = implode( '|', $elements );
1150 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1151
1152 while ( $text != '' ) {
1153 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1154 $stripped .= $p[0];
1155 if ( count( $p ) < 5 ) {
1156 break;
1157 }
1158 if ( count( $p ) > 5 ) {
1159 # comment
1160 $element = $p[4];
1161 $attributes = '';
1162 $close = '';
1163 $inside = $p[5];
1164 } else {
1165 # tag
1166 list( , $element, $attributes, $close, $inside ) = $p;
1167 }
1168
1169 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1170 $stripped .= $marker;
1171
1172 if ( $close === '/>' ) {
1173 # Empty element tag, <tag />
1174 $content = null;
1175 $text = $inside;
1176 $tail = null;
1177 } else {
1178 if ( $element === '!--' ) {
1179 $end = '/(-->)/';
1180 } else {
1181 $end = "/(<\\/$element\\s*>)/i";
1182 }
1183 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1184 $content = $q[0];
1185 if ( count( $q ) < 3 ) {
1186 # No end tag -- let it run out to the end of the text.
1187 $tail = '';
1188 $text = '';
1189 } else {
1190 list( , $tail, $text ) = $q;
1191 }
1192 }
1193
1194 $matches[$marker] = [ $element,
1195 $content,
1196 Sanitizer::decodeTagAttributes( $attributes ),
1197 "<$element$attributes$close$content$tail" ];
1198 }
1199 return $stripped;
1200 }
1201
1202 /**
1203 * Get a list of strippable XML-like elements
1204 *
1205 * @return array
1206 */
1207 public function getStripList() {
1208 return $this->mStripList;
1209 }
1210
1211 /**
1212 * Get the StripState
1213 *
1214 * @return StripState
1215 */
1216 public function getStripState() {
1217 return $this->mStripState;
1218 }
1219
1220 /**
1221 * Add an item to the strip state
1222 * Returns the unique tag which must be inserted into the stripped text
1223 * The tag will be replaced with the original text in unstrip()
1224 *
1225 * @param string $text
1226 *
1227 * @return string
1228 */
1229 public function insertStripItem( $text ) {
1230 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1231 $this->mMarkerIndex++;
1232 $this->mStripState->addGeneral( $marker, $text );
1233 return $marker;
1234 }
1235
1236 /**
1237 * Parse the wiki syntax used to render tables.
1238 *
1239 * @private
1240 * @param string $text
1241 * @return string
1242 * @deprecated since 1.34; should not be used outside parser class.
1243 */
1244 public function doTableStuff( $text ) {
1245 wfDeprecated( __METHOD__, '1.34' );
1246 return $this->handleTables( $text );
1247 }
1248
1249 /**
1250 * Parse the wiki syntax used to render tables.
1251 *
1252 * @param string $text
1253 * @return string
1254 */
1255 private function handleTables( $text ) {
1256 $lines = StringUtils::explode( "\n", $text );
1257 $out = '';
1258 $td_history = []; # Is currently a td tag open?
1259 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1260 $tr_history = []; # Is currently a tr tag open?
1261 $tr_attributes = []; # history of tr attributes
1262 $has_opened_tr = []; # Did this table open a <tr> element?
1263 $indent_level = 0; # indent level of the table
1264
1265 foreach ( $lines as $outLine ) {
1266 $line = trim( $outLine );
1267
1268 if ( $line === '' ) { # empty line, go to next line
1269 $out .= $outLine . "\n";
1270 continue;
1271 }
1272
1273 $first_character = $line[0];
1274 $first_two = substr( $line, 0, 2 );
1275 $matches = [];
1276
1277 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1278 # First check if we are starting a new table
1279 $indent_level = strlen( $matches[1] );
1280
1281 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1282 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1283
1284 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1285 array_push( $td_history, false );
1286 array_push( $last_tag_history, '' );
1287 array_push( $tr_history, false );
1288 array_push( $tr_attributes, '' );
1289 array_push( $has_opened_tr, false );
1290 } elseif ( count( $td_history ) == 0 ) {
1291 # Don't do any of the following
1292 $out .= $outLine . "\n";
1293 continue;
1294 } elseif ( $first_two === '|}' ) {
1295 # We are ending a table
1296 $line = '</table>' . substr( $line, 2 );
1297 $last_tag = array_pop( $last_tag_history );
1298
1299 if ( !array_pop( $has_opened_tr ) ) {
1300 $line = "<tr><td></td></tr>{$line}";
1301 }
1302
1303 if ( array_pop( $tr_history ) ) {
1304 $line = "</tr>{$line}";
1305 }
1306
1307 if ( array_pop( $td_history ) ) {
1308 $line = "</{$last_tag}>{$line}";
1309 }
1310 array_pop( $tr_attributes );
1311 if ( $indent_level > 0 ) {
1312 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1313 } else {
1314 $outLine = $line;
1315 }
1316 } elseif ( $first_two === '|-' ) {
1317 # Now we have a table row
1318 $line = preg_replace( '#^\|-+#', '', $line );
1319
1320 # Whats after the tag is now only attributes
1321 $attributes = $this->mStripState->unstripBoth( $line );
1322 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1323 array_pop( $tr_attributes );
1324 array_push( $tr_attributes, $attributes );
1325
1326 $line = '';
1327 $last_tag = array_pop( $last_tag_history );
1328 array_pop( $has_opened_tr );
1329 array_push( $has_opened_tr, true );
1330
1331 if ( array_pop( $tr_history ) ) {
1332 $line = '</tr>';
1333 }
1334
1335 if ( array_pop( $td_history ) ) {
1336 $line = "</{$last_tag}>{$line}";
1337 }
1338
1339 $outLine = $line;
1340 array_push( $tr_history, false );
1341 array_push( $td_history, false );
1342 array_push( $last_tag_history, '' );
1343 } elseif ( $first_character === '|'
1344 || $first_character === '!'
1345 || $first_two === '|+'
1346 ) {
1347 # This might be cell elements, td, th or captions
1348 if ( $first_two === '|+' ) {
1349 $first_character = '+';
1350 $line = substr( $line, 2 );
1351 } else {
1352 $line = substr( $line, 1 );
1353 }
1354
1355 // Implies both are valid for table headings.
1356 if ( $first_character === '!' ) {
1357 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1358 }
1359
1360 # Split up multiple cells on the same line.
1361 # FIXME : This can result in improper nesting of tags processed
1362 # by earlier parser steps.
1363 $cells = explode( '||', $line );
1364
1365 $outLine = '';
1366
1367 # Loop through each table cell
1368 foreach ( $cells as $cell ) {
1369 $previous = '';
1370 if ( $first_character !== '+' ) {
1371 $tr_after = array_pop( $tr_attributes );
1372 if ( !array_pop( $tr_history ) ) {
1373 $previous = "<tr{$tr_after}>\n";
1374 }
1375 array_push( $tr_history, true );
1376 array_push( $tr_attributes, '' );
1377 array_pop( $has_opened_tr );
1378 array_push( $has_opened_tr, true );
1379 }
1380
1381 $last_tag = array_pop( $last_tag_history );
1382
1383 if ( array_pop( $td_history ) ) {
1384 $previous = "</{$last_tag}>\n{$previous}";
1385 }
1386
1387 if ( $first_character === '|' ) {
1388 $last_tag = 'td';
1389 } elseif ( $first_character === '!' ) {
1390 $last_tag = 'th';
1391 } elseif ( $first_character === '+' ) {
1392 $last_tag = 'caption';
1393 } else {
1394 $last_tag = '';
1395 }
1396
1397 array_push( $last_tag_history, $last_tag );
1398
1399 # A cell could contain both parameters and data
1400 $cell_data = explode( '|', $cell, 2 );
1401
1402 # T2553: Note that a '|' inside an invalid link should not
1403 # be mistaken as delimiting cell parameters
1404 # Bug T153140: Neither should language converter markup.
1405 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1406 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1407 } elseif ( count( $cell_data ) == 1 ) {
1408 // Whitespace in cells is trimmed
1409 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1410 } else {
1411 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1412 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1413 // Whitespace in cells is trimmed
1414 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1415 }
1416
1417 $outLine .= $cell;
1418 array_push( $td_history, true );
1419 }
1420 }
1421 $out .= $outLine . "\n";
1422 }
1423
1424 # Closing open td, tr && table
1425 while ( count( $td_history ) > 0 ) {
1426 if ( array_pop( $td_history ) ) {
1427 $out .= "</td>\n";
1428 }
1429 if ( array_pop( $tr_history ) ) {
1430 $out .= "</tr>\n";
1431 }
1432 if ( !array_pop( $has_opened_tr ) ) {
1433 $out .= "<tr><td></td></tr>\n";
1434 }
1435
1436 $out .= "</table>\n";
1437 }
1438
1439 # Remove trailing line-ending (b/c)
1440 if ( substr( $out, -1 ) === "\n" ) {
1441 $out = substr( $out, 0, -1 );
1442 }
1443
1444 # special case: don't return empty table
1445 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1446 $out = '';
1447 }
1448
1449 return $out;
1450 }
1451
1452 /**
1453 * Helper function for parse() that transforms wiki markup into half-parsed
1454 * HTML. Only called for $mOutputType == self::OT_HTML.
1455 *
1456 * @private
1457 *
1458 * @param string $text The text to parse
1459 * @param-taint $text escapes_html
1460 * @param bool $isMain Whether this is being called from the main parse() function
1461 * @param PPFrame|bool $frame A pre-processor frame
1462 *
1463 * @return string
1464 */
1465 public function internalParse( $text, $isMain = true, $frame = false ) {
1466 $origText = $text;
1467
1468 // Avoid PHP 7.1 warning from passing $this by reference
1469 $parser = $this;
1470
1471 # Hook to suspend the parser in this state
1472 if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1473 return $text;
1474 }
1475
1476 # if $frame is provided, then use $frame for replacing any variables
1477 if ( $frame ) {
1478 # use frame depth to infer how include/noinclude tags should be handled
1479 # depth=0 means this is the top-level document; otherwise it's an included document
1480 if ( !$frame->depth ) {
1481 $flag = 0;
1482 } else {
1483 $flag = self::PTD_FOR_INCLUSION;
1484 }
1485 $dom = $this->preprocessToDom( $text, $flag );
1486 $text = $frame->expand( $dom );
1487 } else {
1488 # if $frame is not provided, then use old-style replaceVariables
1489 $text = $this->replaceVariables( $text );
1490 }
1491
1492 Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1493 $text = Sanitizer::removeHTMLtags(
1494 $text,
1495 [ $this, 'attributeStripCallback' ],
1496 false,
1497 array_keys( $this->mTransparentTagHooks ),
1498 [],
1499 [ $this, 'addTrackingCategory' ]
1500 );
1501 Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1502
1503 # Tables need to come after variable replacement for things to work
1504 # properly; putting them before other transformations should keep
1505 # exciting things like link expansions from showing up in surprising
1506 # places.
1507 $text = $this->handleTables( $text );
1508
1509 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1510
1511 $text = $this->handleDoubleUnderscore( $text );
1512
1513 $text = $this->handleHeadings( $text );
1514 $text = $this->handleInternalLinks( $text );
1515 $text = $this->handleAllQuotes( $text );
1516 $text = $this->handleExternalLinks( $text );
1517
1518 # handleInternalLinks may sometimes leave behind
1519 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1520 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1521
1522 $text = $this->handleMagicLinks( $text );
1523 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1524
1525 return $text;
1526 }
1527
1528 /**
1529 * Helper function for parse() that transforms half-parsed HTML into fully
1530 * parsed HTML.
1531 *
1532 * @param string $text
1533 * @param bool $isMain
1534 * @param bool $linestart
1535 * @return string
1536 */
1537 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1538 $text = $this->mStripState->unstripGeneral( $text );
1539
1540 // Avoid PHP 7.1 warning from passing $this by reference
1541 $parser = $this;
1542
1543 if ( $isMain ) {
1544 Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1545 }
1546
1547 # Clean up special characters, only run once, next-to-last before doBlockLevels
1548 $text = Sanitizer::armorFrenchSpaces( $text );
1549
1550 $text = $this->doBlockLevels( $text, $linestart );
1551
1552 $this->replaceLinkHoldersPrivate( $text );
1553
1554 /**
1555 * The input doesn't get language converted if
1556 * a) It's disabled
1557 * b) Content isn't converted
1558 * c) It's a conversion table
1559 * d) it is an interface message (which is in the user language)
1560 */
1561 if ( !( $this->mOptions->getDisableContentConversion()
1562 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1563 && !$this->mOptions->getInterfaceMessage()
1564 ) {
1565 # The position of the convert() call should not be changed. it
1566 # assumes that the links are all replaced and the only thing left
1567 # is the <nowiki> mark.
1568 $text = $this->getTargetLanguage()->convert( $text );
1569 }
1570
1571 $text = $this->mStripState->unstripNoWiki( $text );
1572
1573 if ( $isMain ) {
1574 Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1575 }
1576
1577 $text = $this->replaceTransparentTags( $text );
1578 $text = $this->mStripState->unstripGeneral( $text );
1579
1580 $text = Sanitizer::normalizeCharReferences( $text );
1581
1582 if ( MWTidy::isEnabled() ) {
1583 if ( $this->mOptions->getTidy() ) {
1584 $text = MWTidy::tidy( $text );
1585 }
1586 } else {
1587 # attempt to sanitize at least some nesting problems
1588 # (T4702 and quite a few others)
1589 # This code path is buggy and deprecated!
1590 wfDeprecated( 'disabling tidy', '1.33' );
1591 $tidyregs = [
1592 # ''Something [http://www.cool.com cool''] -->
1593 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1594 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1595 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1596 # fix up an anchor inside another anchor, only
1597 # at least for a single single nested link (T5695)
1598 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1599 '\\1\\2</a>\\3</a>\\1\\4</a>',
1600 # fix div inside inline elements- doBlockLevels won't wrap a line which
1601 # contains a div, so fix it up here; replace
1602 # div with escaped text
1603 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1604 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1605 # remove empty italic or bold tag pairs, some
1606 # introduced by rules above
1607 '/<([bi])><\/\\1>/' => '',
1608 ];
1609
1610 $text = preg_replace(
1611 array_keys( $tidyregs ),
1612 array_values( $tidyregs ),
1613 $text );
1614 }
1615
1616 if ( $isMain ) {
1617 Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1618 }
1619
1620 return $text;
1621 }
1622
1623 /**
1624 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1625 * magic external links.
1626 *
1627 * DML
1628 * @private
1629 * @param string $text
1630 * @return string
1631 * @deprecated since 1.34; should not be used outside parser class.
1632 */
1633 public function doMagicLinks( $text ) {
1634 wfDeprecated( __METHOD__, '1.34' );
1635 return $this->handleMagicLinks( $text );
1636 }
1637
1638 /**
1639 * Replace special strings like "ISBN xxx" and "RFC xxx" with
1640 * magic external links.
1641 *
1642 * DML
1643 *
1644 * @param string $text
1645 *
1646 * @return string
1647 */
1648 private function handleMagicLinks( $text ) {
1649 $prots = wfUrlProtocolsWithoutProtRel();
1650 $urlChar = self::EXT_LINK_URL_CLASS;
1651 $addr = self::EXT_LINK_ADDR;
1652 $space = self::SPACE_NOT_NL; # non-newline space
1653 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1654 $spaces = "$space++"; # possessive match of 1 or more spaces
1655 $text = preg_replace_callback(
1656 '!(?: # Start cases
1657 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1658 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1659 (\b # m[3]: Free external links
1660 (?i:$prots)
1661 ($addr$urlChar*) # m[4]: Post-protocol path
1662 ) |
1663 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1664 ([0-9]+)\b |
1665 \bISBN $spaces ( # m[6]: ISBN, capture number
1666 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1667 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1668 [0-9Xx] # check digit
1669 )\b
1670 )!xu", [ $this, 'magicLinkCallback' ], $text );
1671 return $text;
1672 }
1673
1674 /**
1675 * @throws MWException
1676 * @param array $m
1677 * @return string HTML
1678 */
1679 public function magicLinkCallback( $m ) {
1680 if ( isset( $m[1] ) && $m[1] !== '' ) {
1681 # Skip anchor
1682 return $m[0];
1683 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1684 # Skip HTML element
1685 return $m[0];
1686 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1687 # Free external link
1688 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1689 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1690 # RFC or PMID
1691 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1692 if ( !$this->mOptions->getMagicRFCLinks() ) {
1693 return $m[0];
1694 }
1695 $keyword = 'RFC';
1696 $urlmsg = 'rfcurl';
1697 $cssClass = 'mw-magiclink-rfc';
1698 $trackingCat = 'magiclink-tracking-rfc';
1699 $id = $m[5];
1700 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1701 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1702 return $m[0];
1703 }
1704 $keyword = 'PMID';
1705 $urlmsg = 'pubmedurl';
1706 $cssClass = 'mw-magiclink-pmid';
1707 $trackingCat = 'magiclink-tracking-pmid';
1708 $id = $m[5];
1709 } else {
1710 throw new MWException( __METHOD__ . ': unrecognised match type "' .
1711 substr( $m[0], 0, 20 ) . '"' );
1712 }
1713 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1714 $this->addTrackingCategory( $trackingCat );
1715 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1716 } elseif ( isset( $m[6] ) && $m[6] !== ''
1717 && $this->mOptions->getMagicISBNLinks()
1718 ) {
1719 # ISBN
1720 $isbn = $m[6];
1721 $space = self::SPACE_NOT_NL; # non-newline space
1722 $isbn = preg_replace( "/$space/", ' ', $isbn );
1723 $num = strtr( $isbn, [
1724 '-' => '',
1725 ' ' => '',
1726 'x' => 'X',
1727 ] );
1728 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1729 return $this->getLinkRenderer()->makeKnownLink(
1730 SpecialPage::getTitleFor( 'Booksources', $num ),
1731 "ISBN $isbn",
1732 [
1733 'class' => 'internal mw-magiclink-isbn',
1734 'title' => false // suppress title attribute
1735 ]
1736 );
1737 } else {
1738 return $m[0];
1739 }
1740 }
1741
1742 /**
1743 * Make a free external link, given a user-supplied URL
1744 *
1745 * @param string $url
1746 * @param int $numPostProto
1747 * The number of characters after the protocol.
1748 * @return string HTML
1749 * @private
1750 */
1751 public function makeFreeExternalLink( $url, $numPostProto ) {
1752 $trail = '';
1753
1754 # The characters '<' and '>' (which were escaped by
1755 # removeHTMLtags()) should not be included in
1756 # URLs, per RFC 2396.
1757 # Make &nbsp; terminate a URL as well (bug T84937)
1758 $m2 = [];
1759 if ( preg_match(
1760 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1761 $url,
1762 $m2,
1763 PREG_OFFSET_CAPTURE
1764 ) ) {
1765 $trail = substr( $url, $m2[0][1] ) . $trail;
1766 $url = substr( $url, 0, $m2[0][1] );
1767 }
1768
1769 # Move trailing punctuation to $trail
1770 $sep = ',;\.:!?';
1771 # If there is no left bracket, then consider right brackets fair game too
1772 if ( strpos( $url, '(' ) === false ) {
1773 $sep .= ')';
1774 }
1775
1776 $urlRev = strrev( $url );
1777 $numSepChars = strspn( $urlRev, $sep );
1778 # Don't break a trailing HTML entity by moving the ; into $trail
1779 # This is in hot code, so use substr_compare to avoid having to
1780 # create a new string object for the comparison
1781 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1782 # more optimization: instead of running preg_match with a $
1783 # anchor, which can be slow, do the match on the reversed
1784 # string starting at the desired offset.
1785 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1786 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1787 $numSepChars--;
1788 }
1789 }
1790 if ( $numSepChars ) {
1791 $trail = substr( $url, -$numSepChars ) . $trail;
1792 $url = substr( $url, 0, -$numSepChars );
1793 }
1794
1795 # Verify that we still have a real URL after trail removal, and
1796 # not just lone protocol
1797 if ( strlen( $trail ) >= $numPostProto ) {
1798 return $url . $trail;
1799 }
1800
1801 $url = Sanitizer::cleanUrl( $url );
1802
1803 # Is this an external image?
1804 $text = $this->maybeMakeExternalImage( $url );
1805 if ( $text === false ) {
1806 # Not an image, make a link
1807 $text = Linker::makeExternalLink( $url,
1808 $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1809 true, 'free',
1810 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1811 # Register it in the output object...
1812 $this->mOutput->addExternalLink( $url );
1813 }
1814 return $text . $trail;
1815 }
1816
1817 /**
1818 * Parse headers and return html.
1819 *
1820 * @private
1821 * @param string $text
1822 * @return string
1823 * @deprecated since 1.34; should not be used outside parser class.
1824 */
1825 public function doHeadings( $text ) {
1826 wfDeprecated( __METHOD__, '1.34' );
1827 return $this->handleHeadings( $text );
1828 }
1829
1830 /**
1831 * Parse headers and return html
1832 *
1833 * @param string $text
1834 * @return string
1835 */
1836 private function handleHeadings( $text ) {
1837 for ( $i = 6; $i >= 1; --$i ) {
1838 $h = str_repeat( '=', $i );
1839 // Trim non-newline whitespace from headings
1840 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1841 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1842 }
1843 return $text;
1844 }
1845
1846 /**
1847 * Replace single quotes with HTML markup
1848 * @private
1849 *
1850 * @param string $text
1851 *
1852 * @return string The altered text
1853 * @deprecated since 1.34; should not be used outside parser class.
1854 */
1855 public function doAllQuotes( $text ) {
1856 wfDeprecated( __METHOD__, '1.34' );
1857 return $this->handleAllQuotes( $text );
1858 }
1859
1860 /**
1861 * Replace single quotes with HTML markup
1862 *
1863 * @param string $text
1864 *
1865 * @return string The altered text
1866 */
1867 private function handleAllQuotes( $text ) {
1868 $outtext = '';
1869 $lines = StringUtils::explode( "\n", $text );
1870 foreach ( $lines as $line ) {
1871 $outtext .= $this->doQuotes( $line ) . "\n";
1872 }
1873 $outtext = substr( $outtext, 0, -1 );
1874 return $outtext;
1875 }
1876
1877 /**
1878 * Helper function for doAllQuotes()
1879 *
1880 * @param string $text
1881 *
1882 * @return string
1883 * @internal
1884 */
1885 public function doQuotes( $text ) {
1886 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1887 $countarr = count( $arr );
1888 if ( $countarr == 1 ) {
1889 return $text;
1890 }
1891
1892 // First, do some preliminary work. This may shift some apostrophes from
1893 // being mark-up to being text. It also counts the number of occurrences
1894 // of bold and italics mark-ups.
1895 $numbold = 0;
1896 $numitalics = 0;
1897 for ( $i = 1; $i < $countarr; $i += 2 ) {
1898 $thislen = strlen( $arr[$i] );
1899 // If there are ever four apostrophes, assume the first is supposed to
1900 // be text, and the remaining three constitute mark-up for bold text.
1901 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1902 if ( $thislen == 4 ) {
1903 $arr[$i - 1] .= "'";
1904 $arr[$i] = "'''";
1905 $thislen = 3;
1906 } elseif ( $thislen > 5 ) {
1907 // If there are more than 5 apostrophes in a row, assume they're all
1908 // text except for the last 5.
1909 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1910 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1911 $arr[$i] = "'''''";
1912 $thislen = 5;
1913 }
1914 // Count the number of occurrences of bold and italics mark-ups.
1915 if ( $thislen == 2 ) {
1916 $numitalics++;
1917 } elseif ( $thislen == 3 ) {
1918 $numbold++;
1919 } elseif ( $thislen == 5 ) {
1920 $numitalics++;
1921 $numbold++;
1922 }
1923 }
1924
1925 // If there is an odd number of both bold and italics, it is likely
1926 // that one of the bold ones was meant to be an apostrophe followed
1927 // by italics. Which one we cannot know for certain, but it is more
1928 // likely to be one that has a single-letter word before it.
1929 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1930 $firstsingleletterword = -1;
1931 $firstmultiletterword = -1;
1932 $firstspace = -1;
1933 for ( $i = 1; $i < $countarr; $i += 2 ) {
1934 if ( strlen( $arr[$i] ) == 3 ) {
1935 $x1 = substr( $arr[$i - 1], -1 );
1936 $x2 = substr( $arr[$i - 1], -2, 1 );
1937 if ( $x1 === ' ' ) {
1938 if ( $firstspace == -1 ) {
1939 $firstspace = $i;
1940 }
1941 } elseif ( $x2 === ' ' ) {
1942 $firstsingleletterword = $i;
1943 // if $firstsingleletterword is set, we don't
1944 // look at the other options, so we can bail early.
1945 break;
1946 } elseif ( $firstmultiletterword == -1 ) {
1947 $firstmultiletterword = $i;
1948 }
1949 }
1950 }
1951
1952 // If there is a single-letter word, use it!
1953 if ( $firstsingleletterword > -1 ) {
1954 $arr[$firstsingleletterword] = "''";
1955 $arr[$firstsingleletterword - 1] .= "'";
1956 } elseif ( $firstmultiletterword > -1 ) {
1957 // If not, but there's a multi-letter word, use that one.
1958 $arr[$firstmultiletterword] = "''";
1959 $arr[$firstmultiletterword - 1] .= "'";
1960 } elseif ( $firstspace > -1 ) {
1961 // ... otherwise use the first one that has neither.
1962 // (notice that it is possible for all three to be -1 if, for example,
1963 // there is only one pentuple-apostrophe in the line)
1964 $arr[$firstspace] = "''";
1965 $arr[$firstspace - 1] .= "'";
1966 }
1967 }
1968
1969 // Now let's actually convert our apostrophic mush to HTML!
1970 $output = '';
1971 $buffer = '';
1972 $state = '';
1973 $i = 0;
1974 foreach ( $arr as $r ) {
1975 if ( ( $i % 2 ) == 0 ) {
1976 if ( $state === 'both' ) {
1977 $buffer .= $r;
1978 } else {
1979 $output .= $r;
1980 }
1981 } else {
1982 $thislen = strlen( $r );
1983 if ( $thislen == 2 ) {
1984 if ( $state === 'i' ) {
1985 $output .= '</i>';
1986 $state = '';
1987 } elseif ( $state === 'bi' ) {
1988 $output .= '</i>';
1989 $state = 'b';
1990 } elseif ( $state === 'ib' ) {
1991 $output .= '</b></i><b>';
1992 $state = 'b';
1993 } elseif ( $state === 'both' ) {
1994 $output .= '<b><i>' . $buffer . '</i>';
1995 $state = 'b';
1996 } else { // $state can be 'b' or ''
1997 $output .= '<i>';
1998 $state .= 'i';
1999 }
2000 } elseif ( $thislen == 3 ) {
2001 if ( $state === 'b' ) {
2002 $output .= '</b>';
2003 $state = '';
2004 } elseif ( $state === 'bi' ) {
2005 $output .= '</i></b><i>';
2006 $state = 'i';
2007 } elseif ( $state === 'ib' ) {
2008 $output .= '</b>';
2009 $state = 'i';
2010 } elseif ( $state === 'both' ) {
2011 $output .= '<i><b>' . $buffer . '</b>';
2012 $state = 'i';
2013 } else { // $state can be 'i' or ''
2014 $output .= '<b>';
2015 $state .= 'b';
2016 }
2017 } elseif ( $thislen == 5 ) {
2018 if ( $state === 'b' ) {
2019 $output .= '</b><i>';
2020 $state = 'i';
2021 } elseif ( $state === 'i' ) {
2022 $output .= '</i><b>';
2023 $state = 'b';
2024 } elseif ( $state === 'bi' ) {
2025 $output .= '</i></b>';
2026 $state = '';
2027 } elseif ( $state === 'ib' ) {
2028 $output .= '</b></i>';
2029 $state = '';
2030 } elseif ( $state === 'both' ) {
2031 $output .= '<i><b>' . $buffer . '</b></i>';
2032 $state = '';
2033 } else { // ($state == '')
2034 $buffer = '';
2035 $state = 'both';
2036 }
2037 }
2038 }
2039 $i++;
2040 }
2041 // Now close all remaining tags. Notice that the order is important.
2042 if ( $state === 'b' || $state === 'ib' ) {
2043 $output .= '</b>';
2044 }
2045 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2046 $output .= '</i>';
2047 }
2048 if ( $state === 'bi' ) {
2049 $output .= '</b>';
2050 }
2051 // There might be lonely ''''', so make sure we have a buffer
2052 if ( $state === 'both' && $buffer ) {
2053 $output .= '<b><i>' . $buffer . '</i></b>';
2054 }
2055 return $output;
2056 }
2057
2058 /**
2059 * Replace external links (REL)
2060 *
2061 * Note: this is all very hackish and the order of execution matters a lot.
2062 * Make sure to run tests/parser/parserTests.php if you change this code.
2063 *
2064 * @private
2065 *
2066 * @param string $text
2067 *
2068 * @throws MWException
2069 * @return string
2070 */
2071 public function replaceExternalLinks( $text ) {
2072 wfDeprecated( __METHOD__, '1.34' );
2073 return $this->handleExternalLinks( $text );
2074 }
2075
2076 /**
2077 * Replace external links (REL)
2078 *
2079 * Note: this is all very hackish and the order of execution matters a lot.
2080 * Make sure to run tests/parser/parserTests.php if you change this code.
2081 *
2082 * @param string $text
2083 * @throws MWException
2084 * @return string
2085 */
2086 private function handleExternalLinks( $text ) {
2087 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2088 // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2089 if ( $bits === false ) {
2090 throw new MWException( "PCRE needs to be compiled with "
2091 . "--enable-unicode-properties in order for MediaWiki to function" );
2092 }
2093 $s = array_shift( $bits );
2094
2095 $i = 0;
2096 while ( $i < count( $bits ) ) {
2097 $url = $bits[$i++];
2098 $i++; // protocol
2099 $text = $bits[$i++];
2100 $trail = $bits[$i++];
2101
2102 # The characters '<' and '>' (which were escaped by
2103 # removeHTMLtags()) should not be included in
2104 # URLs, per RFC 2396.
2105 $m2 = [];
2106 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2107 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2108 $url = substr( $url, 0, $m2[0][1] );
2109 }
2110
2111 # If the link text is an image URL, replace it with an <img> tag
2112 # This happened by accident in the original parser, but some people used it extensively
2113 $img = $this->maybeMakeExternalImage( $text );
2114 if ( $img !== false ) {
2115 $text = $img;
2116 }
2117
2118 $dtrail = '';
2119
2120 # Set linktype for CSS
2121 $linktype = 'text';
2122
2123 # No link text, e.g. [http://domain.tld/some.link]
2124 if ( $text == '' ) {
2125 # Autonumber
2126 $langObj = $this->getTargetLanguage();
2127 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2128 $linktype = 'autonumber';
2129 } else {
2130 # Have link text, e.g. [http://domain.tld/some.link text]s
2131 # Check for trail
2132 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2133 }
2134
2135 // Excluding protocol-relative URLs may avoid many false positives.
2136 if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
2137 $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
2138 }
2139
2140 $url = Sanitizer::cleanUrl( $url );
2141
2142 # Use the encoded URL
2143 # This means that users can paste URLs directly into the text
2144 # Funny characters like ö aren't valid in URLs anyway
2145 # This was changed in August 2004
2146 $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2147 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
2148
2149 # Register link in the output object.
2150 $this->mOutput->addExternalLink( $url );
2151 }
2152
2153 return $s;
2154 }
2155
2156 /**
2157 * Get the rel attribute for a particular external link.
2158 *
2159 * @since 1.21
2160 * @internal
2161 * @param string|bool $url Optional URL, to extract the domain from for rel =>
2162 * nofollow if appropriate
2163 * @param LinkTarget|null $title Optional LinkTarget, for wgNoFollowNsExceptions lookups
2164 * @return string|null Rel attribute for $url
2165 */
2166 public static function getExternalLinkRel( $url = false, $title = null ) {
2167 global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
2168 $ns = $title ? $title->getNamespace() : false;
2169 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2170 && !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
2171 ) {
2172 return 'nofollow';
2173 }
2174 return null;
2175 }
2176
2177 /**
2178 * Get an associative array of additional HTML attributes appropriate for a
2179 * particular external link. This currently may include rel => nofollow
2180 * (depending on configuration, namespace, and the URL's domain) and/or a
2181 * target attribute (depending on configuration).
2182 *
2183 * @internal
2184 * @param string $url URL to extract the domain from for rel =>
2185 * nofollow if appropriate
2186 * @return array Associative array of HTML attributes
2187 */
2188 public function getExternalLinkAttribs( $url ) {
2189 $attribs = [];
2190 $rel = self::getExternalLinkRel( $url, $this->mTitle );
2191
2192 $target = $this->mOptions->getExternalLinkTarget();
2193 if ( $target ) {
2194 $attribs['target'] = $target;
2195 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2196 // T133507. New windows can navigate parent cross-origin.
2197 // Including noreferrer due to lacking browser
2198 // support of noopener. Eventually noreferrer should be removed.
2199 if ( $rel !== '' ) {
2200 $rel .= ' ';
2201 }
2202 $rel .= 'noreferrer noopener';
2203 }
2204 }
2205 $attribs['rel'] = $rel;
2206 return $attribs;
2207 }
2208
2209 /**
2210 * Replace unusual escape codes in a URL with their equivalent characters
2211 *
2212 * This generally follows the syntax defined in RFC 3986, with special
2213 * consideration for HTTP query strings.
2214 *
2215 * @internal
2216 * @param string $url
2217 * @return string
2218 */
2219 public static function normalizeLinkUrl( $url ) {
2220 # Test for RFC 3986 IPv6 syntax
2221 $scheme = '[a-z][a-z0-9+.-]*:';
2222 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2223 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2224 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2225 IP::isValid( rawurldecode( $m[1] ) )
2226 ) {
2227 $isIPv6 = rawurldecode( $m[1] );
2228 } else {
2229 $isIPv6 = false;
2230 }
2231
2232 # Make sure unsafe characters are encoded
2233 $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2234 function ( $m ) {
2235 return rawurlencode( $m[0] );
2236 },
2237 $url
2238 );
2239
2240 $ret = '';
2241 $end = strlen( $url );
2242
2243 # Fragment part - 'fragment'
2244 $start = strpos( $url, '#' );
2245 if ( $start !== false && $start < $end ) {
2246 $ret = self::normalizeUrlComponent(
2247 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2248 $end = $start;
2249 }
2250
2251 # Query part - 'query' minus &=+;
2252 $start = strpos( $url, '?' );
2253 if ( $start !== false && $start < $end ) {
2254 $ret = self::normalizeUrlComponent(
2255 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2256 $end = $start;
2257 }
2258
2259 # Scheme and path part - 'pchar'
2260 # (we assume no userinfo or encoded colons in the host)
2261 $ret = self::normalizeUrlComponent(
2262 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2263
2264 # Fix IPv6 syntax
2265 if ( $isIPv6 !== false ) {
2266 $ipv6Host = "%5B({$isIPv6})%5D";
2267 $ret = preg_replace(
2268 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2269 "$1[$2]",
2270 $ret
2271 );
2272 }
2273
2274 return $ret;
2275 }
2276
2277 private static function normalizeUrlComponent( $component, $unsafe ) {
2278 $callback = function ( $matches ) use ( $unsafe ) {
2279 $char = urldecode( $matches[0] );
2280 $ord = ord( $char );
2281 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2282 # Unescape it
2283 return $char;
2284 } else {
2285 # Leave it escaped, but use uppercase for a-f
2286 return strtoupper( $matches[0] );
2287 }
2288 };
2289 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2290 }
2291
2292 /**
2293 * make an image if it's allowed, either through the global
2294 * option, through the exception, or through the on-wiki whitelist
2295 *
2296 * @param string $url
2297 *
2298 * @return string
2299 */
2300 private function maybeMakeExternalImage( $url ) {
2301 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2302 $imagesexception = !empty( $imagesfrom );
2303 $text = false;
2304 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2305 if ( $imagesexception && is_array( $imagesfrom ) ) {
2306 $imagematch = false;
2307 foreach ( $imagesfrom as $match ) {
2308 if ( strpos( $url, $match ) === 0 ) {
2309 $imagematch = true;
2310 break;
2311 }
2312 }
2313 } elseif ( $imagesexception ) {
2314 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2315 } else {
2316 $imagematch = false;
2317 }
2318
2319 if ( $this->mOptions->getAllowExternalImages()
2320 || ( $imagesexception && $imagematch )
2321 ) {
2322 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2323 # Image found
2324 $text = Linker::makeExternalImage( $url );
2325 }
2326 }
2327 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2328 && preg_match( self::EXT_IMAGE_REGEX, $url )
2329 ) {
2330 $whitelist = explode(
2331 "\n",
2332 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2333 );
2334
2335 foreach ( $whitelist as $entry ) {
2336 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2337 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2338 continue;
2339 }
2340 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2341 # Image matches a whitelist entry
2342 $text = Linker::makeExternalImage( $url );
2343 break;
2344 }
2345 }
2346 }
2347 return $text;
2348 }
2349
2350 /**
2351 * Process [[ ]] wikilinks
2352 *
2353 * @param string $text
2354 *
2355 * @return string Processed text
2356 *
2357 * @private
2358 * @deprecated since 1.34; should not be used outside parser class.
2359 */
2360 public function replaceInternalLinks( $text ) {
2361 wfDeprecated( __METHOD__, '1.34' );
2362 return $this->handleInternalLinks( $text );
2363 }
2364
2365 /**
2366 * Process [[ ]] wikilinks
2367 *
2368 * @param string $text
2369 *
2370 * @return string Processed text
2371 */
2372 private function handleInternalLinks( $text ) {
2373 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2374 return $text;
2375 }
2376
2377 /**
2378 * Process [[ ]] wikilinks (RIL)
2379 * @param string &$text
2380 * @throws MWException
2381 * @return LinkHolderArray
2382 *
2383 * @private
2384 * @deprecated since 1.34; should not be used outside parser class.
2385 */
2386 public function replaceInternalLinks2( &$text ) {
2387 wfDeprecated( __METHOD__, '1.34' );
2388 return $this->handleInternalLinks2( $text );
2389 }
2390
2391 /**
2392 * Process [[ ]] wikilinks (RIL)
2393 * @param string &$s
2394 * @throws MWException
2395 * @return LinkHolderArray
2396 */
2397 private function handleInternalLinks2( &$s ) {
2398 static $tc = false, $e1, $e1_img;
2399 # the % is needed to support urlencoded titles as well
2400 if ( !$tc ) {
2401 $tc = Title::legalChars() . '#%';
2402 # Match a link having the form [[namespace:link|alternate]]trail
2403 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2404 # Match cases where there is no "]]", which might still be images
2405 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2406 }
2407
2408 $holders = new LinkHolderArray( $this );
2409
2410 # split the entire text string on occurrences of [[
2411 $a = StringUtils::explode( '[[', ' ' . $s );
2412 # get the first element (all text up to first [[), and remove the space we added
2413 $s = $a->current();
2414 $a->next();
2415 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2416 $s = substr( $s, 1 );
2417
2418 if ( is_null( $this->mTitle ) ) {
2419 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2420 }
2421 $nottalk = !$this->mTitle->isTalkPage();
2422
2423 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2424 $e2 = null;
2425 if ( $useLinkPrefixExtension ) {
2426 # Match the end of a line for a word that's not followed by whitespace,
2427 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2428 $charset = $this->contLang->linkPrefixCharset();
2429 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2430 $m = [];
2431 if ( preg_match( $e2, $s, $m ) ) {
2432 $first_prefix = $m[2];
2433 } else {
2434 $first_prefix = false;
2435 }
2436 } else {
2437 $prefix = '';
2438 }
2439
2440 # Some namespaces don't allow subpages
2441 $useSubpages = $this->nsInfo->hasSubpages(
2442 $this->mTitle->getNamespace()
2443 );
2444
2445 # Loop for each link
2446 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2447 # Check for excessive memory usage
2448 if ( $holders->isBig() ) {
2449 # Too big
2450 # Do the existence check, replace the link holders and clear the array
2451 $holders->replace( $s );
2452 $holders->clear();
2453 }
2454
2455 if ( $useLinkPrefixExtension ) {
2456 if ( preg_match( $e2, $s, $m ) ) {
2457 list( , $s, $prefix ) = $m;
2458 } else {
2459 $prefix = '';
2460 }
2461 # first link
2462 if ( $first_prefix ) {
2463 $prefix = $first_prefix;
2464 $first_prefix = false;
2465 }
2466 }
2467
2468 $might_be_img = false;
2469
2470 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2471 $text = $m[2];
2472 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2473 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2474 # the real problem is with the $e1 regex
2475 # See T1500.
2476 # Still some problems for cases where the ] is meant to be outside punctuation,
2477 # and no image is in sight. See T4095.
2478 if ( $text !== ''
2479 && substr( $m[3], 0, 1 ) === ']'
2480 && strpos( $text, '[' ) !== false
2481 ) {
2482 $text .= ']'; # so that handleExternalLinks($text) works later
2483 $m[3] = substr( $m[3], 1 );
2484 }
2485 # fix up urlencoded title texts
2486 if ( strpos( $m[1], '%' ) !== false ) {
2487 # Should anchors '#' also be rejected?
2488 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2489 }
2490 $trail = $m[3];
2491 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2492 # Invalid, but might be an image with a link in its caption
2493 $might_be_img = true;
2494 $text = $m[2];
2495 if ( strpos( $m[1], '%' ) !== false ) {
2496 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2497 }
2498 $trail = "";
2499 } else { # Invalid form; output directly
2500 $s .= $prefix . '[[' . $line;
2501 continue;
2502 }
2503
2504 $origLink = ltrim( $m[1], ' ' );
2505
2506 # Don't allow internal links to pages containing
2507 # PROTO: where PROTO is a valid URL protocol; these
2508 # should be external links.
2509 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2510 $s .= $prefix . '[[' . $line;
2511 continue;
2512 }
2513
2514 # Make subpage if necessary
2515 if ( $useSubpages ) {
2516 $link = Linker::normalizeSubpageLink(
2517 $this->mTitle, $origLink, $text
2518 );
2519 } else {
2520 $link = $origLink;
2521 }
2522
2523 // \x7f isn't a default legal title char, so most likely strip
2524 // markers will force us into the "invalid form" path above. But,
2525 // just in case, let's assert that xmlish tags aren't valid in
2526 // the title position.
2527 $unstrip = $this->mStripState->killMarkers( $link );
2528 $noMarkers = ( $unstrip === $link );
2529
2530 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2531 if ( $nt === null ) {
2532 $s .= $prefix . '[[' . $line;
2533 continue;
2534 }
2535
2536 $ns = $nt->getNamespace();
2537 $iw = $nt->getInterwiki();
2538
2539 $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2540
2541 if ( $might_be_img ) { # if this is actually an invalid link
2542 if ( $ns == NS_FILE && $noforce ) { # but might be an image
2543 $found = false;
2544 while ( true ) {
2545 # look at the next 'line' to see if we can close it there
2546 $a->next();
2547 $next_line = $a->current();
2548 if ( $next_line === false || $next_line === null ) {
2549 break;
2550 }
2551 $m = explode( ']]', $next_line, 3 );
2552 if ( count( $m ) == 3 ) {
2553 # the first ]] closes the inner link, the second the image
2554 $found = true;
2555 $text .= "[[{$m[0]}]]{$m[1]}";
2556 $trail = $m[2];
2557 break;
2558 } elseif ( count( $m ) == 2 ) {
2559 # if there's exactly one ]] that's fine, we'll keep looking
2560 $text .= "[[{$m[0]}]]{$m[1]}";
2561 } else {
2562 # if $next_line is invalid too, we need look no further
2563 $text .= '[[' . $next_line;
2564 break;
2565 }
2566 }
2567 if ( !$found ) {
2568 # we couldn't find the end of this imageLink, so output it raw
2569 # but don't ignore what might be perfectly normal links in the text we've examined
2570 $holders->merge( $this->handleInternalLinks2( $text ) );
2571 $s .= "{$prefix}[[$link|$text";
2572 # note: no $trail, because without an end, there *is* no trail
2573 continue;
2574 }
2575 } else { # it's not an image, so output it raw
2576 $s .= "{$prefix}[[$link|$text";
2577 # note: no $trail, because without an end, there *is* no trail
2578 continue;
2579 }
2580 }
2581
2582 $wasblank = ( $text == '' );
2583 if ( $wasblank ) {
2584 $text = $link;
2585 if ( !$noforce ) {
2586 # Strip off leading ':'
2587 $text = substr( $text, 1 );
2588 }
2589 } else {
2590 # T6598 madness. Handle the quotes only if they come from the alternate part
2591 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2592 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2593 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2594 $text = $this->doQuotes( $text );
2595 }
2596
2597 # Link not escaped by : , create the various objects
2598 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2599 # Interwikis
2600 if (
2601 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2602 Language::fetchLanguageName( $iw, null, 'mw' ) ||
2603 in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes' ) )
2604 )
2605 ) {
2606 # T26502: filter duplicates
2607 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2608 $this->mLangLinkLanguages[$iw] = true;
2609 $this->mOutput->addLanguageLink( $nt->getFullText() );
2610 }
2611
2612 /**
2613 * Strip the whitespace interwiki links produce, see T10897
2614 */
2615 $s = rtrim( $s . $prefix ) . $trail; # T175416
2616 continue;
2617 }
2618
2619 if ( $ns == NS_FILE ) {
2620 if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->mTitle ) ) {
2621 if ( $wasblank ) {
2622 # if no parameters were passed, $text
2623 # becomes something like "File:Foo.png",
2624 # which we don't want to pass on to the
2625 # image generator
2626 $text = '';
2627 } else {
2628 # recursively parse links inside the image caption
2629 # actually, this will parse them in any other parameters, too,
2630 # but it might be hard to fix that, and it doesn't matter ATM
2631 $text = $this->handleExternalLinks( $text );
2632 $holders->merge( $this->handleInternalLinks2( $text ) );
2633 }
2634 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2635 $s .= $prefix . $this->armorLinksPrivate(
2636 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2637 continue;
2638 }
2639 } elseif ( $ns == NS_CATEGORY ) {
2640 /**
2641 * Strip the whitespace Category links produce, see T2087
2642 */
2643 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2644
2645 if ( $wasblank ) {
2646 $sortkey = $this->getDefaultSort();
2647 } else {
2648 $sortkey = $text;
2649 }
2650 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2651 $sortkey = str_replace( "\n", '', $sortkey );
2652 $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2653 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2654
2655 continue;
2656 }
2657 }
2658
2659 # Self-link checking. For some languages, variants of the title are checked in
2660 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2661 # for linking to a different variant.
2662 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2663 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2664 continue;
2665 }
2666
2667 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2668 # @todo FIXME: Should do batch file existence checks, see comment below
2669 if ( $ns == NS_MEDIA ) {
2670 # Give extensions a chance to select the file revision for us
2671 $options = [];
2672 $descQuery = false;
2673 Hooks::run( 'BeforeParserFetchFileAndTitle',
2674 [ $this, $nt, &$options, &$descQuery ] );
2675 # Fetch and register the file (file title may be different via hooks)
2676 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2677 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2678 $s .= $prefix . $this->armorLinksPrivate(
2679 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2680 continue;
2681 }
2682
2683 # Some titles, such as valid special pages or files in foreign repos, should
2684 # be shown as bluelinks even though they're not included in the page table
2685 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2686 # batch file existence checks for NS_FILE and NS_MEDIA
2687 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2688 $this->mOutput->addLink( $nt );
2689 $s .= $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2690 } else {
2691 # Links will be added to the output link list after checking
2692 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2693 }
2694 }
2695 return $holders;
2696 }
2697
2698 /**
2699 * Render a forced-blue link inline; protect against double expansion of
2700 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2701 * Since this little disaster has to split off the trail text to avoid
2702 * breaking URLs in the following text without breaking trails on the
2703 * wiki links, it's been made into a horrible function.
2704 *
2705 * @param Title $nt
2706 * @param string $text
2707 * @param string $trail
2708 * @param string $prefix
2709 * @return string HTML-wikitext mix oh yuck
2710 * @deprecated since 1.34; should not be used outside parser class.
2711 */
2712 protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2713 wfDeprecated( __METHOD__, '1.34' );
2714 return $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2715 }
2716
2717 /**
2718 * Render a forced-blue link inline; protect against double expansion of
2719 * URLs if we're in a mode that prepends full URL prefixes to internal links.
2720 * Since this little disaster has to split off the trail text to avoid
2721 * breaking URLs in the following text without breaking trails on the
2722 * wiki links, it's been made into a horrible function.
2723 *
2724 * @param Title $nt
2725 * @param string $text
2726 * @param string $trail
2727 * @param string $prefix
2728 * @return string HTML-wikitext mix oh yuck
2729 */
2730 private function makeKnownLinkHolderPrivate( $nt, $text = '', $trail = '', $prefix = '' ) {
2731 list( $inside, $trail ) = Linker::splitTrail( $trail );
2732
2733 if ( $text == '' ) {
2734 $text = htmlspecialchars( $nt->getPrefixedText() );
2735 }
2736
2737 $link = $this->getLinkRenderer()->makeKnownLink(
2738 $nt, new HtmlArmor( "$prefix$text$inside" )
2739 );
2740
2741 return $this->armorLinksPrivate( $link ) . $trail;
2742 }
2743
2744 /**
2745 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2746 * going to go through further parsing steps before inline URL expansion.
2747 *
2748 * Not needed quite as much as it used to be since free links are a bit
2749 * more sensible these days. But bracketed links are still an issue.
2750 *
2751 * @param string $text More-or-less HTML
2752 * @return string Less-or-more HTML with NOPARSE bits
2753 * @deprecated since 1.34; should not be used outside parser class.
2754 */
2755 public function armorLinks( $text ) {
2756 wfDeprecated( __METHOD__, '1.34' );
2757 return $this->armorLinksPrivate( $text );
2758 }
2759
2760 /**
2761 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
2762 * going to go through further parsing steps before inline URL expansion.
2763 *
2764 * Not needed quite as much as it used to be since free links are a bit
2765 * more sensible these days. But bracketed links are still an issue.
2766 *
2767 * @param string $text More-or-less HTML
2768 * @return string Less-or-more HTML with NOPARSE bits
2769 */
2770 private function armorLinksPrivate( $text ) {
2771 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2772 self::MARKER_PREFIX . "NOPARSE$1", $text );
2773 }
2774
2775 /**
2776 * Return true if subpage links should be expanded on this page.
2777 * @return bool
2778 * @deprecated since 1.34; should not be used outside parser class.
2779 */
2780 public function areSubpagesAllowed() {
2781 # Some namespaces don't allow subpages
2782 wfDeprecated( __METHOD__, '1.34' );
2783 return $this->nsInfo->hasSubpages( $this->mTitle->getNamespace() );
2784 }
2785
2786 /**
2787 * Handle link to subpage if necessary
2788 *
2789 * @param string $target The source of the link
2790 * @param string &$text The link text, modified as necessary
2791 * @return string The full name of the link
2792 * @private
2793 * @deprecated since 1.34; should not be used outside parser class.
2794 */
2795 public function maybeDoSubpageLink( $target, &$text ) {
2796 wfDeprecated( __METHOD__, '1.34' );
2797 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2798 }
2799
2800 /**
2801 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
2802 *
2803 * @param string $text
2804 * @param bool $linestart Whether or not this is at the start of a line.
2805 * @private
2806 * @return string The lists rendered as HTML
2807 */
2808 public function doBlockLevels( $text, $linestart ) {
2809 return BlockLevelPass::doBlockLevels( $text, $linestart );
2810 }
2811
2812 /**
2813 * Return value of a magic variable (like PAGENAME)
2814 *
2815 * @private
2816 *
2817 * @param string $index Magic variable identifier as mapped in MagicWordFactory::$mVariableIDs
2818 * @param bool|PPFrame $frame
2819 *
2820 * @throws MWException
2821 * @return string
2822 * @deprecated since 1.34; should not be used outside parser class.
2823 */
2824 public function getVariableValue( $index, $frame = false ) {
2825 wfDeprecated( __METHOD__, '1.34' );
2826 return $this->expandMagicVariable( $index, $frame );
2827 }
2828
2829 /**
2830 * Return value of a magic variable (like PAGENAME)
2831 *
2832 * @param string $index Magic variable identifier as mapped in MagicWordFactory::$mVariableIDs
2833 * @param bool|PPFrame $frame
2834 *
2835 * @throws MWException
2836 * @return string
2837 */
2838 private function expandMagicVariable( $index, $frame = false ) {
2839 // XXX This function should be moved out of Parser class for
2840 // reuse by Parsoid/etc.
2841 if ( is_null( $this->mTitle ) ) {
2842 // If no title set, bad things are going to happen
2843 // later. Title should always be set since this
2844 // should only be called in the middle of a parse
2845 // operation (but the unit-tests do funky stuff)
2846 throw new MWException( __METHOD__ . ' Should only be '
2847 . ' called while parsing (no title set)' );
2848 }
2849
2850 // Avoid PHP 7.1 warning from passing $this by reference
2851 $parser = $this;
2852
2853 /**
2854 * Some of these require message or data lookups and can be
2855 * expensive to check many times.
2856 */
2857 if (
2858 Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) &&
2859 isset( $this->mVarCache[$index] )
2860 ) {
2861 return $this->mVarCache[$index];
2862 }
2863
2864 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2865 Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2866
2867 $pageLang = $this->getFunctionLang();
2868
2869 switch ( $index ) {
2870 case '!':
2871 $value = '|';
2872 break;
2873 case 'currentmonth':
2874 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2875 break;
2876 case 'currentmonth1':
2877 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2878 break;
2879 case 'currentmonthname':
2880 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2881 break;
2882 case 'currentmonthnamegen':
2883 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2884 break;
2885 case 'currentmonthabbrev':
2886 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2887 break;
2888 case 'currentday':
2889 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2890 break;
2891 case 'currentday2':
2892 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2893 break;
2894 case 'localmonth':
2895 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2896 break;
2897 case 'localmonth1':
2898 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2899 break;
2