Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.26% covered (warning)
85.26%
804 / 943
68.24% covered (warning)
68.24%
101 / 148
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserOutput
85.35% covered (warning)
85.35%
804 / 942
68.24% covered (warning)
68.24%
101 / 148
931.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 hasText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRawText
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getText
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 runOutputPipeline
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 runPipelineInternal
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 addCacheMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addWrapperDivClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearWrapperDivClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWrapperDivClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSpeculativeRevIdUsed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpeculativeRevIdUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSpeculativePageIdUsed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpeculativePageIdUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRevisionTimestampUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionTimestampUsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRevisionUsedSha1Base36
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 getRevisionUsedSha1Base36
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguageLinks
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getInterwikiLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryMap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategorySortKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndicators
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitleText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTOCData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSections
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getLinkList
96.36% covered (success)
96.36%
53 / 55
0.00% covered (danger)
0.00%
0 / 1
20
 getLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasLinks
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getLinksSpecial
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplateIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasImages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFileSearchOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExternalLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNoGallery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNoGallery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeadItems
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModuleStyles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getJsConfigVars
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getWarnings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndexPolicy
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getRevisionTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimitReportData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimitReportJSData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEnableOOUI
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraCSPDefaultSrcs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraCSPScriptSrcs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraCSPStyleSrcs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRawText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLanguageLinks
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 setTitleText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTOCData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSections
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setIndexPolicy
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 setRevisionTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addCategory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setCategories
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIndicator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setEnableOOUI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addLanguageLink
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addWarningMsgVal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addWarningMsg
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
2.12
 setNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHideNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHideNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNewSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLinkInternal
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 addExternalLink
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 addLink
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 addImage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 addTemplate
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
 addInterwikiLink
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 addHeadItem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 addModules
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addModuleStyles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addJsConfigVars
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setJsConfigVar
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 appendJsConfigVar
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 addOutputPageMetadata
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 setDisplayTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDisplayTitle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getLanguage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRedirectHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRedirectHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRenderId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRenderId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAllFlags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPageProperty
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 setNumericPageProperty
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 setUnsortedPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unsetPageProperty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPageProperties
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 setOutputFlag
40.00% covered (danger)
40.00%
10 / 25
0.00% covered (danger)
0.00%
0 / 1
31.60
 getOutputFlag
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 appendOutputStrings
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
10.01
 getOutputStrings
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 setExtensionData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 appendExtensionData
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 getExtensionData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTimes
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 resetParseStartTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 clearParseStartTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recordTimeProfile
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 getTimeProfile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimeSinceStart
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 setLimitReportData
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 hasReducedExpiry
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setPreventClickjacking
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPreventClickjacking
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateRuntimeAdaptiveExpiry
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addExtraCSPDefaultSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addExtraCSPStyleSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addExtraCSPScriptSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 finalizeAdaptiveCacheExpiry
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 setFromParserOptions
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 __sleep
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 mergeInternalMetaDataFrom
70.00% covered (warning)
70.00%
28 / 40
0.00% covered (danger)
0.00%
0 / 1
22.91
 mergeHtmlMetaDataFrom
97.67% covered (success)
97.67%
42 / 43
0.00% covered (danger)
0.00%
0 / 1
14
 mergeTrackingMetaDataFrom
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
7
 collectMetadata
73.81% covered (warning)
73.81%
62 / 84
0.00% covered (danger)
0.00%
0 / 1
76.22
 mergeMixedList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mergeList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mergeMap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mergeMapStrategy
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
12.42
 useEachMinValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 useEachTotalValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 useMaxValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 toJsonArray
98.15% covered (success)
98.15%
53 / 54
0.00% covered (danger)
0.00%
0 / 1
5
 newFromJsonArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 initFromJson
98.11% covered (success)
98.11%
52 / 53
0.00% covered (danger)
0.00%
0 / 1
6
 detectAndEncodeBinary
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 detectAndDecodeBinary
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 __wakeup
100.00% covered (success)
100.00%
74 / 74
100.00% covered (success)
100.00%
1 / 1
14
 __clone
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getContentHolderText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContentHolderText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 __set
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
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.
7 *
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.
12 *
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
17 *
18 * @file
19 */
20
21namespace MediaWiki\Parser;
22
23use InvalidArgumentException;
24use LogicException;
25use MediaWiki\Edit\ParsoidRenderID;
26use MediaWiki\Json\JsonDeserializable;
27use MediaWiki\Json\JsonDeserializableTrait;
28use MediaWiki\Json\JsonDeserializer;
29use MediaWiki\MainConfigNames;
30use MediaWiki\MediaWikiServices;
31use MediaWiki\Output\OutputPage;
32use MediaWiki\Parser\Parsoid\PageBundleParserOutputConverter;
33use MediaWiki\Title\TitleValue;
34use UnexpectedValueException;
35use Wikimedia\Bcp47Code\Bcp47Code;
36use Wikimedia\Bcp47Code\Bcp47CodeValue;
37use Wikimedia\Message\MessageValue;
38use Wikimedia\Parsoid\Core\ContentMetadataCollector;
39use Wikimedia\Parsoid\Core\ContentMetadataCollectorCompat;
40use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
41use Wikimedia\Parsoid\Core\TOCData;
42use Wikimedia\Reflection\GhostFieldAccessTrait;
43
44/**
45 * ParserOutput is a rendering of a Content object or a message.
46 * Content objects and messages often contain wikitext, but not always.
47 *
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.
52 *
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.
57 *
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.
67 *
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.
75 *
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()`.
84 *
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).
90 *
91 * @ingroup Parser
92 */
93class 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;
100
101    /**
102     * Feature flags to indicate to extensions that MediaWiki core supports and
103     * uses getText() stateless transforms.
104     *
105     * @since 1.31
106     */
107    public const SUPPORTS_STATELESS_TRANSFORMS = 1;
108
109    /**
110     * @since 1.31
111     */
112    public const SUPPORTS_UNWRAP_TRANSFORM = 1;
113
114    /**
115     * @internal
116     * @since 1.38
117     */
118    public const MW_MERGE_STRATEGY_KEY = '_mw-strategy';
119
120    /**
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`.
124     *
125     * This constant should be treated as @internal until we expose
126     * alternative merge strategies for external use.
127     * @internal
128     * @since 1.38
129     */
130    public const MW_MERGE_STRATEGY_UNION = 'union';
131
132    /**
133     * @var string|null The output text
134     */
135    private $mRawText = null;
136
137    /**
138     * @var array<string,string> Array mapping interwiki prefix to (non DB key) Titles (e.g. 'fr' => 'Test page')
139     */
140    private $mLanguageLinkMap = [];
141
142    /**
143     * @var array<string,string> Map of category names to sort keys
144     */
145    private $mCategories = [];
146
147    /**
148     * @var array<string,string> Page status indicators, usually displayed in top-right corner.
149     */
150    private $mIndicators = [];
151
152    /**
153     * @var string Title text of the chosen language variant, as HTML.
154     */
155    private $mTitleText;
156
157    /**
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.
160     */
161    private $mLinks = [];
162
163    /**
164     * @var array<string,int> Keys are DBKs for the links to special pages in the document.
165     * @since 1.35
166     */
167    private $mLinksSpecial = [];
168
169    /**
170     * @var array<int,array<string,int>> 2-D map of NS/DBK to ID for the template references.
171     *  ID=zero for broken.
172     */
173    private $mTemplates = [];
174
175    /**
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.
178     */
179    private $mTemplateIds = [];
180
181    /**
182     * @var array<string,int> DB keys of the images used, in the array key only
183     */
184    private $mImages = [];
185
186    /**
187     * @var array<string,array<string,string>> DB keys of the images used mapped to sha1 and MW timestamp.
188     */
189    private $mFileSearchOptions = [];
190
191    /**
192     * @var array<string,int> External link URLs, in the key only.
193     */
194    private array $mExternalLinks = [];
195
196    /**
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.
199     */
200    private $mInterwikiLinks = [];
201
202    /**
203     * @var bool Show a new section link?
204     */
205    private $mNewSection = false;
206
207    /**
208     * @var bool Hide the new section link?
209     */
210    private $mHideNewSection = false;
211
212    /**
213     * @var bool No gallery on category page? (__NOGALLERY__).
214     */
215    private $mNoGallery = false;
216
217    /**
218     * @var string[] Items to put in the <head> section
219     */
220    private $mHeadItems = [];
221
222    /**
223     * @var array<string,true> Modules to be loaded by ResourceLoader
224     */
225    private $mModuleSet = [];
226
227    /**
228     * @var array<string,true> Modules of which only the CSS will be loaded by ResourceLoader.
229     */
230    private $mModuleStyleSet = [];
231
232    /**
233     * @var array JavaScript config variable for mw.config combined with this page.
234     */
235    private $mJsConfigVars = [];
236
237    /**
238     * @var array<string,int> Warning text to be returned to the user.
239     *  Wikitext formatted, in the key only.
240     */
241    private $mWarnings = [];
242
243    /**
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.
247     */
248    private $mWarningMsgs = [];
249
250    /**
251     * @var ?TOCData Table of contents data, or null if it hasn't been set.
252     */
253    private $mTOCData;
254
255    /**
256     * @var array Name/value pairs to be cached in the DB.
257     */
258    private $mProperties = [];
259
260    /**
261     * @var ?string Timestamp of the revision.
262     */
263    private $mTimestamp;
264
265    /**
266     * @var bool Whether OOUI should be enabled.
267     */
268    private $mEnableOOUI = false;
269
270    /**
271     * @var bool Whether the index policy has been set to 'index'.
272     */
273    private $mIndexSet = false;
274
275    /**
276     * @var bool Whether the index policy has been set to 'noindex'.
277     */
278    private $mNoIndexSet = false;
279
280    /**
281     * @var array extra data used by extensions.
282     */
283    private $mExtensionData = [];
284
285    /**
286     * @var array Parser limit report data.
287     */
288    private $mLimitReportData = [];
289
290    /** @var array Parser limit report data for JSON */
291    private $mLimitReportJSData = [];
292
293    /** @var string Debug message added by ParserCache */
294    private $mCacheMessage = '';
295
296    /**
297     * @var array Timestamps for getTimeSinceStart().
298     */
299    private $mParseStartTime = [];
300
301    /**
302     * @var array Durations for getTimeProfile().
303     */
304    private $mTimeProfile = [];
305
306    /**
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.
310     */
311    private $mPreventClickjacking = false;
312
313    /**
314     * @var string[] Extra script-src for CSP
315     */
316    private $mExtraScriptSrcs = [];
317
318    /**
319     * @var string[] Extra default-src for CSP [Everything but script and style]
320     */
321    private $mExtraDefaultSrcs = [];
322
323    /**
324     * @var string[] Extra style-src for CSP
325     */
326    private $mExtraStyleSrcs = [];
327
328    /**
329     * @var array<string,true> Generic flags.
330     */
331    private $mFlags = [];
332
333    private const SPECULATIVE_FIELDS = [
334        'speculativePageIdUsed',
335        'mSpeculativeRevId',
336        'revisionTimestampUsed',
337    ];
338
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;
345
346    /** @var string|null SHA-1 base 36 hash of any self-transclusion */
347    private $revisionUsedSha1Base36;
348
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>
352     */
353    private $mWrapperDivClasses = [];
354
355    /** @var int Upper bound of expiry based on parse duration */
356    private $mMaxAdaptiveExpiry = INF;
357
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)
366
367    /**
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
376     */
377    public function __construct( $text = null, $languageLinks = [], $categoryLinks = [],
378        $unused = false, $titletext = ''
379    ) {
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' );
385        }
386        foreach ( ( $languageLinks ?? [] ) as $ll ) {
387            $this->addLanguageLink( $ll );
388        }
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();
393    }
394
395    /**
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.
399     *
400     * @since 1.32
401     *
402     * @return bool Whether this ParserOutput contains rendered text. If this returns false, the
403     *         ParserOutput contains meta-data only.
404     */
405    public function hasText(): bool {
406        return ( $this->mRawText !== null );
407    }
408
409    /**
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.
413     *
414     * @return string
415     * @since 1.27
416     */
417    public function getRawText() {
418        if ( $this->mRawText === null ) {
419            throw new LogicException( 'This ParserOutput contains no text!' );
420        }
421
422        return $this->mRawText;
423    }
424
425    /**
426     * Get the output HTML
427     *
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!
470     */
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;
481    }
482
483    /**
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.
491     *
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
527     */
528    public function runOutputPipeline( ParserOptions $popts, array $options = [] ): ParserOutput {
529        return $this->runPipelineInternal( $popts, $options );
530    }
531
532    /**
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.
535     */
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 ),
551        ];
552        return $pipeline->run( $this, $popts, $options );
553    }
554
555    /**
556     * Adds a comment notice about cache state to the text of the page
557     * @param string $msg
558     * @internal used by ParserCache
559     */
560    public function addCacheMessage( string $msg ): void {
561        $this->mCacheMessage .= $msg;
562    }
563
564    /**
565     * Add a CSS class to use for the wrapping div. If no class is given, no wrapper is added.
566     *
567     * @param string $class
568     */
569    public function addWrapperDivClass( $class ): void {
570        $this->mWrapperDivClasses[$class] = true;
571    }
572
573    /**
574     * Clears the CSS class to use for the wrapping div, effectively disabling the wrapper div
575     * until addWrapperDivClass() is called.
576     */
577    public function clearWrapperDivClass(): void {
578        $this->mWrapperDivClasses = [];
579    }
580
581    /**
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().
585     *
586     * @return string
587     */
588    public function getWrapperDivClass(): string {
589        return implode( ' ', array_keys( $this->mWrapperDivClasses ) );
590    }
591
592    /**
593     * @param int $id
594     * @since 1.28
595     */
596    public function setSpeculativeRevIdUsed( $id ): void {
597        $this->mSpeculativeRevId = $id;
598    }
599
600    /**
601     * @return int|null
602     * @since 1.28
603     */
604    public function getSpeculativeRevIdUsed(): ?int {
605        return $this->mSpeculativeRevId;
606    }
607
608    /**
609     * @param int $id
610     * @since 1.34
611     */
612    public function setSpeculativePageIdUsed( $id ): void {
613        $this->speculativePageIdUsed = $id;
614    }
615
616    /**
617     * @return int|null
618     * @since 1.34
619     */
620    public function getSpeculativePageIdUsed() {
621        return $this->speculativePageIdUsed;
622    }
623
624    /**
625     * @param string $timestamp TS_MW timestamp
626     * @since 1.34
627     */
628    public function setRevisionTimestampUsed( $timestamp ): void {
629        $this->revisionTimestampUsed = $timestamp;
630    }
631
632    /**
633     * @return string|null TS_MW timestamp or null if not used
634     * @since 1.34
635     */
636    public function getRevisionTimestampUsed() {
637        return $this->revisionTimestampUsed;
638    }
639
640    /**
641     * @param string $hash Lowercase SHA-1 base 36 hash
642     * @since 1.34
643     */
644    public function setRevisionUsedSha1Base36( $hash ): void {
645        if ( $hash === null ) {
646            return; // e.g. RevisionRecord::getSha1() returned null
647        }
648
649        if (
650            $this->revisionUsedSha1Base36 !== null &&
651            $this->revisionUsedSha1Base36 !== $hash
652        ) {
653            $this->revisionUsedSha1Base36 = ''; // mismatched
654        } else {
655            $this->revisionUsedSha1Base36 = $hash;
656        }
657    }
658
659    /**
660     * @return string|null Lowercase SHA-1 base 36 hash, null if unused, or "" on inconsistency
661     * @since 1.34
662     */
663    public function getRevisionUsedSha1Base36() {
664        return $this->revisionUsedSha1Base36;
665    }
666
667    /**
668     * @return string[]
669     * @note Before 1.43, this function returned an array reference.
670     * @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::LANGUAGE)
671     */
672    public function getLanguageLinks() {
673        $result = [];
674        foreach ( $this->mLanguageLinkMap as $lang => $title ) {
675            // T374736: Back-compat with empty prefix; see ::addLanguageLink()
676            $result[] = $title === '|' ? "$lang" : "$lang:$title";
677        }
678        return $result;
679    }
680
681    /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::INTERWIKI) */
682    public function getInterwikiLinks() {
683        return $this->mInterwikiLinks;
684    }
685
686    /**
687     * Return the names of the categories on this page.
688     * Unlike ::getCategories(), sort keys are *not* included in the
689     * return value.
690     * @return array<string> The names of the categories
691     * @since 1.38
692     */
693    public function getCategoryNames(): array {
694        # Note that numeric category names get converted to 'int' when
695        # stored as array keys; stringify the keys to ensure they
696        # return to original string form so as not to confuse callers.
697        return array_map( 'strval', array_keys( $this->mCategories ) );
698    }
699
700    /**
701     * Return category names and sort keys as a map.
702     *
703     * BEWARE that numeric category names get converted to 'int' when stored
704     * as array keys.  Because of this, use of this method is not recommended
705     * in new code; using ::getCategoryNames() and ::getCategorySortKey() will
706     * be less error-prone.
707     *
708     * @return array<string|int,string>
709     * @internal
710     */
711    public function getCategoryMap(): array {
712        return $this->mCategories;
713    }
714
715    /**
716     * Return the sort key for a given category name, or `null` if the
717     * category is not present in this ParserOutput.  Returns the
718     * empty string if the category is to use the default sort key.
719     *
720     * @note The effective sort key in the database may vary from what
721     * is returned here; see note in ParserOutput::addCategory().
722     *
723     * @param string $name The category name
724     * @return ?string The sort key for the category, or `null` if the
725     *  category is not present in this ParserOutput
726     * @since 1.40
727     */
728    public function getCategorySortKey( string $name ): ?string {
729        // This API avoids exposing the fact that numeric string category
730        // names are going to be converted to 'int' when used as array
731        // keys for the `mCategories` field.
732        return $this->mCategories[$name] ?? null;
733    }
734
735    /**
736     * @return string[]
737     * @since 1.25
738     */
739    public function getIndicators(): array {
740        return $this->mIndicators;
741    }
742
743    public function getTitleText() {
744        return $this->mTitleText;
745    }
746
747    /**
748     * @return ?TOCData the table of contents data, or null if it hasn't been
749     * set.
750     */
751    public function getTOCData(): ?TOCData {
752        return $this->mTOCData;
753    }
754
755    /**
756     * @internal
757     * @return string
758     */
759    public function getCacheMessage(): string {
760        return $this->mCacheMessage;
761    }
762
763    /**
764     * @internal
765     * @return array
766     */
767    public function getSections(): array {
768        if ( $this->mTOCData !== null ) {
769            return $this->mTOCData->toLegacy();
770        }
771        // For compatibility
772        return [];
773    }
774
775    /**
776     * Get a list of links of the given type.
777     *
778     * Provides a uniform interface to various lists of links stored in
779     * the metadata.
780     *
781     * Each element of the returned array has a LinkTarget as the 'link'
782     * property.  Local and template links also have 'pageid' set.
783     * Template links have 'revid' set.  Category links have 'sort' set.
784     * Media links optionally have 'time' and 'sha1' set.
785     *
786     * @param string $linkType A link type, which should be a constant from
787     *  ParserOutputLinkTypes.
788     * @return list<array{link:ParsoidLinkTarget,pageid?:int,revid?:int,sort?:string,time?:string|false,sha1?:string|false}>
789     */
790    public function getLinkList( string $linkType ): array {
791        # Note that fragments are dropped for everything except language links
792        $result = [];
793        switch ( $linkType ) {
794            case ParserOutputLinkTypes::CATEGORY:
795                foreach ( $this->mCategories as $dbkey => $sort ) {
796                    $result[] = [
797                        'link' => new TitleValue( NS_CATEGORY, (string)$dbkey ),
798                        'sort' => $sort,
799                    ];
800                }
801                break;
802
803            case ParserOutputLinkTypes::INTERWIKI:
804                foreach ( $this->mInterwikiLinks as $prefix => $arr ) {
805                    foreach ( $arr as $dbkey => $ignore ) {
806                        $result[] = [
807                            'link' => new TitleValue( NS_MAIN, (string)$dbkey, '', (string)$prefix ),
808                        ];
809                    }
810                }
811                break;
812
813            case ParserOutputLinkTypes::LANGUAGE:
814                foreach ( $this->mLanguageLinkMap as $lang => $title ) {
815                    if ( $title === '|' ) {
816                        continue; // T374736
817                    }
818                    # language links can have fragments!
819                    [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
820                    $result[]  = [
821                        'link' => new TitleValue( NS_MAIN, $title, $frag, (string)$lang ),
822                    ];
823                }
824                break;
825
826            case ParserOutputLinkTypes::LOCAL:
827                foreach ( $this->mLinks as $ns => $arr ) {
828                    foreach ( $arr as $dbkey => $id ) {
829                        $result[] = [
830                            'link' => new TitleValue( $ns, (string)$dbkey ),
831                            'pageid' => $id,
832                        ];
833                    }
834                }
835                break;
836
837            case ParserOutputLinkTypes::MEDIA:
838                foreach ( $this->mImages as $dbkey => $ignore ) {
839                    $extra = $this->mFileSearchOptions[$dbkey] ?? [];
840                    $extra['link'] = new TitleValue( NS_FILE, (string)$dbkey );
841                    $result[] = $extra;
842                }
843                break;
844
845            case ParserOutputLinkTypes::SPECIAL:
846                foreach ( $this->mLinksSpecial as $dbkey => $ignore ) {
847                    $result[] = [
848                        'link' => new TitleValue( NS_SPECIAL, (string)$dbkey ),
849                    ];
850                }
851                break;
852
853            case ParserOutputLinkTypes::TEMPLATE:
854                foreach ( $this->mTemplates as $ns => $arr ) {
855                    foreach ( $arr as $dbkey => $pageid ) {
856                        $result[] = [
857                            'link' => new TitleValue( $ns, (string)$dbkey ),
858                            'pageid' => $pageid,
859                            // default to invalid/broken revision if this is not present
860                            'revid' => $this->mTemplateIds[$ns][$dbkey] ?? 0,
861                        ];
862                    }
863                }
864                break;
865
866            default:
867                throw new UnexpectedValueException( "Unknown link type $linkType" );
868        }
869        return $result;
870    }
871
872    /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::LOCAL) */
873    public function &getLinks() {
874        return $this->mLinks;
875    }
876
877    /**
878     * Return true if the given parser output has local links registered
879     * in the metadata.
880     * @return bool
881     * @since 1.44
882     */
883    public function hasLinks(): bool {
884        foreach ( $this->mLinks as $ns => $arr ) {
885            foreach ( $arr as $dbkey => $id ) {
886                return true;
887            }
888        }
889        return false;
890    }
891
892    /**
893     * @return array Keys are DBKs for the links to special pages in the document
894     * @since 1.35
895     * @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::SPECIAL)
896     */
897    public function &getLinksSpecial() {
898        return $this->mLinksSpecial;
899    }
900
901    /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::TEMPLATE) */
902    public function &getTemplates() {
903        return $this->mTemplates;
904    }
905
906    /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::TEMPLATE) */
907    public function &getTemplateIds() {
908        return $this->mTemplateIds;
909    }
910
911    /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::MEDIA) */
912    public function &getImages() {
913        return $this->mImages;
914    }
915
916    /**
917     * Return true if there are image dependencies registered for this
918     * ParserOutput.
919     * @since 1.44
920     */
921    public function hasImages(): bool {
922        return $this->mImages !== [];
923    }
924
925    /** @deprecated since 1.43, use ::getLinkList(ParserOutputLinkTypes::MEDIA) */
926    public function &getFileSearchOptions() {
927        return $this->mFileSearchOptions;
928    }
929
930    /**
931     * @note Use of the reference returned by this method has been
932     *  deprecated since 1.43.  In a future release this will return a
933     *  normal array.  Use ::addExternalLink() to modify the set of
934     *  external links stored in this ParserOutput.
935     */
936    public function &getExternalLinks(): array {
937        return $this->mExternalLinks;
938    }
939
940    public function setNoGallery( $value ): void {
941        $this->mNoGallery = (bool)$value;
942    }
943
944    public function getNoGallery() {
945        return $this->mNoGallery;
946    }
947
948    public function getHeadItems() {
949        return $this->mHeadItems;
950    }
951
952    public function getModules() {
953        return array_keys( $this->mModuleSet );
954    }
955
956    public function getModuleStyles() {
957        return array_keys( $this->mModuleStyleSet );
958    }
959
960    /**
961     * @param bool $showStrategyKeys Defaults to false; if set to true will
962     *  expose the internal `MW_MERGE_STRATEGY_KEY` in the result.  This
963     *  should only be used internally to allow safe merge of config vars.
964     * @return array
965     * @since 1.23
966     */
967    public function getJsConfigVars( bool $showStrategyKeys = false ) {
968        $result = $this->mJsConfigVars;
969        // Don't expose the internal strategy key
970        foreach ( $result as &$value ) {
971            if ( is_array( $value ) && !$showStrategyKeys ) {
972                unset( $value[self::MW_MERGE_STRATEGY_KEY] );
973            }
974        }
975        return $result;
976    }
977
978    public function getWarnings(): array {
979        return array_keys( $this->mWarnings );
980    }
981
982    public function getIndexPolicy(): string {
983        // 'noindex' wins if both are set. (T16899)
984        if ( $this->mNoIndexSet ) {
985            return 'noindex';
986        } elseif ( $this->mIndexSet ) {
987            return 'index';
988        }
989        return '';
990    }
991
992    /**
993     * @return string|null TS_MW timestamp of the revision content
994     */
995    public function getRevisionTimestamp(): ?string {
996        return $this->mTimestamp;
997    }
998
999    /**
1000     * @return string|null TS_MW timestamp of the revision content
1001     * @deprecated since 1.42; use ::getRevisionTimestamp() instead
1002     */
1003    public function getTimestamp() {
1004        return $this->getRevisionTimestamp();
1005    }
1006
1007    public function getLimitReportData() {
1008        return $this->mLimitReportData;
1009    }
1010
1011    public function getLimitReportJSData() {
1012        return $this->mLimitReportJSData;
1013    }
1014
1015    public function getEnableOOUI() {
1016        return $this->mEnableOOUI;
1017    }
1018
1019    /**
1020     * Get extra Content-Security-Policy 'default-src' directives
1021     * @since 1.35
1022     * @return string[]
1023     */
1024    public function getExtraCSPDefaultSrcs() {
1025        return $this->mExtraDefaultSrcs;
1026    }
1027
1028    /**
1029     * Get extra Content-Security-Policy 'script-src' directives
1030     * @since 1.35
1031     * @return string[]
1032     */
1033    public function getExtraCSPScriptSrcs() {
1034        return $this->mExtraScriptSrcs;
1035    }
1036
1037    /**
1038     * Get extra Content-Security-Policy 'style-src' directives
1039     * @since 1.35
1040     * @return string[]
1041     */
1042    public function getExtraCSPStyleSrcs() {
1043        return $this->mExtraStyleSrcs;
1044    }
1045
1046    /**
1047     * Set the raw text of the ParserOutput.
1048     *
1049     * If you did not generate html, pass null to mark it as such.
1050     *
1051     * @since 1.42
1052     * @param string|null $text HTML content of ParserOutput or null if not generated
1053     * @param-taint $text exec_html
1054     */
1055    public function setRawText( ?string $text ): void {
1056        $this->mRawText = $text;
1057    }
1058
1059    /**
1060     * Set the raw text of the ParserOutput.
1061     *
1062     * If you did not generate html, pass null to mark it as such.
1063     *
1064     * @since 1.39 You can now pass null to this function
1065     * @param string|null $text HTML content of ParserOutput or null if not generated
1066     * @param-taint $text exec_html
1067     * @return string|null Previous value of ParserOutput's raw text
1068     * @deprecated since 1.42; use ::setRawText() which matches the getter ::getRawText()
1069     */
1070    public function setText( $text ) {
1071        return wfSetVar( $this->mRawText, $text, true );
1072    }
1073
1074    /**
1075     * @deprecated since 1.42, use ::addLanguageLink() instead.
1076     */
1077    public function setLanguageLinks( $ll ) {
1078        $old = $this->getLanguageLinks();
1079        $this->mLanguageLinkMap = [];
1080        if ( $ll === null ) { // T376323
1081            wfDeprecated( __METHOD__ . ' with null argument', '1.43' );
1082        }
1083        foreach ( ( $ll ?? [] ) as $l ) {
1084            $this->addLanguageLink( $l );
1085        }
1086        return $old;
1087    }
1088
1089    public function setTitleText( $t ) {
1090        return wfSetVar( $this->mTitleText, $t );
1091    }
1092
1093    /**
1094     * @param TOCData $tocData Table of contents data for the page
1095     */
1096    public function setTOCData( TOCData $tocData ): void {
1097        $this->mTOCData = $tocData;
1098    }
1099
1100    /**
1101     * @param array $sectionArray
1102     * @return array Previous value of ::getSections()
1103     */
1104    public function setSections( array $sectionArray ) {
1105        $oldValue = $this->getSections();
1106        $this->setTOCData( TOCData::fromLegacy( $sectionArray ) );
1107        return $oldValue;
1108    }
1109
1110    /**
1111     * Update the index policy of the robots meta tag.
1112     *
1113     * Note that calling this method does not guarantee
1114     * that {@link self::getIndexPolicy()} will return the given policy –
1115     * if different calls set the index policy to 'index' and 'noindex',
1116     * then 'noindex' always wins (T16899), even if the 'index' call happened later.
1117     * If this is not what you want,
1118     * you can reset {@link ParserOutputFlags::NO_INDEX_POLICY} with {@link self::setOutputFlag()}.
1119     *
1120     * @param string $policy 'index' or 'noindex'.
1121     * @return string The previous policy.
1122     */
1123    public function setIndexPolicy( $policy ): string {
1124        $old = $this->getIndexPolicy();
1125        if ( $policy === 'noindex' ) {
1126            $this->mNoIndexSet = true;
1127        } elseif ( $policy === 'index' ) {
1128            $this->mIndexSet = true;
1129        }
1130        return $old;
1131    }
1132
1133    /**
1134     * @param ?string $timestamp TS_MW timestamp of the revision content
1135     */
1136    public function setRevisionTimestamp( ?string $timestamp ): void {
1137        $this->mTimestamp = $timestamp;
1138    }
1139
1140    /**
1141     * @param ?string $timestamp TS_MW timestamp of the revision content
1142     *
1143     * @return ?string The previous value of the timestamp
1144     * @deprecated since 1.42; use ::setRevisionTimestamp() instead
1145     */
1146    public function setTimestamp( $timestamp ) {
1147        return wfSetVar( $this->mTimestamp, $timestamp );
1148    }
1149
1150    /**
1151     * Add a category.
1152     *
1153     * Although ParserOutput::getCategorySortKey() will return exactly
1154     * the sort key you specify here, before storing in the database
1155     * all sort keys will be language converted, HTML entities will be
1156     * decoded, newlines stripped, and then they will be truncated to
1157     * 255 bytes. Thus the "effective" sort key in the DB may be different
1158     * from what is passed to `$sort` here and returned by
1159     * ::getCategorySortKey().
1160     *
1161     * @param string|ParsoidLinkTarget $c The category name
1162     * @param string $sort The sort key; an empty string indicates
1163     *  that the default sort key for the page should be used.
1164     */
1165    public function addCategory( $c, $sort = '' ): void {
1166        if ( $c instanceof ParsoidLinkTarget ) {
1167            $c = $c->getDBkey();
1168        }
1169        $this->mCategories[$c] = $sort;
1170    }
1171
1172    /**
1173     * Overwrite the category map.
1174     * @param array<string,string> $c Map of category names to sort keys
1175     * @since 1.38
1176     */
1177    public function setCategories( array $c ): void {
1178        $this->mCategories = $c;
1179    }
1180
1181    /**
1182     * @param string $id
1183     * @param string $content
1184     * @param-taint $content exec_html
1185     * @since 1.25
1186     */
1187    public function setIndicator( $id, $content ): void {
1188        $this->mIndicators[$id] = $content;
1189    }
1190
1191    /**
1192     * Enables OOUI, if true, in any OutputPage instance this ParserOutput
1193     * object is added to.
1194     *
1195     * @since 1.26
1196     * @param bool $enable If OOUI should be enabled or not
1197     */
1198    public function setEnableOOUI( bool $enable = false ): void {
1199        $this->mEnableOOUI = $enable;
1200    }
1201
1202    /**
1203     * Add a language link.
1204     * @param ParsoidLinkTarget|string $t
1205     */
1206    public function addLanguageLink( $t ): void {
1207        # Note that fragments are preserved
1208        if ( $t instanceof ParsoidLinkTarget ) {
1209            // Language links are unusual in using 'text' rather than 'db key'
1210            // Note that fragments are preserved.
1211            $lang = $t->getInterwiki();
1212            $title = $t->getText();
1213            if ( $t->hasFragment() ) {
1214                $title .= '#' . $t->getFragment();
1215            }
1216        } else {
1217            [ $lang, $title ] = array_pad( explode( ':', $t, 2 ), -2, '' );
1218        }
1219        if ( $lang === '' ) {
1220            // T374736: For backward compatibility with test cases only!
1221            wfDeprecated( __METHOD__ . ' without prefix', '1.43' );
1222            [ $lang, $title ] = [ $title, '|' ]; // | can not occur in valid title
1223        }
1224        $this->mLanguageLinkMap[$lang] ??= $title;
1225    }
1226
1227    /**
1228     * Add a warning to the output for this page.
1229     * @param MessageValue $mv
1230     * @since 1.43
1231     */
1232    public function addWarningMsgVal( MessageValue $mv ) {
1233        // These can eventually be stored as MessageValue directly.
1234        $this->addWarningMsg( $mv->getKey(), ...$mv->getParams() );
1235    }
1236
1237    /**
1238     * Add a warning to the output for this page.
1239     * @param string $msg The localization message key for the warning
1240     * @param mixed|JsonDeserializable ...$args Optional arguments for the
1241     *   message. These arguments must be serializable/deserializable with
1242     *   JsonCodec; see the @note on ParserOutput::setExtensionData()
1243     * @since 1.38
1244     */
1245    public function addWarningMsg( string $msg, ...$args ): void {
1246        // MessageValue objects are defined in core and thus not visible
1247        // to Parsoid or to its ContentMetadataCollector interface.
1248        // Eventually this method (defined in ContentMetadataCollector) should
1249        // call ::addWarningMsgVal() instead of the other way around.
1250
1251        // preserve original arguments in $mWarningMsgs to allow merge
1252        // @todo: these aren't serialized/deserialized yet -- before we
1253        // turn on serialization of $this->mWarningMsgs we need to ensure
1254        // callers aren't passing nonserializable arguments: T343048.
1255        $jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
1256        $path = $jsonCodec->detectNonSerializableData( $args, true );
1257        if ( $path !== null ) {
1258            wfDeprecatedMsg(
1259                "ParserOutput::addWarningMsg() called with nonserializable arguments: $path",
1260                '1.41'
1261            );
1262        }
1263        $this->mWarningMsgs[$msg] = $args;
1264        $s = wfMessage( $msg, ...$args )
1265            // some callers set the title here?
1266            ->inContentLanguage() // because this ends up in cache
1267            ->text();
1268        $this->mWarnings[$s] = 1;
1269    }
1270
1271    public function setNewSection( $value ): void {
1272        $this->mNewSection = (bool)$value;
1273    }
1274
1275    /**
1276     * @param bool $value Hide the new section link?
1277     */
1278    public function setHideNewSection( bool $value ): void {
1279        $this->mHideNewSection = $value;
1280    }
1281
1282    public function getHideNewSection(): bool {
1283        return (bool)$this->mHideNewSection;
1284    }
1285
1286    public function getNewSection(): bool {
1287        return (bool)$this->mNewSection;
1288    }
1289
1290    /**
1291     * Checks, if a url is pointing to the own server
1292     *
1293     * @param string $internal The server to check against
1294     * @param string $url The url to check
1295     * @return bool
1296     * @internal
1297     */
1298    public static function isLinkInternal( $internal, $url ): bool {
1299        return (bool)preg_match( '/^' .
1300            # If server is proto relative, check also for http/https links
1301            ( substr( $internal, 0, 2 ) === '//' ? '(?:https?:)?' : '' ) .
1302            preg_quote( $internal, '/' ) .
1303            # check for query/path/anchor or end of link in each case
1304            '(?:[\?\/\#]|$)/i',
1305            $url
1306        );
1307    }
1308
1309    public function addExternalLink( $url ): void {
1310        # We don't register links pointing to our own server, unless... :-)
1311        $config = MediaWikiServices::getInstance()->getMainConfig();
1312        $server = $config->get( MainConfigNames::Server );
1313        $registerInternalExternals = $config->get( MainConfigNames::RegisterInternalExternals );
1314        # Replace unnecessary URL escape codes with the referenced character
1315        # This prevents spammers from hiding links from the filters
1316        $url = Parser::normalizeLinkUrl( $url );
1317
1318        $registerExternalLink = true;
1319        if ( !$registerInternalExternals ) {
1320            $registerExternalLink = !self::isLinkInternal( $server, $url );
1321        }
1322        if ( $registerExternalLink ) {
1323            $this->mExternalLinks[$url] = 1;
1324        }
1325    }
1326
1327    /**
1328     * Record a local or interwiki inline link for saving in future link tables.
1329     *
1330     * @param ParsoidLinkTarget $link (used to require Title until 1.38)
1331     * @param int|null $id Optional known page_id so we can skip the lookup
1332     */
1333    public function addLink( ParsoidLinkTarget $link, $id = null ): void {
1334        if ( $link->isExternal() ) {
1335            // Don't record interwikis in pagelinks
1336            $this->addInterwikiLink( $link );
1337            return;
1338        }
1339        $ns = $link->getNamespace();
1340        $dbk = $link->getDBkey();
1341        if ( $ns === NS_MEDIA ) {
1342            // Normalize this pseudo-alias if it makes it down here...
1343            $ns = NS_FILE;
1344        } elseif ( $ns === NS_SPECIAL ) {
1345            // We don't want to record Special: links in the database, so put them in a separate place.
1346            // It might actually be wise to, but we'd need to do some normalization.
1347            $this->mLinksSpecial[$dbk] = 1;
1348            return;
1349        } elseif ( $dbk === '' ) {
1350            // Don't record self links -  [[#Foo]]
1351            return;
1352        }
1353        if ( $id === null ) {
1354            // T357048: This actually kills performance; we should batch these.
1355            $page = MediaWikiServices::getInstance()->getPageStore()->getPageForLink( $link );
1356            $id = $page->getId();
1357        }
1358        $this->mLinks[$ns][$dbk] = $id;
1359    }
1360
1361    /**
1362     * Register a file dependency for this output
1363     * @param string|ParsoidLinkTarget $name Title dbKey
1364     * @param string|false|null $timestamp MW timestamp of file creation (or false if non-existing)
1365     * @param string|false|null $sha1 Base 36 SHA-1 of file (or false if non-existing)
1366     */
1367    public function addImage( $name, $timestamp = null, $sha1 = null ): void {
1368        if ( $name instanceof ParsoidLinkTarget ) {
1369            $name = $name->getDBkey();
1370        }
1371        $this->mImages[$name] = 1;
1372        if ( $timestamp !== null && $sha1 !== null ) {
1373            $this->mFileSearchOptions[$name] = [ 'time' => $timestamp, 'sha1' => $sha1 ];
1374        }
1375    }
1376
1377    /**
1378     * Register a template dependency for this output
1379     *
1380     * @param ParsoidLinkTarget $link (used to require Title until 1.38)
1381     * @param int $page_id
1382     * @param int $rev_id
1383     */
1384    public function addTemplate( $link, $page_id, $rev_id ): void {
1385        if ( $link->isExternal() ) {
1386            // Will throw an InvalidArgumentException in a future release.
1387            wfDeprecated( __METHOD__ . " with interwiki link", '1.42' );
1388            return;
1389        }
1390        $ns = $link->getNamespace();
1391        $dbk = $link->getDBkey();
1392        // T357048: Parsoid doesn't have page_id
1393        $this->mTemplates[$ns][$dbk] = $page_id;
1394        $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
1395    }
1396
1397    /**
1398     * @param ParsoidLinkTarget $link must be an interwiki link
1399     *       (used to require Title until 1.38).
1400     */
1401    public function addInterwikiLink( $link ): void {
1402        if ( !$link->isExternal() ) {
1403            throw new InvalidArgumentException( 'Non-interwiki link passed, internal parser error.' );
1404        }
1405        $prefix = $link->getInterwiki();
1406        $this->mInterwikiLinks[$prefix][$link->getDBkey()] = 1;
1407    }
1408
1409    /**
1410     * Add some text to the "<head>".
1411     * If $tag is set, the section with that tag will only be included once
1412     * in a given page.
1413     * @param string $section
1414     * @param string|false $tag
1415     */
1416    public function addHeadItem( $section, $tag = false ): void {
1417        if ( $tag !== false ) {
1418            $this->mHeadItems[$tag] = $section;
1419        } else {
1420            $this->mHeadItems[] = $section;
1421        }
1422    }
1423
1424    /**
1425     * @see OutputPage::addModules
1426     * @param string[] $modules
1427     */
1428    public function addModules( array $modules ): void {
1429        $modules = array_fill_keys( $modules, true );
1430        $this->mModuleSet = array_merge( $this->mModuleSet, $modules );
1431    }
1432
1433    /**
1434     * @see OutputPage::addModuleStyles
1435     * @param string[] $modules
1436     */
1437    public function addModuleStyles( array $modules ): void {
1438        $modules = array_fill_keys( $modules, true );
1439        $this->mModuleStyleSet = array_merge( $this->mModuleStyleSet, $modules );
1440    }
1441
1442    /**
1443     * Add one or more variables to be set in mw.config in JavaScript.
1444     *
1445     * @param string|array $keys Key or array of key/value pairs.
1446     * @param mixed|null $value [optional] Value of the configuration variable.
1447     * @since 1.23
1448     * @deprecated since 1.38, use ::setJsConfigVar() or ::appendJsConfigVar()
1449     *  which ensures compatibility with asynchronous parsing; emitting warnings
1450     *  since 1.43.
1451     */
1452    public function addJsConfigVars( $keys, $value = null ): void {
1453        wfDeprecated( __METHOD__, '1.38' );
1454        if ( is_array( $keys ) ) {
1455            foreach ( $keys as $key => $value ) {
1456                $this->mJsConfigVars[$key] = $value;
1457            }
1458            return;
1459        }
1460
1461        $this->mJsConfigVars[$keys] = $value;
1462    }
1463
1464    /**
1465     * Add a variable to be set in mw.config in JavaScript.
1466     *
1467     * In order to ensure the result is independent of the parse order, the values
1468     * set here must be unique -- that is, you can pass the same $key
1469     * multiple times but ONLY if the $value is identical each time.
1470     * If you want to collect multiple pieces of data under a single key,
1471     * use ::appendJsConfigVar().
1472     *
1473     * @param string $key Key to use under mw.config
1474     * @param mixed|null $value Value of the configuration variable.
1475     * @since 1.38
1476     */
1477    public function setJsConfigVar( string $key, $value ): void {
1478        if (
1479            array_key_exists( $key, $this->mJsConfigVars ) &&
1480            $this->mJsConfigVars[$key] !== $value
1481        ) {
1482            // Ensure that a key is mapped to only a single value in order
1483            // to prevent the resulting array from varying if content
1484            // is parsed in a different order.
1485            throw new InvalidArgumentException( "Multiple conflicting values given for $key" );
1486        }
1487        $this->mJsConfigVars[$key] = $value;
1488    }
1489
1490    /**
1491     * Append a value to a variable to be set in mw.config in JavaScript.
1492     *
1493     * In order to ensure the result is independent of the parse order,
1494     * the value of this key will be an associative array, mapping all of
1495     * the values set under that key to true.  (The array is implicitly
1496     * ordered in PHP, but you should treat it as unordered.)
1497     * If you want a non-array type for the key, and can ensure that only
1498     * a single value will be set, you should use ::setJsConfigVar() instead.
1499     *
1500     * @param string $key Key to use under mw.config
1501     * @param string $value Value to append to the configuration variable.
1502     * @param string $strategy Merge strategy:
1503     *  only MW_MERGE_STRATEGY_UNION is currently supported and external callers
1504     *  should treat this parameter as @internal at this time and omit it.
1505     * @since 1.38
1506     */
1507    public function appendJsConfigVar(
1508        string $key,
1509        string $value,
1510        string $strategy = self::MW_MERGE_STRATEGY_UNION
1511    ): void {
1512        if ( $strategy !== self::MW_MERGE_STRATEGY_UNION ) {
1513            throw new InvalidArgumentException( "Unknown merge strategy $strategy." );
1514        }
1515        if ( !array_key_exists( $key, $this->mJsConfigVars ) ) {
1516            $this->mJsConfigVars[$key] = [
1517                // Indicate how these values are to be merged.
1518                self::MW_MERGE_STRATEGY_KEY => $strategy,
1519            ];
1520        } elseif ( !is_array( $this->mJsConfigVars[$key] ) ) {
1521            throw new InvalidArgumentException( "Mixing set and append for $key" );
1522        } elseif ( ( $this->mJsConfigVars[$key][self::MW_MERGE_STRATEGY_KEY] ?? null ) !== $strategy ) {
1523            throw new InvalidArgumentException( "Conflicting merge strategies for $key" );
1524        }
1525        $this->mJsConfigVars[$key][$value] = true;
1526    }
1527
1528    /**
1529     * Accommodate very basic transcluding of a temporary OutputPage object into parser output.
1530     *
1531     * This is a fragile method that cannot be relied upon in any meaningful way.
1532     * It exists solely to support the wikitext feature of transcluding a SpecialPage, and
1533     * only has to work for that use case to ensure relevant styles are loaded, and that
1534     * essential config vars needed between SpecialPage and a JS feature are added.
1535     *
1536     * This relies on there being no overlap between modules or config vars added by
1537     * the SpecialPage and those added by parser extensions. If there is overlap,
1538     * then arise and break one or both sides. This is expected and unsupported.
1539     *
1540     * @internal For use by Parser for basic special page transclusion
1541     * @param OutputPage $out
1542     */
1543    public function addOutputPageMetadata( OutputPage $out ): void {
1544        // This should eventually use the same merge mechanism used
1545        // internally to merge ParserOutputs together.
1546        // (ie: $this->mergeHtmlMetaDataFrom( $out->getMetadata() )
1547        // once preventClickjacking, moduleStyles, modules, jsconfigvars,
1548        // and head items are moved to OutputPage::$metadata)
1549
1550        // Take the strictest click-jacking policy. This is to ensure any one-click features
1551        // such as patrol or rollback on the transcluded special page will result in the wiki page
1552        // disallowing embedding in cross-origin iframes. Articles are generally allowed to be
1553        // embedded. Pages that transclude special pages are expected to be user pages or
1554        // other non-content pages that content re-users won't discover or care about.
1555        $this->mPreventClickjacking = $this->mPreventClickjacking || $out->getPreventClickjacking();
1556
1557        $this->addModuleStyles( $out->getModuleStyles() );
1558
1559        // TODO: Figure out if style modules suffice, or whether the below is needed as well.
1560        // Are there special pages that permit transcluding/including and also have JS modules
1561        // that should be activate on the host page?
1562        $this->addModules( $out->getModules() );
1563        $this->mJsConfigVars = self::mergeMapStrategy(
1564            $this->mJsConfigVars, $out->getJsConfigVars()
1565        );
1566        $this->mHeadItems = array_merge( $this->mHeadItems, $out->getHeadItemsArray() );
1567    }
1568
1569    /**
1570     * Override the title to be used for display
1571     *
1572     * @note this is assumed to have been validated
1573     * (check equal normalisation, etc.)
1574     *
1575     * @note this is expected to be safe HTML,
1576     * ready to be served to the client.
1577     *
1578     * @param string $text Desired title text
1579     */
1580    public function setDisplayTitle( $text ): void {
1581        $this->setTitleText( $text );
1582        $this->setPageProperty( 'displaytitle', $text );
1583    }
1584
1585    /**
1586     * Get the title to be used for display.
1587     *
1588     * As per the contract of setDisplayTitle(), this is safe HTML,
1589     * ready to be served to the client.
1590     *
1591     * @return string|false HTML
1592     */
1593    public function getDisplayTitle() {
1594        $t = $this->getTitleText();
1595        if ( $t === '' ) {
1596            return false;
1597        }
1598        return $t;
1599    }
1600
1601    /**
1602     * Get the primary language code of the output.
1603     *
1604     * This returns the primary language of the output, including
1605     * any LanguageConverter variant applied.
1606     *
1607     * NOTE: This may differ from the wiki's default content language
1608     * ($wgLanguageCode, MediaWikiServices::getContentLanguage), because
1609     * each page may have its own "page language" set (PageStoreRecord,
1610     * Title::getDbPageLanguageCode, ContentHandler::getPageLanguage).
1611     *
1612     * NOTE: This may differ from the "page language" when parsing
1613     * user interface messages, in which case this reflects the user
1614     * language (including any variant preference).
1615     *
1616     * NOTE: This may differ from the Parser's "target language" that was
1617     * set while the Parser was parsing the page, because the final output
1618     * is converted to the current user's preferred LanguageConverter variant
1619     * (assuming this is a variant of the target language).
1620     * See Parser::getTargetLanguageConverter()->getPreferredVariant(); use
1621     * LanguageFactory::getParentLanguage() on the language code to obtain
1622     * the base language code. LanguageConverter::getPreferredVariant()
1623     * depends on the global RequestContext for the URL and the User
1624     * language preference.
1625     *
1626     * Finally, note that a single ParserOutput object may contain
1627     * HTML content in multiple different languages and directions
1628     * (T114640). Authors of wikitext and of parser extensions are
1629     * expected to mark such subtrees with a `lang` attribute (set to
1630     * a BCP-47 value, see Language::toBcp47Code()) and a corresponding
1631     * `dir` attribute (see Language::getDir()). This method returns
1632     * the language code for wrapper of the HTML content.
1633     *
1634     * @see Parser::internalParseHalfParsed
1635     * @since 1.40
1636     * @return ?Bcp47Code The primary language for this output,
1637     *   or `null` if a language was not set.
1638     */
1639    public function getLanguage(): ?Bcp47Code {
1640        // This information is temporarily stored in extension data (T303329)
1641        $code = $this->getExtensionData( 'core:target-lang-variant' );
1642        // This is null if the ParserOutput was cached by MW 1.40 or earlier,
1643        // or not constructed by Parser/ParserCache.
1644        return $code === null ? null : new Bcp47CodeValue( $code );
1645    }
1646
1647    /**
1648     * Set the primary language of the output.
1649     *
1650     * See the discussion and caveats in ::getLanguage().
1651     *
1652     * @param Bcp47Code $lang The primary language for this output, including
1653     *   any variant specification.
1654     * @since 1.40
1655     */
1656    public function setLanguage( Bcp47Code $lang ): void {
1657        $this->setExtensionData( 'core:target-lang-variant', $lang->toBcp47Code() );
1658    }
1659
1660    /**
1661     * Return an HTML prefix to be applied on redirect pages, or null
1662     * if this is not a redirect.
1663     * @return ?string HTML to prepend to redirect pages, or null
1664     * @internal
1665     */
1666    public function getRedirectHeader(): ?string {
1667        return $this->getExtensionData( 'core:redirect-header' );
1668    }
1669
1670    /**
1671     * Set an HTML prefix to be applied on redirect pages.
1672     * @param string $html HTML to prepend to redirect pages
1673     */
1674    public function setRedirectHeader( string $html ): void {
1675        $this->setExtensionData( 'core:redirect-header', $html );
1676    }
1677
1678    /**
1679     * Store a unique rendering id for this ParserOutput.  This is used
1680     * whenever a client needs to record a dependency on a specific parse.
1681     * It is typically set only when a parser output is cached.
1682     *
1683     * @param string $renderId a UUID identifying a specific parse
1684     * @internal
1685     */
1686    public function setRenderId( string $renderId ): void {
1687        $this->setExtensionData( 'core:render-id', $renderId );
1688    }
1689
1690    /**
1691     * Return the unique rendering id for this ParserOutput. This is used
1692     * whenever a client needs to record a dependency on a specific parse.
1693     *
1694     * @return string|null
1695     * @internal
1696     */
1697    public function getRenderId(): ?string {
1698        // Backward-compatibility with old cache contents
1699        // Can be removed after parser cache contents have expired
1700        $old = $this->getExtensionData( 'parsoid-render-id' );
1701        if ( $old !== null ) {
1702            return ParsoidRenderId::newFromKey( $old )->getUniqueID();
1703        }
1704        return $this->getExtensionData( 'core:render-id' );
1705    }
1706
1707    /**
1708     * @return string[] List of flags signifying special cases
1709     * @internal
1710     */
1711    public function getAllFlags(): array {
1712        return array_keys( $this->mFlags );
1713    }
1714
1715    /**
1716     * Set a page property to be stored in the page_props database table.
1717     *
1718     * page_props is a key-value store indexed by the page ID. This allows
1719     * the parser to set a property on a page which can then be quickly
1720     * retrieved given the page ID or via a DB join when given the page
1721     * title.
1722     *
1723     * Since 1.23, page_props are also indexed by numeric value, to allow
1724     * for efficient "top k" queries of pages wrt a given property.
1725     * This only works if the value is passed as a int, float, or
1726     * bool. Since 1.42 you should use ::setNumericPageProperty()
1727     * if you want your page property value to be indexed, which will ensure
1728     * that the value is of the proper type.
1729     *
1730     * setPageProperty() is thus used to propagate properties from the parsed
1731     * page to request contexts other than a page view of the currently parsed
1732     * article.
1733     *
1734     * Some applications examples:
1735     *
1736     *   * To implement hidden categories, hiding pages from category listings
1737     *     by storing a page property.
1738     *
1739     *   * Overriding the displayed article title (ParserOutput::setDisplayTitle()).
1740     *
1741     *   * To implement image tagging, for example displaying an icon on an
1742     *     image thumbnail to indicate that it is listed for deletion on
1743     *     Wikimedia Commons.
1744     *     This is not actually implemented, yet but would be pretty cool.
1745     *
1746     * @note Use of non-scalar values (anything other than
1747     *  `string|int|float|bool`) has been deprecated in 1.42.
1748     *  Although any JSON-serializable value can be stored/fetched in
1749     *  ParserOutput, when the values are stored to the database
1750     *  (in `deferred/LinksUpdate/PagePropsTable.php`) they will be
1751     *  converted: booleans will be converted to '0' and '1', null
1752     *  will become '', and everything else will be cast to string
1753     *  (not JSON-serialized).  Page properties obtained from the
1754     *  PageProps service will thus always be strings.
1755     *
1756     * @note The sort key stored in the database *will be NULL* unless
1757     *  the value passed here is an `int|float|bool`.  If you *do not*
1758     *  want your property *value* indexed and sorted (for example, the
1759     *  value is a title string which can be numeric but only
1760     *  incidentally, like when it gets retrieved from an array key)
1761     *  be sure to cast to string or use
1762     *  `::setUnsortedPageProperty()`.  If you *do* want your property
1763     *  *value* indexed and sorted, you should use
1764     *  `::setNumericPageProperty()` instead as this will ensure the
1765     *  value type is correct. Note that either way it is possible to
1766     *  efficiently look up all the pages with a certain property; we
1767     *  are only talking about sorting the *values* assigned to the
1768     *  property, for example for a "top N values of the property"
1769     *  query.
1770     *
1771     * @note Note that `::getPageProperty()`/`::setPageProperty()` do
1772     *  not do any conversions themselves; you should therefore be
1773     *  careful to distinguish values returned from the PageProp
1774     *  service (always strings) from values retrieved from a
1775     *  ParserOutput.
1776     *
1777     * @note Do not use setPageProperty() to set a property which is only used
1778     * in a context where the ParserOutput object itself is already available,
1779     * for example a normal page view. There is no need to save such a property
1780     * in the database since the text is already parsed; use
1781     * ::setExtensionData() instead.
1782     *
1783     * @par Example:
1784     * @code
1785     *    $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
1786     * @endcode
1787     *
1788     * And then later, in OutputPageParserOutput or similar:
1789     *
1790     * @par Example:
1791     * @code
1792     *    $output->getExtensionData( 'my_ext_foo' );
1793     * @endcode
1794     *
1795     * @note The use of `null` as a value is deprecated since 1.42; use
1796     * the empty string instead if you need a placeholder value, or
1797     * ::unsetPageProperty() if you mean to remove a page property.
1798     *
1799     * @note The use of non-string values is deprecated since 1.42; if you
1800     * need an page property value with a sort index
1801     * use ::setNumericPageProperty().
1802     *
1803     * @param string $name
1804     * @param ?scalar $value
1805     * @since 1.38
1806     */
1807    public function setPageProperty( string $name, $value ): void {
1808        if ( $value === null ) {
1809            // Use an empty string instead.
1810            wfDeprecated( __METHOD__ . " with null value for $name", '1.42' );
1811        } elseif ( !is_scalar( $value ) ) {
1812            // Use ::setExtensionData() instead.
1813            wfDeprecated( __METHOD__ . " with non-scalar value for $name", '1.42' );
1814        } elseif ( !is_string( $value ) ) {
1815            // Use ::setNumericPageProperty() instead.
1816            wfDeprecated( __METHOD__ . " with non-string value for $name", '1.42' );
1817        }
1818        $this->mProperties[$name] = $value;
1819    }
1820
1821    /**
1822     * Set a numeric page property whose *value* is intended to be sorted
1823     * and indexed.  The sort key used for the property will be the value,
1824     * coerced to a number.
1825     *
1826     * See `::setPageProperty()` for details.
1827     *
1828     * In the future, we may allow the value to be specified independent
1829     * of sort key (T357783).
1830     *
1831     * @param string $propName The name of the page property
1832     * @param int|float|string $numericValue the numeric value
1833     * @since 1.42
1834     */
1835    public function setNumericPageProperty( string $propName, $numericValue ): void {
1836        if ( !is_numeric( $numericValue ) ) {
1837            throw new \TypeError( __METHOD__ . " with non-numeric value" );
1838        }
1839        // Coerce numeric sort key to a number.
1840        $this->mProperties[$propName] = 0 + $numericValue;
1841    }
1842
1843    /**
1844     * Set a page property whose *value* is not intended to be sorted and
1845     * indexed.
1846     *
1847     * See `::setPageProperty()` for details.  It is recommended to
1848     * use the empty string if you need a placeholder value (ie, if
1849     * it is the *presence* of the property which is important, not
1850     * the *value* the property is set to).
1851     *
1852     * It is still possible to efficiently look up all the pages with
1853     * a certain property (the "presence" of it *is* indexed; see
1854     * Special:PagesWithProp, list=pageswithprop).
1855     *
1856     * @param string $propName The name of the page property
1857     * @param string $value Optional value; defaults to the empty string.
1858     * @since 1.42
1859     */
1860    public function setUnsortedPageProperty( string $propName, string $value = '' ): void {
1861        $this->mProperties[$propName] = $value;
1862    }
1863
1864    /**
1865     * Look up a page property.
1866     * @param string $name The page property name to look up.
1867     * @return ?scalar The value previously set using
1868     * ::setPageProperty(), ::setUnsortedPageProperty(), or
1869     * ::setNumericPageProperty().
1870     * Returns null if no value was set for the given property name.
1871     *
1872     * @note You would need to use ::getPageProperties() to test for an
1873     *  explicitly-set null value; but see the note in ::setPageProperty()
1874     *  deprecating the use of null values.
1875     * @since 1.38
1876     */
1877    public function getPageProperty( string $name ) {
1878        return $this->mProperties[$name] ?? null;
1879    }
1880
1881    /**
1882     * Remove a page property.
1883     * @param string $name The page property name.
1884     * @since 1.38
1885     */
1886    public function unsetPageProperty( string $name ): void {
1887        unset( $this->mProperties[$name] );
1888    }
1889
1890    /**
1891     * Return all the page properties set on this ParserOutput.
1892     * @return array<string,?scalar>
1893     * @since 1.38
1894     */
1895    public function getPageProperties(): array {
1896        if ( !isset( $this->mProperties ) ) {
1897            $this->mProperties = [];
1898        }
1899        return $this->mProperties;
1900    }
1901
1902    /**
1903     * Provides a uniform interface to various boolean flags stored
1904     * in the ParserOutput.  Flags internal to MediaWiki core should
1905     * have names which are constants in ParserOutputFlags.  Extensions
1906     * should use ::setExtensionData() rather than creating new flags
1907     * with ::setOutputFlag() in order to prevent namespace conflicts.
1908     *
1909     * Flags are always combined with OR.  That is, the flag is set in
1910     * the resulting ParserOutput if the flag is set in *any* of the
1911     * fragments composing the ParserOutput.
1912     *
1913     * @note The combination policy means that a ParserOutput may end
1914     * up with both INDEX_POLICY and NO_INDEX_POLICY set.  It is
1915     * expected that NO_INDEX_POLICY "wins" in that case. (T16899)
1916     * (This resolution is implemented in ::getIndexPolicy().)
1917     *
1918     * @param string $name A flag name
1919     * @param bool $val
1920     * @since 1.38
1921     */
1922    public function setOutputFlag( string $name, bool $val = true ): void {
1923        switch ( $name ) {
1924            case ParserOutputFlags::NO_GALLERY:
1925                $this->setNoGallery( $val );
1926                break;
1927
1928            case ParserOutputFlags::ENABLE_OOUI:
1929                $this->setEnableOOUI( $val );
1930                break;
1931
1932            case ParserOutputFlags::NO_INDEX_POLICY:
1933                $this->mNoIndexSet = $val;
1934                break;
1935
1936            case ParserOutputFlags::INDEX_POLICY:
1937                $this->mIndexSet = $val;
1938                break;
1939
1940            case ParserOutputFlags::NEW_SECTION:
1941                $this->setNewSection( $val );
1942                break;
1943
1944            case ParserOutputFlags::HIDE_NEW_SECTION:
1945                $this->setHideNewSection( $val );
1946                break;
1947
1948            case ParserOutputFlags::PREVENT_CLICKJACKING:
1949                $this->setPreventClickjacking( $val );
1950                break;
1951
1952            default:
1953                if ( $val ) {
1954                    $this->mFlags[$name] = true;
1955                } else {
1956                    unset( $this->mFlags[$name] );
1957                }
1958                break;
1959        }
1960    }
1961
1962    /**
1963     * Provides a uniform interface to various boolean flags stored
1964     * in the ParserOutput.  Flags internal to MediaWiki core should
1965     * have names which are constants in ParserOutputFlags.  Extensions
1966     * should only use ::getOutputFlag() to query flags defined in
1967     * ParserOutputFlags in core; they should use ::getExtensionData()
1968     * to define their own flags.
1969     *
1970     * @param string $name A flag name
1971     * @return bool The flag value
1972     * @since 1.38
1973     */
1974    public function getOutputFlag( string $name ): bool {
1975        switch ( $name ) {
1976            case ParserOutputFlags::NO_GALLERY:
1977                return $this->getNoGallery();
1978
1979            case ParserOutputFlags::ENABLE_OOUI:
1980                return $this->getEnableOOUI();
1981
1982            case ParserOutputFlags::INDEX_POLICY:
1983                return $this->mIndexSet;
1984
1985            case ParserOutputFlags::NO_INDEX_POLICY:
1986                return $this->mNoIndexSet;
1987
1988            case ParserOutputFlags::NEW_SECTION:
1989                return $this->getNewSection();
1990
1991            case ParserOutputFlags::HIDE_NEW_SECTION:
1992                return $this->getHideNewSection();
1993
1994            case ParserOutputFlags::PREVENT_CLICKJACKING:
1995                return $this->getPreventClickjacking();
1996
1997            default:
1998                return isset( $this->mFlags[$name] );
1999
2000        }
2001    }
2002
2003    /**
2004     * Provides a uniform interface to various string sets stored
2005     * in the ParserOutput.  String sets internal to MediaWiki core should
2006     * have names which are constants in ParserOutputStringSets.  Extensions
2007     * should use ::appendExtensionData() rather than creating new string sets
2008     * with ::appendOutputStrings() in order to prevent namespace conflicts.
2009     *
2010     * @param string $name A string set name
2011     * @param string[] $value
2012     * @since 1.41
2013     */
2014    public function appendOutputStrings( string $name, array $value ): void {
2015        switch ( $name ) {
2016            case ParserOutputStringSets::MODULE:
2017                $this->addModules( $value );
2018                break;
2019            case ParserOutputStringSets::MODULE_STYLE:
2020                $this->addModuleStyles( $value );
2021                break;
2022            case ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC:
2023                foreach ( $value as $v ) {
2024                    $this->addExtraCSPDefaultSrc( $v );
2025                }
2026                break;
2027            case ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC:
2028                foreach ( $value as $v ) {
2029                    $this->addExtraCSPScriptSrc( $v );
2030                }
2031                break;
2032            case ParserOutputStringSets::EXTRA_CSP_STYLE_SRC:
2033                foreach ( $value as $v ) {
2034                    $this->addExtraCSPStyleSrc( $v );
2035                }
2036                break;
2037            default:
2038                throw new UnexpectedValueException( "Unknown output string set name $name" );
2039        }
2040    }
2041
2042    /**
2043     * Provides a uniform interface to various boolean string sets stored
2044     * in the ParserOutput.  String sets internal to MediaWiki core should
2045     * have names which are constants in ParserOutputStringSets.  Extensions
2046     * should only use ::getOutputStrings() to query string sets defined in
2047     * ParserOutputStringSets in core; they should use ::appendExtensionData()
2048     * to define their own string sets.
2049     *
2050     * @param string $name A string set name
2051     * @return string[] The string set value
2052     * @since 1.41
2053     */
2054    public function getOutputStrings( string $name ): array {
2055        switch ( $name ) {
2056            case ParserOutputStringSets::MODULE:
2057                return $this->getModules();
2058            case ParserOutputStringSets::MODULE_STYLE:
2059                return $this->getModuleStyles();
2060            case ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC:
2061                return $this->getExtraCSPDefaultSrcs();
2062            case ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC:
2063                return $this->getExtraCSPScriptSrcs();
2064            case ParserOutputStringSets::EXTRA_CSP_STYLE_SRC:
2065                return $this->getExtraCSPStyleSrcs();
2066            default:
2067                throw new UnexpectedValueException( "Unknown output string set name $name" );
2068        }
2069    }
2070
2071    /**
2072     * Attaches arbitrary data to this ParserObject. This can be used to store some information in
2073     * the ParserOutput object for later use during page output. The data will be cached along with
2074     * the ParserOutput object, but unlike data set using setPageProperty(), it is not recorded in the
2075     * database.
2076     *
2077     * This method is provided to overcome the unsafe practice of attaching extra information to a
2078     * ParserObject by directly assigning member variables.
2079     *
2080     * To use setExtensionData() to pass extension information from a hook inside the parser to a
2081     * hook in the page output, use this in the parser hook:
2082     *
2083     * @par Example:
2084     * @code
2085     *    $parser->getOutput()->setExtensionData( 'my_ext_foo', '...' );
2086     * @endcode
2087     *
2088     * And then later, in OutputPageParserOutput or similar:
2089     *
2090     * @par Example:
2091     * @code
2092     *    $output->getExtensionData( 'my_ext_foo' );
2093     * @endcode
2094     *
2095     * In MediaWiki 1.20 and older, you have to use a custom member variable
2096     * within the ParserOutput object:
2097     *
2098     * @par Example:
2099     * @code
2100     *    $parser->getOutput()->my_ext_foo = '...';
2101     * @endcode
2102     *
2103     * @note Only scalar values, e.g. numbers, strings, arrays or MediaWiki\Json\JsonDeserializable
2104     * instances are supported as a value. Attempt to set other class instance as extension data
2105     * will break ParserCache for the page.
2106     *
2107     * @note Since MW 1.38 the practice of setting conflicting values for
2108     * the same key has been deprecated.  As with ::setJsConfigVar(), if
2109     * you set the same key multiple times on a ParserOutput, it is expected
2110     * that the value will be identical each time.  If you want to collect
2111     * multiple pieces of data under a single key, use ::appendExtensionData().
2112     *
2113     * @param string $key The key for accessing the data. Extensions should take care to avoid
2114     *   conflicts in naming keys. It is suggested to use the extension's name as a prefix.
2115     *
2116     * @param mixed|JsonDeserializable $value The value to set.
2117     *   Setting a value to null is equivalent to removing the value.
2118     * @since 1.21
2119     */
2120    public function setExtensionData( $key, $value ): void {
2121        if (
2122            array_key_exists( $key, $this->mExtensionData ) &&
2123            $this->mExtensionData[$key] !== $value
2124        ) {
2125            // This behavior was deprecated in 1.38.  We will eventually
2126            // emit a warning here, then throw an exception.
2127        }
2128        if ( $value === null ) {
2129            unset( $this->mExtensionData[$key] );
2130        } else {
2131            $this->mExtensionData[$key] = $value;
2132        }
2133    }
2134
2135    /**
2136     * Appends arbitrary data to this ParserObject. This can be used
2137     * to store some information in the ParserOutput object for later
2138     * use during page output. The data will be cached along with the
2139     * ParserOutput object, but unlike data set using
2140     * setPageProperty(), it is not recorded in the database.
2141     *
2142     * See ::setExtensionData() for more details on rationale and use.
2143     *
2144     * In order to provide for out-of-order/asynchronous/incremental
2145     * parsing, this method appends values to a set.  See
2146     * ::setExtensionData() for the flag-like version of this method.
2147     *
2148     * @note Only values which can be array keys are currently supported
2149     * as values.
2150     *
2151     * @param string $key The key for accessing the data. Extensions should take care to avoid
2152     *   conflicts in naming keys. It is suggested to use the extension's name as a prefix.
2153     *
2154     * @param int|string $value The value to append to the list.
2155     * @param string $strategy Merge strategy:
2156     *  only MW_MERGE_STRATEGY_UNION is currently supported and external callers
2157     *  should treat this parameter as @internal at this time and omit it.
2158     * @since 1.38
2159     */
2160    public function appendExtensionData(
2161        string $key,
2162        $value,
2163        string $strategy = self::MW_MERGE_STRATEGY_UNION
2164    ): void {
2165        if ( $strategy !== self::MW_MERGE_STRATEGY_UNION ) {
2166            throw new InvalidArgumentException( "Unknown merge strategy $strategy." );
2167        }
2168        if ( !array_key_exists( $key, $this->mExtensionData ) ) {
2169            $this->mExtensionData[$key] = [
2170                // Indicate how these values are to be merged.
2171                self::MW_MERGE_STRATEGY_KEY => $strategy,
2172            ];
2173        } elseif ( !is_array( $this->mExtensionData[$key] ) ) {
2174            throw new InvalidArgumentException( "Mixing set and append for $key" );
2175        } elseif ( ( $this->mExtensionData[$key][self::MW_MERGE_STRATEGY_KEY] ?? null ) !== $strategy ) {
2176            throw new InvalidArgumentException( "Conflicting merge strategies for $key" );
2177        }
2178        $this->mExtensionData[$key][$value] = true;
2179    }
2180
2181    /**
2182     * Gets extensions data previously attached to this ParserOutput using setExtensionData().
2183     * Typically, such data would be set while parsing the page, e.g. by a parser function.
2184     *
2185     * @since 1.21
2186     *
2187     * @param string $key The key to look up.
2188     *
2189     * @return mixed|null The value previously set for the given key using setExtensionData()
2190     *         or null if no value was set for this key.
2191     */
2192    public function getExtensionData( $key ) {
2193        $value = $this->mExtensionData[$key] ?? null;
2194        if ( is_array( $value ) ) {
2195            // Don't expose our internal merge strategy key.
2196            unset( $value[self::MW_MERGE_STRATEGY_KEY] );
2197        }
2198        return $value;
2199    }
2200
2201    private static function getTimes( $clock = null ): array {
2202        $ret = [];
2203        if ( !$clock || $clock === 'wall' ) {
2204            $ret['wall'] = microtime( true );
2205        }
2206        if ( !$clock || $clock === 'cpu' ) {
2207            $ru = getrusage( 0 /* RUSAGE_SELF */ );
2208            $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
2209            $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
2210        }
2211        return $ret;
2212    }
2213
2214    /**
2215     * Resets the parse start timestamps for future calls to getTimeSinceStart()
2216     * and recordTimeProfile().
2217     *
2218     * @since 1.22
2219     */
2220    public function resetParseStartTime(): void {
2221        $this->mParseStartTime = self::getTimes();
2222        $this->mTimeProfile = [];
2223    }
2224
2225    /**
2226     * Unset the parse start time.
2227     *
2228     * This is intended for testing purposes only, in order to avoid
2229     * spurious differences between testing outputs created at different
2230     * times.
2231     *
2232     * @since 1.43
2233     */
2234    public function clearParseStartTime(): void {
2235        $this->mParseStartTime = [];
2236    }
2237
2238    /**
2239     * Record the time since resetParseStartTime() was last called.
2240     * The recorded time can be accessed using getTimeProfile().
2241     *
2242     * After resetParseStartTime() was called, the first call to recordTimeProfile()
2243     * will record the time profile. Subsequent calls to recordTimeProfile() will have
2244     * no effect until resetParseStartTime() is called again.
2245     *
2246     * @since 1.42
2247     */
2248    public function recordTimeProfile() {
2249        if ( !$this->mParseStartTime ) {
2250            // If resetParseStartTime was never called, there is nothing to record
2251            return;
2252        }
2253
2254        if ( $this->mTimeProfile !== [] ) {
2255            // Don't override the times recorded by the previous call to recordTimeProfile().
2256            return;
2257        }
2258
2259        $now = self::getTimes();
2260        $this->mTimeProfile = [
2261            'wall' => $now['wall'] - $this->mParseStartTime['wall'],
2262            'cpu' => $now['cpu'] - $this->mParseStartTime['cpu'],
2263        ];
2264    }
2265
2266    /**
2267     * Returns the time that elapsed between the most recent call to resetParseStartTime()
2268     * and the first call to recordTimeProfile() after that.
2269     *
2270     * Clocks available are:
2271     *  - wall: Wall clock time
2272     *  - cpu: CPU time (requires getrusage)
2273     *
2274     * If recordTimeProfile() has noit been called since the most recent call to
2275     * resetParseStartTime(), or if resetParseStartTime() was never called, then
2276     * this method will return null.
2277     *
2278     * @param string $clock
2279     *
2280     * @since 1.42
2281     * @return float|null
2282     */
2283    public function getTimeProfile( string $clock ) {
2284        return $this->mTimeProfile[ $clock ] ?? null;
2285    }
2286
2287    /**
2288     * Returns the time since resetParseStartTime() was last called
2289     *
2290     * Clocks available are:
2291     *  - wall: Wall clock time
2292     *  - cpu: CPU time (requires getrusage)
2293     *
2294     * @since 1.22
2295     * @deprecated since 1.42, use getTimeProfile() instead.
2296     * @param string $clock
2297     * @return float|null
2298     */
2299    public function getTimeSinceStart( $clock ) {
2300        wfDeprecated( __METHOD__, '1.42' );
2301
2302        if ( !isset( $this->mParseStartTime[$clock] ) ) {
2303            return null;
2304        }
2305
2306        $end = self::getTimes( $clock );
2307        return $end[$clock] - $this->mParseStartTime[$clock];
2308    }
2309
2310    /**
2311     * Sets parser limit report data for a key
2312     *
2313     * The key is used as the prefix for various messages used for formatting:
2314     *  - $key: The label for the field in the limit report
2315     *  - $key-value-text: Message used to format the value in the "NewPP limit
2316     *      report" HTML comment. If missing, uses $key-format.
2317     *  - $key-value-html: Message used to format the value in the preview
2318     *      limit report table. If missing, uses $key-format.
2319     *  - $key-value: Message used to format the value. If missing, uses "$1".
2320     *
2321     * Note that all values are interpreted as wikitext, and so should be
2322     * encoded with htmlspecialchars() as necessary, but should avoid complex
2323     * HTML for display in the "NewPP limit report" comment.
2324     *
2325     * @since 1.22
2326     * @param string $key Message key
2327     * @param mixed $value Appropriate for Message::params()
2328     */
2329    public function setLimitReportData( $key, $value ): void {
2330        $this->mLimitReportData[$key] = $value;
2331
2332        if ( is_array( $value ) ) {
2333            if ( array_keys( $value ) === [ 0, 1 ]
2334                && is_numeric( $value[0] )
2335                && is_numeric( $value[1] )
2336            ) {
2337                $data = [ 'value' => $value[0], 'limit' => $value[1] ];
2338            } else {
2339                $data = $value;
2340            }
2341        } else {
2342            $data = $value;
2343        }
2344
2345        if ( strpos( $key, '-' ) ) {
2346            [ $ns, $name ] = explode( '-', $key, 2 );
2347            $this->mLimitReportJSData[$ns][$name] = $data;
2348        } else {
2349            $this->mLimitReportJSData[$key] = $data;
2350        }
2351    }
2352
2353    /**
2354     * Check whether the cache TTL was lowered from the site default.
2355     *
2356     * When content is determined by more than hard state (e.g. page edits),
2357     * such as template/file transclusions based on the current timestamp or
2358     * extension tags that generate lists based on queries, this return true.
2359     *
2360     * This method mainly exists to facilitate the logic in
2361     * WikiPage::triggerOpportunisticLinksUpdate. As such, beware that reducing the TTL for
2362     * reasons that do not relate to "dynamic content", may have the side-effect of incurring
2363     * more RefreshLinksJob executions.
2364     *
2365     * @internal For use by Parser and WikiPage
2366     * @since 1.37
2367     * @return bool
2368     */
2369    public function hasReducedExpiry(): bool {
2370        $parserCacheExpireTime = MediaWikiServices::getInstance()->getMainConfig()->get(
2371            MainConfigNames::ParserCacheExpireTime );
2372
2373        return $this->getCacheExpiry() < $parserCacheExpireTime;
2374    }
2375
2376    /**
2377     * Set the prevent-clickjacking flag.  If set this will cause an
2378     * `X-Frame-Options` header appropriate for edit pages to be sent.
2379     * The header value is controlled by `$wgEditPageFrameOptions`.
2380     *
2381     * This is the default for special pages.  If you display a CSRF-protected
2382     * form on an ordinary view page, then you need to call this function
2383     * with `$flag = true`.
2384     *
2385     * @param bool $flag New flag value
2386     * @since 1.38
2387     */
2388    public function setPreventClickjacking( bool $flag ): void {
2389        $this->mPreventClickjacking = $flag;
2390    }
2391
2392    /**
2393     * Get the prevent-clickjacking flag.
2394     *
2395     * @return bool Flag value
2396     * @since 1.38
2397     * @see ::setPreventClickjacking
2398     */
2399    public function getPreventClickjacking(): bool {
2400        return $this->mPreventClickjacking;
2401    }
2402
2403    /**
2404     * Lower the runtime adaptive TTL to at most this value
2405     *
2406     * @param int $ttl
2407     * @since 1.28
2408     */
2409    public function updateRuntimeAdaptiveExpiry( $ttl ): void {
2410        $this->mMaxAdaptiveExpiry = min( $ttl, $this->mMaxAdaptiveExpiry );
2411        $this->updateCacheExpiry( $ttl );
2412    }
2413
2414    /**
2415     * Add an extra value to Content-Security-Policy default-src directive
2416     *
2417     * Call this if you are including a resource (e.g. image) from a third party domain.
2418     * This is used for all source types except style and script.
2419     *
2420     * @since 1.35
2421     * @param string $src CSP source e.g. example.com
2422     */
2423    public function addExtraCSPDefaultSrc( $src ): void {
2424        $this->mExtraDefaultSrcs[] = $src;
2425    }
2426
2427    /**
2428     * Add an extra value to Content-Security-Policy style-src directive
2429     *
2430     * @since 1.35
2431     * @param string $src CSP source e.g. example.com
2432     */
2433    public function addExtraCSPStyleSrc( $src ): void {
2434        $this->mExtraStyleSrcs[] = $src;
2435    }
2436
2437    /**
2438     * Add an extra value to Content-Security-Policy script-src directive
2439     *
2440     * Call this if you are loading third-party Javascript
2441     *
2442     * @since 1.35
2443     * @param string $src CSP source e.g. example.com
2444     */
2445    public function addExtraCSPScriptSrc( $src ): void {
2446        $this->mExtraScriptSrcs[] = $src;
2447    }
2448
2449    /**
2450     * Call this when parsing is done to lower the TTL based on low parse times
2451     *
2452     * @since 1.28
2453     */
2454    public function finalizeAdaptiveCacheExpiry(): void {
2455        if ( is_infinite( $this->mMaxAdaptiveExpiry ) ) {
2456            return; // not set
2457        }
2458
2459        $runtime = $this->getTimeProfile( 'wall' );
2460        if ( is_float( $runtime ) ) {
2461            $slope = ( self::SLOW_AR_TTL - self::FAST_AR_TTL )
2462                / ( self::PARSE_SLOW_SEC - self::PARSE_FAST_SEC );
2463            // SLOW_AR_TTL = PARSE_SLOW_SEC * $slope + $point
2464            $point = self::SLOW_AR_TTL - self::PARSE_SLOW_SEC * $slope;
2465
2466            $adaptiveTTL = min(
2467                max( $slope * $runtime + $point, self::MIN_AR_TTL ),
2468                $this->mMaxAdaptiveExpiry
2469            );
2470            $this->updateCacheExpiry( $adaptiveTTL );
2471        }
2472    }
2473
2474    /**
2475     * Transfer parser options which affect post-processing from ParserOptions
2476     * to this ParserOutput.
2477     * @param ParserOptions $parserOptions
2478     */
2479    public function setFromParserOptions( ParserOptions $parserOptions ) {
2480        // Copied from Parser.php::parse and should probably be abstracted
2481        // into the parent base class (probably as part of T236809)
2482        // Wrap non-interface parser output in a <div> so it can be targeted
2483        // with CSS (T37247)
2484        $class = $parserOptions->getWrapOutputClass();
2485        if ( $class !== false && !$parserOptions->getInterfaceMessage() ) {
2486            $this->addWrapperDivClass( $class );
2487        }
2488
2489        // Record whether we should suppress section edit links
2490        if ( $parserOptions->getSuppressSectionEditLinks() ) {
2491            $this->setOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS );
2492        }
2493
2494        // Record whether we should wrap sections for collapsing them
2495        if ( $parserOptions->getCollapsibleSections() ) {
2496            $this->setOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS );
2497        }
2498
2499        // Record whether this is a preview parse in the output (T341010)
2500        if ( $parserOptions->getIsPreview() ) {
2501            $this->setOutputFlag( ParserOutputFlags::IS_PREVIEW, true );
2502            // Ensure that previews aren't cacheable, just to be safe.
2503            $this->updateCacheExpiry( 0 );
2504        }
2505    }
2506
2507    public function __sleep() {
2508        return array_filter( array_keys( get_object_vars( $this ) ),
2509            static function ( $field ) {
2510                if ( $field === 'mParseStartTime' || $field === 'mWarningMsgs' ) {
2511                    return false;
2512                }
2513                // Unserializing unknown private fields in HHVM causes
2514                // member variables with nulls in their names (T229366)
2515                return strpos( $field, "\0" ) === false;
2516            }
2517        );
2518    }
2519
2520    /**
2521     * Merges internal metadata such as flags, accessed options, and profiling info
2522     * from $source into this ParserOutput. This should be used whenever the state of $source
2523     * has any impact on the state of this ParserOutput.
2524     *
2525     * @param ParserOutput $source
2526     */
2527    public function mergeInternalMetaDataFrom( ParserOutput $source ): void {
2528        $this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter
2529        $this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getRevisionTimestamp() );
2530        if ( $source->hasCacheTime() ) {
2531            $sourceCacheTime = $source->getCacheTime();
2532            if (
2533                !$this->hasCacheTime() ||
2534                // "undocumented use of -1 to mean not cacheable"
2535                // deprecated, but still supported by ::setCacheTime()
2536                strval( $sourceCacheTime ) === '-1' ||
2537                (
2538                    strval( $this->getCacheTime() ) !== '-1' &&
2539                    // use newer of the two times
2540                    $this->getCacheTime() < $sourceCacheTime
2541                )
2542            ) {
2543                $this->setCacheTime( $sourceCacheTime );
2544            }
2545        }
2546        if ( $source->getRenderId() !== null ) {
2547            // Final render ID should be a function of all component POs
2548            $rid = ( $this->getRenderId() ?? '' ) . $source->getRenderId();
2549            $this->setRenderId( $rid );
2550        }
2551        if ( $source->getCacheRevisionId() !== null ) {
2552            $sourceCacheRevisionId = $source->getCacheRevisionId();
2553            $thisCacheRevisionId = $this->getCacheRevisionId();
2554            if ( $thisCacheRevisionId === null ) {
2555                $this->setCacheRevisionId( $sourceCacheRevisionId );
2556            } elseif ( $sourceCacheRevisionId !== $thisCacheRevisionId ) {
2557                // May throw an exception here in the future
2558                wfDeprecated(
2559                    __METHOD__ . ": conflicting revision IDs " .
2560                    "$thisCacheRevisionId and $sourceCacheRevisionId"
2561                );
2562            }
2563        }
2564
2565        foreach ( self::SPECULATIVE_FIELDS as $field ) {
2566            if ( $this->$field && $source->$field && $this->$field !== $source->$field ) {
2567                wfLogWarning( __METHOD__ . ": inconsistent '$field' properties!" );
2568            }
2569            $this->$field = $this->useMaxValue( $this->$field, $source->$field );
2570        }
2571
2572        $this->mParseStartTime = $this->useEachMinValue(
2573            $this->mParseStartTime,
2574            $source->mParseStartTime
2575        );
2576
2577        $this->mTimeProfile = $this->useEachTotalValue(
2578            $this->mTimeProfile,
2579            $source->mTimeProfile
2580        );
2581
2582        $this->mFlags = self::mergeMap( $this->mFlags, $source->mFlags );
2583        $this->mParseUsedOptions = self::mergeMap( $this->mParseUsedOptions, $source->mParseUsedOptions );
2584
2585        // TODO: maintain per-slot limit reports!
2586        if ( !$this->mLimitReportData ) {
2587            $this->mLimitReportData = $source->mLimitReportData;
2588        }
2589        if ( !$this->mLimitReportJSData ) {
2590            $this->mLimitReportJSData = $source->mLimitReportJSData;
2591        }
2592    }
2593
2594    /**
2595     * Merges HTML metadata such as head items, JS config vars, and HTTP cache control info
2596     * from $source into this ParserOutput. This should be used whenever the HTML in $source
2597     * has been somehow merged into the HTML of this ParserOutput.
2598     *
2599     * @param ParserOutput $source
2600     */
2601    public function mergeHtmlMetaDataFrom( ParserOutput $source ): void {
2602        // HTML and HTTP
2603        $this->mHeadItems = self::mergeMixedList( $this->mHeadItems, $source->getHeadItems() );
2604        $this->addModules( $source->getModules() );
2605        $this->addModuleStyles( $source->getModuleStyles() );
2606        $this->mJsConfigVars = self::mergeMapStrategy( $this->mJsConfigVars, $source->mJsConfigVars );
2607        $this->mMaxAdaptiveExpiry = min( $this->mMaxAdaptiveExpiry, $source->mMaxAdaptiveExpiry );
2608        $this->mExtraStyleSrcs = self::mergeList(
2609            $this->mExtraStyleSrcs,
2610            $source->getExtraCSPStyleSrcs()
2611        );
2612        $this->mExtraScriptSrcs = self::mergeList(
2613            $this->mExtraScriptSrcs,
2614            $source->getExtraCSPScriptSrcs()
2615        );
2616        $this->mExtraDefaultSrcs = self::mergeList(
2617            $this->mExtraDefaultSrcs,
2618            $source->getExtraCSPDefaultSrcs()
2619        );
2620
2621        // "noindex" always wins!
2622        $this->mIndexSet = $this->mIndexSet || $source->mIndexSet;
2623        $this->mNoIndexSet = $this->mNoIndexSet || $source->mNoIndexSet;
2624
2625        // Skin control
2626        $this->mNewSection = $this->mNewSection || $source->getNewSection();
2627        $this->mHideNewSection = $this->mHideNewSection || $source->getHideNewSection();
2628        $this->mNoGallery = $this->mNoGallery || $source->getNoGallery();
2629        $this->mEnableOOUI = $this->mEnableOOUI || $source->getEnableOOUI();
2630        $this->mPreventClickjacking = $this->mPreventClickjacking || $source->getPreventClickjacking();
2631
2632        $tocData = $this->getTOCData();
2633        $sourceTocData = $source->getTOCData();
2634        if ( $tocData !== null ) {
2635            if ( $sourceTocData !== null ) {
2636                // T327429: Section merging is broken, since it doesn't respect
2637                // global numbering, but there are tests which expect section
2638                // metadata to be concatenated.
2639                // There should eventually be a deprecation warning here.
2640                foreach ( $sourceTocData->getSections() as $s ) {
2641                    $tocData->addSection( $s );
2642                }
2643            }
2644        } elseif ( $sourceTocData !== null ) {
2645            $this->setTOCData( $sourceTocData );
2646        }
2647
2648        // XXX: we don't want to concatenate title text, so first write wins.
2649        // We should use the first *modified* title text, but we don't have the original to check.
2650        if ( $this->mTitleText === null || $this->mTitleText === '' ) {
2651            $this->mTitleText = $source->mTitleText;
2652        }
2653
2654        // class names are stored in array keys
2655        $this->mWrapperDivClasses = self::mergeMap(
2656            $this->mWrapperDivClasses,
2657            $source->mWrapperDivClasses
2658        );
2659
2660        // NOTE: last write wins, same as within one ParserOutput
2661        $this->mIndicators = self::mergeMap( $this->mIndicators, $source->getIndicators() );
2662
2663        // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
2664        // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
2665        // kinds of extension data to be merged in different ways.
2666        $this->mExtensionData = self::mergeMapStrategy(
2667            $this->mExtensionData,
2668            $source->mExtensionData
2669        );
2670    }
2671
2672    /**
2673     * Merges dependency tracking metadata such as backlinks, images used, and extension data
2674     * from $source into this ParserOutput. This allows dependency tracking to be done for the
2675     * combined output of multiple content slots.
2676     *
2677     * @param ParserOutput $source
2678     */
2679    public function mergeTrackingMetaDataFrom( ParserOutput $source ): void {
2680        foreach (
2681            $source->getLinkList( ParserOutputLinkTypes::LANGUAGE )
2682            as [ 'link' => $link ]
2683        ) {
2684            $this->addLanguageLink( $link );
2685        }
2686        $this->mCategories = self::mergeMap( $this->mCategories, $source->getCategoryMap() );
2687        foreach (
2688            $source->getLinkList( ParserOutputLinkTypes::LOCAL )
2689            as [ 'link' => $link, 'pageid' => $pageid ]
2690        ) {
2691            $this->addLink( $link, $pageid );
2692        }
2693        foreach (
2694            $source->getLinkList( ParserOutputLinkTypes::TEMPLATE )
2695                as [ 'link' => $link, 'pageid' => $pageid, 'revid' => $revid ]
2696        ) {
2697            $this->addTemplate( $link, $pageid, $revid );
2698        }
2699        foreach (
2700            $source->getLinkList( ParserOutputLinkTypes::MEDIA ) as $item
2701        ) {
2702            $this->addImage( $item['link'], $item['time'] ?? null, $item['sha1'] ?? null );
2703        }
2704        $this->mExternalLinks = self::mergeMap( $this->mExternalLinks, $source->getExternalLinks() );
2705        foreach (
2706            $source->getLinkList( ParserOutputLinkTypes::INTERWIKI )
2707            as [ 'link' => $link ]
2708        ) {
2709            $this->addInterwikiLink( $link );
2710        }
2711
2712        foreach (
2713            $source->getLinkList( ParserOutputLinkTypes::SPECIAL )
2714            as [ 'link' => $link ]
2715        ) {
2716            $this->addLink( $link );
2717        }
2718
2719        // TODO: add a $mergeStrategy parameter to setPageProperty to allow different
2720        // kinds of properties to be merged in different ways.
2721        // (Model this after ::appendJsConfigVar(); use ::mergeMapStrategy here)
2722        $this->mProperties = self::mergeMap( $this->mProperties, $source->getPageProperties() );
2723
2724        // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
2725        $this->mExtensionData = self::mergeMapStrategy(
2726            $this->mExtensionData,
2727            $source->mExtensionData
2728        );
2729    }
2730
2731    /**
2732     * Adds the metadata collected in this ParserOutput to the supplied
2733     * ContentMetadataCollector.  This is similar to ::mergeHtmlMetaDataFrom()
2734     * but in the opposite direction, since ParserOutput is read/write while
2735     * ContentMetadataCollector is write-only.
2736     *
2737     * @param ContentMetadataCollector $metadata
2738     * @since 1.38
2739     */
2740    public function collectMetadata( ContentMetadataCollector $metadata ): void {
2741        // Uniform handling of all boolean flags: they are OR'ed together.
2742        $flags = array_keys(
2743            $this->mFlags + array_flip( ParserOutputFlags::cases() )
2744        );
2745        foreach ( $flags as $name ) {
2746            if ( $this->getOutputFlag( $name ) ) {
2747                $metadata->setOutputFlag( $name );
2748            }
2749        }
2750
2751        // Uniform handling of string sets: they are unioned.
2752        // (This includes modules, style modes, and CSP src.)
2753        foreach ( ParserOutputStringSets::cases() as $name ) {
2754            $metadata->appendOutputStrings(
2755                $name, $this->getOutputStrings( $name )
2756            );
2757        }
2758
2759        foreach ( $this->mCategories as $cat => $key ) {
2760            // Numeric category strings are going to come out of the
2761            // `mCategories` array as ints; cast back to string.
2762            // Also convert back to a LinkTarget!
2763            $lt = TitleValue::tryNew( NS_CATEGORY, (string)$cat );
2764            $metadata->addCategory( $lt, $key );
2765        }
2766
2767        foreach ( $this->mLinks as $ns => $arr ) {
2768            foreach ( $arr as $dbk => $id ) {
2769                // Numeric titles are going to come out of the
2770                // `mLinks` array as ints; cast back to string.
2771                $lt = TitleValue::tryNew( $ns, (string)$dbk );
2772                $metadata->addLink( $lt, $id );
2773            }
2774        }
2775
2776        foreach ( $this->mInterwikiLinks as $prefix => $arr ) {
2777            foreach ( $arr as $dbk => $ignore ) {
2778                $lt = TitleValue::tryNew( NS_MAIN, (string)$dbk, '', $prefix );
2779                $metadata->addLink( $lt );
2780            }
2781        }
2782
2783        foreach ( $this->mLinksSpecial as $dbk => $ignore ) {
2784            // Numeric titles are going to come out of the
2785            // `mLinks` array as ints; cast back to string.
2786            $lt = TitleValue::tryNew( NS_SPECIAL, (string)$dbk );
2787            $metadata->addLink( $lt );
2788        }
2789
2790        foreach ( $this->mImages as $name => $ignore ) {
2791            // Numeric titles come out of mImages as ints.
2792            $lt = TitleValue::tryNew( NS_FILE, (string)$name );
2793            $props = $this->mFileSearchOptions[$name] ?? [];
2794            $metadata->addImage( $lt, $props['time'] ?? null, $props['sha1'] ?? null );
2795        }
2796
2797        foreach ( $this->mLanguageLinkMap as $lang => $title ) {
2798            if ( $title === '|' ) {
2799                continue; // T374736: not a valid language link
2800            }
2801            # language links can have fragments!
2802            [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' );
2803            $lt = TitleValue::tryNew( NS_MAIN, $title, $frag, (string)$lang );
2804            $metadata->addLanguageLink( $lt );
2805        }
2806
2807        foreach ( $this->mJsConfigVars as $key => $value ) {
2808            if ( is_array( $value ) && isset( $value[self::MW_MERGE_STRATEGY_KEY] ) ) {
2809                $strategy = $value[self::MW_MERGE_STRATEGY_KEY];
2810                foreach ( $value as $item => $ignore ) {
2811                    if ( $item !== self::MW_MERGE_STRATEGY_KEY ) {
2812                        $metadata->appendJsConfigVar( $key, $item, $strategy );
2813                    }
2814                }
2815            } elseif ( $metadata instanceof ParserOutput &&
2816                array_key_exists( $key, $metadata->mJsConfigVars )
2817            ) {
2818                // This behavior is deprecated, will likely result in
2819                // incorrect output, and we'll eventually emit a
2820                // warning here---but at the moment this is usually
2821                // caused by limitations in Parsoid and/or use of
2822                // the ParserAfterParse hook: T303015#7770480
2823                $metadata->mJsConfigVars[$key] = $value;
2824            } else {
2825                $metadata->setJsConfigVar( $key, $value );
2826            }
2827        }
2828        foreach ( $this->mExtensionData as $key => $value ) {
2829            if ( is_array( $value ) && isset( $value[self::MW_MERGE_STRATEGY_KEY] ) ) {
2830                $strategy = $value[self::MW_MERGE_STRATEGY_KEY];
2831                foreach ( $value as $item => $ignore ) {
2832                    if ( $item !== self::MW_MERGE_STRATEGY_KEY ) {
2833                        $metadata->appendExtensionData( $key, $item, $strategy );
2834                    }
2835                }
2836            } elseif ( $metadata instanceof ParserOutput &&
2837                array_key_exists( $key, $metadata->mExtensionData )
2838            ) {
2839                // This behavior is deprecated, will likely result in
2840                // incorrect output, and we'll eventually emit a
2841                // warning here---but at the moment this is usually
2842                // caused by limitations in Parsoid and/or use of
2843                // the ParserAfterParse hook: T303015#7770480
2844                $metadata->mExtensionData[$key] = $value;
2845            } else {
2846                $metadata->setExtensionData( $key, $value );
2847            }
2848        }
2849        foreach ( $this->mExternalLinks as $url => $ignore ) {
2850            $metadata->addExternalLink( $url );
2851        }
2852        foreach ( $this->mProperties as $prop => $value ) {
2853            if ( is_numeric( $value ) ) {
2854                $metadata->setNumericPageProperty( $prop, $value );
2855            } elseif ( is_string( $value ) ) {
2856                $metadata->setUnsortedPageProperty( $prop, $value );
2857            } else {
2858                // Deprecated, but there are still sites which call
2859                // ::setPageProperty() with "unusual" values (T374046)
2860                $metadata->setPageProperty( $prop, $value );
2861            }
2862        }
2863        foreach ( $this->mWarningMsgs as $msg => $args ) {
2864            $metadata->addWarningMsg( $msg, ...$args );
2865        }
2866        foreach ( $this->mLimitReportData as $key => $value ) {
2867            $metadata->setLimitReportData( $key, $value );
2868        }
2869        foreach ( $this->mIndicators as $id => $content ) {
2870            $metadata->setIndicator( $id, $content );
2871        }
2872
2873        // ParserOutput-only fields; maintained "behind the curtain"
2874        // since Parsoid doesn't have to know about them.
2875        //
2876        // In production use, the $metadata supplied to this method
2877        // will almost always be an instance of ParserOutput, passed to
2878        // Parsoid by core when parsing begins and returned to core by
2879        // Parsoid as a ContentMetadataCollector (Parsoid's name for
2880        // ParserOutput) when DataAccess::parseWikitext() is called.
2881        //
2882        // We may use still Parsoid's StubMetadataCollector for testing or
2883        // when running Parsoid in standalone mode, so forcing a downcast
2884        // here would lose some flexibility.
2885
2886        if ( $metadata instanceof ParserOutput ) {
2887            foreach ( $this->getUsedOptions() as $opt ) {
2888                $metadata->recordOption( $opt );
2889            }
2890            if ( $this->mCacheExpiry !== null ) {
2891                $metadata->updateCacheExpiry( $this->mCacheExpiry );
2892            }
2893            if ( $this->mCacheTime !== '' ) {
2894                $metadata->setCacheTime( $this->mCacheTime );
2895            }
2896            if ( $this->mCacheRevisionId !== null ) {
2897                $metadata->setCacheRevisionId( $this->mCacheRevisionId );
2898            }
2899            // T293514: We should use the first *modified* title text, but
2900            // we don't have the original to check.
2901            $otherTitle = $metadata->getTitleText();
2902            if ( $otherTitle === null || $otherTitle === '' ) {
2903                $metadata->setTitleText( $this->getTitleText() );
2904            }
2905            foreach ( $this->mTemplates as $ns => $arr ) {
2906                foreach ( $arr as $dbk => $page_id ) {
2907                    // default to invalid/broken revision if this is not present
2908                    $rev_id = $this->mTemplateIds[$ns][$dbk] ?? 0;
2909                    $metadata->addTemplate( TitleValue::tryNew( $ns, (string)$dbk ), $page_id, $rev_id );
2910                }
2911            }
2912        }
2913    }
2914
2915    private static function mergeMixedList( array $a, array $b ): array {
2916        return array_unique( array_merge( $a, $b ), SORT_REGULAR );
2917    }
2918
2919    private static function mergeList( array $a, array $b ): array {
2920        return array_values( array_unique( array_merge( $a, $b ), SORT_REGULAR ) );
2921    }
2922
2923    private static function mergeMap( array $a, array $b ): array {
2924        return array_replace( $a, $b );
2925    }
2926
2927    private static function mergeMapStrategy( array $a, array $b ): array {
2928        foreach ( $b as $key => $bValue ) {
2929            if ( !array_key_exists( $key, $a ) ) {
2930                $a[$key] = $bValue;
2931            } elseif (
2932                is_array( $a[$key] ) &&
2933                isset( $a[$key][self::MW_MERGE_STRATEGY_KEY] ) &&
2934                isset( $bValue[self::MW_MERGE_STRATEGY_KEY] )
2935            ) {
2936                $strategy = $bValue[self::MW_MERGE_STRATEGY_KEY];
2937                if ( $strategy !== $a[$key][self::MW_MERGE_STRATEGY_KEY] ) {
2938                    throw new InvalidArgumentException( "Conflicting merge strategy for $key" );
2939                }
2940                if ( $strategy === self::MW_MERGE_STRATEGY_UNION ) {
2941                    // Note the array_merge is *not* safe to use here, because
2942                    // the $bValue is expected to be a map from items to `true`.
2943                    // If the item is a numeric string like '1' then array_merge
2944                    // will convert it to an integer and renumber the array!
2945                    $a[$key] = array_replace( $a[$key], $bValue );
2946                } else {
2947                    throw new InvalidArgumentException( "Unknown merge strategy $strategy" );
2948                }
2949            } else {
2950                $valuesSame = ( $a[$key] === $bValue );
2951                if ( ( !$valuesSame ) &&
2952                    is_object( $a[$key] ) &&
2953                    is_object( $bValue )
2954                ) {
2955                    $jsonCodec = MediaWikiServices::getInstance()->getJsonCodec();
2956                    $valuesSame = ( $jsonCodec->serialize( $a[$key] ) === $jsonCodec->serialize( $bValue ) );
2957                }
2958                if ( !$valuesSame ) {
2959                    // Silently replace for now; in the future will first emit
2960                    // a deprecation warning, and then (later) throw.
2961                    $a[$key] = $bValue;
2962                }
2963            }
2964        }
2965        return $a;
2966    }
2967
2968    private static function useEachMinValue( array $a, array $b ): array {
2969        $values = [];
2970        $keys = array_merge( array_keys( $a ), array_keys( $b ) );
2971
2972        foreach ( $keys as $k ) {
2973            $values[$k] = min( $a[$k] ?? INF, $b[$k] ?? INF );
2974        }
2975
2976        return $values;
2977    }
2978
2979    private static function useEachTotalValue( array $a, array $b ): array {
2980        $values = [];
2981        $keys = array_merge( array_keys( $a ), array_keys( $b ) );
2982
2983        foreach ( $keys as $k ) {
2984            $values[$k] = ( $a[$k] ?? 0 ) + ( $b[$k] ?? 0 );
2985        }
2986
2987        return $values;
2988    }
2989
2990    private static function useMaxValue( $a, $b ) {
2991        if ( $a === null ) {
2992            return $b;
2993        }
2994
2995        if ( $b === null ) {
2996            return $a;
2997        }
2998
2999        return max( $a, $b );
3000    }
3001
3002    /**
3003     * Returns a JSON serializable structure representing this ParserOutput instance.
3004     * @see newFromJson()
3005     *
3006     * @return array
3007     */
3008    protected function toJsonArray(): array {
3009        // WARNING: When changing how this class is serialized, follow the instructions
3010        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
3011
3012        $data = [
3013            'Text' => $this->mRawText,
3014            'LanguageLinks' => $this->getLanguageLinks(),
3015            'Categories' => $this->mCategories,
3016            'Indicators' => $this->mIndicators,
3017            'TitleText' => $this->mTitleText,
3018            'Links' => $this->mLinks,
3019            'LinksSpecial' => $this->mLinksSpecial,
3020            'Templates' => $this->mTemplates,
3021            'TemplateIds' => $this->mTemplateIds,
3022            'Images' => $this->mImages,
3023            'FileSearchOptions' => $this->mFileSearchOptions,
3024            'ExternalLinks' => $this->mExternalLinks,
3025            'InterwikiLinks' => $this->mInterwikiLinks,
3026            'NewSection' => $this->mNewSection,
3027            'HideNewSection' => $this->mHideNewSection,
3028            'NoGallery' => $this->mNoGallery,
3029            'HeadItems' => $this->mHeadItems,
3030            'Modules' => array_keys( $this->mModuleSet ),
3031            'ModuleStyles' => array_keys( $this->mModuleStyleSet ),
3032            'JsConfigVars' => $this->mJsConfigVars,
3033            'Warnings' => $this->mWarnings,
3034            'Sections' => $this->getSections(),
3035            'Properties' => self::detectAndEncodeBinary( $this->mProperties ),
3036            'Timestamp' => $this->mTimestamp,
3037            'EnableOOUI' => $this->mEnableOOUI,
3038            'IndexPolicy' => $this->getIndexPolicy(),
3039            // may contain arbitrary structures!
3040            'ExtensionData' => $this->mExtensionData,
3041            'LimitReportData' => $this->mLimitReportData,
3042            'LimitReportJSData' => $this->mLimitReportJSData,
3043            'CacheMessage' => $this->mCacheMessage,
3044            'TimeProfile' => $this->mTimeProfile,
3045            'ParseStartTime' => [], // don't serialize this
3046            'PreventClickjacking' => $this->mPreventClickjacking,
3047            'ExtraScriptSrcs' => $this->mExtraScriptSrcs,
3048            'ExtraDefaultSrcs' => $this->mExtraDefaultSrcs,
3049            'ExtraStyleSrcs' => $this->mExtraStyleSrcs,
3050            'Flags' => $this->mFlags + (
3051                // backward-compatibility: distinguish "no sections" from
3052                // "sections not set" (Will be unnecessary after T327439.)
3053                $this->mTOCData === null ? [] : [ 'mw:toc-set' => true ]
3054            ),
3055            'SpeculativeRevId' => $this->mSpeculativeRevId,
3056            'SpeculativePageIdUsed' => $this->speculativePageIdUsed,
3057            'RevisionTimestampUsed' => $this->revisionTimestampUsed,
3058            'RevisionUsedSha1Base36' => $this->revisionUsedSha1Base36,
3059            'WrapperDivClasses' => $this->mWrapperDivClasses,
3060        ];
3061
3062        // Fill in missing fields from parents. Array addition does not override existing fields.
3063        $data += parent::toJsonArray();
3064
3065        // TODO: make more fields optional!
3066
3067        if ( $this->mMaxAdaptiveExpiry !== INF ) {
3068            // NOTE: JSON can't encode infinity!
3069            $data['MaxAdaptiveExpiry'] = $this->mMaxAdaptiveExpiry;
3070        }
3071
3072        if ( $this->mTOCData ) {
3073            // Temporarily add information from TOCData extension data
3074            // T327439: We should eventually make the entire mTOCData
3075            // serializable
3076            $toc = $this->mTOCData->jsonSerialize();
3077            if ( isset( $toc['extensionData'] ) ) {
3078                $data['TOCExtensionData'] = $toc['extensionData'];
3079            }
3080        }
3081
3082        return $data;
3083    }
3084
3085    public static function newFromJsonArray( JsonDeserializer $deserializer, array $json ): ParserOutput {
3086        $parserOutput = new ParserOutput();
3087        $parserOutput->initFromJson( $deserializer, $json );
3088        return $parserOutput;
3089    }
3090
3091    /**
3092     * Initialize member fields from an array returned by jsonSerialize().
3093     * @param JsonDeserializer $deserializer
3094     * @param array $jsonData
3095     */
3096    protected function initFromJson( JsonDeserializer $deserializer, array $jsonData ): void {
3097        parent::initFromJson( $deserializer, $jsonData );
3098
3099        // WARNING: When changing how this class is serialized, follow the instructions
3100        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
3101
3102        $this->mRawText = $jsonData['Text'];
3103        $this->mLanguageLinkMap = [];
3104        foreach ( ( $jsonData['LanguageLinks'] ?? [] ) as $l ) {
3105            $this->addLanguageLink( $l );
3106        }
3107        $this->mCategories = $jsonData['Categories'];
3108        $this->mIndicators = $jsonData['Indicators'];
3109        $this->mTitleText = $jsonData['TitleText'];
3110        $this->mLinks = $jsonData['Links'];
3111        $this->mLinksSpecial = $jsonData['LinksSpecial'];
3112        $this->mTemplates = $jsonData['Templates'];
3113        $this->mTemplateIds = $jsonData['TemplateIds'];
3114        $this->mImages = $jsonData['Images'];
3115        $this->mFileSearchOptions = $jsonData['FileSearchOptions'];
3116        $this->mExternalLinks = $jsonData['ExternalLinks'];
3117        $this->mInterwikiLinks = $jsonData['InterwikiLinks'];
3118        $this->mNewSection = $jsonData['NewSection'];
3119        $this->mHideNewSection = $jsonData['HideNewSection'];
3120        $this->mNoGallery = $jsonData['NoGallery'];
3121        $this->mHeadItems = $jsonData['HeadItems'];
3122        $this->mModuleSet = array_fill_keys( $jsonData['Modules'], true );
3123        $this->mModuleStyleSet = array_fill_keys( $jsonData['ModuleStyles'], true );
3124        $this->mJsConfigVars = $jsonData['JsConfigVars'];
3125        $this->mWarnings = $jsonData['Warnings'];
3126        $this->mFlags = $jsonData['Flags'];
3127        if (
3128            $jsonData['Sections'] !== [] ||
3129            // backward-compatibility: distinguish "no sections" from
3130            // "sections not set" (Will be unnecessary after T327439.)
3131            $this->getOutputFlag( 'mw:toc-set' )
3132        ) {
3133            $this->setSections( $jsonData['Sections'] );
3134            unset( $this->mFlags['mw:toc-set'] );
3135            if ( isset( $jsonData['TOCExtensionData'] ) ) {
3136                $tocData = $this->getTOCData(); // created by setSections() above
3137                foreach ( $jsonData['TOCExtensionData'] as $key => $value ) {
3138                    $tocData->setExtensionData( $key, $value );
3139                }
3140            }
3141        }
3142        $this->mProperties = self::detectAndDecodeBinary( $jsonData['Properties'] );
3143        $this->mTimestamp = $jsonData['Timestamp'];
3144        $this->mEnableOOUI = $jsonData['EnableOOUI'];
3145        $this->setIndexPolicy( $jsonData['IndexPolicy'] );
3146        $this->mExtensionData = $jsonData['ExtensionData'] ?? [];
3147        $this->mLimitReportData = $jsonData['LimitReportData'];
3148        $this->mLimitReportJSData = $jsonData['LimitReportJSData'];
3149        $this->mCacheMessage = $jsonData['CacheMessage'] ?? '';
3150        $this->mParseStartTime = []; // invalid after reloading
3151        $this->mTimeProfile = $jsonData['TimeProfile'] ?? [];
3152        $this->mPreventClickjacking = $jsonData['PreventClickjacking'];
3153        $this->mExtraScriptSrcs = $jsonData['ExtraScriptSrcs'];
3154        $this->mExtraDefaultSrcs = $jsonData['ExtraDefaultSrcs'];
3155        $this->mExtraStyleSrcs = $jsonData['ExtraStyleSrcs'];
3156        $this->mSpeculativeRevId = $jsonData['SpeculativeRevId'];
3157        $this->speculativePageIdUsed = $jsonData['SpeculativePageIdUsed'];
3158        $this->revisionTimestampUsed = $jsonData['RevisionTimestampUsed'];
3159        $this->revisionUsedSha1Base36 = $jsonData['RevisionUsedSha1Base36'];
3160        $this->mWrapperDivClasses = $jsonData['WrapperDivClasses'];
3161        $this->mMaxAdaptiveExpiry = $jsonData['MaxAdaptiveExpiry'] ?? INF;
3162    }
3163
3164    /**
3165     * Finds any non-utf8 strings in the given array and replaces them with
3166     * an associative array that wraps a base64 encoded version of the data.
3167     * Inverse of detectAndDecodeBinary().
3168     *
3169     * @param array $properties
3170     *
3171     * @return array
3172     */
3173    private static function detectAndEncodeBinary( array $properties ) {
3174        foreach ( $properties as $key => $value ) {
3175            if ( is_string( $value ) ) {
3176                if ( !mb_detect_encoding( $value, 'UTF-8', true ) ) {
3177                    $properties[$key] = [
3178                        // T313818: This key name conflicts with JsonCodec
3179                        '_type_' => 'string',
3180                        '_encoding_' => 'base64',
3181                        '_data_' => base64_encode( $value ),
3182                    ];
3183                }
3184            }
3185        }
3186
3187        return $properties;
3188    }
3189
3190    /**
3191     * Finds any associative arrays that represent encoded binary strings, and
3192     * replaces them with the decoded binary data.
3193     *
3194     * @param array $properties
3195     *
3196     * @return array
3197     */
3198    private static function detectAndDecodeBinary( array $properties ) {
3199        foreach ( $properties as $key => $value ) {
3200            if ( is_array( $value ) && isset( $value['_encoding_'] ) ) {
3201                if ( $value['_encoding_'] === 'base64' ) {
3202                    $properties[$key] = base64_decode( $value['_data_'] );
3203                }
3204            }
3205        }
3206
3207        return $properties;
3208    }
3209
3210    public function __wakeup() {
3211        $oldAliases = [
3212            // This was the pre-namespace name of the class, which is still
3213            // used in pre-1.42 serialized objects.
3214            'ParserOutput',
3215        ];
3216        // Backwards compatibility, pre 1.36
3217        $priorAccessedOptions = $this->getGhostFieldValue( 'mAccessedOptions', ...$oldAliases );
3218        if ( $priorAccessedOptions ) {
3219            $this->mParseUsedOptions = $priorAccessedOptions;
3220        }
3221        // Backwards compatibility, pre 1.39
3222        $priorIndexPolicy = $this->getGhostFieldValue( 'mIndexPolicy', ...$oldAliases );
3223        if ( $priorIndexPolicy ) {
3224            $this->setIndexPolicy( $priorIndexPolicy );
3225        }
3226        // Backwards compatibility, pre 1.40
3227        $mSections = $this->getGhostFieldValue( 'mSections', ...$oldAliases );
3228        if ( $mSections !== null && $mSections !== [] ) {
3229            $this->setSections( $mSections );
3230        }
3231        // Backwards compatibility, pre 1.42
3232        $mModules = $this->getGhostFieldValue( 'mModules', ...$oldAliases );
3233        if ( $mModules !== null && $mModules !== [] ) {
3234            $this->addModules( $mModules );
3235        }
3236        // Backwards compatibility, pre 1.42
3237        $mModuleStyles = $this->getGhostFieldValue( 'mModuleStyles', ...$oldAliases );
3238        if ( $mModuleStyles !== null && $mModuleStyles !== [] ) {
3239            $this->addModuleStyles( $mModuleStyles );
3240        }
3241        // Backwards compatibility, pre 1.42
3242        $mText = $this->getGhostFieldValue( 'mText', ...$oldAliases );
3243        if ( $mText !== null ) {
3244            $this->setRawText( $mText );
3245        }
3246        // Backwards compatibility, pre 1.42
3247        $ll = $this->getGhostFieldValue( 'mLanguageLinks', ...$oldAliases );
3248        if ( $ll !== null && $ll !== [] ) {
3249            foreach ( $ll as $l ) {
3250                $this->addLanguageLink( $l );
3251            }
3252        }
3253        // Backward compatibility with private fields, pre 1.42
3254        $oldPrivateFields = [
3255            'mRawText',
3256            'mCategories',
3257            'mIndicators',
3258            'mTitleText',
3259            'mLinks',
3260            'mLinksSpecial',
3261            'mTemplates',
3262            'mTemplateIds',
3263            'mImages',
3264            'mFileSearchOptions',
3265            'mExternalLinks',
3266            'mInterwikiLinks',
3267            'mNewSection',
3268            'mHideNewSection',
3269            'mNoGallery',
3270            'mHeadItems',
3271            'mModuleSet',
3272            'mModuleStyleSet',
3273            'mJsConfigVars',
3274            'mWarnings',
3275            'mWarningMsgs',
3276            'mTOCData',
3277            'mProperties',
3278            'mTimestamp',
3279            'mEnableOOUI',
3280            'mIndexSet',
3281            'mNoIndexSet',
3282            'mExtensionData',
3283            'mLimitReportData',
3284            'mLimitReportJSData',
3285            'mCacheMessage',
3286            'mParseStartTime',
3287            'mTimeProfile',
3288            'mPreventClickjacking',
3289            'mExtraScriptSrcs',
3290            'mExtraDefaultSrcs',
3291            'mExtraStyleSrcs',
3292            'mFlags',
3293            'mSpeculativeRevId',
3294            'speculativePageIdUsed',
3295            'revisionTimestampUsed',
3296            'revisionUsedSha1Base36',
3297            'mWrapperDivClasses',
3298            'mMaxAdaptiveExpiry',
3299        ];
3300        foreach ( $oldPrivateFields as $f ) {
3301            $this->restoreAliasedGhostField( $f, ...$oldAliases );
3302        }
3303        $this->clearParseStartTime();
3304    }
3305
3306    public function __clone() {
3307        // It seems that very little of this object needs to be explicitly deep-cloned
3308        // while keeping copies reasonably separated.
3309        // Most of the non-scalar properties of this object are either
3310        // - (potentially multi-nested) arrays of scalars (which get deep-cloned), or
3311        // - arrays that may contain arbitrary elements (which don't necessarily get
3312        //   deep-cloned), but for which no particular care elsewhere is given to
3313        //   copying their references around (e.g. mJsConfigVars).
3314        // Hence, we are not going out of our way to ensure that the references to innermost
3315        // objects that may appear in a ParserOutput are unique. If that becomes the
3316        // expectation at any point, this method will require updating as well.
3317        // The exception is TOCData (which is an object), which we clone explicitly.
3318        if ( $this->mTOCData ) {
3319            $this->mTOCData = clone $this->mTOCData;
3320        }
3321    }
3322
3323    /**
3324     * Returns the content holder text of the ParserOutput.
3325     * This will eventually be replaced by something like getContentHolder()->getText() when we have a
3326     * ContentHolder/HtmlHolder class.
3327     * @internal
3328     * @unstable
3329     * @return string
3330     */
3331    public function getContentHolderText(): string {
3332        return $this->getRawText();
3333    }
3334
3335    /**
3336     * Sets the content holder text of the ParserOutput.
3337     * This will eventually be replaced by something like getContentHolder()->setText() when we have a
3338     * ContentHolder/HtmlHolder class.
3339     * @internal
3340     * @unstable
3341     */
3342    public function setContentHolderText( string $s ): void {
3343        $this->setRawText( $s );
3344    }
3345
3346    public function __get( $name ) {
3347        if ( property_exists( get_called_class(), $name ) ) {
3348            // Direct access to a public property, deprecated.
3349            wfDeprecatedMsg( "ParserOutput::{$name} public read access deprecated", '1.38' );
3350            return $this->$name;
3351        } elseif ( property_exists( $this, $name ) ) {
3352            // Dynamic property access, deprecated.
3353            wfDeprecatedMsg( "ParserOutput::{$name} dynamic property read access deprecated", '1.38' );
3354            return $this->$name;
3355        } else {
3356            trigger_error( "Inaccessible property via __get(): $name" );
3357            return null;
3358        }
3359    }
3360
3361    public function __set( $name, $value ) {
3362        if ( property_exists( get_called_class(), $name ) ) {
3363            // Direct access to a public property, deprecated.
3364            wfDeprecatedMsg( "ParserOutput::$name public write access deprecated", '1.38' );
3365            $this->$name = $value;
3366        } else {
3367            // Dynamic property access, deprecated.
3368            wfDeprecatedMsg( "ParserOutput::$name dynamic property write access deprecated", '1.38' );
3369            $this->$name = $value;
3370        }
3371    }
3372}
3373
3374/** @deprecated class alias since 1.42 */
3375class_alias( ParserOutput::class, 'ParserOutput' );