Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / parser / ParserOutput.php
blob0ba3a375107c18b67c0316ea9e36c3967b4e43eb
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
21 namespace MediaWiki\Parser;
23 use InvalidArgumentException;
24 use LogicException;
25 use MediaWiki\Edit\ParsoidRenderID;
26 use MediaWiki\Json\JsonDeserializable;
27 use MediaWiki\Json\JsonDeserializableTrait;
28 use MediaWiki\Json\JsonDeserializer;
29 use MediaWiki\MainConfigNames;
30 use MediaWiki\MediaWikiServices;
31 use MediaWiki\Output\OutputPage;
32 use MediaWiki\Parser\Parsoid\PageBundleParserOutputConverter;
33 use MediaWiki\Title\TitleValue;
34 use UnexpectedValueException;
35 use Wikimedia\Bcp47Code\Bcp47Code;
36 use Wikimedia\Bcp47Code\Bcp47CodeValue;
37 use Wikimedia\Message\MessageValue;
38 use Wikimedia\Parsoid\Core\ContentMetadataCollector;
39 use Wikimedia\Parsoid\Core\ContentMetadataCollectorCompat;
40 use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
41 use Wikimedia\Parsoid\Core\TOCData;
42 use Wikimedia\Reflection\GhostFieldAccessTrait;
44 /**
45 * ParserOutput is a rendering of a Content object or a message.
46 * Content objects and messages often contain wikitext, but not always.
48 * `ParserOutput` object combine the HTML rendering of Content objects
49 * or messages, available via `::getRawText()`, with various bits of
50 * metadata generated during rendering, which may include categories,
51 * links, page properties, and extension data, among others.
53 * `ParserOutput` objects corresponding to the content of page revisions
54 * are created by the `ParserOutputAccess` service, which
55 * automatically caches them via `ParserCache` where appropriate and
56 * produces new output via `ContentHandler` as needed.
58 * In addition, wikitext from system messages as well as odd bits of
59 * wikitext rendered to create special pages and other UX elements are
60 * rendered to `ParserOutput` objects. In these cases the metadata
61 * from the `ParserOutput` is generally discarded and the
62 * `ParserOutput` is not cached. These bits of wikitext are generally
63 * rendered with `ParserOptions::setInterfaceMessage(true)` when
64 * content is intended to be in the user interface language, but
65 * sometimes rendered to the content language and displayed in the
66 * content area instead.
68 * A `ParserOutput` object corresponding to a given revision may be a
69 * combination of the renderings of multiple "slots":
70 * the Multi-Content Revisions (MCR) work allows articles to be
71 * composed from multiple `Content` objects. Each `Content` renders
72 * to a `ParserOutput`, and those `ParserOutput`s are merged by
73 * `RevisionRenderer::combineSlotOutput()` to create the final article
74 * output.
76 * Similarly, `OutputPage` maintains metadata overlapping
77 * with the metadata kept by `ParserOutput` (T301020) and may merge
78 * several `ParserOutput`s using `OutputPage::addParserOutput()` to
79 * create the final output page. Parsoid parses certain transclusions
80 * in independent top-level contexts using
81 * `Parser::parseExtensionTagAsTopLevelDoc()` and these also result in
82 * `ParserOutput`s which are merged via
83 * `ParserOutput::collectMetadata()`.
85 * Future plans for incremental parsing and asynchronous rendering may
86 * result in several of these component `ParserOutput` objects being
87 * cached independently and then recombined asynchronously, so
88 * operations on `ParserOutput` objects should be compatible with that
89 * model (T300979).
91 * @ingroup Parser
93 class ParserOutput extends CacheTime implements ContentMetadataCollector {
94 use GhostFieldAccessTrait;
95 use JsonDeserializableTrait;
96 // This is used to break cyclic dependencies and allow a measure
97 // of compatibility when new methods are added to ContentMetadataCollector
98 // by Parsoid.
99 use ContentMetadataCollectorCompat;
102 * Feature flags to indicate to extensions that MediaWiki core supports and
103 * uses getText() stateless transforms.
105 * @since 1.31
107 public const SUPPORTS_STATELESS_TRANSFORMS = 1;
110 * @since 1.31
112 public const SUPPORTS_UNWRAP_TRANSFORM = 1;
115 * @internal
116 * @since 1.38
118 public const MW_MERGE_STRATEGY_KEY = '_mw-strategy';
121 * Merge strategy to use for ParserOutput accumulators: "union"
122 * means that values are strings, stored as a set, and exposed as
123 * a PHP associative array mapping from values to `true`.
125 * This constant should be treated as @internal until we expose
126 * alternative merge strategies for external use.
127 * @internal
128 * @since 1.38
130 public const MW_MERGE_STRATEGY_UNION = 'union';
133 * @var string|null The output text
135 private $mRawText = null;
138 * @var array<string,string> Array mapping interwiki prefix to (non DB key) Titles (e.g. 'fr' => 'Test page')
140 private $mLanguageLinkMap = [];
143 * @var array<string,string> Map of category names to sort keys
145 private $mCategories = [];
148 * @var array<string,string> Page status indicators, usually displayed in top-right corner.
150 private $mIndicators = [];
153 * @var string Title text of the chosen language variant, as HTML.
155 private $mTitleText;
158 * @var array<int,array<string,int>> 2-D map of NS/DBK to ID for the links in the document.
159 * ID=zero for broken.
161 private $mLinks = [];
164 * @var array<string,int> Keys are DBKs for the links to special pages in the document.
165 * @since 1.35
167 private $mLinksSpecial = [];
170 * @var array<int,array<string,int>> 2-D map of NS/DBK to ID for the template references.
171 * ID=zero for broken.
173 private $mTemplates = [];
176 * @var array<int,array<string,int>> 2-D map of NS/DBK to rev ID for the template references.
177 * ID=zero for broken.
179 private $mTemplateIds = [];
182 * @var array<string,int> DB keys of the images used, in the array key only
184 private $mImages = [];
187 * @var array<string,array<string,string>> DB keys of the images used mapped to sha1 and MW timestamp.
189 private $mFileSearchOptions = [];
192 * @var array<string,int> External link URLs, in the key only.
194 private array $mExternalLinks = [];
197 * @var array<string,array<string,int>> 2-D map of prefix/DBK (in keys only)
198 * for the inline interwiki links in the document.
200 private $mInterwikiLinks = [];
203 * @var bool Show a new section link?
205 private $mNewSection = false;
208 * @var bool Hide the new section link?
210 private $mHideNewSection = false;
213 * @var bool No gallery on category page? (__NOGALLERY__).
215 private $mNoGallery = false;
218 * @var string[] Items to put in the <head> section
220 private $mHeadItems = [];
223 * @var array<string,true> Modules to be loaded by ResourceLoader
225 private $mModuleSet = [];
228 * @var array<string,true> Modules of which only the CSS will be loaded by ResourceLoader.
230 private $mModuleStyleSet = [];
233 * @var array JavaScript config variable for mw.config combined with this page.
235 private $mJsConfigVars = [];
238 * @var array<string,int> Warning text to be returned to the user.
239 * Wikitext formatted, in the key only.
241 private $mWarnings = [];
244 * @var array<string,array> *Unformatted* warning messages and
245 * arguments to be returned to the user. This is for internal use
246 * when merging ParserOutputs and are not serialized/deserialized.
248 private $mWarningMsgs = [];
251 * @var ?TOCData Table of contents data, or null if it hasn't been set.
253 private $mTOCData;
256 * @var array Name/value pairs to be cached in the DB.
258 private $mProperties = [];
261 * @var ?string Timestamp of the revision.
263 private $mTimestamp;
266 * @var bool Whether OOUI should be enabled.
268 private $mEnableOOUI = false;
271 * @var bool Whether the index policy has been set to 'index'.
273 private $mIndexSet = false;
276 * @var bool Whether the index policy has been set to 'noindex'.
278 private $mNoIndexSet = false;
281 * @var array extra data used by extensions.
283 private $mExtensionData = [];
286 * @var array Parser limit report data.
288 private $mLimitReportData = [];
290 /** @var array Parser limit report data for JSON */
291 private $mLimitReportJSData = [];
293 /** @var string Debug message added by ParserCache */
294 private $mCacheMessage = '';
297 * @var array Timestamps for getTimeSinceStart().
299 private $mParseStartTime = [];
302 * @var array Durations for getTimeProfile().
304 private $mTimeProfile = [];
307 * @var bool Whether to emit X-Frame-Options: DENY.
308 * This controls if anti-clickjacking / frame-breaking headers will
309 * be sent. This should be done for pages where edit actions are possible.
311 private $mPreventClickjacking = false;
314 * @var string[] Extra script-src for CSP
316 private $mExtraScriptSrcs = [];
319 * @var string[] Extra default-src for CSP [Everything but script and style]
321 private $mExtraDefaultSrcs = [];
324 * @var string[] Extra style-src for CSP
326 private $mExtraStyleSrcs = [];
329 * @var array<string,true> Generic flags.
331 private $mFlags = [];
333 private const SPECULATIVE_FIELDS = [
334 'speculativePageIdUsed',
335 'mSpeculativeRevId',
336 'revisionTimestampUsed',
339 /** @var int|null Assumed rev ID for {{REVISIONID}} if no revision is set */
340 private $mSpeculativeRevId;
341 /** @var int|null Assumed page ID for {{PAGEID}} if no revision is set */
342 private $speculativePageIdUsed;
343 /** @var string|null Assumed rev timestamp for {{REVISIONTIMESTAMP}} if no revision is set */
344 private $revisionTimestampUsed;
346 /** @var string|null SHA-1 base 36 hash of any self-transclusion */
347 private $revisionUsedSha1Base36;
349 /** string CSS classes to use for the wrapping div, stored in the array keys.
350 * If no class is given, no wrapper is added.
351 * @var array<string,true>
353 private $mWrapperDivClasses = [];
355 /** @var int Upper bound of expiry based on parse duration */
356 private $mMaxAdaptiveExpiry = INF;
358 // finalizeAdaptiveCacheExpiry() uses TTL = MAX( m * PARSE_TIME + b, MIN_AR_TTL)
359 // Current values imply that m=3933.333333 and b=-333.333333
360 // See https://www.nngroup.com/articles/website-response-times/
361 private const PARSE_FAST_SEC = 0.100; // perceived "fast" page parse
362 private const PARSE_SLOW_SEC = 1.0; // perceived "slow" page parse
363 private const FAST_AR_TTL = 60; // adaptive TTL for "fast" pages
364 private const SLOW_AR_TTL = 3600; // adaptive TTL for "slow" pages
365 private const MIN_AR_TTL = 15; // min adaptive TTL (for pool counter, and edit stashing)
368 * @param string|null $text HTML. Use null to indicate that this ParserOutput contains only
369 * meta-data, and the HTML output is undetermined, as opposed to empty. Passing null
370 * here causes hasText() to return false. In 1.39 the default value changed from ''
371 * to null.
372 * @param array $languageLinks
373 * @param array $categoryLinks
374 * @param bool $unused
375 * @param string $titletext
377 public function __construct( $text = null, $languageLinks = [], $categoryLinks = [],
378 $unused = false, $titletext = ''
380 $this->mRawText = $text;
381 $this->mCategories = $categoryLinks;
382 $this->mTitleText = $titletext;
383 if ( $languageLinks === null ) { // T376323
384 wfDeprecated( __METHOD__ . ' with null $languageLinks', '1.43' );
386 foreach ( ( $languageLinks ?? [] ) as $ll ) {
387 $this->addLanguageLink( $ll );
389 // If the content handler does not specify an alternative (by
390 // calling ::resetParseStartTime() at a later point) then use
391 // the creation of the ParserOutput as the "start of parse" time.
392 $this->resetParseStartTime();
396 * Returns true if text was passed to the constructor, or set using setText(). Returns false
397 * if null was passed to the $text parameter of the constructor to indicate that this
398 * ParserOutput only contains meta-data, and the HTML output is undetermined.
400 * @since 1.32
402 * @return bool Whether this ParserOutput contains rendered text. If this returns false, the
403 * ParserOutput contains meta-data only.
405 public function hasText(): bool {
406 return ( $this->mRawText !== null );
410 * Get the cacheable text with <mw:editsection> markers still in it. The
411 * return value is suitable for writing back via setText() but is not valid
412 * for display to the user.
414 * @return string
415 * @since 1.27
417 public function getRawText() {
418 if ( $this->mRawText === null ) {
419 throw new LogicException( 'This ParserOutput contains no text!' );
422 return $this->mRawText;
426 * Get the output HTML
428 * T293512: in the future, ParserOutput::getText() will be deprecated in favor of invoking
429 * the OutputTransformPipeline directly on a ParserOutput.
430 * @param array $options (since 1.31) Transformations to apply to the HTML
431 * - allowClone: (bool) Whether to clone the ParserOutput before
432 * applying transformations. Default is false.
433 * - allowTOC: (bool) Show the TOC, assuming there were enough headings
434 * to generate one and `__NOTOC__` wasn't used. Default is true,
435 * but might be statefully overridden.
436 * - injectTOC: (bool) Replace the TOC_PLACEHOLDER with TOC contents;
437 * otherwise the marker will be left in the article (and the skin
438 * will be responsible for replacing or removing it). Default is
439 * true.
440 * - enableSectionEditLinks: (bool) Include section edit links, assuming
441 * section edit link tokens are present in the HTML. Default is true,
442 * but might be statefully overridden.
443 * - userLang: (Language) Language object used for localizing UX messages,
444 * for example the heading of the table of contents. If omitted, will
445 * use the language of the main request context.
446 * - skin: (Skin) Skin object used for transforming section edit links.
447 * - unwrap: (bool) Return text without a wrapper div. Default is false,
448 * meaning a wrapper div will be added if getWrapperDivClass() returns
449 * a non-empty string.
450 * - wrapperDivClass: (string) Wrap the output in a div and apply the given
451 * CSS class to that div. This overrides the output of getWrapperDivClass().
452 * Setting this to an empty string has the same effect as 'unwrap' => true.
453 * - deduplicateStyles: (bool) When true, which is the default, `<style>`
454 * tags with the `data-mw-deduplicate` attribute set are deduplicated by
455 * value of the attribute: all but the first will be replaced by `<link
456 * rel="mw-deduplicated-inline-style" href="mw-data:..."/>` tags, where
457 * the scheme-specific-part of the href is the (percent-encoded) value
458 * of the `data-mw-deduplicate` attribute.
459 * - absoluteURLs: (bool) use absolute URLs in all links. Default: false
460 * - includeDebugInfo: (bool) render PP limit report in HTML. Default: false
461 * @return string HTML
462 * @return-taint escaped
463 * @deprecated since 1.42, this method has side-effects on the ParserOutput
464 * (see T353257) and so should be avoided in favor of directly invoking
465 * the default output pipeline on a ParserOutput; for now, use of
466 * ::runOutputPipeline() is preferred to ensure that ParserOptions are
467 * available.
468 * Do NOT hard-deprecate this method until the corresponding patch
469 * (1093952) is merged to CentralNotice wmf_deploy branch!
471 public function getText( $options = [] ) {
472 $oldText = $this->mRawText; // T353257
473 $options += [ 'allowClone' => false ];
474 $po = $this->runPipelineInternal( null, $options );
475 $newText = $po->getContentHolderText();
476 // T353257: for back-compat only mutations to metadata performed by
477 // the pipeline should be preserved; mutations to $mText should be
478 // discarded.
479 $this->setRawText( $oldText );
480 return $newText;
484 * @unstable This method is transitional and will be replaced by a method
485 * in another class, maybe ContentRenderer. It allows us to break our
486 * porting work into two steps; in the first we bring ParserOptions to
487 * to each ::getText() callsite to ensure it is made available to the
488 * postprocessing pipeline. In the second we move this functionality
489 * into the Content hierarchy and out of ParserOutput, which should become
490 * a pure value object.
492 * @param ParserOptions $popts
493 * @param array $options (since 1.31) Transformations to apply to the HTML
494 * - allowClone: (bool) Whether to clone the ParserOutput before
495 * applying transformations. Default is true.
496 * - allowTOC: (bool) Show the TOC, assuming there were enough headings
497 * to generate one and `__NOTOC__` wasn't used. Default is true,
498 * but might be statefully overridden.
499 * - injectTOC: (bool) Replace the TOC_PLACEHOLDER with TOC contents;
500 * otherwise the marker will be left in the article (and the skin
501 * will be responsible for replacing or removing it). Default is
502 * true.
503 * - enableSectionEditLinks: (bool) Include section edit links, assuming
504 * section edit link tokens are present in the HTML. Default is true,
505 * but might be statefully overridden.
506 * - userLang: (Language) Language object used for localizing UX messages,
507 * for example the heading of the table of contents. If omitted, will
508 * use the language of the main request context.
509 * - skin: (Skin) Skin object used for transforming section edit links.
510 * - unwrap: (bool) Return text without a wrapper div. Default is false,
511 * meaning a wrapper div will be added if getWrapperDivClass() returns
512 * a non-empty string.
513 * - wrapperDivClass: (string) Wrap the output in a div and apply the given
514 * CSS class to that div. This overrides the output of getWrapperDivClass().
515 * Setting this to an empty string has the same effect as 'unwrap' => true.
516 * - deduplicateStyles: (bool) When true, which is the default, `<style>`
517 * tags with the `data-mw-deduplicate` attribute set are deduplicated by
518 * value of the attribute: all but the first will be replaced by `<link
519 * rel="mw-deduplicated-inline-style" href="mw-data:..."/>` tags, where
520 * the scheme-specific-part of the href is the (percent-encoded) value
521 * of the `data-mw-deduplicate` attribute.
522 * - absoluteURLs: (bool) use absolute URLs in all links. Default: false
523 * - includeDebugInfo: (bool) render PP limit report in HTML. Default: false
524 * It is planned to eventually deprecate this $options array and to be able to
525 * pass its content in the $popts ParserOptions.
526 * @return ParserOutput
528 public function runOutputPipeline( ParserOptions $popts, array $options = [] ): ParserOutput {
529 return $this->runPipelineInternal( $popts, $options );
533 * Temporary helper method to allow running the pipeline with null $popts for now, although
534 * passing a null ParserOptions is a temporary backward-compatibility hack and will be deprecated.
536 private function runPipelineInternal( ?ParserOptions $popts, array $options = [] ): ParserOutput {
537 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
538 $options += [
539 'allowClone' => true,
540 'allowTOC' => true,
541 'injectTOC' => true,
542 'enableSectionEditLinks' => !$this->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ),
543 'userLang' => null,
544 'skin' => null,
545 'unwrap' => false,
546 'wrapperDivClass' => $this->getWrapperDivClass(),
547 'deduplicateStyles' => true,
548 'absoluteURLs' => false,
549 'includeDebugInfo' => false,
550 'isParsoidContent' => PageBundleParserOutputConverter::hasPageBundle( $this ),
552 return $pipeline->run( $this, $popts, $options );
556 * Adds a comment notice about cache state to the text of the page
557 * @param string $msg
558 * @internal used by ParserCache
560 public function addCacheMessage( string $msg ): void {
561 $this->mCacheMessage .= $msg;
565 * Add a CSS class to use for the wrapping div. If no class is given, no wrapper is added.
567 * @param string $class
569 public function addWrapperDivClass( $class ): void {
570 $this->mWrapperDivClasses[$class] = true;
574 * Clears the CSS class to use for the wrapping div, effectively disabling the wrapper div
575 * until addWrapperDivClass() is called.
577 public function clearWrapperDivClass(): void {
578 $this->mWrapperDivClasses = [];
582 * Returns the class (or classes) to be used with the wrapper div for this output.
583 * If there is no wrapper class given, no wrapper div should be added.
584 * The wrapper div is added automatically by getText().
586 * @return string
588 public function getWrapperDivClass(): string {
589 return implode( ' ', array_keys( $this->mWrapperDivClasses ) );
593 * @param int $id
594 * @since 1.28
596 public function setSpeculativeRevIdUsed( $id ): void {
597 $this->mSpeculativeRevId = $id;
601 * @return int|null
602 * @since 1.28
604 public function getSpeculativeRevIdUsed(): ?int {
605 return $this->mSpeculativeRevId;
609 * @param int $id
610 * @since 1.34
612 public function setSpeculativePageIdUsed( $id ): void {
613 $this->speculativePageIdUsed = $id;
617 * @return int|null
618 * @since 1.34
620 public function getSpeculativePageIdUsed() {
621 return $this->speculativePageIdUsed;
625 * @param string $timestamp TS_MW timestamp
626 * @since 1.34
628 public function setRevisionTimestampUsed( $timestamp ): void {
629 $this->revisionTimestampUsed = $timestamp;
633 * @return string|null TS_MW timestamp or null if not used
634 * @since 1.34
636 public function getRevisionTimestampUsed() {
637 return $this->revisionTimestampUsed;
641 * @param string $hash Lowercase SHA-1 base 36 hash
642 * @since 1.34
644 public function setRevisionUsedSha1Base36( $hash ): void {
645 if ( $hash === null ) {
646 return; // e.g. RevisionRecord::getSha1() returned null
649 if (
650 $this->revisionUsedSha1Base36 !== null &&
651 $this->revisionUsedSha1Base36 !== $hash
653 $this->revisionUsedSha1Base36 = ''; // mismatched
654 } else {
655 $this->revisionUsedSha1Base36 = $hash;
660 * @return string|null Lowercase SHA-1 base 36 hash, null if unused, or "" on inconsistency
661 * @since 1.34
663 public function getRevisionUsedSha1Base36() {
664 return $this->revisionUsedSha1Base36;
668 * @return string[]
669 * @note Before 1.43, this function returned an array reference.
670 * @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::LANGUAGE)
672 public function getLanguageLinks() {
673 $result = [];
674 foreach ( $this->mLanguageLinkMap as $lang => $title ) {
675 // T374736: Back-compat with empty prefix; see ::addLanguageLink()
676 $result[] = $title === '|' ? "$lang" : "$lang:$title";
678 return $result;
681 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::INTERWIKI) */
682 public function getInterwikiLinks() {
683 return $this->mInterwikiLinks;
687 * Return the names of the categories on this page.
688 * Unlike ::getCategories(), sort keys are *not* included in the
689 * return value.
690 * @return array<string> The names of the categories
691 * @since 1.38
693 public function getCategoryNames(): array {
694 # Note that numeric category names get converted to 'int' when
695 # stored as array keys; stringify the keys to ensure they
696 # return to original string form so as not to confuse callers.
697 return array_map( 'strval', array_keys( $this->mCategories ) );
701 * Return category names and sort keys as a map.
703 * BEWARE that numeric category names get converted to 'int' when stored
704 * as array keys. Because of this, use of this method is not recommended
705 * in new code; using ::getCategoryNames() and ::getCategorySortKey() will
706 * be less error-prone.
708 * @return array<string|int,string>
709 * @internal
711 public function getCategoryMap(): array {
712 return $this->mCategories;
716 * Return the sort key for a given category name, or `null` if the
717 * category is not present in this ParserOutput. Returns the
718 * empty string if the category is to use the default sort key.
720 * @note The effective sort key in the database may vary from what
721 * is returned here; see note in ParserOutput::addCategory().
723 * @param string $name The category name
724 * @return ?string The sort key for the category, or `null` if the
725 * category is not present in this ParserOutput
726 * @since 1.40
728 public function getCategorySortKey( string $name ): ?string {
729 // This API avoids exposing the fact that numeric string category
730 // names are going to be converted to 'int' when used as array
731 // keys for the `mCategories` field.
732 return $this->mCategories[$name] ?? null;
736 * @return string[]
737 * @since 1.25
739 public function getIndicators(): array {
740 return $this->mIndicators;
743 public function getTitleText() {
744 return $this->mTitleText;
748 * @return ?TOCData the table of contents data, or null if it hasn't been
749 * set.
751 public function getTOCData(): ?TOCData {
752 return $this->mTOCData;
756 * @internal
757 * @return string
759 public function getCacheMessage(): string {
760 return $this->mCacheMessage;
764 * @internal
765 * @return array
767 public function getSections(): array {
768 if ( $this->mTOCData !== null ) {
769 return $this->mTOCData->toLegacy();
771 // For compatibility
772 return [];
776 * Get a list of links of the given type.
778 * Provides a uniform interface to various lists of links stored in
779 * the metadata.
781 * Each element of the returned array has a LinkTarget as the 'link'
782 * property. Local and template links also have 'pageid' set.
783 * Template links have 'revid' set. Category links have 'sort' set.
784 * Media links optionally have 'time' and 'sha1' set.
786 * @param string $linkType A link type, which should be a constant from
787 * ParserOutputLinkTypes.
788 * @return list<array{link:ParsoidLinkTarget,pageid?:int,revid?:int,sort?:string,time?:string|false,sha1?:string|false}>
790 public function getLinkList( string $linkType ): array {
791 # Note that fragments are dropped for everything except language links
792 $result = [];
793 switch ( $linkType ) {
794 case ParserOutputLinkTypes::CATEGORY:
795 foreach ( $this->mCategories as $dbkey => $sort ) {
796 $result[] = [
797 'link' => new TitleValue( NS_CATEGORY, (string)$dbkey ),
798 'sort' => $sort,
801 break;
803 case ParserOutputLinkTypes::INTERWIKI:
804 foreach ( $this->mInterwikiLinks as $prefix => $arr ) {
805 foreach ( $arr as $dbkey => $ignore ) {
806 $result[] = [
807 'link' => new TitleValue( NS_MAIN, (string)$dbkey, '', (string)$prefix ),
811 break;
813 case ParserOutputLinkTypes::LANGUAGE:
814 foreach ( $this->mLanguageLinkMap as $lang => $title ) {
815 if ( $title === '|' ) {
816 continue; // T374736
818 # language links can have fragments!
819 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
820 $result[] = [
821 'link' => new TitleValue( NS_MAIN, $title, $frag, (string)$lang ),
824 break;
826 case ParserOutputLinkTypes::LOCAL:
827 foreach ( $this->mLinks as $ns => $arr ) {
828 foreach ( $arr as $dbkey => $id ) {
829 $result[] = [
830 'link' => new TitleValue( $ns, (string)$dbkey ),
831 'pageid' => $id,
835 break;
837 case ParserOutputLinkTypes::MEDIA:
838 foreach ( $this->mImages as $dbkey => $ignore ) {
839 $extra = $this->mFileSearchOptions[$dbkey] ?? [];
840 $extra['link'] = new TitleValue( NS_FILE, (string)$dbkey );
841 $result[] = $extra;
843 break;
845 case ParserOutputLinkTypes::SPECIAL:
846 foreach ( $this->mLinksSpecial as $dbkey => $ignore ) {
847 $result[] = [
848 'link' => new TitleValue( NS_SPECIAL, (string)$dbkey ),
851 break;
853 case ParserOutputLinkTypes::TEMPLATE:
854 foreach ( $this->mTemplates as $ns => $arr ) {
855 foreach ( $arr as $dbkey => $pageid ) {
856 $result[] = [
857 'link' => new TitleValue( $ns, (string)$dbkey ),
858 'pageid' => $pageid,
859 // default to invalid/broken revision if this is not present
860 'revid' => $this->mTemplateIds[$ns][$dbkey] ?? 0,
864 break;
866 default:
867 throw new UnexpectedValueException( "Unknown link type $linkType" );
869 return $result;
872 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::LOCAL) */
873 public function &getLinks() {
874 return $this->mLinks;
878 * Return true if the given parser output has local links registered
879 * in the metadata.
880 * @return bool
881 * @since 1.44
883 public function hasLinks(): bool {
884 foreach ( $this->mLinks as $ns => $arr ) {
885 foreach ( $arr as $dbkey => $id ) {
886 return true;
889 return false;
893 * @return array Keys are DBKs for the links to special pages in the document
894 * @since 1.35
895 * @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::SPECIAL)
897 public function &getLinksSpecial() {
898 return $this->mLinksSpecial;
901 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::TEMPLATE) */
902 public function &getTemplates() {
903 return $this->mTemplates;
906 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::TEMPLATE) */
907 public function &getTemplateIds() {
908 return $this->mTemplateIds;
911 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::MEDIA) */
912 public function &getImages() {
913 return $this->mImages;
917 * Return true if there are image dependencies registered for this
918 * ParserOutput.
919 * @since 1.44
921 public function hasImages(): bool {
922 return $this->mImages !== [];
925 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::MEDIA) */
926 public function &getFileSearchOptions() {
927 return $this->mFileSearchOptions;
931 * @note Use of the reference returned by this method has been
932 * deprecated since 1.43. In a future release this will return a
933 * normal array. Use ::addExternalLink() to modify the set of
934 * external links stored in this ParserOutput.
936 public function &getExternalLinks(): array {
937 return $this->mExternalLinks;
940 public function setNoGallery( $value ): void {
941 $this->mNoGallery = (bool)$value;
944 public function getNoGallery() {
945 return $this->mNoGallery;
948 public function getHeadItems() {
949 return $this->mHeadItems;
952 public function getModules() {
953 return array_keys( $this->mModuleSet );
956 public function getModuleStyles() {
957 return array_keys( $this->mModuleStyleSet );
961 * @param bool $showStrategyKeys Defaults to false; if set to true will
962 * expose the internal `MW_MERGE_STRATEGY_KEY` in the result. This
963 * should only be used internally to allow safe merge of config vars.
964 * @return array
965 * @since 1.23
967 public function getJsConfigVars( bool $showStrategyKeys = false ) {
968 $result = $this->mJsConfigVars;
969 // Don't expose the internal strategy key
970 foreach ( $result as &$value ) {
971 if ( is_array( $value ) && !$showStrategyKeys ) {
972 unset( $value[self::MW_MERGE_STRATEGY_KEY] );
975 return $result;
978 public function getWarnings(): array {
979 return array_keys( $this->mWarnings );
982 public function getIndexPolicy(): string {
983 // 'noindex' wins if both are set. (T16899)
984 if ( $this->mNoIndexSet ) {
985 return 'noindex';
986 } elseif ( $this->mIndexSet ) {
987 return 'index';
989 return '';
993 * @return string|null TS_MW timestamp of the revision content
995 public function getRevisionTimestamp(): ?string {
996 return $this->mTimestamp;
1000 * @return string|null TS_MW timestamp of the revision content
1001 * @deprecated since 1.42; use ::getRevisionTimestamp() instead
1003 public function getTimestamp() {
1004 return $this->getRevisionTimestamp();
1007 public function getLimitReportData() {
1008 return $this->mLimitReportData;
1011 public function getLimitReportJSData() {
1012 return $this->mLimitReportJSData;
1015 public function getEnableOOUI() {
1016 return $this->mEnableOOUI;
1020 * Get extra Content-Security-Policy 'default-src' directives
1021 * @since 1.35
1022 * @return string[]
1024 public function getExtraCSPDefaultSrcs() {
1025 return $this->mExtraDefaultSrcs;
1029 * Get extra Content-Security-Policy 'script-src' directives
1030 * @since 1.35
1031 * @return string[]
1033 public function getExtraCSPScriptSrcs() {
1034 return $this->mExtraScriptSrcs;
1038 * Get extra Content-Security-Policy 'style-src' directives
1039 * @since 1.35
1040 * @return string[]
1042 public function getExtraCSPStyleSrcs() {
1043 return $this->mExtraStyleSrcs;
1047 * Set the raw text of the ParserOutput.
1049 * If you did not generate html, pass null to mark it as such.
1051 * @since 1.42
1052 * @param string|null $text HTML content of ParserOutput or null if not generated
1053 * @param-taint $text exec_html
1055 public function setRawText( ?string $text ): void {
1056 $this->mRawText = $text;
1060 * Set the raw text of the ParserOutput.
1062 * If you did not generate html, pass null to mark it as such.
1064 * @since 1.39 You can now pass null to this function
1065 * @param string|null $text HTML content of ParserOutput or null if not generated
1066 * @param-taint $text exec_html
1067 * @return string|null Previous value of ParserOutput's raw text
1068 * @deprecated since 1.42; use ::setRawText() which matches the getter ::getRawText()
1070 public function setText( $text ) {
1071 return wfSetVar( $this->mRawText, $text, true );
1075 * @deprecated since 1.42, use ::addLanguageLink() instead.
1077 public function setLanguageLinks( $ll ) {
1078 $old = $this->getLanguageLinks();
1079 $this->mLanguageLinkMap = [];
1080 if ( $ll === null ) { // T376323
1081 wfDeprecated( __METHOD__ . ' with null argument', '1.43' );
1083 foreach ( ( $ll ?? [] ) as $l ) {
1084 $this->addLanguageLink( $l );
1086 return $old;
1089 public function setTitleText( $t ) {
1090 return wfSetVar( $this->mTitleText, $t );
1094 * @param TOCData $tocData Table of contents data for the page
1096 public function setTOCData( TOCData $tocData ): void {
1097 $this->mTOCData = $tocData;
1101 * @param array $sectionArray
1102 * @return array Previous value of ::getSections()
1104 public function setSections( array $sectionArray ) {
1105 $oldValue = $this->getSections();
1106 $this->setTOCData( TOCData::fromLegacy( $sectionArray ) );
1107 return $oldValue;
1111 * Update the index policy of the robots meta tag.
1113 * Note that calling this method does not guarantee
1114 * that {@link self::getIndexPolicy()} will return the given policy –
1115 * if different calls set the index policy to 'index' and 'noindex',
1116 * then 'noindex' always wins (T16899), even if the 'index' call happened later.
1117 * If this is not what you want,
1118 * you can reset {@link ParserOutputFlags::NO_INDEX_POLICY} with {@link self::setOutputFlag()}.
1120 * @param string $policy 'index' or 'noindex'.
1121 * @return string The previous policy.
1123 public function setIndexPolicy( $policy ): string {
1124 $old = $this->getIndexPolicy();
1125 if ( $policy === 'noindex' ) {
1126 $this->mNoIndexSet = true;
1127 } elseif ( $policy === 'index' ) {
1128 $this->mIndexSet = true;
1130 return $old;
1134 * @param ?string $timestamp TS_MW timestamp of the revision content
1136 public function setRevisionTimestamp( ?string $timestamp ): void {
1137 $this->mTimestamp = $timestamp;
1141 * @param ?string $timestamp TS_MW timestamp of the revision content
1143 * @return ?string The previous value of the timestamp
1144 * @deprecated since 1.42; use ::setRevisionTimestamp() instead
1146 public function setTimestamp( $timestamp ) {
1147 return wfSetVar( $this->mTimestamp, $timestamp );
1151 * Add a category.
1153 * Although ParserOutput::getCategorySortKey() will return exactly
1154 * the sort key you specify here, before storing in the database
1155 * all sort keys will be language converted, HTML entities will be
1156 * decoded, newlines stripped, and then they will be truncated to
1157 * 255 bytes. Thus the "effective" sort key in the DB may be different
1158 * from what is passed to `$sort` here and returned by
1159 * ::getCategorySortKey().
1161 * @param string|ParsoidLinkTarget $c The category name
1162 * @param string $sort The sort key; an empty string indicates
1163 * that the default sort key for the page should be used.
1165 public function addCategory( $c, $sort = '' ): void {
1166 if ( $c instanceof ParsoidLinkTarget ) {
1167 $c = $c->getDBkey();
1169 $this->mCategories[$c] = $sort;
1173 * Overwrite the category map.
1174 * @param array<string,string> $c Map of category names to sort keys
1175 * @since 1.38
1177 public function setCategories( array $c ): void {
1178 $this->mCategories = $c;
1182 * @param string $id
1183 * @param string $content
1184 * @param-taint $content exec_html
1185 * @since 1.25
1187 public function setIndicator( $id, $content ): void {
1188 $this->mIndicators[$id] = $content;
1192 * Enables OOUI, if true, in any OutputPage instance this ParserOutput
1193 * object is added to.
1195 * @since 1.26
1196 * @param bool $enable If OOUI should be enabled or not
1198 public function setEnableOOUI( bool $enable = false ): void {
1199 $this->mEnableOOUI = $enable;
1203 * Add a language link.
1204 * @param ParsoidLinkTarget|string $t
1206 public function addLanguageLink( $t ): void {
1207 # Note that fragments are preserved
1208 if ( $t instanceof ParsoidLinkTarget ) {
1209 // Language links are unusual in using 'text' rather than 'db key'
1210 // Note that fragments are preserved.
1211 $lang = $t->getInterwiki();
1212 $title = $t->getText();
1213 if ( $t->hasFragment() ) {
1214 $title .= '#' . $t->getFragment();
1216 } else {
1217 [ $lang, $title ] = array_pad( explode( ':', $t, 2 ), -2, '' );
1219 if ( $lang === '' ) {
1220 // T374736: For backward compatibility with test cases only!
1221 wfDeprecated( __METHOD__ . ' without prefix', '1.43' );
1222 [ $lang, $title ] = [ $title, '|' ]; // | can not occur in valid title
1224 $this->mLanguageLinkMap[$lang] ??= $title;
1228 * Add a warning to the output for this page.
1229 * @param MessageValue $mv
1230 * @since 1.43
1232 public function addWarningMsgVal( MessageValue $mv ) {
1233 // These can eventually be stored as MessageValue directly.
1234 $this->addWarningMsg( $mv->getKey(), ...$mv->getParams() );
1238 * Add a warning to the output for this page.
1239 * @param string $msg The localization message key for the warning
1240 * @param mixed|JsonDeserializable ...$args Optional arguments for the
1241 * message. These arguments must be serializable/deserializable with
1242 * JsonCodec; see the @note on ParserOutput::setExtensionData()
1243 * @since 1.38
1245 public function addWarningMsg( string $msg, ...$args ): void {
1246 // MessageValue objects are defined in core and thus not visible
1247 // to Parsoid or to its ContentMetadataCollector interface.
1248 // Eventually this method (defined in ContentMetadataCollector) should
1249 // call ::addWarningMsgVal() instead of the other way around.
1251 // preserve original arguments in $mWarningMsgs to allow merge
1252 // @todo: these aren't serialized/deserialized yet -- before we
1253 // turn on serialization of $this->mWarningMsgs we need to ensure
1254 // callers aren't passing nonserializable arguments: T343048.
1255 $jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
1256 $path = $jsonCodec->detectNonSerializableData( $args, true );
1257 if ( $path !== null ) {
1258 wfDeprecatedMsg(
1259 "ParserOutput::addWarningMsg() called with nonserializable arguments: $path",
1260 '1.41'
1263 $this->mWarningMsgs[$msg] = $args;
1264 $s = wfMessage( $msg, ...$args )
1265 // some callers set the title here?
1266 ->inContentLanguage() // because this ends up in cache
1267 ->text();
1268 $this->mWarnings[$s] = 1;
1271 public function setNewSection( $value ): void {
1272 $this->mNewSection = (bool)$value;
1276 * @param bool $value Hide the new section link?
1278 public function setHideNewSection( bool $value ): void {
1279 $this->mHideNewSection = $value;
1282 public function getHideNewSection(): bool {
1283 return (bool)$this->mHideNewSection;
1286 public function getNewSection(): bool {
1287 return (bool)$this->mNewSection;
1291 * Checks, if a url is pointing to the own server
1293 * @param string $internal The server to check against
1294 * @param string $url The url to check
1295 * @return bool
1296 * @internal
1298 public static function isLinkInternal( $internal, $url ): bool {
1299 return (bool)preg_match( '/^' .
1300 # If server is proto relative, check also for http/https links
1301 ( substr( $internal, 0, 2 ) === '//' ? '(?:https?:)?' : '' ) .
1302 preg_quote( $internal, '/' ) .
1303 # check for query/path/anchor or end of link in each case
1304 '(?:[\?\/\#]|$)/i',
1305 $url
1309 public function addExternalLink( $url ): void {
1310 # We don't register links pointing to our own server, unless... :-)
1311 $config = MediaWikiServices::getInstance()->getMainConfig();
1312 $server = $config->get( MainConfigNames::Server );
1313 $registerInternalExternals = $config->get( MainConfigNames::RegisterInternalExternals );
1314 # Replace unnecessary URL escape codes with the referenced character
1315 # This prevents spammers from hiding links from the filters
1316 $url = Parser::normalizeLinkUrl( $url );
1318 $registerExternalLink = true;
1319 if ( !$registerInternalExternals ) {
1320 $registerExternalLink = !self::isLinkInternal( $server, $url );
1322 if ( $registerExternalLink ) {
1323 $this->mExternalLinks[$url] = 1;
1328 * Record a local or interwiki inline link for saving in future link tables.
1330 * @param ParsoidLinkTarget $link (used to require Title until 1.38)
1331 * @param int|null $id Optional known page_id so we can skip the lookup
1333 public function addLink( ParsoidLinkTarget $link, $id = null ): void {
1334 if ( $link->isExternal() ) {
1335 // Don't record interwikis in pagelinks
1336 $this->addInterwikiLink( $link );
1337 return;
1339 $ns = $link->getNamespace();
1340 $dbk = $link->getDBkey();
1341 if ( $ns === NS_MEDIA ) {
1342 // Normalize this pseudo-alias if it makes it down here...
1343 $ns = NS_FILE;
1344 } elseif ( $ns === NS_SPECIAL ) {
1345 // We don't want to record Special: links in the database, so put them in a separate place.
1346 // It might actually be wise to, but we'd need to do some normalization.
1347 $this->mLinksSpecial[$dbk] = 1;
1348 return;
1349 } elseif ( $dbk === '' ) {
1350 // Don't record self links - [[#Foo]]
1351 return;
1353 if ( $id === null ) {
1354 // T357048: This actually kills performance; we should batch these.
1355 $page = MediaWikiServices::getInstance()->getPageStore()->getPageForLink( $link );
1356 $id = $page->getId();
1358 $this->mLinks[$ns][$dbk] = $id;
1362 * Register a file dependency for this output
1363 * @param string|ParsoidLinkTarget $name Title dbKey
1364 * @param string|false|null $timestamp MW timestamp of file creation (or false if non-existing)
1365 * @param string|false|null $sha1 Base 36 SHA-1 of file (or false if non-existing)
1367 public function addImage( $name, $timestamp = null, $sha1 = null ): void {
1368 if ( $name instanceof ParsoidLinkTarget ) {
1369 $name = $name->getDBkey();
1371 $this->mImages[$name] = 1;
1372 if ( $timestamp !== null && $sha1 !== null ) {
1373 $this->mFileSearchOptions[$name] = [ 'time' => $timestamp, 'sha1' => $sha1 ];
1378 * Register a template dependency for this output
1380 * @param ParsoidLinkTarget $link (used to require Title until 1.38)
1381 * @param int $page_id
1382 * @param int $rev_id
1384 public function addTemplate( $link, $page_id, $rev_id ): void {
1385 if ( $link->isExternal() ) {
1386 // Will throw an InvalidArgumentException in a future release.
1387 wfDeprecated( __METHOD__ . " with interwiki link", '1.42' );
1388 return;
1390 $ns = $link->getNamespace();
1391 $dbk = $link->getDBkey();
1392 // T357048: Parsoid doesn't have page_id
1393 $this->mTemplates[$ns][$dbk] = $page_id;
1394 $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
1398 * @param ParsoidLinkTarget $link must be an interwiki link
1399 * (used to require Title until 1.38).
1401 public function addInterwikiLink( $link ): void {
1402 if ( !$link->isExternal() ) {
1403 throw new InvalidArgumentException( 'Non-interwiki link passed, internal parser error.' );
1405 $prefix = $link->getInterwiki();
1406 $this->mInterwikiLinks[$prefix][$link->getDBkey()] = 1;
1410 * Add some text to the "<head>".
1411 * If $tag is set, the section with that tag will only be included once
1412 * in a given page.
1413 * @param string $section
1414 * @param string|false $tag
1416 public function addHeadItem( $section, $tag = false ): void {
1417 if ( $tag !== false ) {
1418 $this->mHeadItems[$tag] = $section;
1419 } else {
1420 $this->mHeadItems[] = $section;
1425 * @see OutputPage::addModules
1426 * @param string[] $modules
1428 public function addModules( array $modules ): void {
1429 $modules = array_fill_keys( $modules, true );
1430 $this->mModuleSet = array_merge( $this->mModuleSet, $modules );
1434 * @see OutputPage::addModuleStyles
1435 * @param string[] $modules
1437 public function addModuleStyles( array $modules ): void {
1438 $modules = array_fill_keys( $modules, true );
1439 $this->mModuleStyleSet = array_merge( $this->mModuleStyleSet, $modules );
1443 * Add one or more variables to be set in mw.config in JavaScript.
1445 * @param string|array $keys Key or array of key/value pairs.
1446 * @param mixed|null $value [optional] Value of the configuration variable.
1447 * @since 1.23
1448 * @deprecated since 1.38, use ::setJsConfigVar() or ::appendJsConfigVar()
1449 * which ensures compatibility with asynchronous parsing; emitting warnings
1450 * since 1.43.
1452 public function addJsConfigVars( $keys, $value = null ): void {
1453 wfDeprecated( __METHOD__, '1.38' );
1454 if ( is_array( $keys ) ) {
1455 foreach ( $keys as $key => $value ) {
1456 $this->mJsConfigVars[$key] = $value;
1458 return;
1461 $this->mJsConfigVars[$keys] = $value;
1465 * Add a variable to be set in mw.config in JavaScript.
1467 * In order to ensure the result is independent of the parse order, the values
1468 * set here must be unique -- that is, you can pass the same $key
1469 * multiple times but ONLY if the $value is identical each time.
1470 * If you want to collect multiple pieces of data under a single key,
1471 * use ::appendJsConfigVar().
1473 * @param string $key Key to use under mw.config
1474 * @param mixed|null $value Value of the configuration variable.
1475 * @since 1.38
1477 public function setJsConfigVar( string $key, $value ): void {
1478 if (
1479 array_key_exists( $key, $this->mJsConfigVars ) &&
1480 $this->mJsConfigVars[$key] !== $value
1482 // Ensure that a key is mapped to only a single value in order
1483 // to prevent the resulting array from varying if content
1484 // is parsed in a different order.
1485 throw new InvalidArgumentException( "Multiple conflicting values given for $key" );
1487 $this->mJsConfigVars[$key] = $value;
1491 * Append a value to a variable to be set in mw.config in JavaScript.
1493 * In order to ensure the result is independent of the parse order,
1494 * the value of this key will be an associative array, mapping all of
1495 * the values set under that key to true. (The array is implicitly
1496 * ordered in PHP, but you should treat it as unordered.)
1497 * If you want a non-array type for the key, and can ensure that only
1498 * a single value will be set, you should use ::setJsConfigVar() instead.
1500 * @param string $key Key to use under mw.config
1501 * @param string $value Value to append to the configuration variable.
1502 * @param string $strategy Merge strategy:
1503 * only MW_MERGE_STRATEGY_UNION is currently supported and external callers
1504 * should treat this parameter as @internal at this time and omit it.
1505 * @since 1.38
1507 public function appendJsConfigVar(
1508 string $key,
1509 string $value,
1510 string $strategy = self::MW_MERGE_STRATEGY_UNION
1511 ): void {
1512 if ( $strategy !== self::MW_MERGE_STRATEGY_UNION ) {
1513 throw new InvalidArgumentException( "Unknown merge strategy $strategy." );
1515 if ( !array_key_exists( $key, $this->mJsConfigVars ) ) {
1516 $this->mJsConfigVars[$key] = [
1517 // Indicate how these values are to be merged.
1518 self::MW_MERGE_STRATEGY_KEY => $strategy,
1520 } elseif ( !is_array( $this->mJsConfigVars[$key] ) ) {
1521 throw new InvalidArgumentException( "Mixing set and append for $key" );
1522 } elseif ( ( $this->mJsConfigVars[$key][self::MW_MERGE_STRATEGY_KEY] ?? null ) !== $strategy ) {
1523 throw new InvalidArgumentException( "Conflicting merge strategies for $key" );
1525 $this->mJsConfigVars[$key][$value] = true;
1529 * Accommodate very basic transcluding of a temporary OutputPage object into parser output.
1531 * This is a fragile method that cannot be relied upon in any meaningful way.
1532 * It exists solely to support the wikitext feature of transcluding a SpecialPage, and
1533 * only has to work for that use case to ensure relevant styles are loaded, and that
1534 * essential config vars needed between SpecialPage and a JS feature are added.
1536 * This relies on there being no overlap between modules or config vars added by
1537 * the SpecialPage and those added by parser extensions. If there is overlap,
1538 * then arise and break one or both sides. This is expected and unsupported.
1540 * @internal For use by Parser for basic special page transclusion
1541 * @param OutputPage $out
1543 public function addOutputPageMetadata( OutputPage $out ): void {
1544 // This should eventually use the same merge mechanism used
1545 // internally to merge ParserOutputs together.
1546 // (ie: $this->mergeHtmlMetaDataFrom( $out->getMetadata() )
1547 // once preventClickjacking, moduleStyles, modules, jsconfigvars,
1548 // and head items are moved to OutputPage::$metadata)
1550 // Take the strictest click-jacking policy. This is to ensure any one-click features
1551 // such as patrol or rollback on the transcluded special page will result in the wiki page
1552 // disallowing embedding in cross-origin iframes. Articles are generally allowed to be
1553 // embedded. Pages that transclude special pages are expected to be user pages or
1554 // other non-content pages that content re-users won't discover or care about.
1555 $this->mPreventClickjacking = $this->mPreventClickjacking || $out->getPreventClickjacking();
1557 $this->addModuleStyles( $out->getModuleStyles() );
1559 // TODO: Figure out if style modules suffice, or whether the below is needed as well.
1560 // Are there special pages that permit transcluding/including and also have JS modules
1561 // that should be activate on the host page?
1562 $this->addModules( $out->getModules() );
1563 $this->mJsConfigVars = self::mergeMapStrategy(
1564 $this->mJsConfigVars, $out->getJsConfigVars()
1566 $this->mHeadItems = array_merge( $this->mHeadItems, $out->getHeadItemsArray() );
1570 * Override the title to be used for display
1572 * @note this is assumed to have been validated
1573 * (check equal normalisation, etc.)
1575 * @note this is expected to be safe HTML,
1576 * ready to be served to the client.
1578 * @param string $text Desired title text
1580 public function setDisplayTitle( $text ): void {
1581 $this->setTitleText( $text );
1582 $this->setPageProperty( 'displaytitle', $text );
1586 * Get the title to be used for display.
1588 * As per the contract of setDisplayTitle(), this is safe HTML,
1589 * ready to be served to the client.
1591 * @return string|false HTML
1593 public function getDisplayTitle() {
1594 $t = $this->getTitleText();
1595 if ( $t === '' ) {
1596 return false;
1598 return $t;
1602 * Get the primary language code of the output.
1604 * This returns the primary language of the output, including
1605 * any LanguageConverter variant applied.
1607 * NOTE: This may differ from the wiki's default content language
1608 * ($wgLanguageCode, MediaWikiServices::getContentLanguage), because
1609 * each page may have its own "page language" set (PageStoreRecord,
1610 * Title::getDbPageLanguageCode, ContentHandler::getPageLanguage).
1612 * NOTE: This may differ from the "page language" when parsing
1613 * user interface messages, in which case this reflects the user
1614 * language (including any variant preference).
1616 * NOTE: This may differ from the Parser's "target language" that was
1617 * set while the Parser was parsing the page, because the final output
1618 * is converted to the current user's preferred LanguageConverter variant
1619 * (assuming this is a variant of the target language).
1620 * See Parser::getTargetLanguageConverter()->getPreferredVariant(); use
1621 * LanguageFactory::getParentLanguage() on the language code to obtain
1622 * the base language code. LanguageConverter::getPreferredVariant()
1623 * depends on the global RequestContext for the URL and the User
1624 * language preference.
1626 * Finally, note that a single ParserOutput object may contain
1627 * HTML content in multiple different languages and directions
1628 * (T114640). Authors of wikitext and of parser extensions are
1629 * expected to mark such subtrees with a `lang` attribute (set to
1630 * a BCP-47 value, see Language::toBcp47Code()) and a corresponding
1631 * `dir` attribute (see Language::getDir()). This method returns
1632 * the language code for wrapper of the HTML content.
1634 * @see Parser::internalParseHalfParsed
1635 * @since 1.40
1636 * @return ?Bcp47Code The primary language for this output,
1637 * or `null` if a language was not set.
1639 public function getLanguage(): ?Bcp47Code {
1640 // This information is temporarily stored in extension data (T303329)
1641 $code = $this->getExtensionData( 'core:target-lang-variant' );
1642 // This is null if the ParserOutput was cached by MW 1.40 or earlier,
1643 // or not constructed by Parser/ParserCache.
1644 return $code === null ? null : new Bcp47CodeValue( $code );
1648 * Set the primary language of the output.
1650 * See the discussion and caveats in ::getLanguage().
1652 * @param Bcp47Code $lang The primary language for this output, including
1653 * any variant specification.
1654 * @since 1.40
1656 public function setLanguage( Bcp47Code $lang ): void {
1657 $this->setExtensionData( 'core:target-lang-variant', $lang->toBcp47Code() );
1661 * Return an HTML prefix to be applied on redirect pages, or null
1662 * if this is not a redirect.
1663 * @return ?string HTML to prepend to redirect pages, or null
1664 * @internal
1666 public function getRedirectHeader(): ?string {
1667 return $this->getExtensionData( 'core:redirect-header' );
1671 * Set an HTML prefix to be applied on redirect pages.
1672 * @param string $html HTML to prepend to redirect pages
1674 public function setRedirectHeader( string $html ): void {
1675 $this->setExtensionData( 'core:redirect-header', $html );
1679 * Store a unique rendering id for this ParserOutput. This is used
1680 * whenever a client needs to record a dependency on a specific parse.
1681 * It is typically set only when a parser output is cached.
1683 * @param string $renderId a UUID identifying a specific parse
1684 * @internal
1686 public function setRenderId( string $renderId ): void {
1687 $this->setExtensionData( 'core:render-id', $renderId );
1691 * Return the unique rendering id for this ParserOutput. This is used
1692 * whenever a client needs to record a dependency on a specific parse.
1694 * @return string|null
1695 * @internal
1697 public function getRenderId(): ?string {
1698 // Backward-compatibility with old cache contents
1699 // Can be removed after parser cache contents have expired
1700 $old = $this->getExtensionData( 'parsoid-render-id' );
1701 if ( $old !== null ) {
1702 return ParsoidRenderId::newFromKey( $old )->getUniqueID();
1704 return $this->getExtensionData( 'core:render-id' );
1708 * @return string[] List of flags signifying special cases
1709 * @internal
1711 public function getAllFlags(): array {
1712 return array_keys( $this->mFlags );
1716 * Set a page property to be stored in the page_props database table.
1718 * page_props is a key-value store indexed by the page ID. This allows
1719 * the parser to set a property on a page which can then be quickly
1720 * retrieved given the page ID or via a DB join when given the page
1721 * title.
1723 * Since 1.23, page_props are also indexed by numeric value, to allow
1724 * for efficient "top k" queries of pages wrt a given property.
1725 * This only works if the value is passed as a int, float, or
1726 * bool. Since 1.42 you should use ::setNumericPageProperty()
1727 * if you want your page property value to be indexed, which will ensure
1728 * that the value is of the proper type.
1730 * setPageProperty() is thus used to propagate properties from the parsed
1731 * page to request contexts other than a page view of the currently parsed
1732 * article.
1734 * Some applications examples:
1736 * * To implement hidden categories, hiding pages from category listings
1737 * by storing a page property.
1739 * * Overriding the displayed article title (ParserOutput::setDisplayTitle()).
1741 * * To implement image tagging, for example displaying an icon on an
1742 * image thumbnail to indicate that it is listed for deletion on
1743 * Wikimedia Commons.
1744 * This is not actually implemented, yet but would be pretty cool.
1746 * @note Use of non-scalar values (anything other than
1747 * `string|int|float|bool`) has been deprecated in 1.42.
1748 * Although any JSON-serializable value can be stored/fetched in
1749 * ParserOutput, when the values are stored to the database
1750 * (in `deferred/LinksUpdate/PagePropsTable.php`) they will be
1751 * converted: booleans will be converted to '0' and '1', null
1752 * will become '', and everything else will be cast to string
1753 * (not JSON-serialized). Page properties obtained from the
1754 * PageProps service will thus always be strings.
1756 * @note The sort key stored in the database *will be NULL* unless
1757 * the value passed here is an `int|float|bool`. If you *do not*
1758 * want your property *value* indexed and sorted (for example, the
1759 * value is a title string which can be numeric but only
1760 * incidentally, like when it gets retrieved from an array key)
1761 * be sure to cast to string or use
1762 * `::setUnsortedPageProperty()`. If you *do* want your property
1763 * *value* indexed and sorted, you should use
1764 * `::setNumericPageProperty()` instead as this will ensure the
1765 * value type is correct. Note that either way it is possible to
1766 * efficiently look up all the pages with a certain property; we
1767 * are only talking about sorting the *values* assigned to the
1768 * property, for example for a "top N values of the property"
1769 * query.
1771 * @note Note that `::getPageProperty()`/`::setPageProperty()` do
1772 * not do any conversions themselves; you should therefore be
1773 * careful to distinguish values returned from the PageProp
1774 * service (always strings) from values retrieved from a
1775 * ParserOutput.
1777 * @note Do not use setPageProperty() to set a property which is only used
1778 * in a context where the ParserOutput object itself is already available,
1779 * for example a normal page view. There is no need to save such a property
1780 * in the database since the text is already parsed; use
1781 * ::setExtensionData() instead.
1783 * @par Example:
1784 * @code
1785 * $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
1786 * @endcode
1788 * And then later, in OutputPageParserOutput or similar:
1790 * @par Example:
1791 * @code
1792 * $output->getExtensionData( 'my_ext_foo' );
1793 * @endcode
1795 * @note The use of `null` as a value is deprecated since 1.42; use
1796 * the empty string instead if you need a placeholder value, or
1797 * ::unsetPageProperty() if you mean to remove a page property.
1799 * @note The use of non-string values is deprecated since 1.42; if you
1800 * need an page property value with a sort index
1801 * use ::setNumericPageProperty().
1803 * @param string $name
1804 * @param ?scalar $value
1805 * @since 1.38
1807 public function setPageProperty( string $name, $value ): void {
1808 if ( $value === null ) {
1809 // Use an empty string instead.
1810 wfDeprecated( __METHOD__ . " with null value for $name", '1.42' );
1811 } elseif ( !is_scalar( $value ) ) {
1812 // Use ::setExtensionData() instead.
1813 wfDeprecated( __METHOD__ . " with non-scalar value for $name", '1.42' );
1814 } elseif ( !is_string( $value ) ) {
1815 // Use ::setNumericPageProperty() instead.
1816 wfDeprecated( __METHOD__ . " with non-string value for $name", '1.42' );
1818 $this->mProperties[$name] = $value;
1822 * Set a numeric page property whose *value* is intended to be sorted
1823 * and indexed. The sort key used for the property will be the value,
1824 * coerced to a number.
1826 * See `::setPageProperty()` for details.
1828 * In the future, we may allow the value to be specified independent
1829 * of sort key (T357783).
1831 * @param string $propName The name of the page property
1832 * @param int|float|string $numericValue the numeric value
1833 * @since 1.42
1835 public function setNumericPageProperty( string $propName, $numericValue ): void {
1836 if ( !is_numeric( $numericValue ) ) {
1837 throw new \TypeError( __METHOD__ . " with non-numeric value" );
1839 // Coerce numeric sort key to a number.
1840 $this->mProperties[$propName] = 0 + $numericValue;
1844 * Set a page property whose *value* is not intended to be sorted and
1845 * indexed.
1847 * See `::setPageProperty()` for details. It is recommended to
1848 * use the empty string if you need a placeholder value (ie, if
1849 * it is the *presence* of the property which is important, not
1850 * the *value* the property is set to).
1852 * It is still possible to efficiently look up all the pages with
1853 * a certain property (the "presence" of it *is* indexed; see
1854 * Special:PagesWithProp, list=pageswithprop).
1856 * @param string $propName The name of the page property
1857 * @param string $value Optional value; defaults to the empty string.
1858 * @since 1.42
1860 public function setUnsortedPageProperty( string $propName, string $value = '' ): void {
1861 $this->mProperties[$propName] = $value;
1865 * Look up a page property.
1866 * @param string $name The page property name to look up.
1867 * @return ?scalar The value previously set using
1868 * ::setPageProperty(), ::setUnsortedPageProperty(), or
1869 * ::setNumericPageProperty().
1870 * Returns null if no value was set for the given property name.
1872 * @note You would need to use ::getPageProperties() to test for an
1873 * explicitly-set null value; but see the note in ::setPageProperty()
1874 * deprecating the use of null values.
1875 * @since 1.38
1877 public function getPageProperty( string $name ) {
1878 return $this->mProperties[$name] ?? null;
1882 * Remove a page property.
1883 * @param string $name The page property name.
1884 * @since 1.38
1886 public function unsetPageProperty( string $name ): void {
1887 unset( $this->mProperties[$name] );
1891 * Return all the page properties set on this ParserOutput.
1892 * @return array<string,?scalar>
1893 * @since 1.38
1895 public function getPageProperties(): array {
1896 if ( !isset( $this->mProperties ) ) {
1897 $this->mProperties = [];
1899 return $this->mProperties;
1903 * Provides a uniform interface to various boolean flags stored
1904 * in the ParserOutput. Flags internal to MediaWiki core should
1905 * have names which are constants in ParserOutputFlags. Extensions
1906 * should use ::setExtensionData() rather than creating new flags
1907 * with ::setOutputFlag() in order to prevent namespace conflicts.
1909 * Flags are always combined with OR. That is, the flag is set in
1910 * the resulting ParserOutput if the flag is set in *any* of the
1911 * fragments composing the ParserOutput.
1913 * @note The combination policy means that a ParserOutput may end
1914 * up with both INDEX_POLICY and NO_INDEX_POLICY set. It is
1915 * expected that NO_INDEX_POLICY "wins" in that case. (T16899)
1916 * (This resolution is implemented in ::getIndexPolicy().)
1918 * @param string $name A flag name
1919 * @param bool $val
1920 * @since 1.38
1922 public function setOutputFlag( string $name, bool $val = true ): void {
1923 switch ( $name ) {
1924 case ParserOutputFlags::NO_GALLERY:
1925 $this->setNoGallery( $val );
1926 break;
1928 case ParserOutputFlags::ENABLE_OOUI:
1929 $this->setEnableOOUI( $val );
1930 break;
1932 case ParserOutputFlags::NO_INDEX_POLICY:
1933 $this->mNoIndexSet = $val;
1934 break;
1936 case ParserOutputFlags::INDEX_POLICY:
1937 $this->mIndexSet = $val;
1938 break;
1940 case ParserOutputFlags::NEW_SECTION:
1941 $this->setNewSection( $val );
1942 break;
1944 case ParserOutputFlags::HIDE_NEW_SECTION:
1945 $this->setHideNewSection( $val );
1946 break;
1948 case ParserOutputFlags::PREVENT_CLICKJACKING:
1949 $this->setPreventClickjacking( $val );
1950 break;
1952 default:
1953 if ( $val ) {
1954 $this->mFlags[$name] = true;
1955 } else {
1956 unset( $this->mFlags[$name] );
1958 break;
1963 * Provides a uniform interface to various boolean flags stored
1964 * in the ParserOutput. Flags internal to MediaWiki core should
1965 * have names which are constants in ParserOutputFlags. Extensions
1966 * should only use ::getOutputFlag() to query flags defined in
1967 * ParserOutputFlags in core; they should use ::getExtensionData()
1968 * to define their own flags.
1970 * @param string $name A flag name
1971 * @return bool The flag value
1972 * @since 1.38
1974 public function getOutputFlag( string $name ): bool {
1975 switch ( $name ) {
1976 case ParserOutputFlags::NO_GALLERY:
1977 return $this->getNoGallery();
1979 case ParserOutputFlags::ENABLE_OOUI:
1980 return $this->getEnableOOUI();
1982 case ParserOutputFlags::INDEX_POLICY:
1983 return $this->mIndexSet;
1985 case ParserOutputFlags::NO_INDEX_POLICY:
1986 return $this->mNoIndexSet;
1988 case ParserOutputFlags::NEW_SECTION:
1989 return $this->getNewSection();
1991 case ParserOutputFlags::HIDE_NEW_SECTION:
1992 return $this->getHideNewSection();
1994 case ParserOutputFlags::PREVENT_CLICKJACKING:
1995 return $this->getPreventClickjacking();
1997 default:
1998 return isset( $this->mFlags[$name] );
2004 * Provides a uniform interface to various string sets stored
2005 * in the ParserOutput. String sets internal to MediaWiki core should
2006 * have names which are constants in ParserOutputStringSets. Extensions
2007 * should use ::appendExtensionData() rather than creating new string sets
2008 * with ::appendOutputStrings() in order to prevent namespace conflicts.
2010 * @param string $name A string set name
2011 * @param string[] $value
2012 * @since 1.41
2014 public function appendOutputStrings( string $name, array $value ): void {
2015 switch ( $name ) {
2016 case ParserOutputStringSets::MODULE:
2017 $this->addModules( $value );
2018 break;
2019 case ParserOutputStringSets::MODULE_STYLE:
2020 $this->addModuleStyles( $value );
2021 break;
2022 case ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC:
2023 foreach ( $value as $v ) {
2024 $this->addExtraCSPDefaultSrc( $v );
2026 break;
2027 case ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC:
2028 foreach ( $value as $v ) {
2029 $this->addExtraCSPScriptSrc( $v );
2031 break;
2032 case ParserOutputStringSets::EXTRA_CSP_STYLE_SRC:
2033 foreach ( $value as $v ) {
2034 $this->addExtraCSPStyleSrc( $v );
2036 break;
2037 default:
2038 throw new UnexpectedValueException( "Unknown output string set name $name" );
2043 * Provides a uniform interface to various boolean string sets stored
2044 * in the ParserOutput. String sets internal to MediaWiki core should
2045 * have names which are constants in ParserOutputStringSets. Extensions
2046 * should only use ::getOutputStrings() to query string sets defined in
2047 * ParserOutputStringSets in core; they should use ::appendExtensionData()
2048 * to define their own string sets.
2050 * @param string $name A string set name
2051 * @return string[] The string set value
2052 * @since 1.41
2054 public function getOutputStrings( string $name ): array {
2055 switch ( $name ) {
2056 case ParserOutputStringSets::MODULE:
2057 return $this->getModules();
2058 case ParserOutputStringSets::MODULE_STYLE:
2059 return $this->getModuleStyles();
2060 case ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC:
2061 return $this->getExtraCSPDefaultSrcs();
2062 case ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC:
2063 return $this->getExtraCSPScriptSrcs();
2064 case ParserOutputStringSets::EXTRA_CSP_STYLE_SRC:
2065 return $this->getExtraCSPStyleSrcs();
2066 default:
2067 throw new UnexpectedValueException( "Unknown output string set name $name" );
2072 * Attaches arbitrary data to this ParserObject. This can be used to store some information in
2073 * the ParserOutput object for later use during page output. The data will be cached along with
2074 * the ParserOutput object, but unlike data set using setPageProperty(), it is not recorded in the
2075 * database.
2077 * This method is provided to overcome the unsafe practice of attaching extra information to a
2078 * ParserObject by directly assigning member variables.
2080 * To use setExtensionData() to pass extension information from a hook inside the parser to a
2081 * hook in the page output, use this in the parser hook:
2083 * @par Example:
2084 * @code
2085 * $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
2086 * @endcode
2088 * And then later, in OutputPageParserOutput or similar:
2090 * @par Example:
2091 * @code
2092 * $output->getExtensionData( 'my_ext_foo' );
2093 * @endcode
2095 * In MediaWiki 1.20 and older, you have to use a custom member variable
2096 * within the ParserOutput object:
2098 * @par Example:
2099 * @code
2100 * $parser->getOutput()->my_ext_foo = '...';
2101 * @endcode
2103 * @note Only scalar values, e.g. numbers, strings, arrays or MediaWiki\Json\JsonDeserializable
2104 * instances are supported as a value. Attempt to set other class instance as extension data
2105 * will break ParserCache for the page.
2107 * @note Since MW 1.38 the practice of setting conflicting values for
2108 * the same key has been deprecated. As with ::setJsConfigVar(), if
2109 * you set the same key multiple times on a ParserOutput, it is expected
2110 * that the value will be identical each time. If you want to collect
2111 * multiple pieces of data under a single key, use ::appendExtensionData().
2113 * @param string $key The key for accessing the data. Extensions should take care to avoid
2114 * conflicts in naming keys. It is suggested to use the extension's name as a prefix.
2116 * @param mixed|JsonDeserializable $value The value to set.
2117 * Setting a value to null is equivalent to removing the value.
2118 * @since 1.21
2120 public function setExtensionData( $key, $value ): void {
2121 if (
2122 array_key_exists( $key, $this->mExtensionData ) &&
2123 $this->mExtensionData[$key] !== $value
2125 // This behavior was deprecated in 1.38. We will eventually
2126 // emit a warning here, then throw an exception.
2128 if ( $value === null ) {
2129 unset( $this->mExtensionData[$key] );
2130 } else {
2131 $this->mExtensionData[$key] = $value;
2136 * Appends arbitrary data to this ParserObject. This can be used
2137 * to store some information in the ParserOutput object for later
2138 * use during page output. The data will be cached along with the
2139 * ParserOutput object, but unlike data set using
2140 * setPageProperty(), it is not recorded in the database.
2142 * See ::setExtensionData() for more details on rationale and use.
2144 * In order to provide for out-of-order/asynchronous/incremental
2145 * parsing, this method appends values to a set. See
2146 * ::setExtensionData() for the flag-like version of this method.
2148 * @note Only values which can be array keys are currently supported
2149 * as values.
2151 * @param string $key The key for accessing the data. Extensions should take care to avoid
2152 * conflicts in naming keys. It is suggested to use the extension's name as a prefix.
2154 * @param int|string $value The value to append to the list.
2155 * @param string $strategy Merge strategy:
2156 * only MW_MERGE_STRATEGY_UNION is currently supported and external callers
2157 * should treat this parameter as @internal at this time and omit it.
2158 * @since 1.38
2160 public function appendExtensionData(
2161 string $key,
2162 $value,
2163 string $strategy = self::MW_MERGE_STRATEGY_UNION
2164 ): void {
2165 if ( $strategy !== self::MW_MERGE_STRATEGY_UNION ) {
2166 throw new InvalidArgumentException( "Unknown merge strategy $strategy." );
2168 if ( !array_key_exists( $key, $this->mExtensionData ) ) {
2169 $this->mExtensionData[$key] = [
2170 // Indicate how these values are to be merged.
2171 self::MW_MERGE_STRATEGY_KEY => $strategy,
2173 } elseif ( !is_array( $this->mExtensionData[$key] ) ) {
2174 throw new InvalidArgumentException( "Mixing set and append for $key" );
2175 } elseif ( ( $this->mExtensionData[$key][self::MW_MERGE_STRATEGY_KEY] ?? null ) !== $strategy ) {
2176 throw new InvalidArgumentException( "Conflicting merge strategies for $key" );
2178 $this->mExtensionData[$key][$value] = true;
2182 * Gets extensions data previously attached to this ParserOutput using setExtensionData().
2183 * Typically, such data would be set while parsing the page, e.g. by a parser function.
2185 * @since 1.21
2187 * @param string $key The key to look up.
2189 * @return mixed|null The value previously set for the given key using setExtensionData()
2190 * or null if no value was set for this key.
2192 public function getExtensionData( $key ) {
2193 $value = $this->mExtensionData[$key] ?? null;
2194 if ( is_array( $value ) ) {
2195 // Don't expose our internal merge strategy key.
2196 unset( $value[self::MW_MERGE_STRATEGY_KEY] );
2198 return $value;
2201 private static function getTimes( $clock = null ): array {
2202 $ret = [];
2203 if ( !$clock || $clock === 'wall' ) {
2204 $ret['wall'] = microtime( true );
2206 if ( !$clock || $clock === 'cpu' ) {
2207 $ru = getrusage( 0 /* RUSAGE_SELF */ );
2208 $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
2209 $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
2211 return $ret;
2215 * Resets the parse start timestamps for future calls to getTimeSinceStart()
2216 * and recordTimeProfile().
2218 * @since 1.22
2220 public function resetParseStartTime(): void {
2221 $this->mParseStartTime = self::getTimes();
2222 $this->mTimeProfile = [];
2226 * Unset the parse start time.
2228 * This is intended for testing purposes only, in order to avoid
2229 * spurious differences between testing outputs created at different
2230 * times.
2232 * @since 1.43
2234 public function clearParseStartTime(): void {
2235 $this->mParseStartTime = [];
2239 * Record the time since resetParseStartTime() was last called.
2240 * The recorded time can be accessed using getTimeProfile().
2242 * After resetParseStartTime() was called, the first call to recordTimeProfile()
2243 * will record the time profile. Subsequent calls to recordTimeProfile() will have
2244 * no effect until resetParseStartTime() is called again.
2246 * @since 1.42
2248 public function recordTimeProfile() {
2249 if ( !$this->mParseStartTime ) {
2250 // If resetParseStartTime was never called, there is nothing to record
2251 return;
2254 if ( $this->mTimeProfile !== [] ) {
2255 // Don't override the times recorded by the previous call to recordTimeProfile().
2256 return;
2259 $now = self::getTimes();
2260 $this->mTimeProfile = [
2261 'wall' => $now['wall'] - $this->mParseStartTime['wall'],
2262 'cpu' => $now['cpu'] - $this->mParseStartTime['cpu'],
2267 * Returns the time that elapsed between the most recent call to resetParseStartTime()
2268 * and the first call to recordTimeProfile() after that.
2270 * Clocks available are:
2271 * - wall: Wall clock time
2272 * - cpu: CPU time (requires getrusage)
2274 * If recordTimeProfile() has noit been called since the most recent call to
2275 * resetParseStartTime(), or if resetParseStartTime() was never called, then
2276 * this method will return null.
2278 * @param string $clock
2280 * @since 1.42
2281 * @return float|null
2283 public function getTimeProfile( string $clock ) {
2284 return $this->mTimeProfile[ $clock ] ?? null;
2288 * Returns the time since resetParseStartTime() was last called
2290 * Clocks available are:
2291 * - wall: Wall clock time
2292 * - cpu: CPU time (requires getrusage)
2294 * @since 1.22
2295 * @deprecated since 1.42, use getTimeProfile() instead.
2296 * @param string $clock
2297 * @return float|null
2299 public function getTimeSinceStart( $clock ) {
2300 wfDeprecated( __METHOD__, '1.42' );
2302 if ( !isset( $this->mParseStartTime[$clock] ) ) {
2303 return null;
2306 $end = self::getTimes( $clock );
2307 return $end[$clock] - $this->mParseStartTime[$clock];
2311 * Sets parser limit report data for a key
2313 * The key is used as the prefix for various messages used for formatting:
2314 * - $key: The label for the field in the limit report
2315 * - $key-value-text: Message used to format the value in the "NewPP limit
2316 * report" HTML comment. If missing, uses $key-format.
2317 * - $key-value-html: Message used to format the value in the preview
2318 * limit report table. If missing, uses $key-format.
2319 * - $key-value: Message used to format the value. If missing, uses "$1".
2321 * Note that all values are interpreted as wikitext, and so should be
2322 * encoded with htmlspecialchars() as necessary, but should avoid complex
2323 * HTML for display in the "NewPP limit report" comment.
2325 * @since 1.22
2326 * @param string $key Message key
2327 * @param mixed $value Appropriate for Message::params()
2329 public function setLimitReportData( $key, $value ): void {
2330 $this->mLimitReportData[$key] = $value;
2332 if ( is_array( $value ) ) {
2333 if ( array_keys( $value ) === [ 0, 1 ]
2334 && is_numeric( $value[0] )
2335 && is_numeric( $value[1] )
2337 $data = [ 'value' => $value[0], 'limit' => $value[1] ];
2338 } else {
2339 $data = $value;
2341 } else {
2342 $data = $value;
2345 if ( strpos( $key, '-' ) ) {
2346 [ $ns, $name ] = explode( '-', $key, 2 );
2347 $this->mLimitReportJSData[$ns][$name] = $data;
2348 } else {
2349 $this->mLimitReportJSData[$key] = $data;
2354 * Check whether the cache TTL was lowered from the site default.
2356 * When content is determined by more than hard state (e.g. page edits),
2357 * such as template/file transclusions based on the current timestamp or
2358 * extension tags that generate lists based on queries, this return true.
2360 * This method mainly exists to facilitate the logic in
2361 * WikiPage::triggerOpportunisticLinksUpdate. As such, beware that reducing the TTL for
2362 * reasons that do not relate to "dynamic content", may have the side-effect of incurring
2363 * more RefreshLinksJob executions.
2365 * @internal For use by Parser and WikiPage
2366 * @since 1.37
2367 * @return bool
2369 public function hasReducedExpiry(): bool {
2370 $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()->get(
2371 MainConfigNames::ParserCacheExpireTime );
2373 return $this->getCacheExpiry() < $parserCacheExpireTime;
2377 * Set the prevent-clickjacking flag. If set this will cause an
2378 * `X-Frame-Options` header appropriate for edit pages to be sent.
2379 * The header value is controlled by `$wgEditPageFrameOptions`.
2381 * This is the default for special pages. If you display a CSRF-protected
2382 * form on an ordinary view page, then you need to call this function
2383 * with `$flag = true`.
2385 * @param bool $flag New flag value
2386 * @since 1.38
2388 public function setPreventClickjacking( bool $flag ): void {
2389 $this->mPreventClickjacking = $flag;
2393 * Get the prevent-clickjacking flag.
2395 * @return bool Flag value
2396 * @since 1.38
2397 * @see ::setPreventClickjacking
2399 public function getPreventClickjacking(): bool {
2400 return $this->mPreventClickjacking;
2404 * Lower the runtime adaptive TTL to at most this value
2406 * @param int $ttl
2407 * @since 1.28
2409 public function updateRuntimeAdaptiveExpiry( $ttl ): void {
2410 $this->mMaxAdaptiveExpiry = min( $ttl, $this->mMaxAdaptiveExpiry );
2411 $this->updateCacheExpiry( $ttl );
2415 * Add an extra value to Content-Security-Policy default-src directive
2417 * Call this if you are including a resource (e.g. image) from a third party domain.
2418 * This is used for all source types except style and script.
2420 * @since 1.35
2421 * @param string $src CSP source e.g. example.com
2423 public function addExtraCSPDefaultSrc( $src ): void {
2424 $this->mExtraDefaultSrcs[] = $src;
2428 * Add an extra value to Content-Security-Policy style-src directive
2430 * @since 1.35
2431 * @param string $src CSP source e.g. example.com
2433 public function addExtraCSPStyleSrc( $src ): void {
2434 $this->mExtraStyleSrcs[] = $src;
2438 * Add an extra value to Content-Security-Policy script-src directive
2440 * Call this if you are loading third-party Javascript
2442 * @since 1.35
2443 * @param string $src CSP source e.g. example.com
2445 public function addExtraCSPScriptSrc( $src ): void {
2446 $this->mExtraScriptSrcs[] = $src;
2450 * Call this when parsing is done to lower the TTL based on low parse times
2452 * @since 1.28
2454 public function finalizeAdaptiveCacheExpiry(): void {
2455 if ( is_infinite( $this->mMaxAdaptiveExpiry ) ) {
2456 return; // not set
2459 $runtime = $this->getTimeProfile( 'wall' );
2460 if ( is_float( $runtime ) ) {
2461 $slope = ( self::SLOW_AR_TTL - self::FAST_AR_TTL )
2462 / ( self::PARSE_SLOW_SEC - self::PARSE_FAST_SEC );
2463 // SLOW_AR_TTL = PARSE_SLOW_SEC * $slope + $point
2464 $point = self::SLOW_AR_TTL - self::PARSE_SLOW_SEC * $slope;
2466 $adaptiveTTL = min(
2467 max( $slope * $runtime + $point, self::MIN_AR_TTL ),
2468 $this->mMaxAdaptiveExpiry
2470 $this->updateCacheExpiry( $adaptiveTTL );
2475 * Transfer parser options which affect post-processing from ParserOptions
2476 * to this ParserOutput.
2477 * @param ParserOptions $parserOptions
2479 public function setFromParserOptions( ParserOptions $parserOptions ) {
2480 // Copied from Parser.php::parse and should probably be abstracted
2481 // into the parent base class (probably as part of T236809)
2482 // Wrap non-interface parser output in a <div> so it can be targeted
2483 // with CSS (T37247)
2484 $class = $parserOptions->getWrapOutputClass();
2485 if ( $class !== false && !$parserOptions->getInterfaceMessage() ) {
2486 $this->addWrapperDivClass( $class );
2489 // Record whether we should suppress section edit links
2490 if ( $parserOptions->getSuppressSectionEditLinks() ) {
2491 $this->setOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS );
2494 // Record whether we should wrap sections for collapsing them
2495 if ( $parserOptions->getCollapsibleSections() ) {
2496 $this->setOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS );
2499 // Record whether this is a preview parse in the output (T341010)
2500 if ( $parserOptions->getIsPreview() ) {
2501 $this->setOutputFlag( ParserOutputFlags::IS_PREVIEW, true );
2502 // Ensure that previews aren't cacheable, just to be safe.
2503 $this->updateCacheExpiry( 0 );
2507 public function __sleep() {
2508 return array_filter( array_keys( get_object_vars( $this ) ),
2509 static function ( $field ) {
2510 if ( $field === 'mParseStartTime' || $field === 'mWarningMsgs' ) {
2511 return false;
2513 // Unserializing unknown private fields in HHVM causes
2514 // member variables with nulls in their names (T229366)
2515 return strpos( $field, "\0" ) === false;
2521 * Merges internal metadata such as flags, accessed options, and profiling info
2522 * from $source into this ParserOutput. This should be used whenever the state of $source
2523 * has any impact on the state of this ParserOutput.
2525 * @param ParserOutput $source
2527 public function mergeInternalMetaDataFrom( ParserOutput $source ): void {
2528 $this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter
2529 $this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getRevisionTimestamp() );
2530 if ( $source->hasCacheTime() ) {
2531 $sourceCacheTime = $source->getCacheTime();
2532 if (
2533 !$this->hasCacheTime() ||
2534 // "undocumented use of -1 to mean not cacheable"
2535 // deprecated, but still supported by ::setCacheTime()
2536 strval( $sourceCacheTime ) === '-1' ||
2538 strval( $this->getCacheTime() ) !== '-1' &&
2539 // use newer of the two times
2540 $this->getCacheTime() < $sourceCacheTime
2543 $this->setCacheTime( $sourceCacheTime );
2546 if ( $source->getRenderId() !== null ) {
2547 // Final render ID should be a function of all component POs
2548 $rid = ( $this->getRenderId() ?? '' ) . $source->getRenderId();
2549 $this->setRenderId( $rid );
2551 if ( $source->getCacheRevisionId() !== null ) {
2552 $sourceCacheRevisionId = $source->getCacheRevisionId();
2553 $thisCacheRevisionId = $this->getCacheRevisionId();
2554 if ( $thisCacheRevisionId === null ) {
2555 $this->setCacheRevisionId( $sourceCacheRevisionId );
2556 } elseif ( $sourceCacheRevisionId !== $thisCacheRevisionId ) {
2557 // May throw an exception here in the future
2558 wfDeprecated(
2559 __METHOD__ . ": conflicting revision IDs " .
2560 "$thisCacheRevisionId and $sourceCacheRevisionId"
2565 foreach ( self::SPECULATIVE_FIELDS as $field ) {
2566 if ( $this->$field && $source->$field && $this->$field !== $source->$field ) {
2567 wfLogWarning( __METHOD__ . ": inconsistent '$field' properties!" );
2569 $this->$field = $this->useMaxValue( $this->$field, $source->$field );
2572 $this->mParseStartTime = $this->useEachMinValue(
2573 $this->mParseStartTime,
2574 $source->mParseStartTime
2577 $this->mTimeProfile = $this->useEachTotalValue(
2578 $this->mTimeProfile,
2579 $source->mTimeProfile
2582 $this->mFlags = self::mergeMap( $this->mFlags, $source->mFlags );
2583 $this->mParseUsedOptions = self::mergeMap( $this->mParseUsedOptions, $source->mParseUsedOptions );
2585 // TODO: maintain per-slot limit reports!
2586 if ( !$this->mLimitReportData ) {
2587 $this->mLimitReportData = $source->mLimitReportData;
2589 if ( !$this->mLimitReportJSData ) {
2590 $this->mLimitReportJSData = $source->mLimitReportJSData;
2595 * Merges HTML metadata such as head items, JS config vars, and HTTP cache control info
2596 * from $source into this ParserOutput. This should be used whenever the HTML in $source
2597 * has been somehow merged into the HTML of this ParserOutput.
2599 * @param ParserOutput $source
2601 public function mergeHtmlMetaDataFrom( ParserOutput $source ): void {
2602 // HTML and HTTP
2603 $this->mHeadItems = self::mergeMixedList( $this->mHeadItems, $source->getHeadItems() );
2604 $this->addModules( $source->getModules() );
2605 $this->addModuleStyles( $source->getModuleStyles() );
2606 $this->mJsConfigVars = self::mergeMapStrategy( $this->mJsConfigVars, $source->mJsConfigVars );
2607 $this->mMaxAdaptiveExpiry = min( $this->mMaxAdaptiveExpiry, $source->mMaxAdaptiveExpiry );
2608 $this->mExtraStyleSrcs = self::mergeList(
2609 $this->mExtraStyleSrcs,
2610 $source->getExtraCSPStyleSrcs()
2612 $this->mExtraScriptSrcs = self::mergeList(
2613 $this->mExtraScriptSrcs,
2614 $source->getExtraCSPScriptSrcs()
2616 $this->mExtraDefaultSrcs = self::mergeList(
2617 $this->mExtraDefaultSrcs,
2618 $source->getExtraCSPDefaultSrcs()
2621 // "noindex" always wins!
2622 $this->mIndexSet = $this->mIndexSet || $source->mIndexSet;
2623 $this->mNoIndexSet = $this->mNoIndexSet || $source->mNoIndexSet;
2625 // Skin control
2626 $this->mNewSection = $this->mNewSection || $source->getNewSection();
2627 $this->mHideNewSection = $this->mHideNewSection || $source->getHideNewSection();
2628 $this->mNoGallery = $this->mNoGallery || $source->getNoGallery();
2629 $this->mEnableOOUI = $this->mEnableOOUI || $source->getEnableOOUI();
2630 $this->mPreventClickjacking = $this->mPreventClickjacking || $source->getPreventClickjacking();
2632 $tocData = $this->getTOCData();
2633 $sourceTocData = $source->getTOCData();
2634 if ( $tocData !== null ) {
2635 if ( $sourceTocData !== null ) {
2636 // T327429: Section merging is broken, since it doesn't respect
2637 // global numbering, but there are tests which expect section
2638 // metadata to be concatenated.
2639 // There should eventually be a deprecation warning here.
2640 foreach ( $sourceTocData->getSections() as $s ) {
2641 $tocData->addSection( $s );
2644 } elseif ( $sourceTocData !== null ) {
2645 $this->setTOCData( $sourceTocData );
2648 // XXX: we don't want to concatenate title text, so first write wins.
2649 // We should use the first *modified* title text, but we don't have the original to check.
2650 if ( $this->mTitleText === null || $this->mTitleText === '' ) {
2651 $this->mTitleText = $source->mTitleText;
2654 // class names are stored in array keys
2655 $this->mWrapperDivClasses = self::mergeMap(
2656 $this->mWrapperDivClasses,
2657 $source->mWrapperDivClasses
2660 // NOTE: last write wins, same as within one ParserOutput
2661 $this->mIndicators = self::mergeMap( $this->mIndicators, $source->getIndicators() );
2663 // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
2664 // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
2665 // kinds of extension data to be merged in different ways.
2666 $this->mExtensionData = self::mergeMapStrategy(
2667 $this->mExtensionData,
2668 $source->mExtensionData
2673 * Merges dependency tracking metadata such as backlinks, images used, and extension data
2674 * from $source into this ParserOutput. This allows dependency tracking to be done for the
2675 * combined output of multiple content slots.
2677 * @param ParserOutput $source
2679 public function mergeTrackingMetaDataFrom( ParserOutput $source ): void {
2680 foreach (
2681 $source->getLinkList( ParserOutputLinkTypes::LANGUAGE )
2682 as [ 'link' => $link ]
2684 $this->addLanguageLink( $link );
2686 $this->mCategories = self::mergeMap( $this->mCategories, $source->getCategoryMap() );
2687 foreach (
2688 $source->getLinkList( ParserOutputLinkTypes::LOCAL )
2689 as [ 'link' => $link, 'pageid' => $pageid ]
2691 $this->addLink( $link, $pageid );
2693 foreach (
2694 $source->getLinkList( ParserOutputLinkTypes::TEMPLATE )
2695 as [ 'link' => $link, 'pageid' => $pageid, 'revid' => $revid ]
2697 $this->addTemplate( $link, $pageid, $revid );
2699 foreach (
2700 $source->getLinkList( ParserOutputLinkTypes::MEDIA ) as $item
2702 $this->addImage( $item['link'], $item['time'] ?? null, $item['sha1'] ?? null );
2704 $this->mExternalLinks = self::mergeMap( $this->mExternalLinks, $source->getExternalLinks() );
2705 foreach (
2706 $source->getLinkList( ParserOutputLinkTypes::INTERWIKI )
2707 as [ 'link' => $link ]
2709 $this->addInterwikiLink( $link );
2712 foreach (
2713 $source->getLinkList( ParserOutputLinkTypes::SPECIAL )
2714 as [ 'link' => $link ]
2716 $this->addLink( $link );
2719 // TODO: add a $mergeStrategy parameter to setPageProperty to allow different
2720 // kinds of properties to be merged in different ways.
2721 // (Model this after ::appendJsConfigVar(); use ::mergeMapStrategy here)
2722 $this->mProperties = self::mergeMap( $this->mProperties, $source->getPageProperties() );
2724 // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
2725 $this->mExtensionData = self::mergeMapStrategy(
2726 $this->mExtensionData,
2727 $source->mExtensionData
2732 * Adds the metadata collected in this ParserOutput to the supplied
2733 * ContentMetadataCollector. This is similar to ::mergeHtmlMetaDataFrom()
2734 * but in the opposite direction, since ParserOutput is read/write while
2735 * ContentMetadataCollector is write-only.
2737 * @param ContentMetadataCollector $metadata
2738 * @since 1.38
2740 public function collectMetadata( ContentMetadataCollector $metadata ): void {
2741 // Uniform handling of all boolean flags: they are OR'ed together.
2742 $flags = array_keys(
2743 $this->mFlags + array_flip( ParserOutputFlags::cases() )
2745 foreach ( $flags as $name ) {
2746 if ( $this->getOutputFlag( $name ) ) {
2747 $metadata->setOutputFlag( $name );
2751 // Uniform handling of string sets: they are unioned.
2752 // (This includes modules, style modes, and CSP src.)
2753 foreach ( ParserOutputStringSets::cases() as $name ) {
2754 $metadata->appendOutputStrings(
2755 $name, $this->getOutputStrings( $name )
2759 foreach ( $this->mCategories as $cat => $key ) {
2760 // Numeric category strings are going to come out of the
2761 // `mCategories` array as ints; cast back to string.
2762 // Also convert back to a LinkTarget!
2763 $lt = TitleValue::tryNew( NS_CATEGORY, (string)$cat );
2764 $metadata->addCategory( $lt, $key );
2767 foreach ( $this->mLinks as $ns => $arr ) {
2768 foreach ( $arr as $dbk => $id ) {
2769 // Numeric titles are going to come out of the
2770 // `mLinks` array as ints; cast back to string.
2771 $lt = TitleValue::tryNew( $ns, (string)$dbk );
2772 $metadata->addLink( $lt, $id );
2776 foreach ( $this->mInterwikiLinks as $prefix => $arr ) {
2777 foreach ( $arr as $dbk => $ignore ) {
2778 $lt = TitleValue::tryNew( NS_MAIN, (string)$dbk, '', $prefix );
2779 $metadata->addLink( $lt );
2783 foreach ( $this->mLinksSpecial as $dbk => $ignore ) {
2784 // Numeric titles are going to come out of the
2785 // `mLinks` array as ints; cast back to string.
2786 $lt = TitleValue::tryNew( NS_SPECIAL, (string)$dbk );
2787 $metadata->addLink( $lt );
2790 foreach ( $this->mImages as $name => $ignore ) {
2791 // Numeric titles come out of mImages as ints.
2792 $lt = TitleValue::tryNew( NS_FILE, (string)$name );
2793 $props = $this->mFileSearchOptions[$name] ?? [];
2794 $metadata->addImage( $lt, $props['time'] ?? null, $props['sha1'] ?? null );
2797 foreach ( $this->mLanguageLinkMap as $lang => $title ) {
2798 if ( $title === '|' ) {
2799 continue; // T374736: not a valid language link
2801 # language links can have fragments!
2802 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
2803 $lt = TitleValue::tryNew( NS_MAIN, $title, $frag, (string)$lang );
2804 $metadata->addLanguageLink( $lt );
2807 foreach ( $this->mJsConfigVars as $key => $value ) {
2808 if ( is_array( $value ) && isset( $value[self::MW_MERGE_STRATEGY_KEY] ) ) {
2809 $strategy = $value[self::MW_MERGE_STRATEGY_KEY];
2810 foreach ( $value as $item => $ignore ) {
2811 if ( $item !== self::MW_MERGE_STRATEGY_KEY ) {
2812 $metadata->appendJsConfigVar( $key, $item, $strategy );
2815 } elseif ( $metadata instanceof ParserOutput &&
2816 array_key_exists( $key, $metadata->mJsConfigVars )
2818 // This behavior is deprecated, will likely result in
2819 // incorrect output, and we'll eventually emit a
2820 // warning here---but at the moment this is usually
2821 // caused by limitations in Parsoid and/or use of
2822 // the ParserAfterParse hook: T303015#7770480
2823 $metadata->mJsConfigVars[$key] = $value;
2824 } else {
2825 $metadata->setJsConfigVar( $key, $value );
2828 foreach ( $this->mExtensionData as $key => $value ) {
2829 if ( is_array( $value ) && isset( $value[self::MW_MERGE_STRATEGY_KEY] ) ) {
2830 $strategy = $value[self::MW_MERGE_STRATEGY_KEY];
2831 foreach ( $value as $item => $ignore ) {
2832 if ( $item !== self::MW_MERGE_STRATEGY_KEY ) {
2833 $metadata->appendExtensionData( $key, $item, $strategy );
2836 } elseif ( $metadata instanceof ParserOutput &&
2837 array_key_exists( $key, $metadata->mExtensionData )
2839 // This behavior is deprecated, will likely result in
2840 // incorrect output, and we'll eventually emit a
2841 // warning here---but at the moment this is usually
2842 // caused by limitations in Parsoid and/or use of
2843 // the ParserAfterParse hook: T303015#7770480
2844 $metadata->mExtensionData[$key] = $value;
2845 } else {
2846 $metadata->setExtensionData( $key, $value );
2849 foreach ( $this->mExternalLinks as $url => $ignore ) {
2850 $metadata->addExternalLink( $url );
2852 foreach ( $this->mProperties as $prop => $value ) {
2853 if ( is_numeric( $value ) ) {
2854 $metadata->setNumericPageProperty( $prop, $value );
2855 } elseif ( is_string( $value ) ) {
2856 $metadata->setUnsortedPageProperty( $prop, $value );
2857 } else {
2858 // Deprecated, but there are still sites which call
2859 // ::setPageProperty() with "unusual" values (T374046)
2860 $metadata->setPageProperty( $prop, $value );
2863 foreach ( $this->mWarningMsgs as $msg => $args ) {
2864 $metadata->addWarningMsg( $msg, ...$args );
2866 foreach ( $this->mLimitReportData as $key => $value ) {
2867 $metadata->setLimitReportData( $key, $value );
2869 foreach ( $this->mIndicators as $id => $content ) {
2870 $metadata->setIndicator( $id, $content );
2873 // ParserOutput-only fields; maintained "behind the curtain"
2874 // since Parsoid doesn't have to know about them.
2876 // In production use, the $metadata supplied to this method
2877 // will almost always be an instance of ParserOutput, passed to
2878 // Parsoid by core when parsing begins and returned to core by
2879 // Parsoid as a ContentMetadataCollector (Parsoid's name for
2880 // ParserOutput) when DataAccess::parseWikitext() is called.
2882 // We may use still Parsoid's StubMetadataCollector for testing or
2883 // when running Parsoid in standalone mode, so forcing a downcast
2884 // here would lose some flexibility.
2886 if ( $metadata instanceof ParserOutput ) {
2887 foreach ( $this->getUsedOptions() as $opt ) {
2888 $metadata->recordOption( $opt );
2890 if ( $this->mCacheExpiry !== null ) {
2891 $metadata->updateCacheExpiry( $this->mCacheExpiry );
2893 if ( $this->mCacheTime !== '' ) {
2894 $metadata->setCacheTime( $this->mCacheTime );
2896 if ( $this->mCacheRevisionId !== null ) {
2897 $metadata->setCacheRevisionId( $this->mCacheRevisionId );
2899 // T293514: We should use the first *modified* title text, but
2900 // we don't have the original to check.
2901 $otherTitle = $metadata->getTitleText();
2902 if ( $otherTitle === null || $otherTitle === '' ) {
2903 $metadata->setTitleText( $this->getTitleText() );
2905 foreach ( $this->mTemplates as $ns => $arr ) {
2906 foreach ( $arr as $dbk => $page_id ) {
2907 // default to invalid/broken revision if this is not present
2908 $rev_id = $this->mTemplateIds[$ns][$dbk] ?? 0;
2909 $metadata->addTemplate( TitleValue::tryNew( $ns, (string)$dbk ), $page_id, $rev_id );
2915 private static function mergeMixedList( array $a, array $b ): array {
2916 return array_unique( array_merge( $a, $b ), SORT_REGULAR );
2919 private static function mergeList( array $a, array $b ): array {
2920 return array_values( array_unique( array_merge( $a, $b ), SORT_REGULAR ) );
2923 private static function mergeMap( array $a, array $b ): array {
2924 return array_replace( $a, $b );
2927 private static function mergeMapStrategy( array $a, array $b ): array {
2928 foreach ( $b as $key => $bValue ) {
2929 if ( !array_key_exists( $key, $a ) ) {
2930 $a[$key] = $bValue;
2931 } elseif (
2932 is_array( $a[$key] ) &&
2933 isset( $a[$key][self::MW_MERGE_STRATEGY_KEY] ) &&
2934 isset( $bValue[self::MW_MERGE_STRATEGY_KEY] )
2936 $strategy = $bValue[self::MW_MERGE_STRATEGY_KEY];
2937 if ( $strategy !== $a[$key][self::MW_MERGE_STRATEGY_KEY] ) {
2938 throw new InvalidArgumentException( "Conflicting merge strategy for $key" );
2940 if ( $strategy === self::MW_MERGE_STRATEGY_UNION ) {
2941 // Note the array_merge is *not* safe to use here, because
2942 // the $bValue is expected to be a map from items to `true`.
2943 // If the item is a numeric string like '1' then array_merge
2944 // will convert it to an integer and renumber the array!
2945 $a[$key] = array_replace( $a[$key], $bValue );
2946 } else {
2947 throw new InvalidArgumentException( "Unknown merge strategy $strategy" );
2949 } else {
2950 $valuesSame = ( $a[$key] === $bValue );
2951 if ( ( !$valuesSame ) &&
2952 is_object( $a[$key] ) &&
2953 is_object( $bValue )
2955 $jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
2956 $valuesSame = ( $jsonCodec->serialize( $a[$key] ) === $jsonCodec->serialize( $bValue ) );
2958 if ( !$valuesSame ) {
2959 // Silently replace for now; in the future will first emit
2960 // a deprecation warning, and then (later) throw.
2961 $a[$key] = $bValue;
2965 return $a;
2968 private static function useEachMinValue( array $a, array $b ): array {
2969 $values = [];
2970 $keys = array_merge( array_keys( $a ), array_keys( $b ) );
2972 foreach ( $keys as $k ) {
2973 $values[$k] = min( $a[$k] ?? INF, $b[$k] ?? INF );
2976 return $values;
2979 private static function useEachTotalValue( array $a, array $b ): array {
2980 $values = [];
2981 $keys = array_merge( array_keys( $a ), array_keys( $b ) );
2983 foreach ( $keys as $k ) {
2984 $values[$k] = ( $a[$k] ?? 0 ) + ( $b[$k] ?? 0 );
2987 return $values;
2990 private static function useMaxValue( $a, $b ) {
2991 if ( $a === null ) {
2992 return $b;
2995 if ( $b === null ) {
2996 return $a;
2999 return max( $a, $b );
3003 * Returns a JSON serializable structure representing this ParserOutput instance.
3004 * @see newFromJson()
3006 * @return array
3008 protected function toJsonArray(): array {
3009 // WARNING: When changing how this class is serialized, follow the instructions
3010 // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
3012 $data = [
3013 'Text' => $this->mRawText,
3014 'LanguageLinks' => $this->getLanguageLinks(),
3015 'Categories' => $this->mCategories,
3016 'Indicators' => $this->mIndicators,
3017 'TitleText' => $this->mTitleText,
3018 'Links' => $this->mLinks,
3019 'LinksSpecial' => $this->mLinksSpecial,
3020 'Templates' => $this->mTemplates,
3021 'TemplateIds' => $this->mTemplateIds,
3022 'Images' => $this->mImages,
3023 'FileSearchOptions' => $this->mFileSearchOptions,
3024 'ExternalLinks' => $this->mExternalLinks,
3025 'InterwikiLinks' => $this->mInterwikiLinks,
3026 'NewSection' => $this->mNewSection,
3027 'HideNewSection' => $this->mHideNewSection,
3028 'NoGallery' => $this->mNoGallery,
3029 'HeadItems' => $this->mHeadItems,
3030 'Modules' => array_keys( $this->mModuleSet ),
3031 'ModuleStyles' => array_keys( $this->mModuleStyleSet ),
3032 'JsConfigVars' => $this->mJsConfigVars,
3033 'Warnings' => $this->mWarnings,
3034 'Sections' => $this->getSections(),
3035 'Properties' => self::detectAndEncodeBinary( $this->mProperties ),
3036 'Timestamp' => $this->mTimestamp,
3037 'EnableOOUI' => $this->mEnableOOUI,
3038 'IndexPolicy' => $this->getIndexPolicy(),
3039 // may contain arbitrary structures!
3040 'ExtensionData' => $this->mExtensionData,
3041 'LimitReportData' => $this->mLimitReportData,
3042 'LimitReportJSData' => $this->mLimitReportJSData,
3043 'CacheMessage' => $this->mCacheMessage,
3044 'TimeProfile' => $this->mTimeProfile,
3045 'ParseStartTime' => [], // don't serialize this
3046 'PreventClickjacking' => $this->mPreventClickjacking,
3047 'ExtraScriptSrcs' => $this->mExtraScriptSrcs,
3048 'ExtraDefaultSrcs' => $this->mExtraDefaultSrcs,
3049 'ExtraStyleSrcs' => $this->mExtraStyleSrcs,
3050 'Flags' => $this->mFlags + (
3051 // backward-compatibility: distinguish "no sections" from
3052 // "sections not set" (Will be unnecessary after T327439.)
3053 $this->mTOCData === null ? [] : [ 'mw:toc-set' => true ]
3055 'SpeculativeRevId' => $this->mSpeculativeRevId,
3056 'SpeculativePageIdUsed' => $this->speculativePageIdUsed,
3057 'RevisionTimestampUsed' => $this->revisionTimestampUsed,
3058 'RevisionUsedSha1Base36' => $this->revisionUsedSha1Base36,
3059 'WrapperDivClasses' => $this->mWrapperDivClasses,
3062 // Fill in missing fields from parents. Array addition does not override existing fields.
3063 $data += parent::toJsonArray();
3065 // TODO: make more fields optional!
3067 if ( $this->mMaxAdaptiveExpiry !== INF ) {
3068 // NOTE: JSON can't encode infinity!
3069 $data['MaxAdaptiveExpiry'] = $this->mMaxAdaptiveExpiry;
3072 if ( $this->mTOCData ) {
3073 // Temporarily add information from TOCData extension data
3074 // T327439: We should eventually make the entire mTOCData
3075 // serializable
3076 $toc = $this->mTOCData->jsonSerialize();
3077 if ( isset( $toc['extensionData'] ) ) {
3078 $data['TOCExtensionData'] = $toc['extensionData'];
3082 return $data;
3085 public static function newFromJsonArray( JsonDeserializer $deserializer, array $json ): ParserOutput {
3086 $parserOutput = new ParserOutput();
3087 $parserOutput->initFromJson( $deserializer, $json );
3088 return $parserOutput;
3092 * Initialize member fields from an array returned by jsonSerialize().
3093 * @param JsonDeserializer $deserializer
3094 * @param array $jsonData
3096 protected function initFromJson( JsonDeserializer $deserializer, array $jsonData ): void {
3097 parent::initFromJson( $deserializer, $jsonData );
3099 // WARNING: When changing how this class is serialized, follow the instructions
3100 // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
3102 $this->mRawText = $jsonData['Text'];
3103 $this->mLanguageLinkMap = [];
3104 foreach ( ( $jsonData['LanguageLinks'] ?? [] ) as $l ) {
3105 $this->addLanguageLink( $l );
3107 $this->mCategories = $jsonData['Categories'];
3108 $this->mIndicators = $jsonData['Indicators'];
3109 $this->mTitleText = $jsonData['TitleText'];
3110 $this->mLinks = $jsonData['Links'];
3111 $this->mLinksSpecial = $jsonData['LinksSpecial'];
3112 $this->mTemplates = $jsonData['Templates'];
3113 $this->mTemplateIds = $jsonData['TemplateIds'];
3114 $this->mImages = $jsonData['Images'];
3115 $this->mFileSearchOptions = $jsonData['FileSearchOptions'];
3116 $this->mExternalLinks = $jsonData['ExternalLinks'];
3117 $this->mInterwikiLinks = $jsonData['InterwikiLinks'];
3118 $this->mNewSection = $jsonData['NewSection'];
3119 $this->mHideNewSection = $jsonData['HideNewSection'];
3120 $this->mNoGallery = $jsonData['NoGallery'];
3121 $this->mHeadItems = $jsonData['HeadItems'];
3122 $this->mModuleSet = array_fill_keys( $jsonData['Modules'], true );
3123 $this->mModuleStyleSet = array_fill_keys( $jsonData['ModuleStyles'], true );
3124 $this->mJsConfigVars = $jsonData['JsConfigVars'];
3125 $this->mWarnings = $jsonData['Warnings'];
3126 $this->mFlags = $jsonData['Flags'];
3127 if (
3128 $jsonData['Sections'] !== [] ||
3129 // backward-compatibility: distinguish "no sections" from
3130 // "sections not set" (Will be unnecessary after T327439.)
3131 $this->getOutputFlag( 'mw:toc-set' )
3133 $this->setSections( $jsonData['Sections'] );
3134 unset( $this->mFlags['mw:toc-set'] );
3135 if ( isset( $jsonData['TOCExtensionData'] ) ) {
3136 $tocData = $this->getTOCData(); // created by setSections() above
3137 foreach ( $jsonData['TOCExtensionData'] as $key => $value ) {
3138 $tocData->setExtensionData( $key, $value );
3142 $this->mProperties = self::detectAndDecodeBinary( $jsonData['Properties'] );
3143 $this->mTimestamp = $jsonData['Timestamp'];
3144 $this->mEnableOOUI = $jsonData['EnableOOUI'];
3145 $this->setIndexPolicy( $jsonData['IndexPolicy'] );
3146 $this->mExtensionData = $jsonData['ExtensionData'] ?? [];
3147 $this->mLimitReportData = $jsonData['LimitReportData'];
3148 $this->mLimitReportJSData = $jsonData['LimitReportJSData'];
3149 $this->mCacheMessage = $jsonData['CacheMessage'] ?? '';
3150 $this->mParseStartTime = []; // invalid after reloading
3151 $this->mTimeProfile = $jsonData['TimeProfile'] ?? [];
3152 $this->mPreventClickjacking = $jsonData['PreventClickjacking'];
3153 $this->mExtraScriptSrcs = $jsonData['ExtraScriptSrcs'];
3154 $this->mExtraDefaultSrcs = $jsonData['ExtraDefaultSrcs'];
3155 $this->mExtraStyleSrcs = $jsonData['ExtraStyleSrcs'];
3156 $this->mSpeculativeRevId = $jsonData['SpeculativeRevId'];
3157 $this->speculativePageIdUsed = $jsonData['SpeculativePageIdUsed'];
3158 $this->revisionTimestampUsed = $jsonData['RevisionTimestampUsed'];
3159 $this->revisionUsedSha1Base36 = $jsonData['RevisionUsedSha1Base36'];
3160 $this->mWrapperDivClasses = $jsonData['WrapperDivClasses'];
3161 $this->mMaxAdaptiveExpiry = $jsonData['MaxAdaptiveExpiry'] ?? INF;
3165 * Finds any non-utf8 strings in the given array and replaces them with
3166 * an associative array that wraps a base64 encoded version of the data.
3167 * Inverse of detectAndDecodeBinary().
3169 * @param array $properties
3171 * @return array
3173 private static function detectAndEncodeBinary( array $properties ) {
3174 foreach ( $properties as $key => $value ) {
3175 if ( is_string( $value ) ) {
3176 if ( !mb_detect_encoding( $value, 'UTF-8', true ) ) {
3177 $properties[$key] = [
3178 // T313818: This key name conflicts with JsonCodec
3179 '_type_' => 'string',
3180 '_encoding_' => 'base64',
3181 '_data_' => base64_encode( $value ),
3187 return $properties;
3191 * Finds any associative arrays that represent encoded binary strings, and
3192 * replaces them with the decoded binary data.
3194 * @param array $properties
3196 * @return array
3198 private static function detectAndDecodeBinary( array $properties ) {
3199 foreach ( $properties as $key => $value ) {
3200 if ( is_array( $value ) && isset( $value['_encoding_'] ) ) {
3201 if ( $value['_encoding_'] === 'base64' ) {
3202 $properties[$key] = base64_decode( $value['_data_'] );
3207 return $properties;
3210 public function __wakeup() {
3211 $oldAliases = [
3212 // This was the pre-namespace name of the class, which is still
3213 // used in pre-1.42 serialized objects.
3214 'ParserOutput',
3216 // Backwards compatibility, pre 1.36
3217 $priorAccessedOptions = $this->getGhostFieldValue( 'mAccessedOptions', ...$oldAliases );
3218 if ( $priorAccessedOptions ) {
3219 $this->mParseUsedOptions = $priorAccessedOptions;
3221 // Backwards compatibility, pre 1.39
3222 $priorIndexPolicy = $this->getGhostFieldValue( 'mIndexPolicy', ...$oldAliases );
3223 if ( $priorIndexPolicy ) {
3224 $this->setIndexPolicy( $priorIndexPolicy );
3226 // Backwards compatibility, pre 1.40
3227 $mSections = $this->getGhostFieldValue( 'mSections', ...$oldAliases );
3228 if ( $mSections !== null && $mSections !== [] ) {
3229 $this->setSections( $mSections );
3231 // Backwards compatibility, pre 1.42
3232 $mModules = $this->getGhostFieldValue( 'mModules', ...$oldAliases );
3233 if ( $mModules !== null && $mModules !== [] ) {
3234 $this->addModules( $mModules );
3236 // Backwards compatibility, pre 1.42
3237 $mModuleStyles = $this->getGhostFieldValue( 'mModuleStyles', ...$oldAliases );
3238 if ( $mModuleStyles !== null && $mModuleStyles !== [] ) {
3239 $this->addModuleStyles( $mModuleStyles );
3241 // Backwards compatibility, pre 1.42
3242 $mText = $this->getGhostFieldValue( 'mText', ...$oldAliases );
3243 if ( $mText !== null ) {
3244 $this->setRawText( $mText );
3246 // Backwards compatibility, pre 1.42
3247 $ll = $this->getGhostFieldValue( 'mLanguageLinks', ...$oldAliases );
3248 if ( $ll !== null && $ll !== [] ) {
3249 foreach ( $ll as $l ) {
3250 $this->addLanguageLink( $l );
3253 // Backward compatibility with private fields, pre 1.42
3254 $oldPrivateFields = [
3255 'mRawText',
3256 'mCategories',
3257 'mIndicators',
3258 'mTitleText',
3259 'mLinks',
3260 'mLinksSpecial',
3261 'mTemplates',
3262 'mTemplateIds',
3263 'mImages',
3264 'mFileSearchOptions',
3265 'mExternalLinks',
3266 'mInterwikiLinks',
3267 'mNewSection',
3268 'mHideNewSection',
3269 'mNoGallery',
3270 'mHeadItems',
3271 'mModuleSet',
3272 'mModuleStyleSet',
3273 'mJsConfigVars',
3274 'mWarnings',
3275 'mWarningMsgs',
3276 'mTOCData',
3277 'mProperties',
3278 'mTimestamp',
3279 'mEnableOOUI',
3280 'mIndexSet',
3281 'mNoIndexSet',
3282 'mExtensionData',
3283 'mLimitReportData',
3284 'mLimitReportJSData',
3285 'mCacheMessage',
3286 'mParseStartTime',
3287 'mTimeProfile',
3288 'mPreventClickjacking',
3289 'mExtraScriptSrcs',
3290 'mExtraDefaultSrcs',
3291 'mExtraStyleSrcs',
3292 'mFlags',
3293 'mSpeculativeRevId',
3294 'speculativePageIdUsed',
3295 'revisionTimestampUsed',
3296 'revisionUsedSha1Base36',
3297 'mWrapperDivClasses',
3298 'mMaxAdaptiveExpiry',
3300 foreach ( $oldPrivateFields as $f ) {
3301 $this->restoreAliasedGhostField( $f, ...$oldAliases );
3303 $this->clearParseStartTime();
3306 public function __clone() {
3307 // It seems that very little of this object needs to be explicitly deep-cloned
3308 // while keeping copies reasonably separated.
3309 // Most of the non-scalar properties of this object are either
3310 // - (potentially multi-nested) arrays of scalars (which get deep-cloned), or
3311 // - arrays that may contain arbitrary elements (which don't necessarily get
3312 // deep-cloned), but for which no particular care elsewhere is given to
3313 // copying their references around (e.g. mJsConfigVars).
3314 // Hence, we are not going out of our way to ensure that the references to innermost
3315 // objects that may appear in a ParserOutput are unique. If that becomes the
3316 // expectation at any point, this method will require updating as well.
3317 // The exception is TOCData (which is an object), which we clone explicitly.
3318 if ( $this->mTOCData ) {
3319 $this->mTOCData = clone $this->mTOCData;
3324 * Returns the content holder text of the ParserOutput.
3325 * This will eventually be replaced by something like getContentHolder()->getText() when we have a
3326 * ContentHolder/HtmlHolder class.
3327 * @internal
3328 * @unstable
3329 * @return string
3331 public function getContentHolderText(): string {
3332 return $this->getRawText();
3336 * Sets the content holder text of the ParserOutput.
3337 * This will eventually be replaced by something like getContentHolder()->setText() when we have a
3338 * ContentHolder/HtmlHolder class.
3339 * @internal
3340 * @unstable
3342 public function setContentHolderText( string $s ): void {
3343 $this->setRawText( $s );
3346 public function __get( $name ) {
3347 if ( property_exists( get_called_class(), $name ) ) {
3348 // Direct access to a public property, deprecated.
3349 wfDeprecatedMsg( "ParserOutput::{$name} public read access deprecated", '1.38' );
3350 return $this->$name;
3351 } elseif ( property_exists( $this, $name ) ) {
3352 // Dynamic property access, deprecated.
3353 wfDeprecatedMsg( "ParserOutput::{$name} dynamic property read access deprecated", '1.38' );
3354 return $this->$name;
3355 } else {
3356 trigger_error( "Inaccessible property via __get(): $name" );
3357 return null;
3361 public function __set( $name, $value ) {
3362 if ( property_exists( get_called_class(), $name ) ) {
3363 // Direct access to a public property, deprecated.
3364 wfDeprecatedMsg( "ParserOutput::$name public write access deprecated", '1.38' );
3365 $this->$name = $value;
3366 } else {
3367 // Dynamic property access, deprecated.
3368 wfDeprecatedMsg( "ParserOutput::$name dynamic property write access deprecated", '1.38' );
3369 $this->$name = $value;
3374 /** @deprecated class alias since 1.42 */
3375 class_alias( ParserOutput::class, 'ParserOutput' );