Update Codex from v1.20.0 to v1.20.1
[mediawiki.git] / includes / parser / ParserOutput.php
blob3ce3075f3c6cce9e66f3f0f55d60cca4bcc03837
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 public function getWrapperDivClass(): string {
587 return implode( ' ', array_keys( $this->mWrapperDivClasses ) );
591 * @param int $id
592 * @since 1.28
594 public function setSpeculativeRevIdUsed( $id ): void {
595 $this->mSpeculativeRevId = $id;
599 * @return int|null
600 * @since 1.28
602 public function getSpeculativeRevIdUsed(): ?int {
603 return $this->mSpeculativeRevId;
607 * @param int $id
608 * @since 1.34
610 public function setSpeculativePageIdUsed( $id ): void {
611 $this->speculativePageIdUsed = $id;
615 * @return int|null
616 * @since 1.34
618 public function getSpeculativePageIdUsed() {
619 return $this->speculativePageIdUsed;
623 * @param string $timestamp TS_MW timestamp
624 * @since 1.34
626 public function setRevisionTimestampUsed( $timestamp ): void {
627 $this->revisionTimestampUsed = $timestamp;
631 * @return string|null TS_MW timestamp or null if not used
632 * @since 1.34
634 public function getRevisionTimestampUsed() {
635 return $this->revisionTimestampUsed;
639 * @param string $hash Lowercase SHA-1 base 36 hash
640 * @since 1.34
642 public function setRevisionUsedSha1Base36( $hash ): void {
643 if ( $hash === null ) {
644 return; // e.g. RevisionRecord::getSha1() returned null
647 if (
648 $this->revisionUsedSha1Base36 !== null &&
649 $this->revisionUsedSha1Base36 !== $hash
651 $this->revisionUsedSha1Base36 = ''; // mismatched
652 } else {
653 $this->revisionUsedSha1Base36 = $hash;
658 * @return string|null Lowercase SHA-1 base 36 hash, null if unused, or "" on inconsistency
659 * @since 1.34
661 public function getRevisionUsedSha1Base36() {
662 return $this->revisionUsedSha1Base36;
666 * @return string[]
667 * @note Before 1.43, this function returned an array reference.
668 * @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::LANGUAGE)
670 public function getLanguageLinks() {
671 $result = [];
672 foreach ( $this->mLanguageLinkMap as $lang => $title ) {
673 // T374736: Back-compat with empty prefix; see ::addLanguageLink()
674 $result[] = $title === '|' ? "$lang" : "$lang:$title";
676 return $result;
679 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::INTERWIKI) */
680 public function getInterwikiLinks() {
681 return $this->mInterwikiLinks;
685 * Return the names of the categories on this page.
686 * Unlike ::getCategories(), sort keys are *not* included in the
687 * return value.
688 * @return array<string> The names of the categories
689 * @since 1.38
691 public function getCategoryNames(): array {
692 # Note that numeric category names get converted to 'int' when
693 # stored as array keys; stringify the keys to ensure they
694 # return to original string form so as not to confuse callers.
695 return array_map( 'strval', array_keys( $this->mCategories ) );
699 * Return category names and sort keys as a map.
701 * BEWARE that numeric category names get converted to 'int' when stored
702 * as array keys. Because of this, use of this method is not recommended
703 * in new code; using ::getCategoryNames() and ::getCategorySortKey() will
704 * be less error-prone.
706 * @return array<string|int,string>
707 * @internal
709 public function getCategoryMap(): array {
710 return $this->mCategories;
714 * Return the sort key for a given category name, or `null` if the
715 * category is not present in this ParserOutput. Returns the
716 * empty string if the category is to use the default sort key.
718 * @note The effective sort key in the database may vary from what
719 * is returned here; see note in ParserOutput::addCategory().
721 * @param string $name The category name
722 * @return ?string The sort key for the category, or `null` if the
723 * category is not present in this ParserOutput
724 * @since 1.40
726 public function getCategorySortKey( string $name ): ?string {
727 // This API avoids exposing the fact that numeric string category
728 // names are going to be converted to 'int' when used as array
729 // keys for the `mCategories` field.
730 return $this->mCategories[$name] ?? null;
734 * @return string[]
735 * @since 1.25
737 public function getIndicators(): array {
738 return $this->mIndicators;
741 public function getTitleText() {
742 return $this->mTitleText;
746 * @return ?TOCData the table of contents data, or null if it hasn't been
747 * set.
749 public function getTOCData(): ?TOCData {
750 return $this->mTOCData;
754 * @internal
755 * @return string
757 public function getCacheMessage(): string {
758 return $this->mCacheMessage;
762 * @internal
763 * @return array
765 public function getSections(): array {
766 if ( $this->mTOCData !== null ) {
767 return $this->mTOCData->toLegacy();
769 // For compatibility
770 return [];
774 * Get a list of links of the given type.
776 * Provides a uniform interface to various lists of links stored in
777 * the metadata.
779 * Each element of the returned array has a LinkTarget as the 'link'
780 * property. Local and template links also have 'pageid' set.
781 * Template links have 'revid' set. Category links have 'sort' set.
782 * Media links optionally have 'time' and 'sha1' set.
784 * @param string $linkType A link type, which should be a constant from
785 * ParserOutputLinkTypes.
786 * @return list<array{link:ParsoidLinkTarget,pageid?:int,revid?:int,sort?:string,time?:string|false,sha1?:string|false}>
788 public function getLinkList( string $linkType ): array {
789 # Note that fragments are dropped for everything except language links
790 $result = [];
791 switch ( $linkType ) {
792 case ParserOutputLinkTypes::CATEGORY:
793 foreach ( $this->mCategories as $dbkey => $sort ) {
794 $result[] = [
795 'link' => new TitleValue( NS_CATEGORY, (string)$dbkey ),
796 'sort' => $sort,
799 break;
801 case ParserOutputLinkTypes::INTERWIKI:
802 foreach ( $this->mInterwikiLinks as $prefix => $arr ) {
803 foreach ( $arr as $dbkey => $ignore ) {
804 $result[] = [
805 'link' => new TitleValue( NS_MAIN, (string)$dbkey, '', (string)$prefix ),
809 break;
811 case ParserOutputLinkTypes::LANGUAGE:
812 foreach ( $this->mLanguageLinkMap as $lang => $title ) {
813 if ( $title === '|' ) {
814 continue; // T374736
816 # language links can have fragments!
817 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
818 $result[] = [
819 'link' => new TitleValue( NS_MAIN, $title, $frag, (string)$lang ),
822 break;
824 case ParserOutputLinkTypes::LOCAL:
825 foreach ( $this->mLinks as $ns => $arr ) {
826 foreach ( $arr as $dbkey => $id ) {
827 $result[] = [
828 'link' => new TitleValue( $ns, (string)$dbkey ),
829 'pageid' => $id,
833 break;
835 case ParserOutputLinkTypes::MEDIA:
836 foreach ( $this->mImages as $dbkey => $ignore ) {
837 $extra = $this->mFileSearchOptions[$dbkey] ?? [];
838 $extra['link'] = new TitleValue( NS_FILE, (string)$dbkey );
839 $result[] = $extra;
841 break;
843 case ParserOutputLinkTypes::SPECIAL:
844 foreach ( $this->mLinksSpecial as $dbkey => $ignore ) {
845 $result[] = [
846 'link' => new TitleValue( NS_SPECIAL, (string)$dbkey ),
849 break;
851 case ParserOutputLinkTypes::TEMPLATE:
852 foreach ( $this->mTemplates as $ns => $arr ) {
853 foreach ( $arr as $dbkey => $pageid ) {
854 $result[] = [
855 'link' => new TitleValue( $ns, (string)$dbkey ),
856 'pageid' => $pageid,
857 // default to invalid/broken revision if this is not present
858 'revid' => $this->mTemplateIds[$ns][$dbkey] ?? 0,
862 break;
864 default:
865 throw new UnexpectedValueException( "Unknown link type $linkType" );
867 return $result;
870 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::LOCAL) */
871 public function &getLinks() {
872 return $this->mLinks;
876 * Return true if the given parser output has local links registered
877 * in the metadata.
878 * @return bool
879 * @since 1.44
881 public function hasLinks(): bool {
882 foreach ( $this->mLinks as $ns => $arr ) {
883 foreach ( $arr as $dbkey => $id ) {
884 return true;
887 return false;
891 * @return array Keys are DBKs for the links to special pages in the document
892 * @since 1.35
893 * @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::SPECIAL)
895 public function &getLinksSpecial() {
896 return $this->mLinksSpecial;
899 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::TEMPLATE) */
900 public function &getTemplates() {
901 return $this->mTemplates;
904 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::TEMPLATE) */
905 public function &getTemplateIds() {
906 return $this->mTemplateIds;
909 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::MEDIA) */
910 public function &getImages() {
911 return $this->mImages;
915 * Return true if there are image dependencies registered for this
916 * ParserOutput.
917 * @since 1.44
919 public function hasImages(): bool {
920 return $this->mImages !== [];
923 /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::MEDIA) */
924 public function &getFileSearchOptions() {
925 return $this->mFileSearchOptions;
929 * @note Use of the reference returned by this method has been
930 * deprecated since 1.43. In a future release this will return a
931 * normal array. Use ::addExternalLink() to modify the set of
932 * external links stored in this ParserOutput.
934 public function &getExternalLinks(): array {
935 return $this->mExternalLinks;
938 public function setNoGallery( $value ): void {
939 $this->mNoGallery = (bool)$value;
942 public function getNoGallery() {
943 return $this->mNoGallery;
946 public function getHeadItems() {
947 return $this->mHeadItems;
950 public function getModules() {
951 return array_keys( $this->mModuleSet );
954 public function getModuleStyles() {
955 return array_keys( $this->mModuleStyleSet );
959 * @param bool $showStrategyKeys Defaults to false; if set to true will
960 * expose the internal `MW_MERGE_STRATEGY_KEY` in the result. This
961 * should only be used internally to allow safe merge of config vars.
962 * @return array
963 * @since 1.23
965 public function getJsConfigVars( bool $showStrategyKeys = false ) {
966 $result = $this->mJsConfigVars;
967 // Don't expose the internal strategy key
968 foreach ( $result as &$value ) {
969 if ( is_array( $value ) && !$showStrategyKeys ) {
970 unset( $value[self::MW_MERGE_STRATEGY_KEY] );
973 return $result;
976 public function getWarnings(): array {
977 return array_keys( $this->mWarnings );
980 public function getIndexPolicy(): string {
981 // 'noindex' wins if both are set. (T16899)
982 if ( $this->mNoIndexSet ) {
983 return 'noindex';
984 } elseif ( $this->mIndexSet ) {
985 return 'index';
987 return '';
991 * @return string|null TS_MW timestamp of the revision content
993 public function getRevisionTimestamp(): ?string {
994 return $this->mTimestamp;
998 * @return string|null TS_MW timestamp of the revision content
999 * @deprecated since 1.42; use ::getRevisionTimestamp() instead
1001 public function getTimestamp() {
1002 return $this->getRevisionTimestamp();
1005 public function getLimitReportData() {
1006 return $this->mLimitReportData;
1009 public function getLimitReportJSData() {
1010 return $this->mLimitReportJSData;
1013 public function getEnableOOUI() {
1014 return $this->mEnableOOUI;
1018 * Get extra Content-Security-Policy 'default-src' directives
1019 * @since 1.35
1020 * @return string[]
1022 public function getExtraCSPDefaultSrcs() {
1023 return $this->mExtraDefaultSrcs;
1027 * Get extra Content-Security-Policy 'script-src' directives
1028 * @since 1.35
1029 * @return string[]
1031 public function getExtraCSPScriptSrcs() {
1032 return $this->mExtraScriptSrcs;
1036 * Get extra Content-Security-Policy 'style-src' directives
1037 * @since 1.35
1038 * @return string[]
1040 public function getExtraCSPStyleSrcs() {
1041 return $this->mExtraStyleSrcs;
1045 * Set the raw text of the ParserOutput.
1047 * If you did not generate html, pass null to mark it as such.
1049 * @since 1.42
1050 * @param string|null $text HTML content of ParserOutput or null if not generated
1051 * @param-taint $text exec_html
1053 public function setRawText( ?string $text ): void {
1054 $this->mRawText = $text;
1058 * Set the raw text of the ParserOutput.
1060 * If you did not generate html, pass null to mark it as such.
1062 * @since 1.39 You can now pass null to this function
1063 * @param string|null $text HTML content of ParserOutput or null if not generated
1064 * @param-taint $text exec_html
1065 * @return string|null Previous value of ParserOutput's raw text
1066 * @deprecated since 1.42; use ::setRawText() which matches the getter ::getRawText()
1068 public function setText( $text ) {
1069 return wfSetVar( $this->mRawText, $text, true );
1073 * @deprecated since 1.42, use ::addLanguageLink() instead.
1075 public function setLanguageLinks( $ll ) {
1076 $old = $this->getLanguageLinks();
1077 $this->mLanguageLinkMap = [];
1078 if ( $ll === null ) { // T376323
1079 wfDeprecated( __METHOD__ . ' with null argument', '1.43' );
1081 foreach ( ( $ll ?? [] ) as $l ) {
1082 $this->addLanguageLink( $l );
1084 return $old;
1087 public function setTitleText( $t ) {
1088 return wfSetVar( $this->mTitleText, $t );
1092 * @param TOCData $tocData Table of contents data for the page
1094 public function setTOCData( TOCData $tocData ): void {
1095 $this->mTOCData = $tocData;
1099 * @param array $sectionArray
1100 * @return array Previous value of ::getSections()
1102 public function setSections( array $sectionArray ) {
1103 $oldValue = $this->getSections();
1104 $this->setTOCData( TOCData::fromLegacy( $sectionArray ) );
1105 return $oldValue;
1109 * Update the index policy of the robots meta tag.
1111 * Note that calling this method does not guarantee
1112 * that {@link self::getIndexPolicy()} will return the given policy –
1113 * if different calls set the index policy to 'index' and 'noindex',
1114 * then 'noindex' always wins (T16899), even if the 'index' call happened later.
1115 * If this is not what you want,
1116 * you can reset {@link ParserOutputFlags::NO_INDEX_POLICY} with {@link self::setOutputFlag()}.
1118 * @param string $policy 'index' or 'noindex'.
1119 * @return string The previous policy.
1121 public function setIndexPolicy( $policy ): string {
1122 $old = $this->getIndexPolicy();
1123 if ( $policy === 'noindex' ) {
1124 $this->mNoIndexSet = true;
1125 } elseif ( $policy === 'index' ) {
1126 $this->mIndexSet = true;
1128 return $old;
1132 * @param ?string $timestamp TS_MW timestamp of the revision content
1134 public function setRevisionTimestamp( ?string $timestamp ): void {
1135 $this->mTimestamp = $timestamp;
1139 * @param ?string $timestamp TS_MW timestamp of the revision content
1141 * @return ?string The previous value of the timestamp
1142 * @deprecated since 1.42; use ::setRevisionTimestamp() instead
1144 public function setTimestamp( $timestamp ) {
1145 return wfSetVar( $this->mTimestamp, $timestamp );
1149 * Add a category.
1151 * Although ParserOutput::getCategorySortKey() will return exactly
1152 * the sort key you specify here, before storing in the database
1153 * all sort keys will be language converted, HTML entities will be
1154 * decoded, newlines stripped, and then they will be truncated to
1155 * 255 bytes. Thus the "effective" sort key in the DB may be different
1156 * from what is passed to `$sort` here and returned by
1157 * ::getCategorySortKey().
1159 * @param string|ParsoidLinkTarget $c The category name
1160 * @param string $sort The sort key; an empty string indicates
1161 * that the default sort key for the page should be used.
1163 public function addCategory( $c, $sort = '' ): void {
1164 if ( $c instanceof ParsoidLinkTarget ) {
1165 $c = $c->getDBkey();
1167 $this->mCategories[$c] = $sort;
1171 * Overwrite the category map.
1172 * @param array<string,string> $c Map of category names to sort keys
1173 * @since 1.38
1175 public function setCategories( array $c ): void {
1176 $this->mCategories = $c;
1180 * @param string $id
1181 * @param string $content
1182 * @param-taint $content exec_html
1183 * @since 1.25
1185 public function setIndicator( $id, $content ): void {
1186 $this->mIndicators[$id] = $content;
1190 * Enables OOUI, if true, in any OutputPage instance this ParserOutput
1191 * object is added to.
1193 * @since 1.26
1194 * @param bool $enable If OOUI should be enabled or not
1196 public function setEnableOOUI( bool $enable = false ): void {
1197 $this->mEnableOOUI = $enable;
1201 * Add a language link.
1202 * @param ParsoidLinkTarget|string $t
1204 public function addLanguageLink( $t ): void {
1205 # Note that fragments are preserved
1206 if ( $t instanceof ParsoidLinkTarget ) {
1207 // Language links are unusual in using 'text' rather than 'db key'
1208 // Note that fragments are preserved.
1209 $lang = $t->getInterwiki();
1210 $title = $t->getText();
1211 if ( $t->hasFragment() ) {
1212 $title .= '#' . $t->getFragment();
1214 } else {
1215 [ $lang, $title ] = array_pad( explode( ':', $t, 2 ), -2, '' );
1217 if ( $lang === '' ) {
1218 // T374736: For backward compatibility with test cases only!
1219 wfDeprecated( __METHOD__ . ' without prefix', '1.43' );
1220 [ $lang, $title ] = [ $title, '|' ]; // | can not occur in valid title
1222 $this->mLanguageLinkMap[$lang] ??= $title;
1226 * Add a warning to the output for this page.
1227 * @param MessageValue $mv
1228 * @since 1.43
1230 public function addWarningMsgVal( MessageValue $mv ) {
1231 // These can eventually be stored as MessageValue directly.
1232 $this->addWarningMsg( $mv->getKey(), ...$mv->getParams() );
1236 * Add a warning to the output for this page.
1237 * @param string $msg The localization message key for the warning
1238 * @param mixed|JsonDeserializable ...$args Optional arguments for the
1239 * message. These arguments must be serializable/deserializable with
1240 * JsonCodec; see the @note on ParserOutput::setExtensionData()
1241 * @since 1.38
1243 public function addWarningMsg( string $msg, ...$args ): void {
1244 // MessageValue objects are defined in core and thus not visible
1245 // to Parsoid or to its ContentMetadataCollector interface.
1246 // Eventually this method (defined in ContentMetadataCollector) should
1247 // call ::addWarningMsgVal() instead of the other way around.
1249 // preserve original arguments in $mWarningMsgs to allow merge
1250 // @todo: these aren't serialized/deserialized yet -- before we
1251 // turn on serialization of $this->mWarningMsgs we need to ensure
1252 // callers aren't passing nonserializable arguments: T343048.
1253 $jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
1254 $path = $jsonCodec->detectNonSerializableData( $args, true );
1255 if ( $path !== null ) {
1256 wfDeprecatedMsg(
1257 "ParserOutput::addWarningMsg() called with nonserializable arguments: $path",
1258 '1.41'
1261 $this->mWarningMsgs[$msg] = $args;
1262 $s = wfMessage( $msg, ...$args )
1263 // some callers set the title here?
1264 ->inContentLanguage() // because this ends up in cache
1265 ->text();
1266 $this->mWarnings[$s] = 1;
1269 public function setNewSection( $value ): void {
1270 $this->mNewSection = (bool)$value;
1274 * @param bool $value Hide the new section link?
1276 public function setHideNewSection( bool $value ): void {
1277 $this->mHideNewSection = $value;
1280 public function getHideNewSection(): bool {
1281 return (bool)$this->mHideNewSection;
1284 public function getNewSection(): bool {
1285 return (bool)$this->mNewSection;
1289 * Checks, if a url is pointing to the own server
1291 * @param string $internal The server to check against
1292 * @param string $url The url to check
1293 * @return bool
1294 * @internal
1296 public static function isLinkInternal( $internal, $url ): bool {
1297 return (bool)preg_match( '/^' .
1298 # If server is proto relative, check also for http/https links
1299 ( substr( $internal, 0, 2 ) === '//' ? '(?:https?:)?' : '' ) .
1300 preg_quote( $internal, '/' ) .
1301 # check for query/path/anchor or end of link in each case
1302 '(?:[\?\/\#]|$)/i',
1303 $url
1307 public function addExternalLink( $url ): void {
1308 # We don't register links pointing to our own server, unless... :-)
1309 $config = MediaWikiServices::getInstance()->getMainConfig();
1310 $server = $config->get( MainConfigNames::Server );
1311 $registerInternalExternals = $config->get( MainConfigNames::RegisterInternalExternals );
1312 # Replace unnecessary URL escape codes with the referenced character
1313 # This prevents spammers from hiding links from the filters
1314 $url = Parser::normalizeLinkUrl( $url );
1316 $registerExternalLink = true;
1317 if ( !$registerInternalExternals ) {
1318 $registerExternalLink = !self::isLinkInternal( $server, $url );
1320 if ( $registerExternalLink ) {
1321 $this->mExternalLinks[$url] = 1;
1326 * Record a local or interwiki inline link for saving in future link tables.
1328 * @param ParsoidLinkTarget $link (used to require Title until 1.38)
1329 * @param int|null $id Optional known page_id so we can skip the lookup
1331 public function addLink( ParsoidLinkTarget $link, $id = null ): void {
1332 if ( $link->isExternal() ) {
1333 // Don't record interwikis in pagelinks
1334 $this->addInterwikiLink( $link );
1335 return;
1337 $ns = $link->getNamespace();
1338 $dbk = $link->getDBkey();
1339 if ( $ns === NS_MEDIA ) {
1340 // Normalize this pseudo-alias if it makes it down here...
1341 $ns = NS_FILE;
1342 } elseif ( $ns === NS_SPECIAL ) {
1343 // We don't want to record Special: links in the database, so put them in a separate place.
1344 // It might actually be wise to, but we'd need to do some normalization.
1345 $this->mLinksSpecial[$dbk] = 1;
1346 return;
1347 } elseif ( $dbk === '' ) {
1348 // Don't record self links - [[#Foo]]
1349 return;
1351 if ( $id === null ) {
1352 // T357048: This actually kills performance; we should batch these.
1353 $page = MediaWikiServices::getInstance()->getPageStore()->getPageForLink( $link );
1354 $id = $page->getId();
1356 $this->mLinks[$ns][$dbk] = $id;
1360 * Register a file dependency for this output
1361 * @param string|ParsoidLinkTarget $name Title dbKey
1362 * @param string|false|null $timestamp MW timestamp of file creation (or false if non-existing)
1363 * @param string|false|null $sha1 Base 36 SHA-1 of file (or false if non-existing)
1365 public function addImage( $name, $timestamp = null, $sha1 = null ): void {
1366 if ( $name instanceof ParsoidLinkTarget ) {
1367 $name = $name->getDBkey();
1369 $this->mImages[$name] = 1;
1370 if ( $timestamp !== null && $sha1 !== null ) {
1371 $this->mFileSearchOptions[$name] = [ 'time' => $timestamp, 'sha1' => $sha1 ];
1376 * Register a template dependency for this output
1378 * @param ParsoidLinkTarget $link (used to require Title until 1.38)
1379 * @param int $page_id
1380 * @param int $rev_id
1382 public function addTemplate( $link, $page_id, $rev_id ): void {
1383 if ( $link->isExternal() ) {
1384 // Will throw an InvalidArgumentException in a future release.
1385 wfDeprecated( __METHOD__ . " with interwiki link", '1.42' );
1386 return;
1388 $ns = $link->getNamespace();
1389 $dbk = $link->getDBkey();
1390 // T357048: Parsoid doesn't have page_id
1391 $this->mTemplates[$ns][$dbk] = $page_id;
1392 $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
1396 * @param ParsoidLinkTarget $link must be an interwiki link
1397 * (used to require Title until 1.38).
1399 public function addInterwikiLink( $link ): void {
1400 if ( !$link->isExternal() ) {
1401 throw new InvalidArgumentException( 'Non-interwiki link passed, internal parser error.' );
1403 $prefix = $link->getInterwiki();
1404 $this->mInterwikiLinks[$prefix][$link->getDBkey()] = 1;
1408 * Add some text to the "<head>".
1409 * If $tag is set, the section with that tag will only be included once
1410 * in a given page.
1411 * @param string $section
1412 * @param string|false $tag
1414 public function addHeadItem( $section, $tag = false ): void {
1415 if ( $tag !== false ) {
1416 $this->mHeadItems[$tag] = $section;
1417 } else {
1418 $this->mHeadItems[] = $section;
1423 * @see OutputPage::addModules
1424 * @param string[] $modules
1426 public function addModules( array $modules ): void {
1427 $modules = array_fill_keys( $modules, true );
1428 $this->mModuleSet = array_merge( $this->mModuleSet, $modules );
1432 * @see OutputPage::addModuleStyles
1433 * @param string[] $modules
1435 public function addModuleStyles( array $modules ): void {
1436 $modules = array_fill_keys( $modules, true );
1437 $this->mModuleStyleSet = array_merge( $this->mModuleStyleSet, $modules );
1441 * Add one or more variables to be set in mw.config in JavaScript.
1443 * @param string|array $keys Key or array of key/value pairs.
1444 * @param mixed|null $value [optional] Value of the configuration variable.
1445 * @since 1.23
1446 * @deprecated since 1.38, use ::setJsConfigVar() or ::appendJsConfigVar()
1447 * which ensures compatibility with asynchronous parsing; emitting warnings
1448 * since 1.43.
1450 public function addJsConfigVars( $keys, $value = null ): void {
1451 wfDeprecated( __METHOD__, '1.38' );
1452 if ( is_array( $keys ) ) {
1453 foreach ( $keys as $key => $value ) {
1454 $this->mJsConfigVars[$key] = $value;
1456 return;
1459 $this->mJsConfigVars[$keys] = $value;
1463 * Add a variable to be set in mw.config in JavaScript.
1465 * In order to ensure the result is independent of the parse order, the values
1466 * set here must be unique -- that is, you can pass the same $key
1467 * multiple times but ONLY if the $value is identical each time.
1468 * If you want to collect multiple pieces of data under a single key,
1469 * use ::appendJsConfigVar().
1471 * @param string $key Key to use under mw.config
1472 * @param mixed|null $value Value of the configuration variable.
1473 * @since 1.38
1475 public function setJsConfigVar( string $key, $value ): void {
1476 if (
1477 array_key_exists( $key, $this->mJsConfigVars ) &&
1478 $this->mJsConfigVars[$key] !== $value
1480 // Ensure that a key is mapped to only a single value in order
1481 // to prevent the resulting array from varying if content
1482 // is parsed in a different order.
1483 throw new InvalidArgumentException( "Multiple conflicting values given for $key" );
1485 $this->mJsConfigVars[$key] = $value;
1489 * Append a value to a variable to be set in mw.config in JavaScript.
1491 * In order to ensure the result is independent of the parse order,
1492 * the value of this key will be an associative array, mapping all of
1493 * the values set under that key to true. (The array is implicitly
1494 * ordered in PHP, but you should treat it as unordered.)
1495 * If you want a non-array type for the key, and can ensure that only
1496 * a single value will be set, you should use ::setJsConfigVar() instead.
1498 * @param string $key Key to use under mw.config
1499 * @param string $value Value to append to the configuration variable.
1500 * @param string $strategy Merge strategy:
1501 * only MW_MERGE_STRATEGY_UNION is currently supported and external callers
1502 * should treat this parameter as @internal at this time and omit it.
1503 * @since 1.38
1505 public function appendJsConfigVar(
1506 string $key,
1507 string $value,
1508 string $strategy = self::MW_MERGE_STRATEGY_UNION
1509 ): void {
1510 if ( $strategy !== self::MW_MERGE_STRATEGY_UNION ) {
1511 throw new InvalidArgumentException( "Unknown merge strategy $strategy." );
1513 if ( !array_key_exists( $key, $this->mJsConfigVars ) ) {
1514 $this->mJsConfigVars[$key] = [
1515 // Indicate how these values are to be merged.
1516 self::MW_MERGE_STRATEGY_KEY => $strategy,
1518 } elseif ( !is_array( $this->mJsConfigVars[$key] ) ) {
1519 throw new InvalidArgumentException( "Mixing set and append for $key" );
1520 } elseif ( ( $this->mJsConfigVars[$key][self::MW_MERGE_STRATEGY_KEY] ?? null ) !== $strategy ) {
1521 throw new InvalidArgumentException( "Conflicting merge strategies for $key" );
1523 $this->mJsConfigVars[$key][$value] = true;
1527 * Accommodate very basic transcluding of a temporary OutputPage object into parser output.
1529 * This is a fragile method that cannot be relied upon in any meaningful way.
1530 * It exists solely to support the wikitext feature of transcluding a SpecialPage, and
1531 * only has to work for that use case to ensure relevant styles are loaded, and that
1532 * essential config vars needed between SpecialPage and a JS feature are added.
1534 * This relies on there being no overlap between modules or config vars added by
1535 * the SpecialPage and those added by parser extensions. If there is overlap,
1536 * then arise and break one or both sides. This is expected and unsupported.
1538 * @internal For use by Parser for basic special page transclusion
1539 * @param OutputPage $out
1541 public function addOutputPageMetadata( OutputPage $out ): void {
1542 // This should eventually use the same merge mechanism used
1543 // internally to merge ParserOutputs together.
1544 // (ie: $this->mergeHtmlMetaDataFrom( $out->getMetadata() )
1545 // once preventClickjacking, moduleStyles, modules, jsconfigvars,
1546 // and head items are moved to OutputPage::$metadata)
1548 // Take the strictest click-jacking policy. This is to ensure any one-click features
1549 // such as patrol or rollback on the transcluded special page will result in the wiki page
1550 // disallowing embedding in cross-origin iframes. Articles are generally allowed to be
1551 // embedded. Pages that transclude special pages are expected to be user pages or
1552 // other non-content pages that content re-users won't discover or care about.
1553 $this->mPreventClickjacking = $this->mPreventClickjacking || $out->getPreventClickjacking();
1555 $this->addModuleStyles( $out->getModuleStyles() );
1557 // TODO: Figure out if style modules suffice, or whether the below is needed as well.
1558 // Are there special pages that permit transcluding/including and also have JS modules
1559 // that should be activate on the host page?
1560 $this->addModules( $out->getModules() );
1561 $this->mJsConfigVars = self::mergeMapStrategy(
1562 $this->mJsConfigVars, $out->getJsConfigVars()
1564 $this->mHeadItems = array_merge( $this->mHeadItems, $out->getHeadItemsArray() );
1568 * Override the title to be used for display
1570 * @note this is assumed to have been validated
1571 * (check equal normalisation, etc.)
1573 * @note this is expected to be safe HTML,
1574 * ready to be served to the client.
1576 * @param string $text Desired title text
1578 public function setDisplayTitle( $text ): void {
1579 $this->setTitleText( $text );
1580 $this->setPageProperty( 'displaytitle', $text );
1584 * Get the title to be used for display.
1586 * As per the contract of setDisplayTitle(), this is safe HTML,
1587 * ready to be served to the client.
1589 * @return string|false HTML
1591 public function getDisplayTitle() {
1592 $t = $this->getTitleText();
1593 if ( $t === '' ) {
1594 return false;
1596 return $t;
1600 * Get the primary language code of the output.
1602 * This returns the primary language of the output, including
1603 * any LanguageConverter variant applied.
1605 * NOTE: This may differ from the wiki's default content language
1606 * ($wgLanguageCode, MediaWikiServices::getContentLanguage), because
1607 * each page may have its own "page language" set (PageStoreRecord,
1608 * Title::getDbPageLanguageCode, ContentHandler::getPageLanguage).
1610 * NOTE: This may differ from the "page language" when parsing
1611 * user interface messages, in which case this reflects the user
1612 * language (including any variant preference).
1614 * NOTE: This may differ from the Parser's "target language" that was
1615 * set while the Parser was parsing the page, because the final output
1616 * is converted to the current user's preferred LanguageConverter variant
1617 * (assuming this is a variant of the target language).
1618 * See Parser::getTargetLanguageConverter()->getPreferredVariant(); use
1619 * LanguageFactory::getParentLanguage() on the language code to obtain
1620 * the base language code. LanguageConverter::getPreferredVariant()
1621 * depends on the global RequestContext for the URL and the User
1622 * language preference.
1624 * Finally, note that a single ParserOutput object may contain
1625 * HTML content in multiple different languages and directions
1626 * (T114640). Authors of wikitext and of parser extensions are
1627 * expected to mark such subtrees with a `lang` attribute (set to
1628 * a BCP-47 value, see Language::toBcp47Code()) and a corresponding
1629 * `dir` attribute (see Language::getDir()). This method returns
1630 * the language code for wrapper of the HTML content.
1632 * @see Parser::internalParseHalfParsed
1633 * @since 1.40
1634 * @return ?Bcp47Code The primary language for this output,
1635 * or `null` if a language was not set.
1637 public function getLanguage(): ?Bcp47Code {
1638 // This information is temporarily stored in extension data (T303329)
1639 $code = $this->getExtensionData( 'core:target-lang-variant' );
1640 // This is null if the ParserOutput was cached by MW 1.40 or earlier,
1641 // or not constructed by Parser/ParserCache.
1642 return $code === null ? null : new Bcp47CodeValue( $code );
1646 * Set the primary language of the output.
1648 * See the discussion and caveats in ::getLanguage().
1650 * @param Bcp47Code $lang The primary language for this output, including
1651 * any variant specification.
1652 * @since 1.40
1654 public function setLanguage( Bcp47Code $lang ): void {
1655 $this->setExtensionData( 'core:target-lang-variant', $lang->toBcp47Code() );
1659 * Return an HTML prefix to be applied on redirect pages, or null
1660 * if this is not a redirect.
1661 * @return ?string HTML to prepend to redirect pages, or null
1662 * @internal
1664 public function getRedirectHeader(): ?string {
1665 return $this->getExtensionData( 'core:redirect-header' );
1669 * Set an HTML prefix to be applied on redirect pages.
1670 * @param string $html HTML to prepend to redirect pages
1672 public function setRedirectHeader( string $html ): void {
1673 $this->setExtensionData( 'core:redirect-header', $html );
1677 * Store a unique rendering id for this ParserOutput. This is used
1678 * whenever a client needs to record a dependency on a specific parse.
1679 * It is typically set only when a parser output is cached.
1681 * @param string $renderId a UUID identifying a specific parse
1682 * @internal
1684 public function setRenderId( string $renderId ): void {
1685 $this->setExtensionData( 'core:render-id', $renderId );
1689 * Return the unique rendering id for this ParserOutput. This is used
1690 * whenever a client needs to record a dependency on a specific parse.
1692 * @return string|null
1693 * @internal
1695 public function getRenderId(): ?string {
1696 // Backward-compatibility with old cache contents
1697 // Can be removed after parser cache contents have expired
1698 $old = $this->getExtensionData( 'parsoid-render-id' );
1699 if ( $old !== null ) {
1700 return ParsoidRenderId::newFromKey( $old )->getUniqueID();
1702 return $this->getExtensionData( 'core:render-id' );
1706 * @return string[] List of flags signifying special cases
1707 * @internal
1709 public function getAllFlags(): array {
1710 return array_keys( $this->mFlags );
1714 * Set a page property to be stored in the page_props database table.
1716 * page_props is a key-value store indexed by the page ID. This allows
1717 * the parser to set a property on a page which can then be quickly
1718 * retrieved given the page ID or via a DB join when given the page
1719 * title.
1721 * Since 1.23, page_props are also indexed by numeric value, to allow
1722 * for efficient "top k" queries of pages wrt a given property.
1723 * This only works if the value is passed as a int, float, or
1724 * bool. Since 1.42 you should use ::setNumericPageProperty()
1725 * if you want your page property value to be indexed, which will ensure
1726 * that the value is of the proper type.
1728 * setPageProperty() is thus used to propagate properties from the parsed
1729 * page to request contexts other than a page view of the currently parsed
1730 * article.
1732 * Some applications examples:
1734 * * To implement hidden categories, hiding pages from category listings
1735 * by storing a page property.
1737 * * Overriding the displayed article title (ParserOutput::setDisplayTitle()).
1739 * * To implement image tagging, for example displaying an icon on an
1740 * image thumbnail to indicate that it is listed for deletion on
1741 * Wikimedia Commons.
1742 * This is not actually implemented, yet but would be pretty cool.
1744 * @note Use of non-scalar values (anything other than
1745 * `string|int|float|bool`) has been deprecated in 1.42.
1746 * Although any JSON-serializable value can be stored/fetched in
1747 * ParserOutput, when the values are stored to the database
1748 * (in `deferred/LinksUpdate/PagePropsTable.php`) they will be
1749 * converted: booleans will be converted to '0' and '1', null
1750 * will become '', and everything else will be cast to string
1751 * (not JSON-serialized). Page properties obtained from the
1752 * PageProps service will thus always be strings.
1754 * @note The sort key stored in the database *will be NULL* unless
1755 * the value passed here is an `int|float|bool`. If you *do not*
1756 * want your property *value* indexed and sorted (for example, the
1757 * value is a title string which can be numeric but only
1758 * incidentally, like when it gets retrieved from an array key)
1759 * be sure to cast to string or use
1760 * `::setUnsortedPageProperty()`. If you *do* want your property
1761 * *value* indexed and sorted, you should use
1762 * `::setNumericPageProperty()` instead as this will ensure the
1763 * value type is correct. Note that either way it is possible to
1764 * efficiently look up all the pages with a certain property; we
1765 * are only talking about sorting the *values* assigned to the
1766 * property, for example for a "top N values of the property"
1767 * query.
1769 * @note Note that `::getPageProperty()`/`::setPageProperty()` do
1770 * not do any conversions themselves; you should therefore be
1771 * careful to distinguish values returned from the PageProp
1772 * service (always strings) from values retrieved from a
1773 * ParserOutput.
1775 * @note Do not use setPageProperty() to set a property which is only used
1776 * in a context where the ParserOutput object itself is already available,
1777 * for example a normal page view. There is no need to save such a property
1778 * in the database since the text is already parsed; use
1779 * ::setExtensionData() instead.
1781 * @par Example:
1782 * @code
1783 * $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
1784 * @endcode
1786 * And then later, in OutputPageParserOutput or similar:
1788 * @par Example:
1789 * @code
1790 * $output->getExtensionData( 'my_ext_foo' );
1791 * @endcode
1793 * @note The use of `null` as a value is deprecated since 1.42; use
1794 * the empty string instead if you need a placeholder value, or
1795 * ::unsetPageProperty() if you mean to remove a page property.
1797 * @note The use of non-string values is deprecated since 1.42; if you
1798 * need an page property value with a sort index
1799 * use ::setNumericPageProperty().
1801 * @param string $name
1802 * @param ?scalar $value
1803 * @since 1.38
1805 public function setPageProperty( string $name, $value ): void {
1806 if ( $value === null ) {
1807 // Use an empty string instead.
1808 wfDeprecated( __METHOD__ . " with null value for $name", '1.42' );
1809 } elseif ( !is_scalar( $value ) ) {
1810 // Use ::setExtensionData() instead.
1811 wfDeprecated( __METHOD__ . " with non-scalar value for $name", '1.42' );
1812 } elseif ( !is_string( $value ) ) {
1813 // Use ::setNumericPageProperty() instead.
1814 wfDeprecated( __METHOD__ . " with non-string value for $name", '1.42' );
1816 $this->mProperties[$name] = $value;
1820 * Set a numeric page property whose *value* is intended to be sorted
1821 * and indexed. The sort key used for the property will be the value,
1822 * coerced to a number.
1824 * See `::setPageProperty()` for details.
1826 * In the future, we may allow the value to be specified independent
1827 * of sort key (T357783).
1829 * @param string $propName The name of the page property
1830 * @param int|float|string $numericValue the numeric value
1831 * @since 1.42
1833 public function setNumericPageProperty( string $propName, $numericValue ): void {
1834 if ( !is_numeric( $numericValue ) ) {
1835 throw new \TypeError( __METHOD__ . " with non-numeric value" );
1837 // Coerce numeric sort key to a number.
1838 $this->mProperties[$propName] = 0 + $numericValue;
1842 * Set a page property whose *value* is not intended to be sorted and
1843 * indexed.
1845 * See `::setPageProperty()` for details. It is recommended to
1846 * use the empty string if you need a placeholder value (ie, if
1847 * it is the *presence* of the property which is important, not
1848 * the *value* the property is set to).
1850 * It is still possible to efficiently look up all the pages with
1851 * a certain property (the "presence" of it *is* indexed; see
1852 * Special:PagesWithProp, list=pageswithprop).
1854 * @param string $propName The name of the page property
1855 * @param string $value Optional value; defaults to the empty string.
1856 * @since 1.42
1858 public function setUnsortedPageProperty( string $propName, string $value = '' ): void {
1859 $this->mProperties[$propName] = $value;
1863 * Look up a page property.
1864 * @param string $name The page property name to look up.
1865 * @return ?scalar The value previously set using
1866 * ::setPageProperty(), ::setUnsortedPageProperty(), or
1867 * ::setNumericPageProperty().
1868 * Returns null if no value was set for the given property name.
1870 * @note You would need to use ::getPageProperties() to test for an
1871 * explicitly-set null value; but see the note in ::setPageProperty()
1872 * deprecating the use of null values.
1873 * @since 1.38
1875 public function getPageProperty( string $name ) {
1876 return $this->mProperties[$name] ?? null;
1880 * Remove a page property.
1881 * @param string $name The page property name.
1882 * @since 1.38
1884 public function unsetPageProperty( string $name ): void {
1885 unset( $this->mProperties[$name] );
1889 * Return all the page properties set on this ParserOutput.
1890 * @return array<string,?scalar>
1891 * @since 1.38
1893 public function getPageProperties(): array {
1894 // @phan-suppress-next-line MediaWikiNoIssetIfDefined
1895 if ( !isset( $this->mProperties ) ) {
1896 $this->mProperties = [];
1898 return $this->mProperties;
1902 * Provides a uniform interface to various boolean flags stored
1903 * in the ParserOutput. Flags internal to MediaWiki core should
1904 * have names which are constants in ParserOutputFlags. Extensions
1905 * should use ::setExtensionData() rather than creating new flags
1906 * with ::setOutputFlag() in order to prevent namespace conflicts.
1908 * Flags are always combined with OR. That is, the flag is set in
1909 * the resulting ParserOutput if the flag is set in *any* of the
1910 * fragments composing the ParserOutput.
1912 * @note The combination policy means that a ParserOutput may end
1913 * up with both INDEX_POLICY and NO_INDEX_POLICY set. It is
1914 * expected that NO_INDEX_POLICY "wins" in that case. (T16899)
1915 * (This resolution is implemented in ::getIndexPolicy().)
1917 * @param string $name A flag name
1918 * @param bool $val
1919 * @since 1.38
1921 public function setOutputFlag( string $name, bool $val = true ): void {
1922 switch ( $name ) {
1923 case ParserOutputFlags::NO_GALLERY:
1924 $this->setNoGallery( $val );
1925 break;
1927 case ParserOutputFlags::ENABLE_OOUI:
1928 $this->setEnableOOUI( $val );
1929 break;
1931 case ParserOutputFlags::NO_INDEX_POLICY:
1932 $this->mNoIndexSet = $val;
1933 break;
1935 case ParserOutputFlags::INDEX_POLICY:
1936 $this->mIndexSet = $val;
1937 break;
1939 case ParserOutputFlags::NEW_SECTION:
1940 $this->setNewSection( $val );
1941 break;
1943 case ParserOutputFlags::HIDE_NEW_SECTION:
1944 $this->setHideNewSection( $val );
1945 break;
1947 case ParserOutputFlags::PREVENT_CLICKJACKING:
1948 $this->setPreventClickjacking( $val );
1949 break;
1951 default:
1952 if ( $val ) {
1953 $this->mFlags[$name] = true;
1954 } else {
1955 unset( $this->mFlags[$name] );
1957 break;
1962 * Provides a uniform interface to various boolean flags stored
1963 * in the ParserOutput. Flags internal to MediaWiki core should
1964 * have names which are constants in ParserOutputFlags. Extensions
1965 * should only use ::getOutputFlag() to query flags defined in
1966 * ParserOutputFlags in core; they should use ::getExtensionData()
1967 * to define their own flags.
1969 * @param string $name A flag name
1970 * @return bool The flag value
1971 * @since 1.38
1973 public function getOutputFlag( string $name ): bool {
1974 switch ( $name ) {
1975 case ParserOutputFlags::NO_GALLERY:
1976 return $this->getNoGallery();
1978 case ParserOutputFlags::ENABLE_OOUI:
1979 return $this->getEnableOOUI();
1981 case ParserOutputFlags::INDEX_POLICY:
1982 return $this->mIndexSet;
1984 case ParserOutputFlags::NO_INDEX_POLICY:
1985 return $this->mNoIndexSet;
1987 case ParserOutputFlags::NEW_SECTION:
1988 return $this->getNewSection();
1990 case ParserOutputFlags::HIDE_NEW_SECTION:
1991 return $this->getHideNewSection();
1993 case ParserOutputFlags::PREVENT_CLICKJACKING:
1994 return $this->getPreventClickjacking();
1996 default:
1997 return isset( $this->mFlags[$name] );
2003 * Provides a uniform interface to various string sets stored
2004 * in the ParserOutput. String sets internal to MediaWiki core should
2005 * have names which are constants in ParserOutputStringSets. Extensions
2006 * should use ::appendExtensionData() rather than creating new string sets
2007 * with ::appendOutputStrings() in order to prevent namespace conflicts.
2009 * @param string $name A string set name
2010 * @param string[] $value
2011 * @since 1.41
2013 public function appendOutputStrings( string $name, array $value ): void {
2014 switch ( $name ) {
2015 case ParserOutputStringSets::MODULE:
2016 $this->addModules( $value );
2017 break;
2018 case ParserOutputStringSets::MODULE_STYLE:
2019 $this->addModuleStyles( $value );
2020 break;
2021 case ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC:
2022 foreach ( $value as $v ) {
2023 $this->addExtraCSPDefaultSrc( $v );
2025 break;
2026 case ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC:
2027 foreach ( $value as $v ) {
2028 $this->addExtraCSPScriptSrc( $v );
2030 break;
2031 case ParserOutputStringSets::EXTRA_CSP_STYLE_SRC:
2032 foreach ( $value as $v ) {
2033 $this->addExtraCSPStyleSrc( $v );
2035 break;
2036 default:
2037 throw new UnexpectedValueException( "Unknown output string set name $name" );
2042 * Provides a uniform interface to various boolean string sets stored
2043 * in the ParserOutput. String sets internal to MediaWiki core should
2044 * have names which are constants in ParserOutputStringSets. Extensions
2045 * should only use ::getOutputStrings() to query string sets defined in
2046 * ParserOutputStringSets in core; they should use ::appendExtensionData()
2047 * to define their own string sets.
2049 * @param string $name A string set name
2050 * @return string[] The string set value
2051 * @since 1.41
2053 public function getOutputStrings( string $name ): array {
2054 switch ( $name ) {
2055 case ParserOutputStringSets::MODULE:
2056 return $this->getModules();
2057 case ParserOutputStringSets::MODULE_STYLE:
2058 return $this->getModuleStyles();
2059 case ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC:
2060 return $this->getExtraCSPDefaultSrcs();
2061 case ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC:
2062 return $this->getExtraCSPScriptSrcs();
2063 case ParserOutputStringSets::EXTRA_CSP_STYLE_SRC:
2064 return $this->getExtraCSPStyleSrcs();
2065 default:
2066 throw new UnexpectedValueException( "Unknown output string set name $name" );
2071 * Attaches arbitrary data to this ParserObject. This can be used to store some information in
2072 * the ParserOutput object for later use during page output. The data will be cached along with
2073 * the ParserOutput object, but unlike data set using setPageProperty(), it is not recorded in the
2074 * database.
2076 * This method is provided to overcome the unsafe practice of attaching extra information to a
2077 * ParserObject by directly assigning member variables.
2079 * To use setExtensionData() to pass extension information from a hook inside the parser to a
2080 * hook in the page output, use this in the parser hook:
2082 * @par Example:
2083 * @code
2084 * $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
2085 * @endcode
2087 * And then later, in OutputPageParserOutput or similar:
2089 * @par Example:
2090 * @code
2091 * $output->getExtensionData( 'my_ext_foo' );
2092 * @endcode
2094 * In MediaWiki 1.20 and older, you have to use a custom member variable
2095 * within the ParserOutput object:
2097 * @par Example:
2098 * @code
2099 * $parser->getOutput()->my_ext_foo = '...';
2100 * @endcode
2102 * @note Only scalar values, e.g. numbers, strings, arrays or MediaWiki\Json\JsonDeserializable
2103 * instances are supported as a value. Attempt to set other class instance as extension data
2104 * will break ParserCache for the page.
2106 * @note Since MW 1.38 the practice of setting conflicting values for
2107 * the same key has been deprecated. As with ::setJsConfigVar(), if
2108 * you set the same key multiple times on a ParserOutput, it is expected
2109 * that the value will be identical each time. If you want to collect
2110 * multiple pieces of data under a single key, use ::appendExtensionData().
2112 * @param string $key The key for accessing the data. Extensions should take care to avoid
2113 * conflicts in naming keys. It is suggested to use the extension's name as a prefix.
2115 * @param mixed|JsonDeserializable $value The value to set.
2116 * Setting a value to null is equivalent to removing the value.
2117 * @since 1.21
2119 public function setExtensionData( $key, $value ): void {
2120 if (
2121 array_key_exists( $key, $this->mExtensionData ) &&
2122 $this->mExtensionData[$key] !== $value
2124 // This behavior was deprecated in 1.38. We will eventually
2125 // emit a warning here, then throw an exception.
2127 if ( $value === null ) {
2128 unset( $this->mExtensionData[$key] );
2129 } else {
2130 $this->mExtensionData[$key] = $value;
2135 * Appends arbitrary data to this ParserObject. This can be used
2136 * to store some information in the ParserOutput object for later
2137 * use during page output. The data will be cached along with the
2138 * ParserOutput object, but unlike data set using
2139 * setPageProperty(), it is not recorded in the database.
2141 * See ::setExtensionData() for more details on rationale and use.
2143 * In order to provide for out-of-order/asynchronous/incremental
2144 * parsing, this method appends values to a set. See
2145 * ::setExtensionData() for the flag-like version of this method.
2147 * @note Only values which can be array keys are currently supported
2148 * as values.
2150 * @param string $key The key for accessing the data. Extensions should take care to avoid
2151 * conflicts in naming keys. It is suggested to use the extension's name as a prefix.
2153 * @param int|string $value The value to append to the list.
2154 * @param string $strategy Merge strategy:
2155 * only MW_MERGE_STRATEGY_UNION is currently supported and external callers
2156 * should treat this parameter as @internal at this time and omit it.
2157 * @since 1.38
2159 public function appendExtensionData(
2160 string $key,
2161 $value,
2162 string $strategy = self::MW_MERGE_STRATEGY_UNION
2163 ): void {
2164 if ( $strategy !== self::MW_MERGE_STRATEGY_UNION ) {
2165 throw new InvalidArgumentException( "Unknown merge strategy $strategy." );
2167 if ( !array_key_exists( $key, $this->mExtensionData ) ) {
2168 $this->mExtensionData[$key] = [
2169 // Indicate how these values are to be merged.
2170 self::MW_MERGE_STRATEGY_KEY => $strategy,
2172 } elseif ( !is_array( $this->mExtensionData[$key] ) ) {
2173 throw new InvalidArgumentException( "Mixing set and append for $key" );
2174 } elseif ( ( $this->mExtensionData[$key][self::MW_MERGE_STRATEGY_KEY] ?? null ) !== $strategy ) {
2175 throw new InvalidArgumentException( "Conflicting merge strategies for $key" );
2177 $this->mExtensionData[$key][$value] = true;
2181 * Gets extensions data previously attached to this ParserOutput using setExtensionData().
2182 * Typically, such data would be set while parsing the page, e.g. by a parser function.
2184 * @since 1.21
2186 * @param string $key The key to look up.
2188 * @return mixed|null The value previously set for the given key using setExtensionData()
2189 * or null if no value was set for this key.
2191 public function getExtensionData( $key ) {
2192 $value = $this->mExtensionData[$key] ?? null;
2193 if ( is_array( $value ) ) {
2194 // Don't expose our internal merge strategy key.
2195 unset( $value[self::MW_MERGE_STRATEGY_KEY] );
2197 return $value;
2200 private static function getTimes( $clock = null ): array {
2201 $ret = [];
2202 if ( !$clock || $clock === 'wall' ) {
2203 $ret['wall'] = microtime( true );
2205 if ( !$clock || $clock === 'cpu' ) {
2206 $ru = getrusage( 0 /* RUSAGE_SELF */ );
2207 $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
2208 $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
2210 return $ret;
2214 * Resets the parse start timestamps for future calls to getTimeSinceStart()
2215 * and recordTimeProfile().
2217 * @since 1.22
2219 public function resetParseStartTime(): void {
2220 $this->mParseStartTime = self::getTimes();
2221 $this->mTimeProfile = [];
2225 * Unset the parse start time.
2227 * This is intended for testing purposes only, in order to avoid
2228 * spurious differences between testing outputs created at different
2229 * times.
2231 * @since 1.43
2233 public function clearParseStartTime(): void {
2234 $this->mParseStartTime = [];
2238 * Record the time since resetParseStartTime() was last called.
2239 * The recorded time can be accessed using getTimeProfile().
2241 * After resetParseStartTime() was called, the first call to recordTimeProfile()
2242 * will record the time profile. Subsequent calls to recordTimeProfile() will have
2243 * no effect until resetParseStartTime() is called again.
2245 * @since 1.42
2247 public function recordTimeProfile() {
2248 if ( !$this->mParseStartTime ) {
2249 // If resetParseStartTime was never called, there is nothing to record
2250 return;
2253 if ( $this->mTimeProfile !== [] ) {
2254 // Don't override the times recorded by the previous call to recordTimeProfile().
2255 return;
2258 $now = self::getTimes();
2259 $this->mTimeProfile = [
2260 'wall' => $now['wall'] - $this->mParseStartTime['wall'],
2261 'cpu' => $now['cpu'] - $this->mParseStartTime['cpu'],
2266 * Returns the time that elapsed between the most recent call to resetParseStartTime()
2267 * and the first call to recordTimeProfile() after that.
2269 * Clocks available are:
2270 * - wall: Wall clock time
2271 * - cpu: CPU time (requires getrusage)
2273 * If recordTimeProfile() has noit been called since the most recent call to
2274 * resetParseStartTime(), or if resetParseStartTime() was never called, then
2275 * this method will return null.
2277 * @param string $clock
2279 * @since 1.42
2280 * @return float|null
2282 public function getTimeProfile( string $clock ) {
2283 return $this->mTimeProfile[ $clock ] ?? null;
2287 * Returns the time since resetParseStartTime() was last called
2289 * Clocks available are:
2290 * - wall: Wall clock time
2291 * - cpu: CPU time (requires getrusage)
2293 * @since 1.22
2294 * @deprecated since 1.42, use getTimeProfile() instead.
2295 * @param string $clock
2296 * @return float|null
2298 public function getTimeSinceStart( $clock ) {
2299 wfDeprecated( __METHOD__, '1.42' );
2301 if ( !isset( $this->mParseStartTime[$clock] ) ) {
2302 return null;
2305 $end = self::getTimes( $clock );
2306 return $end[$clock] - $this->mParseStartTime[$clock];
2310 * Sets parser limit report data for a key
2312 * The key is used as the prefix for various messages used for formatting:
2313 * - $key: The label for the field in the limit report
2314 * - $key-value-text: Message used to format the value in the "NewPP limit
2315 * report" HTML comment. If missing, uses $key-format.
2316 * - $key-value-html: Message used to format the value in the preview
2317 * limit report table. If missing, uses $key-format.
2318 * - $key-value: Message used to format the value. If missing, uses "$1".
2320 * Note that all values are interpreted as wikitext, and so should be
2321 * encoded with htmlspecialchars() as necessary, but should avoid complex
2322 * HTML for display in the "NewPP limit report" comment.
2324 * @since 1.22
2325 * @param string $key Message key
2326 * @param mixed $value Appropriate for Message::params()
2328 public function setLimitReportData( $key, $value ): void {
2329 $this->mLimitReportData[$key] = $value;
2331 if ( is_array( $value ) ) {
2332 if ( array_keys( $value ) === [ 0, 1 ]
2333 && is_numeric( $value[0] )
2334 && is_numeric( $value[1] )
2336 $data = [ 'value' => $value[0], 'limit' => $value[1] ];
2337 } else {
2338 $data = $value;
2340 } else {
2341 $data = $value;
2344 if ( strpos( $key, '-' ) ) {
2345 [ $ns, $name ] = explode( '-', $key, 2 );
2346 $this->mLimitReportJSData[$ns][$name] = $data;
2347 } else {
2348 $this->mLimitReportJSData[$key] = $data;
2353 * Check whether the cache TTL was lowered from the site default.
2355 * When content is determined by more than hard state (e.g. page edits),
2356 * such as template/file transclusions based on the current timestamp or
2357 * extension tags that generate lists based on queries, this return true.
2359 * This method mainly exists to facilitate the logic in
2360 * WikiPage::triggerOpportunisticLinksUpdate. As such, beware that reducing the TTL for
2361 * reasons that do not relate to "dynamic content", may have the side-effect of incurring
2362 * more RefreshLinksJob executions.
2364 * @internal For use by Parser and WikiPage
2365 * @since 1.37
2366 * @return bool
2368 public function hasReducedExpiry(): bool {
2369 if ( $this->getOutputFlag( ParserOutputFlags::HAS_ASYNC_CONTENT ) ) {
2370 // If this page has async content, then we should re-run
2371 // RefreshLinksJob whenever we regenerate the page.
2372 return true;
2374 $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()->get(
2375 MainConfigNames::ParserCacheExpireTime );
2377 return $this->getCacheExpiry() < $parserCacheExpireTime;
2380 public function getCacheExpiry(): int {
2381 $expiry = parent::getCacheExpiry();
2382 if ( $this->getOutputFlag( ParserOutputFlags::ASYNC_NOT_READY ) ) {
2383 $asyncExpireTime = MediaWikiServices::getInstance()->getMainConfig()->get(
2384 MainConfigNames::ParserCacheAsyncExpireTime
2386 $expiry = min( $expiry, $asyncExpireTime );
2388 return $expiry;
2392 * Set the prevent-clickjacking flag. If set this will cause an
2393 * `X-Frame-Options` header appropriate for edit pages to be sent.
2394 * The header value is controlled by `$wgEditPageFrameOptions`.
2396 * This is the default for special pages. If you display a CSRF-protected
2397 * form on an ordinary view page, then you need to call this function
2398 * with `$flag = true`.
2400 * @param bool $flag New flag value
2401 * @since 1.38
2403 public function setPreventClickjacking( bool $flag ): void {
2404 $this->mPreventClickjacking = $flag;
2408 * Get the prevent-clickjacking flag.
2410 * @return bool Flag value
2411 * @since 1.38
2412 * @see ::setPreventClickjacking
2414 public function getPreventClickjacking(): bool {
2415 return $this->mPreventClickjacking;
2419 * Lower the runtime adaptive TTL to at most this value
2421 * @param int $ttl
2422 * @since 1.28
2424 public function updateRuntimeAdaptiveExpiry( $ttl ): void {
2425 $this->mMaxAdaptiveExpiry = min( $ttl, $this->mMaxAdaptiveExpiry );
2426 $this->updateCacheExpiry( $ttl );
2430 * Add an extra value to Content-Security-Policy default-src directive
2432 * Call this if you are including a resource (e.g. image) from a third party domain.
2433 * This is used for all source types except style and script.
2435 * @since 1.35
2436 * @param string $src CSP source e.g. example.com
2438 public function addExtraCSPDefaultSrc( $src ): void {
2439 $this->mExtraDefaultSrcs[] = $src;
2443 * Add an extra value to Content-Security-Policy style-src directive
2445 * @since 1.35
2446 * @param string $src CSP source e.g. example.com
2448 public function addExtraCSPStyleSrc( $src ): void {
2449 $this->mExtraStyleSrcs[] = $src;
2453 * Add an extra value to Content-Security-Policy script-src directive
2455 * Call this if you are loading third-party Javascript
2457 * @since 1.35
2458 * @param string $src CSP source e.g. example.com
2460 public function addExtraCSPScriptSrc( $src ): void {
2461 $this->mExtraScriptSrcs[] = $src;
2465 * Call this when parsing is done to lower the TTL based on low parse times
2467 * @since 1.28
2469 public function finalizeAdaptiveCacheExpiry(): void {
2470 if ( is_infinite( $this->mMaxAdaptiveExpiry ) ) {
2471 return; // not set
2474 $runtime = $this->getTimeProfile( 'wall' );
2475 if ( is_float( $runtime ) ) {
2476 $slope = ( self::SLOW_AR_TTL - self::FAST_AR_TTL )
2477 / ( self::PARSE_SLOW_SEC - self::PARSE_FAST_SEC );
2478 // SLOW_AR_TTL = PARSE_SLOW_SEC * $slope + $point
2479 $point = self::SLOW_AR_TTL - self::PARSE_SLOW_SEC * $slope;
2481 $adaptiveTTL = min(
2482 max( $slope * $runtime + $point, self::MIN_AR_TTL ),
2483 $this->mMaxAdaptiveExpiry
2485 $this->updateCacheExpiry( $adaptiveTTL );
2490 * Transfer parser options which affect post-processing from ParserOptions
2491 * to this ParserOutput.
2493 public function setFromParserOptions( ParserOptions $parserOptions ) {
2494 // Copied from Parser.php::parse and should probably be abstracted
2495 // into the parent base class (probably as part of T236809)
2496 // Wrap non-interface parser output in a <div> so it can be targeted
2497 // with CSS (T37247)
2498 $class = $parserOptions->getWrapOutputClass();
2499 if ( $class !== false && !$parserOptions->getInterfaceMessage() ) {
2500 $this->addWrapperDivClass( $class );
2503 // Record whether we should suppress section edit links
2504 if ( $parserOptions->getSuppressSectionEditLinks() ) {
2505 $this->setOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS );
2508 // Record whether we should wrap sections for collapsing them
2509 if ( $parserOptions->getCollapsibleSections() ) {
2510 $this->setOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS );
2513 // Record whether this is a preview parse in the output (T341010)
2514 if ( $parserOptions->getIsPreview() ) {
2515 $this->setOutputFlag( ParserOutputFlags::IS_PREVIEW, true );
2516 // Ensure that previews aren't cacheable, just to be safe.
2517 $this->updateCacheExpiry( 0 );
2521 public function __sleep() {
2522 return array_filter( array_keys( get_object_vars( $this ) ),
2523 static function ( $field ) {
2524 if ( $field === 'mParseStartTime' || $field === 'mWarningMsgs' ) {
2525 return false;
2527 // Unserializing unknown private fields in HHVM causes
2528 // member variables with nulls in their names (T229366)
2529 return strpos( $field, "\0" ) === false;
2535 * Merges internal metadata such as flags, accessed options, and profiling info
2536 * from $source into this ParserOutput. This should be used whenever the state of $source
2537 * has any impact on the state of this ParserOutput.
2539 public function mergeInternalMetaDataFrom( ParserOutput $source ): void {
2540 $this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter
2541 $this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getRevisionTimestamp() );
2542 if ( $source->hasCacheTime() ) {
2543 $sourceCacheTime = $source->getCacheTime();
2544 if (
2545 !$this->hasCacheTime() ||
2546 // "undocumented use of -1 to mean not cacheable"
2547 // deprecated, but still supported by ::setCacheTime()
2548 strval( $sourceCacheTime ) === '-1' ||
2550 strval( $this->getCacheTime() ) !== '-1' &&
2551 // use newer of the two times
2552 $this->getCacheTime() < $sourceCacheTime
2555 $this->setCacheTime( $sourceCacheTime );
2558 if ( $source->getRenderId() !== null ) {
2559 // Final render ID should be a function of all component POs
2560 $rid = ( $this->getRenderId() ?? '' ) . $source->getRenderId();
2561 $this->setRenderId( $rid );
2563 if ( $source->getCacheRevisionId() !== null ) {
2564 $sourceCacheRevisionId = $source->getCacheRevisionId();
2565 $thisCacheRevisionId = $this->getCacheRevisionId();
2566 if ( $thisCacheRevisionId === null ) {
2567 $this->setCacheRevisionId( $sourceCacheRevisionId );
2568 } elseif ( $sourceCacheRevisionId !== $thisCacheRevisionId ) {
2569 // May throw an exception here in the future
2570 wfDeprecated(
2571 __METHOD__ . ": conflicting revision IDs " .
2572 "$thisCacheRevisionId and $sourceCacheRevisionId"
2577 foreach ( self::SPECULATIVE_FIELDS as $field ) {
2578 if ( $this->$field && $source->$field && $this->$field !== $source->$field ) {
2579 wfLogWarning( __METHOD__ . ": inconsistent '$field' properties!" );
2581 $this->$field = $this->useMaxValue( $this->$field, $source->$field );
2584 $this->mParseStartTime = $this->useEachMinValue(
2585 $this->mParseStartTime,
2586 $source->mParseStartTime
2589 $this->mTimeProfile = $this->useEachTotalValue(
2590 $this->mTimeProfile,
2591 $source->mTimeProfile
2594 $this->mFlags = self::mergeMap( $this->mFlags, $source->mFlags );
2595 $this->mParseUsedOptions = self::mergeMap( $this->mParseUsedOptions, $source->mParseUsedOptions );
2597 // TODO: maintain per-slot limit reports!
2598 if ( !$this->mLimitReportData ) {
2599 $this->mLimitReportData = $source->mLimitReportData;
2601 if ( !$this->mLimitReportJSData ) {
2602 $this->mLimitReportJSData = $source->mLimitReportJSData;
2607 * Merges HTML metadata such as head items, JS config vars, and HTTP cache control info
2608 * from $source into this ParserOutput. This should be used whenever the HTML in $source
2609 * has been somehow merged into the HTML of this ParserOutput.
2611 public function mergeHtmlMetaDataFrom( ParserOutput $source ): void {
2612 // HTML and HTTP
2613 $this->mHeadItems = self::mergeMixedList( $this->mHeadItems, $source->getHeadItems() );
2614 $this->addModules( $source->getModules() );
2615 $this->addModuleStyles( $source->getModuleStyles() );
2616 $this->mJsConfigVars = self::mergeMapStrategy( $this->mJsConfigVars, $source->mJsConfigVars );
2617 $this->mMaxAdaptiveExpiry = min( $this->mMaxAdaptiveExpiry, $source->mMaxAdaptiveExpiry );
2618 $this->mExtraStyleSrcs = self::mergeList(
2619 $this->mExtraStyleSrcs,
2620 $source->getExtraCSPStyleSrcs()
2622 $this->mExtraScriptSrcs = self::mergeList(
2623 $this->mExtraScriptSrcs,
2624 $source->getExtraCSPScriptSrcs()
2626 $this->mExtraDefaultSrcs = self::mergeList(
2627 $this->mExtraDefaultSrcs,
2628 $source->getExtraCSPDefaultSrcs()
2631 // "noindex" always wins!
2632 $this->mIndexSet = $this->mIndexSet || $source->mIndexSet;
2633 $this->mNoIndexSet = $this->mNoIndexSet || $source->mNoIndexSet;
2635 // Skin control
2636 $this->mNewSection = $this->mNewSection || $source->getNewSection();
2637 $this->mHideNewSection = $this->mHideNewSection || $source->getHideNewSection();
2638 $this->mNoGallery = $this->mNoGallery || $source->getNoGallery();
2639 $this->mEnableOOUI = $this->mEnableOOUI || $source->getEnableOOUI();
2640 $this->mPreventClickjacking = $this->mPreventClickjacking || $source->getPreventClickjacking();
2642 $tocData = $this->getTOCData();
2643 $sourceTocData = $source->getTOCData();
2644 if ( $tocData !== null ) {
2645 if ( $sourceTocData !== null ) {
2646 // T327429: Section merging is broken, since it doesn't respect
2647 // global numbering, but there are tests which expect section
2648 // metadata to be concatenated.
2649 // There should eventually be a deprecation warning here.
2650 foreach ( $sourceTocData->getSections() as $s ) {
2651 $tocData->addSection( $s );
2654 } elseif ( $sourceTocData !== null ) {
2655 $this->setTOCData( $sourceTocData );
2658 // XXX: we don't want to concatenate title text, so first write wins.
2659 // We should use the first *modified* title text, but we don't have the original to check.
2660 if ( $this->mTitleText === null || $this->mTitleText === '' ) {
2661 $this->mTitleText = $source->mTitleText;
2664 // class names are stored in array keys
2665 $this->mWrapperDivClasses = self::mergeMap(
2666 $this->mWrapperDivClasses,
2667 $source->mWrapperDivClasses
2670 // NOTE: last write wins, same as within one ParserOutput
2671 $this->mIndicators = self::mergeMap( $this->mIndicators, $source->getIndicators() );
2673 // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
2674 // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
2675 // kinds of extension data to be merged in different ways.
2676 $this->mExtensionData = self::mergeMapStrategy(
2677 $this->mExtensionData,
2678 $source->mExtensionData
2683 * Merges dependency tracking metadata such as backlinks, images used, and extension data
2684 * from $source into this ParserOutput. This allows dependency tracking to be done for the
2685 * combined output of multiple content slots.
2687 public function mergeTrackingMetaDataFrom( ParserOutput $source ): void {
2688 foreach (
2689 $source->getLinkList( ParserOutputLinkTypes::LANGUAGE )
2690 as [ 'link' => $link ]
2692 $this->addLanguageLink( $link );
2694 $this->mCategories = self::mergeMap( $this->mCategories, $source->getCategoryMap() );
2695 foreach (
2696 $source->getLinkList( ParserOutputLinkTypes::LOCAL )
2697 as [ 'link' => $link, 'pageid' => $pageid ]
2699 $this->addLink( $link, $pageid );
2701 foreach (
2702 $source->getLinkList( ParserOutputLinkTypes::TEMPLATE )
2703 as [ 'link' => $link, 'pageid' => $pageid, 'revid' => $revid ]
2705 $this->addTemplate( $link, $pageid, $revid );
2707 foreach (
2708 $source->getLinkList( ParserOutputLinkTypes::MEDIA ) as $item
2710 $this->addImage( $item['link'], $item['time'] ?? null, $item['sha1'] ?? null );
2712 $this->mExternalLinks = self::mergeMap( $this->mExternalLinks, $source->getExternalLinks() );
2713 foreach (
2714 $source->getLinkList( ParserOutputLinkTypes::INTERWIKI )
2715 as [ 'link' => $link ]
2717 $this->addInterwikiLink( $link );
2720 foreach (
2721 $source->getLinkList( ParserOutputLinkTypes::SPECIAL )
2722 as [ 'link' => $link ]
2724 $this->addLink( $link );
2727 // TODO: add a $mergeStrategy parameter to setPageProperty to allow different
2728 // kinds of properties to be merged in different ways.
2729 // (Model this after ::appendJsConfigVar(); use ::mergeMapStrategy here)
2730 $this->mProperties = self::mergeMap( $this->mProperties, $source->getPageProperties() );
2732 // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
2733 $this->mExtensionData = self::mergeMapStrategy(
2734 $this->mExtensionData,
2735 $source->mExtensionData
2740 * Adds the metadata collected in this ParserOutput to the supplied
2741 * ContentMetadataCollector. This is similar to ::mergeHtmlMetaDataFrom()
2742 * but in the opposite direction, since ParserOutput is read/write while
2743 * ContentMetadataCollector is write-only.
2745 * @param ContentMetadataCollector $metadata
2746 * @since 1.38
2748 public function collectMetadata( ContentMetadataCollector $metadata ): void {
2749 // Uniform handling of all boolean flags: they are OR'ed together.
2750 $flags = array_keys(
2751 $this->mFlags + array_flip( ParserOutputFlags::cases() )
2753 foreach ( $flags as $name ) {
2754 if ( $this->getOutputFlag( $name ) ) {
2755 $metadata->setOutputFlag( $name );
2759 // Uniform handling of string sets: they are unioned.
2760 // (This includes modules, style modes, and CSP src.)
2761 foreach ( ParserOutputStringSets::cases() as $name ) {
2762 $metadata->appendOutputStrings(
2763 $name, $this->getOutputStrings( $name )
2767 foreach ( $this->mCategories as $cat => $key ) {
2768 // Numeric category strings are going to come out of the
2769 // `mCategories` array as ints; cast back to string.
2770 // Also convert back to a LinkTarget!
2771 $lt = TitleValue::tryNew( NS_CATEGORY, (string)$cat );
2772 $metadata->addCategory( $lt, $key );
2775 foreach ( $this->mLinks as $ns => $arr ) {
2776 foreach ( $arr as $dbk => $id ) {
2777 // Numeric titles are going to come out of the
2778 // `mLinks` array as ints; cast back to string.
2779 $lt = TitleValue::tryNew( $ns, (string)$dbk );
2780 $metadata->addLink( $lt, $id );
2784 foreach ( $this->mInterwikiLinks as $prefix => $arr ) {
2785 foreach ( $arr as $dbk => $ignore ) {
2786 $lt = TitleValue::tryNew( NS_MAIN, (string)$dbk, '', $prefix );
2787 $metadata->addLink( $lt );
2791 foreach ( $this->mLinksSpecial as $dbk => $ignore ) {
2792 // Numeric titles are going to come out of the
2793 // `mLinks` array as ints; cast back to string.
2794 $lt = TitleValue::tryNew( NS_SPECIAL, (string)$dbk );
2795 $metadata->addLink( $lt );
2798 foreach ( $this->mImages as $name => $ignore ) {
2799 // Numeric titles come out of mImages as ints.
2800 $lt = TitleValue::tryNew( NS_FILE, (string)$name );
2801 $props = $this->mFileSearchOptions[$name] ?? [];
2802 $metadata->addImage( $lt, $props['time'] ?? null, $props['sha1'] ?? null );
2805 foreach ( $this->mLanguageLinkMap as $lang => $title ) {
2806 if ( $title === '|' ) {
2807 continue; // T374736: not a valid language link
2809 # language links can have fragments!
2810 [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
2811 $lt = TitleValue::tryNew( NS_MAIN, $title, $frag, (string)$lang );
2812 $metadata->addLanguageLink( $lt );
2815 foreach ( $this->mJsConfigVars as $key => $value ) {
2816 if ( is_array( $value ) && isset( $value[self::MW_MERGE_STRATEGY_KEY] ) ) {
2817 $strategy = $value[self::MW_MERGE_STRATEGY_KEY];
2818 foreach ( $value as $item => $ignore ) {
2819 if ( $item !== self::MW_MERGE_STRATEGY_KEY ) {
2820 $metadata->appendJsConfigVar( $key, $item, $strategy );
2823 } elseif ( $metadata instanceof ParserOutput &&
2824 array_key_exists( $key, $metadata->mJsConfigVars )
2826 // This behavior is deprecated, will likely result in
2827 // incorrect output, and we'll eventually emit a
2828 // warning here---but at the moment this is usually
2829 // caused by limitations in Parsoid and/or use of
2830 // the ParserAfterParse hook: T303015#7770480
2831 $metadata->mJsConfigVars[$key] = $value;
2832 } else {
2833 $metadata->setJsConfigVar( $key, $value );
2836 foreach ( $this->mExtensionData as $key => $value ) {
2837 if ( is_array( $value ) && isset( $value[self::MW_MERGE_STRATEGY_KEY] ) ) {
2838 $strategy = $value[self::MW_MERGE_STRATEGY_KEY];
2839 foreach ( $value as $item => $ignore ) {
2840 if ( $item !== self::MW_MERGE_STRATEGY_KEY ) {
2841 $metadata->appendExtensionData( $key, $item, $strategy );
2844 } elseif ( $metadata instanceof ParserOutput &&
2845 array_key_exists( $key, $metadata->mExtensionData )
2847 // This behavior is deprecated, will likely result in
2848 // incorrect output, and we'll eventually emit a
2849 // warning here---but at the moment this is usually
2850 // caused by limitations in Parsoid and/or use of
2851 // the ParserAfterParse hook: T303015#7770480
2852 $metadata->mExtensionData[$key] = $value;
2853 } else {
2854 $metadata->setExtensionData( $key, $value );
2857 foreach ( $this->mExternalLinks as $url => $ignore ) {
2858 $metadata->addExternalLink( $url );
2860 foreach ( $this->mProperties as $prop => $value ) {
2861 if ( is_numeric( $value ) ) {
2862 $metadata->setNumericPageProperty( $prop, $value );
2863 } elseif ( is_string( $value ) ) {
2864 $metadata->setUnsortedPageProperty( $prop, $value );
2865 } else {
2866 // Deprecated, but there are still sites which call
2867 // ::setPageProperty() with "unusual" values (T374046)
2868 $metadata->setPageProperty( $prop, $value );
2871 foreach ( $this->mWarningMsgs as $msg => $args ) {
2872 $metadata->addWarningMsg( $msg, ...$args );
2874 foreach ( $this->mLimitReportData as $key => $value ) {
2875 $metadata->setLimitReportData( $key, $value );
2877 foreach ( $this->mIndicators as $id => $content ) {
2878 $metadata->setIndicator( $id, $content );
2881 // ParserOutput-only fields; maintained "behind the curtain"
2882 // since Parsoid doesn't have to know about them.
2884 // In production use, the $metadata supplied to this method
2885 // will almost always be an instance of ParserOutput, passed to
2886 // Parsoid by core when parsing begins and returned to core by
2887 // Parsoid as a ContentMetadataCollector (Parsoid's name for
2888 // ParserOutput) when DataAccess::parseWikitext() is called.
2890 // We may use still Parsoid's StubMetadataCollector for testing or
2891 // when running Parsoid in standalone mode, so forcing a downcast
2892 // here would lose some flexibility.
2894 if ( $metadata instanceof ParserOutput ) {
2895 foreach ( $this->getUsedOptions() as $opt ) {
2896 $metadata->recordOption( $opt );
2898 if ( $this->mCacheExpiry !== null ) {
2899 $metadata->updateCacheExpiry( $this->mCacheExpiry );
2901 if ( $this->mCacheTime !== '' ) {
2902 $metadata->setCacheTime( $this->mCacheTime );
2904 if ( $this->mCacheRevisionId !== null ) {
2905 $metadata->setCacheRevisionId( $this->mCacheRevisionId );
2907 // T293514: We should use the first *modified* title text, but
2908 // we don't have the original to check.
2909 $otherTitle = $metadata->getTitleText();
2910 if ( $otherTitle === null || $otherTitle === '' ) {
2911 $metadata->setTitleText( $this->getTitleText() );
2913 foreach ( $this->mTemplates as $ns => $arr ) {
2914 foreach ( $arr as $dbk => $page_id ) {
2915 // default to invalid/broken revision if this is not present
2916 $rev_id = $this->mTemplateIds[$ns][$dbk] ?? 0;
2917 $metadata->addTemplate( TitleValue::tryNew( $ns, (string)$dbk ), $page_id, $rev_id );
2923 private static function mergeMixedList( array $a, array $b ): array {
2924 return array_unique( array_merge( $a, $b ), SORT_REGULAR );
2927 private static function mergeList( array $a, array $b ): array {
2928 return array_values( array_unique( array_merge( $a, $b ), SORT_REGULAR ) );
2931 private static function mergeMap( array $a, array $b ): array {
2932 return array_replace( $a, $b );
2935 private static function mergeMapStrategy( array $a, array $b ): array {
2936 foreach ( $b as $key => $bValue ) {
2937 if ( !array_key_exists( $key, $a ) ) {
2938 $a[$key] = $bValue;
2939 } elseif (
2940 is_array( $a[$key] ) &&
2941 isset( $a[$key][self::MW_MERGE_STRATEGY_KEY] ) &&
2942 isset( $bValue[self::MW_MERGE_STRATEGY_KEY] )
2944 $strategy = $bValue[self::MW_MERGE_STRATEGY_KEY];
2945 if ( $strategy !== $a[$key][self::MW_MERGE_STRATEGY_KEY] ) {
2946 throw new InvalidArgumentException( "Conflicting merge strategy for $key" );
2948 if ( $strategy === self::MW_MERGE_STRATEGY_UNION ) {
2949 // Note the array_merge is *not* safe to use here, because
2950 // the $bValue is expected to be a map from items to `true`.
2951 // If the item is a numeric string like '1' then array_merge
2952 // will convert it to an integer and renumber the array!
2953 $a[$key] = array_replace( $a[$key], $bValue );
2954 } else {
2955 throw new InvalidArgumentException( "Unknown merge strategy $strategy" );
2957 } else {
2958 $valuesSame = ( $a[$key] === $bValue );
2959 if ( ( !$valuesSame ) &&
2960 is_object( $a[$key] ) &&
2961 is_object( $bValue )
2963 $jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
2964 $valuesSame = ( $jsonCodec->serialize( $a[$key] ) === $jsonCodec->serialize( $bValue ) );
2966 if ( !$valuesSame ) {
2967 // Silently replace for now; in the future will first emit
2968 // a deprecation warning, and then (later) throw.
2969 $a[$key] = $bValue;
2973 return $a;
2976 private static function useEachMinValue( array $a, array $b ): array {
2977 $values = [];
2978 $keys = array_merge( array_keys( $a ), array_keys( $b ) );
2980 foreach ( $keys as $k ) {
2981 $values[$k] = min( $a[$k] ?? INF, $b[$k] ?? INF );
2984 return $values;
2987 private static function useEachTotalValue( array $a, array $b ): array {
2988 $values = [];
2989 $keys = array_merge( array_keys( $a ), array_keys( $b ) );
2991 foreach ( $keys as $k ) {
2992 $values[$k] = ( $a[$k] ?? 0 ) + ( $b[$k] ?? 0 );
2995 return $values;
2998 private static function useMaxValue( $a, $b ) {
2999 if ( $a === null ) {
3000 return $b;
3003 if ( $b === null ) {
3004 return $a;
3007 return max( $a, $b );
3011 * Returns a JSON serializable structure representing this ParserOutput instance.
3012 * @see newFromJson()
3014 * @return array
3016 protected function toJsonArray(): array {
3017 // WARNING: When changing how this class is serialized, follow the instructions
3018 // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
3020 $data = [
3021 'Text' => $this->mRawText,
3022 'LanguageLinks' => $this->getLanguageLinks(),
3023 'Categories' => $this->mCategories,
3024 'Indicators' => $this->mIndicators,
3025 'TitleText' => $this->mTitleText,
3026 'Links' => $this->mLinks,
3027 'LinksSpecial' => $this->mLinksSpecial,
3028 'Templates' => $this->mTemplates,
3029 'TemplateIds' => $this->mTemplateIds,
3030 'Images' => $this->mImages,
3031 'FileSearchOptions' => $this->mFileSearchOptions,
3032 'ExternalLinks' => $this->mExternalLinks,
3033 'InterwikiLinks' => $this->mInterwikiLinks,
3034 'NewSection' => $this->mNewSection,
3035 'HideNewSection' => $this->mHideNewSection,
3036 'NoGallery' => $this->mNoGallery,
3037 'HeadItems' => $this->mHeadItems,
3038 'Modules' => array_keys( $this->mModuleSet ),
3039 'ModuleStyles' => array_keys( $this->mModuleStyleSet ),
3040 'JsConfigVars' => $this->mJsConfigVars,
3041 'Warnings' => $this->mWarnings,
3042 'Sections' => $this->getSections(),
3043 'Properties' => self::detectAndEncodeBinary( $this->mProperties ),
3044 'Timestamp' => $this->mTimestamp,
3045 'EnableOOUI' => $this->mEnableOOUI,
3046 'IndexPolicy' => $this->getIndexPolicy(),
3047 // may contain arbitrary structures!
3048 'ExtensionData' => $this->mExtensionData,
3049 'LimitReportData' => $this->mLimitReportData,
3050 'LimitReportJSData' => $this->mLimitReportJSData,
3051 'CacheMessage' => $this->mCacheMessage,
3052 'TimeProfile' => $this->mTimeProfile,
3053 'ParseStartTime' => [], // don't serialize this
3054 'PreventClickjacking' => $this->mPreventClickjacking,
3055 'ExtraScriptSrcs' => $this->mExtraScriptSrcs,
3056 'ExtraDefaultSrcs' => $this->mExtraDefaultSrcs,
3057 'ExtraStyleSrcs' => $this->mExtraStyleSrcs,
3058 'Flags' => $this->mFlags + (
3059 // backward-compatibility: distinguish "no sections" from
3060 // "sections not set" (Will be unnecessary after T327439.)
3061 $this->mTOCData === null ? [] : [ 'mw:toc-set' => true ]
3063 'SpeculativeRevId' => $this->mSpeculativeRevId,
3064 'SpeculativePageIdUsed' => $this->speculativePageIdUsed,
3065 'RevisionTimestampUsed' => $this->revisionTimestampUsed,
3066 'RevisionUsedSha1Base36' => $this->revisionUsedSha1Base36,
3067 'WrapperDivClasses' => $this->mWrapperDivClasses,
3070 // Fill in missing fields from parents. Array addition does not override existing fields.
3071 $data += parent::toJsonArray();
3073 // TODO: make more fields optional!
3075 if ( $this->mMaxAdaptiveExpiry !== INF ) {
3076 // NOTE: JSON can't encode infinity!
3077 $data['MaxAdaptiveExpiry'] = $this->mMaxAdaptiveExpiry;
3080 if ( $this->mTOCData ) {
3081 // Temporarily add information from TOCData extension data
3082 // T327439: We should eventually make the entire mTOCData
3083 // serializable
3084 $toc = $this->mTOCData->jsonSerialize();
3085 if ( isset( $toc['extensionData'] ) ) {
3086 $data['TOCExtensionData'] = $toc['extensionData'];
3090 return $data;
3093 public static function newFromJsonArray( JsonDeserializer $deserializer, array $json ): ParserOutput {
3094 $parserOutput = new ParserOutput();
3095 $parserOutput->initFromJson( $deserializer, $json );
3096 return $parserOutput;
3100 * Initialize member fields from an array returned by jsonSerialize().
3101 * @param JsonDeserializer $deserializer
3102 * @param array $jsonData
3104 protected function initFromJson( JsonDeserializer $deserializer, array $jsonData ): void {
3105 parent::initFromJson( $deserializer, $jsonData );
3107 // WARNING: When changing how this class is serialized, follow the instructions
3108 // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
3110 $this->mRawText = $jsonData['Text'];
3111 $this->mLanguageLinkMap = [];
3112 foreach ( ( $jsonData['LanguageLinks'] ?? [] ) as $l ) {
3113 $this->addLanguageLink( $l );
3115 $this->mCategories = $jsonData['Categories'];
3116 $this->mIndicators = $jsonData['Indicators'];
3117 $this->mTitleText = $jsonData['TitleText'];
3118 $this->mLinks = $jsonData['Links'];
3119 $this->mLinksSpecial = $jsonData['LinksSpecial'];
3120 $this->mTemplates = $jsonData['Templates'];
3121 $this->mTemplateIds = $jsonData['TemplateIds'];
3122 $this->mImages = $jsonData['Images'];
3123 $this->mFileSearchOptions = $jsonData['FileSearchOptions'];
3124 $this->mExternalLinks = $jsonData['ExternalLinks'];
3125 $this->mInterwikiLinks = $jsonData['InterwikiLinks'];
3126 $this->mNewSection = $jsonData['NewSection'];
3127 $this->mHideNewSection = $jsonData['HideNewSection'];
3128 $this->mNoGallery = $jsonData['NoGallery'];
3129 $this->mHeadItems = $jsonData['HeadItems'];
3130 $this->mModuleSet = array_fill_keys( $jsonData['Modules'], true );
3131 $this->mModuleStyleSet = array_fill_keys( $jsonData['ModuleStyles'], true );
3132 $this->mJsConfigVars = $jsonData['JsConfigVars'];
3133 $this->mWarnings = $jsonData['Warnings'];
3134 $this->mFlags = $jsonData['Flags'];
3135 if (
3136 $jsonData['Sections'] !== [] ||
3137 // backward-compatibility: distinguish "no sections" from
3138 // "sections not set" (Will be unnecessary after T327439.)
3139 $this->getOutputFlag( 'mw:toc-set' )
3141 $this->setSections( $jsonData['Sections'] );
3142 unset( $this->mFlags['mw:toc-set'] );
3143 if ( isset( $jsonData['TOCExtensionData'] ) ) {
3144 $tocData = $this->getTOCData(); // created by setSections() above
3145 foreach ( $jsonData['TOCExtensionData'] as $key => $value ) {
3146 $tocData->setExtensionData( $key, $value );
3150 $this->mProperties = self::detectAndDecodeBinary( $jsonData['Properties'] );
3151 $this->mTimestamp = $jsonData['Timestamp'];
3152 $this->mEnableOOUI = $jsonData['EnableOOUI'];
3153 $this->setIndexPolicy( $jsonData['IndexPolicy'] );
3154 $this->mExtensionData = $jsonData['ExtensionData'] ?? [];
3155 $this->mLimitReportData = $jsonData['LimitReportData'];
3156 $this->mLimitReportJSData = $jsonData['LimitReportJSData'];
3157 $this->mCacheMessage = $jsonData['CacheMessage'] ?? '';
3158 $this->mParseStartTime = []; // invalid after reloading
3159 $this->mTimeProfile = $jsonData['TimeProfile'] ?? [];
3160 $this->mPreventClickjacking = $jsonData['PreventClickjacking'];
3161 $this->mExtraScriptSrcs = $jsonData['ExtraScriptSrcs'];
3162 $this->mExtraDefaultSrcs = $jsonData['ExtraDefaultSrcs'];
3163 $this->mExtraStyleSrcs = $jsonData['ExtraStyleSrcs'];
3164 $this->mSpeculativeRevId = $jsonData['SpeculativeRevId'];
3165 $this->speculativePageIdUsed = $jsonData['SpeculativePageIdUsed'];
3166 $this->revisionTimestampUsed = $jsonData['RevisionTimestampUsed'];
3167 $this->revisionUsedSha1Base36 = $jsonData['RevisionUsedSha1Base36'];
3168 $this->mWrapperDivClasses = $jsonData['WrapperDivClasses'];
3169 $this->mMaxAdaptiveExpiry = $jsonData['MaxAdaptiveExpiry'] ?? INF;
3173 * Finds any non-utf8 strings in the given array and replaces them with
3174 * an associative array that wraps a base64 encoded version of the data.
3175 * Inverse of detectAndDecodeBinary().
3177 * @param array $properties
3179 * @return array
3181 private static function detectAndEncodeBinary( array $properties ) {
3182 foreach ( $properties as $key => $value ) {
3183 if ( is_string( $value ) ) {
3184 if ( !mb_detect_encoding( $value, 'UTF-8', true ) ) {
3185 $properties[$key] = [
3186 // T313818: This key name conflicts with JsonCodec
3187 '_type_' => 'string',
3188 '_encoding_' => 'base64',
3189 '_data_' => base64_encode( $value ),
3195 return $properties;
3199 * Finds any associative arrays that represent encoded binary strings, and
3200 * replaces them with the decoded binary data.
3202 * @param array $properties
3204 * @return array
3206 private static function detectAndDecodeBinary( array $properties ) {
3207 foreach ( $properties as $key => $value ) {
3208 if ( is_array( $value ) && isset( $value['_encoding_'] ) ) {
3209 if ( $value['_encoding_'] === 'base64' ) {
3210 $properties[$key] = base64_decode( $value['_data_'] );
3215 return $properties;
3218 public function __wakeup() {
3219 $oldAliases = [
3220 // This was the pre-namespace name of the class, which is still
3221 // used in pre-1.42 serialized objects.
3222 'ParserOutput',
3224 // Backwards compatibility, pre 1.36
3225 $priorAccessedOptions = $this->getGhostFieldValue( 'mAccessedOptions', ...$oldAliases );
3226 if ( $priorAccessedOptions ) {
3227 $this->mParseUsedOptions = $priorAccessedOptions;
3229 // Backwards compatibility, pre 1.39
3230 $priorIndexPolicy = $this->getGhostFieldValue( 'mIndexPolicy', ...$oldAliases );
3231 if ( $priorIndexPolicy ) {
3232 $this->setIndexPolicy( $priorIndexPolicy );
3234 // Backwards compatibility, pre 1.40
3235 $mSections = $this->getGhostFieldValue( 'mSections', ...$oldAliases );
3236 if ( $mSections !== null && $mSections !== [] ) {
3237 $this->setSections( $mSections );
3239 // Backwards compatibility, pre 1.42
3240 $mModules = $this->getGhostFieldValue( 'mModules', ...$oldAliases );
3241 if ( $mModules !== null && $mModules !== [] ) {
3242 $this->addModules( $mModules );
3244 // Backwards compatibility, pre 1.42
3245 $mModuleStyles = $this->getGhostFieldValue( 'mModuleStyles', ...$oldAliases );
3246 if ( $mModuleStyles !== null && $mModuleStyles !== [] ) {
3247 $this->addModuleStyles( $mModuleStyles );
3249 // Backwards compatibility, pre 1.42
3250 $mText = $this->getGhostFieldValue( 'mText', ...$oldAliases );
3251 if ( $mText !== null ) {
3252 $this->setRawText( $mText );
3254 // Backwards compatibility, pre 1.42
3255 $ll = $this->getGhostFieldValue( 'mLanguageLinks', ...$oldAliases );
3256 if ( $ll !== null && $ll !== [] ) {
3257 foreach ( $ll as $l ) {
3258 $this->addLanguageLink( $l );
3261 // Backward compatibility with private fields, pre 1.42
3262 $oldPrivateFields = [
3263 'mRawText',
3264 'mCategories',
3265 'mIndicators',
3266 'mTitleText',
3267 'mLinks',
3268 'mLinksSpecial',
3269 'mTemplates',
3270 'mTemplateIds',
3271 'mImages',
3272 'mFileSearchOptions',
3273 'mExternalLinks',
3274 'mInterwikiLinks',
3275 'mNewSection',
3276 'mHideNewSection',
3277 'mNoGallery',
3278 'mHeadItems',
3279 'mModuleSet',
3280 'mModuleStyleSet',
3281 'mJsConfigVars',
3282 'mWarnings',
3283 'mWarningMsgs',
3284 'mTOCData',
3285 'mProperties',
3286 'mTimestamp',
3287 'mEnableOOUI',
3288 'mIndexSet',
3289 'mNoIndexSet',
3290 'mExtensionData',
3291 'mLimitReportData',
3292 'mLimitReportJSData',
3293 'mCacheMessage',
3294 'mParseStartTime',
3295 'mTimeProfile',
3296 'mPreventClickjacking',
3297 'mExtraScriptSrcs',
3298 'mExtraDefaultSrcs',
3299 'mExtraStyleSrcs',
3300 'mFlags',
3301 'mSpeculativeRevId',
3302 'speculativePageIdUsed',
3303 'revisionTimestampUsed',
3304 'revisionUsedSha1Base36',
3305 'mWrapperDivClasses',
3306 'mMaxAdaptiveExpiry',
3308 foreach ( $oldPrivateFields as $f ) {
3309 $this->restoreAliasedGhostField( $f, ...$oldAliases );
3311 $this->clearParseStartTime();
3314 public function __clone() {
3315 // It seems that very little of this object needs to be explicitly deep-cloned
3316 // while keeping copies reasonably separated.
3317 // Most of the non-scalar properties of this object are either
3318 // - (potentially multi-nested) arrays of scalars (which get deep-cloned), or
3319 // - arrays that may contain arbitrary elements (which don't necessarily get
3320 // deep-cloned), but for which no particular care elsewhere is given to
3321 // copying their references around (e.g. mJsConfigVars).
3322 // Hence, we are not going out of our way to ensure that the references to innermost
3323 // objects that may appear in a ParserOutput are unique. If that becomes the
3324 // expectation at any point, this method will require updating as well.
3325 // The exception is TOCData (which is an object), which we clone explicitly.
3326 if ( $this->mTOCData ) {
3327 $this->mTOCData = clone $this->mTOCData;
3332 * Returns the content holder text of the ParserOutput.
3333 * This will eventually be replaced by something like getContentHolder()->getText() when we have a
3334 * ContentHolder/HtmlHolder class.
3335 * @internal
3336 * @unstable
3337 * @return string
3339 public function getContentHolderText(): string {
3340 return $this->getRawText();
3344 * Sets the content holder text of the ParserOutput.
3345 * This will eventually be replaced by something like getContentHolder()->setText() when we have a
3346 * ContentHolder/HtmlHolder class.
3347 * @internal
3348 * @unstable
3350 public function setContentHolderText( string $s ): void {
3351 $this->setRawText( $s );
3354 public function __get( $name ) {
3355 if ( property_exists( get_called_class(), $name ) ) {
3356 // Direct access to a public property, deprecated.
3357 wfDeprecatedMsg( "ParserOutput::{$name} public read access deprecated", '1.38' );
3358 return $this->$name;
3359 } elseif ( property_exists( $this, $name ) ) {
3360 // Dynamic property access, deprecated.
3361 wfDeprecatedMsg( "ParserOutput::{$name} dynamic property read access deprecated", '1.38' );
3362 return $this->$name;
3363 } else {
3364 trigger_error( "Inaccessible property via __get(): $name" );
3365 return null;
3369 public function __set( $name, $value ) {
3370 if ( property_exists( get_called_class(), $name ) ) {
3371 // Direct access to a public property, deprecated.
3372 wfDeprecatedMsg( "ParserOutput::$name public write access deprecated", '1.38' );
3373 $this->$name = $value;
3374 } else {
3375 // Dynamic property access, deprecated.
3376 wfDeprecatedMsg( "ParserOutput::$name dynamic property write access deprecated", '1.38' );
3377 $this->$name = $value;
3382 /** @deprecated class alias since 1.42 */
3383 class_alias( ParserOutput::class, 'ParserOutput' );