Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
70.33% |
1057 / 1503 |
|
70.85% |
141 / 199 |
CRAP | |
0.00% |
0 / 1 |
OutputPage | |
70.37% |
1057 / 1502 |
|
70.85% |
141 / 199 |
7523.95 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
redirect | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getRedirect | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCopyrightUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setStatusCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addMeta | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMetaTags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addLink | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLinkTags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCanonicalUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCanonicalUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addScript | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addScriptFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addInlineScript | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
filterModules | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getModules | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addModules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getModuleStyles | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
addModuleStyles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTarget | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addContentOverride | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
addContentOverrideCallback | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addHtmlClasses | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHeadItemsArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addHeadItem | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addHeadItems | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasHeadItem | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addBodyClasses | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setArticleBodyOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getArticleBodyOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setProperty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getProperty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkLastModified | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
10 | |||
getCdnCacheEpoch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLastModified | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRobotPolicy | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getRobotPolicy | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatRobotsOptions | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
setRobotsOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRobotsContent | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
setIndexPolicy | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getIndexPolicy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setFollowPolicy | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getFollowPolicy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setHTMLTitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getHTMLTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRedirectedFrom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPageTitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setPageTitleMsg | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setPageTitleInternal | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getPageTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setDisplayTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayTitle | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getUnprefixedDisplayTitle | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
setTitle | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setSubtitle | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addSubtitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
buildBacklinkSubtitle | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
addBacklinkSubtitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clearSubtitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSubtitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPrintable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isPrintable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
disable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDisabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showNewSectionLink | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
forceHideNewSectionLink | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setSyndicated | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getAdvertisedFeedTypes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setFeedAppendQuery | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
addFeedLink | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isSyndicated | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSyndicationLinks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFeedAppendQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setArticleFlag | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isArticle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setArticleRelated | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isArticleRelated | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCopyright | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showsCopyright | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
addLanguageLinks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLanguageLinks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguageLinks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addCategoryLinks | |
93.33% |
28 / 30 |
|
0.00% |
0 / 1 |
11.04 | |||
addCategoryLinksToLBAndGetResult | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
2 | |||
setCategoryLinks | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCategoryLinks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCategories | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
setIndicators | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getIndicators | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addHelpLink | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
disallowUserJs | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getAllowedModules | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
reduceAllowedModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prependHTML | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addHTML | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addElement | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
clearHTML | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHTML | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parserOptions | |
62.50% |
5 / 8 |
|
0.00% |
0 / 1 |
3.47 | |||
setRevisionId | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getRevisionId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRevisionIsCurrent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isRevisionCurrent | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setRevisionTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRevisionTimestamp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFileVersion | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getFileVersion | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTemplateIds | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFileSearchOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addWikiTextAsInterface | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
wrapWikiTextAsInterface | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
addWikiTextAsContent | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addWikiTextTitleInternal | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setTOCData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTOCData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOutputFlag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentLangForJS | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getContentLangForJS | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
addParserOutputMetadata | |
81.82% |
54 / 66 |
|
0.00% |
0 / 1 |
27.46 | |||
getParserOutputText | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
addParserOutputContent | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
addParserOutputText | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
addParserOutput | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
addTemplate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseAsContent | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
parseAsInterface | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
parseInlineAsInterface | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
parseInternal | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
setCdnMaxage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
lowerCdnMaxage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
adaptCdnTTL | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
enableClientCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
disableClientCache | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
couldBePublicCached | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
considerCacheSettingsFinal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheVaryCookies | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
haveCacheVaryCookies | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
addVaryHeader | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getVaryHeader | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
addLinkHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLinkHeader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addAcceptLanguage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
setPreventClickjacking | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPreventClickjacking | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFrameOptions | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getReportTo | |
54.55% |
6 / 11 |
|
0.00% |
0 / 1 |
5.50 | |||
getFeaturePolicyReportOnly | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
sendCacheControl | |
87.50% |
28 / 32 |
|
0.00% |
0 / 1 |
10.20 | |||
loadSkinModules | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
output | |
49.33% |
37 / 75 |
|
0.00% |
0 / 1 |
106.29 | |||
prepareErrorPage | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
showErrorPage | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
showPermissionsErrorPage | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
342 | |||
versionRequired | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
formatPermissionStatus | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
formatPermissionsErrorMessage | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
showLagWarning | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
showFatalError | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
addReturnTo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
returnToMain | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
showPendingTakeover | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
getRlClientContext | |
53.12% |
17 / 32 |
|
0.00% |
0 / 1 |
14.59 | |||
getRlClient | |
94.92% |
56 / 59 |
|
0.00% |
0 / 1 |
9.01 | |||
headElement | |
90.91% |
40 / 44 |
|
0.00% |
0 / 1 |
7.04 | |||
getResourceLoader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
makeResourceLoaderLink | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
combineWrappedStrings | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getBottomScripts | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
4.01 | |||
getJsConfigVars | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addJsConfigVars | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getJSVars | |
86.11% |
93 / 108 |
|
0.00% |
0 / 1 |
24.42 | |||
getLastSeenUserTalkRevId | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
userCanPreview | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
getHeadLinksArray | |
71.74% |
66 / 92 |
|
0.00% |
0 / 1 |
29.03 | |||
getHeadLinksCanonicalURLArray | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
8 | |||
getHeadLinksAlternateURLsArray | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
8 | |||
getHeadLinksCopyrightArray | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
7.77 | |||
getHeadLinksSyndicationArray | |
81.25% |
26 / 32 |
|
0.00% |
0 / 1 |
7.32 | |||
feedLink | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
addStyle | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
addInlineStyle | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
buildExemptModules | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
5 | |||
buildCssLinksArray | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
4.12 | |||
styleLink | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
90 | |||
transformResourcePath | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
transformFilePath | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
transformCssMedia | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
addWikiMsg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addWikiMsgArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
wrapWikiMsg | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
isTOCEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setupOOUI | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
resetOOUI | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
3.19 | |||
enableOOUI | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getCSPNonce | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getCSP | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCspOutputMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tailElement | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Preparation for the final page rendering. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Output; |
24 | |
25 | use Article; |
26 | use Content; |
27 | use CSSJanus; |
28 | use Exception; |
29 | use ExtensionRegistry; |
30 | use File; |
31 | use HtmlArmor; |
32 | use InvalidArgumentException; |
33 | use JavaScriptContent; |
34 | use Language; |
35 | use LanguageCode; |
36 | use MediaWiki\Cache\LinkCache; |
37 | use MediaWiki\Config\Config; |
38 | use MediaWiki\Context\ContextSource; |
39 | use MediaWiki\Context\IContextSource; |
40 | use MediaWiki\Context\RequestContext; |
41 | use MediaWiki\HookContainer\ProtectedHookAccessorTrait; |
42 | use MediaWiki\Html\Html; |
43 | use MediaWiki\Linker\LinkTarget; |
44 | use MediaWiki\MainConfigNames; |
45 | use MediaWiki\MediaWikiServices; |
46 | use MediaWiki\Message\Message; |
47 | use MediaWiki\Page\PageRecord; |
48 | use MediaWiki\Page\PageReference; |
49 | use MediaWiki\Parser\Parser; |
50 | use MediaWiki\Parser\ParserOutput; |
51 | use MediaWiki\Parser\ParserOutputFlags; |
52 | use MediaWiki\Parser\Sanitizer; |
53 | use MediaWiki\Permissions\PermissionStatus; |
54 | use MediaWiki\Request\ContentSecurityPolicy; |
55 | use MediaWiki\Request\FauxRequest; |
56 | use MediaWiki\Request\WebRequest; |
57 | use MediaWiki\ResourceLoader as RL; |
58 | use MediaWiki\ResourceLoader\ResourceLoader; |
59 | use MediaWiki\Session\SessionManager; |
60 | use MediaWiki\SpecialPage\SpecialPage; |
61 | use MediaWiki\Title\Title; |
62 | use MediaWiki\Title\TitleValue; |
63 | use MediaWiki\Utils\MWTimestamp; |
64 | use MessageSpecifier; |
65 | use MWDebug; |
66 | use OOUI\Element; |
67 | use OOUI\Theme; |
68 | use ParserOptions; |
69 | use RuntimeException; |
70 | use Skin; |
71 | use TextContent; |
72 | use Wikimedia\AtEase\AtEase; |
73 | use Wikimedia\Bcp47Code\Bcp47Code; |
74 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
75 | use Wikimedia\Parsoid\Core\TOCData; |
76 | use Wikimedia\Rdbms\IResultWrapper; |
77 | use Wikimedia\RelPath; |
78 | use Wikimedia\WrappedString; |
79 | use Wikimedia\WrappedStringList; |
80 | |
81 | /** |
82 | * This is one of the Core classes and should |
83 | * be read at least once by any new developers. Also documented at |
84 | * https://www.mediawiki.org/wiki/Manual:Architectural_modules/OutputPage |
85 | * |
86 | * This class is used to prepare the final rendering. A skin is then |
87 | * applied to the output parameters (links, javascript, html, categories ...). |
88 | * |
89 | * @todo FIXME: Another class handles sending the whole page to the client. |
90 | * |
91 | * Some comments comes from a pairing session between Zak Greant and Antoine Musso |
92 | * in November 2010. |
93 | * |
94 | * @todo document |
95 | */ |
96 | class OutputPage extends ContextSource { |
97 | use ProtectedHookAccessorTrait; |
98 | |
99 | /** Output CSP policies as headers */ |
100 | public const CSP_HEADERS = 'headers'; |
101 | /** Output CSP policies as meta tags */ |
102 | public const CSP_META = 'meta'; |
103 | |
104 | // Constants for getJSVars() |
105 | private const JS_VAR_EARLY = 1; |
106 | private const JS_VAR_LATE = 2; |
107 | |
108 | // Core config vars that opt-in to JS_VAR_LATE. |
109 | // Extensions use the 'LateJSConfigVarNames' attribute instead. |
110 | private const CORE_LATE_JS_CONFIG_VAR_NAMES = []; |
111 | |
112 | /** @var bool Whether setupOOUI() has been called */ |
113 | private static $oouiSetupDone = false; |
114 | |
115 | /** @var string[][] Should be private. Used with addMeta() which adds "<meta>" */ |
116 | protected $mMetatags = []; |
117 | |
118 | /** @var array */ |
119 | protected $mLinktags = []; |
120 | |
121 | /** @var string|false */ |
122 | protected $mCanonicalUrl = false; |
123 | |
124 | /** |
125 | * @var string The contents of <h1> |
126 | */ |
127 | private $mPageTitle = ''; |
128 | |
129 | /** |
130 | * @var string The displayed title of the page. Different from page title |
131 | * if overridden by display title magic word or hooks. Can contain safe |
132 | * HTML. Different from page title which may contain messages such as |
133 | * "Editing X" which is displayed in h1. This can be used for other places |
134 | * where the page name is referred on the page. |
135 | */ |
136 | private $displayTitle; |
137 | |
138 | /** @var bool See OutputPage::couldBePublicCached. */ |
139 | private $cacheIsFinal = false; |
140 | |
141 | /** |
142 | * @var string Contains all of the "<body>" content. Should be private we |
143 | * got set/get accessors and the append() method. |
144 | */ |
145 | public $mBodytext = ''; |
146 | |
147 | /** @var string Stores contents of "<title>" tag */ |
148 | private $mHTMLtitle = ''; |
149 | |
150 | /** |
151 | * @var bool Is the displayed content related to the source of the |
152 | * corresponding wiki article. |
153 | */ |
154 | private $mIsArticle = false; |
155 | |
156 | /** @var bool Stores "article flag" toggle. */ |
157 | private $mIsArticleRelated = true; |
158 | |
159 | /** @var bool Is the content subject to copyright */ |
160 | private $mHasCopyright = false; |
161 | |
162 | /** |
163 | * @var bool We have to set isPrintable(). Some pages should |
164 | * never be printed (ex: redirections). |
165 | */ |
166 | private $mPrintable = false; |
167 | |
168 | /** |
169 | * @var ?TOCData Table of Contents information from ParserOutput, or |
170 | * null if no TOCData was ever set. |
171 | */ |
172 | private $tocData; |
173 | |
174 | /** |
175 | * @var array Contains the page subtitle. Special pages usually have some |
176 | * links here. Don't confuse with site subtitle added by skins. |
177 | */ |
178 | private $mSubtitle = []; |
179 | |
180 | /** @var string */ |
181 | public $mRedirect = ''; |
182 | |
183 | /** @var int */ |
184 | protected $mStatusCode; |
185 | |
186 | /** |
187 | * @var string Used for sending cache control. |
188 | * The whole caching system should probably be moved into its own class. |
189 | */ |
190 | protected $mLastModified = ''; |
191 | |
192 | /** |
193 | * @var string[][] |
194 | * @deprecated since 1.38; will be made private (T301020) |
195 | */ |
196 | protected $mCategoryLinks = []; |
197 | |
198 | /** |
199 | * @var string[][] |
200 | * @deprecated since 1.38, will be made private (T301020) |
201 | */ |
202 | protected $mCategories = [ |
203 | 'hidden' => [], |
204 | 'normal' => [], |
205 | ]; |
206 | |
207 | /** |
208 | * @var string[] |
209 | * @deprecated since 1.38; will be made private (T301020) |
210 | */ |
211 | protected $mIndicators = []; |
212 | |
213 | /** |
214 | * @var string[] Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') |
215 | */ |
216 | private $mLanguageLinks = []; |
217 | |
218 | /** |
219 | * Used for JavaScript (predates ResourceLoader) |
220 | * @todo We should split JS / CSS. |
221 | * mScripts content is inserted as is in "<head>" by Skin. This might |
222 | * contain either a link to a stylesheet or inline CSS. |
223 | */ |
224 | private $mScripts = ''; |
225 | |
226 | /** @var string Inline CSS styles. Use addInlineStyle() sparingly */ |
227 | protected $mInlineStyles = ''; |
228 | |
229 | /** |
230 | * Additional <html> classes; This should be rarely modified; prefer mAdditionalBodyClasses. |
231 | * @var array |
232 | */ |
233 | protected $mAdditionalHtmlClasses = []; |
234 | |
235 | /** |
236 | * @var string[] Array of elements in "<head>". Parser might add its own headers! |
237 | * @deprecated since 1.38; will be made private (T301020) |
238 | */ |
239 | protected $mHeadItems = []; |
240 | |
241 | /** @var array Additional <body> classes; there are also <body> classes from other sources */ |
242 | protected $mAdditionalBodyClasses = []; |
243 | |
244 | /** |
245 | * @var array |
246 | * @deprecated since 1.38; will be made private (T301020) |
247 | */ |
248 | protected $mModules = []; |
249 | |
250 | /** |
251 | * @var array |
252 | * @deprecated since 1.38; will be made private (T301020) |
253 | */ |
254 | protected $mModuleStyles = []; |
255 | |
256 | /** @var ResourceLoader */ |
257 | protected $mResourceLoader; |
258 | |
259 | /** @var RL\ClientHtml */ |
260 | private $rlClient; |
261 | |
262 | /** @var RL\Context */ |
263 | private $rlClientContext; |
264 | |
265 | /** @var array */ |
266 | private $rlExemptStyleModules; |
267 | |
268 | /** |
269 | * @var array |
270 | * @deprecated since 1.38; will be made private (T301020) |
271 | */ |
272 | protected $mJsConfigVars = []; |
273 | |
274 | /** |
275 | * @var array<int,array<string,int>> |
276 | * @deprecated since 1.38; will be made private (T301020) |
277 | */ |
278 | protected $mTemplateIds = []; |
279 | |
280 | /** @var array */ |
281 | protected $mImageTimeKeys = []; |
282 | |
283 | /** @var string */ |
284 | public $mRedirectCode = ''; |
285 | |
286 | protected $mFeedLinksAppendQuery = null; |
287 | |
288 | /** @var array |
289 | * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page? |
290 | * @see RL\Module::$origin |
291 | * RL\Module::ORIGIN_ALL is assumed unless overridden; |
292 | */ |
293 | protected $mAllowedModules = [ |
294 | RL\Module::TYPE_COMBINED => RL\Module::ORIGIN_ALL, |
295 | ]; |
296 | |
297 | /** @var bool Whether output is disabled. If this is true, the 'output' method will do nothing. */ |
298 | protected $mDoNothing = false; |
299 | |
300 | // Parser related. |
301 | |
302 | /** |
303 | * lazy initialised, use parserOptions() |
304 | * @var ParserOptions |
305 | */ |
306 | protected $mParserOptions = null; |
307 | |
308 | /** |
309 | * Handles the Atom / RSS links. |
310 | * We probably only support Atom in 2011. |
311 | * @see $wgAdvertisedFeedTypes |
312 | */ |
313 | private $mFeedLinks = []; |
314 | |
315 | /** |
316 | * @var bool Set to false to send no-cache headers, disabling |
317 | * client-side caching. (This variable should really be named |
318 | * in the opposite sense; see ::disableClientCache().) |
319 | * @deprecated since 1.38; will be made private (T301020) |
320 | */ |
321 | protected $mEnableClientCache = true; |
322 | |
323 | /** @var bool Flag if output should only contain the body of the article. */ |
324 | private $mArticleBodyOnly = false; |
325 | |
326 | /** |
327 | * @var bool |
328 | * @deprecated since 1.38; will be made private (T301020) |
329 | */ |
330 | protected $mNewSectionLink = false; |
331 | |
332 | /** |
333 | * @var bool |
334 | * @deprecated since 1.38; will be made private (T301020) |
335 | */ |
336 | protected $mHideNewSectionLink = false; |
337 | |
338 | /** |
339 | * @var bool Comes from the parser. This was probably made to load CSS/JS |
340 | * only if we had "<gallery>". Used directly in CategoryPage.php. |
341 | * Looks like ResourceLoader can replace this. |
342 | * @deprecated since 1.38; will be made private (T301020) |
343 | */ |
344 | public $mNoGallery = false; |
345 | |
346 | /** @var int Cache stuff. Looks like mEnableClientCache */ |
347 | protected $mCdnMaxage = 0; |
348 | /** @var int Upper limit on mCdnMaxage */ |
349 | protected $mCdnMaxageLimit = INF; |
350 | |
351 | /** |
352 | * @var bool Controls if anti-clickjacking / frame-breaking headers will |
353 | * be sent. This should be done for pages where edit actions are possible. |
354 | * Setter: $this->setPreventClickjacking() |
355 | */ |
356 | protected $mPreventClickjacking = true; |
357 | |
358 | /** @var int|null To include the variable {{REVISIONID}} */ |
359 | private $mRevisionId = null; |
360 | |
361 | /** @var bool|null */ |
362 | private $mRevisionIsCurrent = null; |
363 | |
364 | /** @var string */ |
365 | private $mRevisionTimestamp = null; |
366 | |
367 | /** @var array */ |
368 | protected $mFileVersion = null; |
369 | |
370 | /** |
371 | * @var array An array of stylesheet filenames (relative from skins path), |
372 | * with options for CSS media, IE conditions, and RTL/LTR direction. |
373 | * For internal use; add settings in the skin via $this->addStyle() |
374 | * |
375 | * Style again! This seems like a code duplication since we already have |
376 | * mStyles. This is what makes Open Source amazing. |
377 | */ |
378 | protected $styles = []; |
379 | |
380 | private $mIndexPolicy = 'index'; |
381 | private $mFollowPolicy = 'follow'; |
382 | |
383 | /** @var array */ |
384 | private $mRobotsOptions = [ 'max-image-preview' => 'standard' ]; |
385 | |
386 | /** |
387 | * @var array Headers that cause the cache to vary. Key is header name, |
388 | * value should always be null. (Value was an array of options for |
389 | * the `Key` header, which was deprecated in 1.32 and removed in 1.34.) |
390 | */ |
391 | private $mVaryHeader = [ |
392 | 'Accept-Encoding' => null, |
393 | ]; |
394 | |
395 | /** |
396 | * If the current page was reached through a redirect, $mRedirectedFrom contains the title |
397 | * of the redirect. |
398 | * |
399 | * @var PageReference |
400 | */ |
401 | private $mRedirectedFrom = null; |
402 | |
403 | /** |
404 | * Additional key => value data |
405 | */ |
406 | private $mProperties = []; |
407 | |
408 | /** |
409 | * @var string|null ResourceLoader target for load.php links. If null, will be omitted |
410 | */ |
411 | private $mTarget = null; |
412 | |
413 | /** |
414 | * @var bool Whether parser output contains a table of contents |
415 | */ |
416 | private $mEnableTOC = false; |
417 | |
418 | /** |
419 | * @var array<string,bool> Flags set in the ParserOutput |
420 | */ |
421 | private $mOutputFlags = []; |
422 | |
423 | /** |
424 | * @var string|null The URL to send in a <link> element with rel=license |
425 | */ |
426 | private $copyrightUrl; |
427 | |
428 | /** |
429 | * @var Language|null |
430 | */ |
431 | private $contentLang; |
432 | |
433 | /** @var array Profiling data */ |
434 | private $limitReportJSData = []; |
435 | |
436 | /** @var array Map Title to Content */ |
437 | private $contentOverrides = []; |
438 | |
439 | /** @var callable[] */ |
440 | private $contentOverrideCallbacks = []; |
441 | |
442 | /** |
443 | * Link: header contents |
444 | */ |
445 | private $mLinkHeader = []; |
446 | |
447 | /** |
448 | * @var ContentSecurityPolicy |
449 | */ |
450 | private $CSP; |
451 | |
452 | private string $cspOutputMode = self::CSP_HEADERS; |
453 | |
454 | /** |
455 | * @var array A cache of the names of the cookies that will influence the cache |
456 | */ |
457 | private static $cacheVaryCookies = null; |
458 | |
459 | /** |
460 | * Constructor for OutputPage. This should not be called directly. |
461 | * Instead a new RequestContext should be created and it will implicitly create |
462 | * a OutputPage tied to that context. |
463 | * @param IContextSource $context |
464 | */ |
465 | public function __construct( IContextSource $context ) { |
466 | $this->setContext( $context ); |
467 | $this->CSP = new ContentSecurityPolicy( |
468 | $context->getRequest()->response(), |
469 | $context->getConfig(), |
470 | $this->getHookContainer() |
471 | ); |
472 | } |
473 | |
474 | /** |
475 | * Redirect to $url rather than displaying the normal page |
476 | * |
477 | * @param string $url |
478 | * @param string|int $responsecode HTTP status code |
479 | */ |
480 | public function redirect( $url, $responsecode = '302' ) { |
481 | # Strip newlines as a paranoia check for header injection in PHP<5.1.2 |
482 | $this->mRedirect = str_replace( "\n", '', $url ); |
483 | $this->mRedirectCode = (string)$responsecode; |
484 | } |
485 | |
486 | /** |
487 | * Get the URL to redirect to, or an empty string if not redirect URL set |
488 | * |
489 | * @return string |
490 | */ |
491 | public function getRedirect() { |
492 | return $this->mRedirect; |
493 | } |
494 | |
495 | /** |
496 | * Set the copyright URL to send with the output. |
497 | * Empty string to omit, null to reset. |
498 | * |
499 | * @since 1.26 |
500 | * |
501 | * @param string|null $url |
502 | */ |
503 | public function setCopyrightUrl( $url ) { |
504 | $this->copyrightUrl = $url; |
505 | } |
506 | |
507 | /** |
508 | * Set the HTTP status code to send with the output. |
509 | * |
510 | * @param int $statusCode |
511 | */ |
512 | public function setStatusCode( $statusCode ) { |
513 | $this->mStatusCode = $statusCode; |
514 | } |
515 | |
516 | /** |
517 | * Add a new "<meta>" tag |
518 | * To add an http-equiv meta tag, precede the name with "http:" |
519 | * |
520 | * @param string $name Name of the meta tag |
521 | * @param string $val Value of the meta tag |
522 | */ |
523 | public function addMeta( $name, $val ) { |
524 | $this->mMetatags[] = [ $name, $val ]; |
525 | } |
526 | |
527 | /** |
528 | * Returns the current <meta> tags |
529 | * |
530 | * @since 1.25 |
531 | * @return array |
532 | */ |
533 | public function getMetaTags() { |
534 | return $this->mMetatags; |
535 | } |
536 | |
537 | /** |
538 | * Add a new \<link\> tag to the page header. |
539 | * |
540 | * Note: use setCanonicalUrl() for rel=canonical. |
541 | * |
542 | * @param array $linkarr Associative array of attributes. |
543 | */ |
544 | public function addLink( array $linkarr ) { |
545 | $this->mLinktags[] = $linkarr; |
546 | } |
547 | |
548 | /** |
549 | * Returns the current <link> tags |
550 | * |
551 | * @since 1.25 |
552 | * @return array |
553 | */ |
554 | public function getLinkTags() { |
555 | return $this->mLinktags; |
556 | } |
557 | |
558 | /** |
559 | * Set the URL to be used for the <link rel=canonical>. This should be used |
560 | * in preference to addLink(), to avoid duplicate link tags. |
561 | * @param string $url |
562 | */ |
563 | public function setCanonicalUrl( $url ) { |
564 | $this->mCanonicalUrl = $url; |
565 | } |
566 | |
567 | /** |
568 | * Returns the URL to be used for the <link rel=canonical> if |
569 | * one is set. |
570 | * |
571 | * @since 1.25 |
572 | * @return bool|string |
573 | */ |
574 | public function getCanonicalUrl() { |
575 | return $this->mCanonicalUrl; |
576 | } |
577 | |
578 | /** |
579 | * Add raw HTML to the list of scripts (including \<script\> tag, etc.) |
580 | * Internal use only. Use OutputPage::addModules() or OutputPage::addJsConfigVars() |
581 | * if possible. |
582 | * |
583 | * @param string $script Raw HTML |
584 | * @param-taint $script exec_html |
585 | */ |
586 | public function addScript( $script ) { |
587 | $this->mScripts .= $script; |
588 | } |
589 | |
590 | /** |
591 | * Add a JavaScript file to be loaded as `<script>` on this page. |
592 | * |
593 | * Internal use only. Use OutputPage::addModules() if possible. |
594 | * |
595 | * @param string $file URL to file (absolute path, protocol-relative, or full url) |
596 | * @param string|null $unused Previously used to change the cache-busting query parameter |
597 | */ |
598 | public function addScriptFile( $file, $unused = null ) { |
599 | $this->addScript( Html::linkedScript( $file ) ); |
600 | } |
601 | |
602 | /** |
603 | * Add a self-contained script tag with the given contents |
604 | * Internal use only. Use OutputPage::addModules() if possible. |
605 | * |
606 | * @param string $script JavaScript text, no script tags |
607 | * @param-taint $script exec_html |
608 | */ |
609 | public function addInlineScript( $script ) { |
610 | $this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n"; |
611 | } |
612 | |
613 | /** |
614 | * Filter an array of modules to remove insufficiently trustworthy members, and modules |
615 | * which are no longer registered (eg a page is cached before an extension is disabled) |
616 | * @param string[] $modules |
617 | * @param string|null $position Unused |
618 | * @param string $type |
619 | * @return string[] |
620 | */ |
621 | protected function filterModules( array $modules, $position = null, |
622 | $type = RL\Module::TYPE_COMBINED |
623 | ) { |
624 | $resourceLoader = $this->getResourceLoader(); |
625 | $filteredModules = []; |
626 | foreach ( $modules as $val ) { |
627 | $module = $resourceLoader->getModule( $val ); |
628 | if ( $module instanceof RL\Module |
629 | && $module->getOrigin() <= $this->getAllowedModules( $type ) |
630 | ) { |
631 | $filteredModules[] = $val; |
632 | } |
633 | } |
634 | return $filteredModules; |
635 | } |
636 | |
637 | /** |
638 | * Get the list of modules to include on this page |
639 | * |
640 | * @param bool $filter Whether to filter out insufficiently trustworthy modules |
641 | * @param string|null $position Unused |
642 | * @param string $param |
643 | * @param string $type |
644 | * @return string[] Array of module names |
645 | */ |
646 | public function getModules( $filter = false, $position = null, $param = 'mModules', |
647 | $type = RL\Module::TYPE_COMBINED |
648 | ) { |
649 | $modules = array_values( array_unique( $this->$param ) ); |
650 | return $filter |
651 | ? $this->filterModules( $modules, null, $type ) |
652 | : $modules; |
653 | } |
654 | |
655 | /** |
656 | * Load one or more ResourceLoader modules on this page. |
657 | * |
658 | * @param string|array $modules Module name (string) or array of module names |
659 | */ |
660 | public function addModules( $modules ) { |
661 | $this->mModules = array_merge( $this->mModules, (array)$modules ); |
662 | } |
663 | |
664 | /** |
665 | * Get the list of style-only modules to load on this page. |
666 | * |
667 | * @param bool $filter |
668 | * @param string|null $position Unused |
669 | * @return string[] Array of module names |
670 | */ |
671 | public function getModuleStyles( $filter = false, $position = null ) { |
672 | return $this->getModules( $filter, null, 'mModuleStyles', |
673 | RL\Module::TYPE_STYLES |
674 | ); |
675 | } |
676 | |
677 | /** |
678 | * Load the styles of one or more style-only ResourceLoader modules on this page. |
679 | * |
680 | * Module styles added through this function will be loaded as a stylesheet, |
681 | * using a standard `<link rel=stylesheet>` HTML tag, rather than as a combined |
682 | * Javascript and CSS package. Thus, they will even load when JavaScript is disabled. |
683 | * |
684 | * @param string|array $modules Module name (string) or array of module names |
685 | */ |
686 | public function addModuleStyles( $modules ) { |
687 | $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules ); |
688 | } |
689 | |
690 | /** |
691 | * @return null|string ResourceLoader target |
692 | */ |
693 | public function getTarget() { |
694 | return $this->mTarget; |
695 | } |
696 | |
697 | /** |
698 | * Force the given Content object for the given page, for things like page preview. |
699 | * @see self::addContentOverrideCallback() |
700 | * @since 1.32 |
701 | * @param LinkTarget|PageReference $target |
702 | * @param Content $content |
703 | */ |
704 | public function addContentOverride( $target, Content $content ) { |
705 | if ( !$this->contentOverrides ) { |
706 | // Register a callback for $this->contentOverrides on the first call |
707 | $this->addContentOverrideCallback( function ( $target ) { |
708 | $key = $target->getNamespace() . ':' . $target->getDBkey(); |
709 | return $this->contentOverrides[$key] ?? null; |
710 | } ); |
711 | } |
712 | |
713 | $key = $target->getNamespace() . ':' . $target->getDBkey(); |
714 | $this->contentOverrides[$key] = $content; |
715 | } |
716 | |
717 | /** |
718 | * Add a callback for mapping from a Title to a Content object, for things |
719 | * like page preview. |
720 | * @see RL\Context::getContentOverrideCallback() |
721 | * @since 1.32 |
722 | * @param callable $callback |
723 | */ |
724 | public function addContentOverrideCallback( callable $callback ) { |
725 | $this->contentOverrideCallbacks[] = $callback; |
726 | } |
727 | |
728 | /** |
729 | * Add a class to the <html> element. This should rarely be used. |
730 | * Instead use OutputPage::addBodyClasses() if possible. |
731 | * |
732 | * @unstable Experimental since 1.35. Prefer OutputPage::addBodyClasses() |
733 | * @param string|string[] $classes One or more classes to add |
734 | */ |
735 | public function addHtmlClasses( $classes ) { |
736 | $this->mAdditionalHtmlClasses = array_merge( $this->mAdditionalHtmlClasses, (array)$classes ); |
737 | } |
738 | |
739 | /** |
740 | * Get an array of head items |
741 | * |
742 | * @return string[] |
743 | */ |
744 | public function getHeadItemsArray() { |
745 | return $this->mHeadItems; |
746 | } |
747 | |
748 | /** |
749 | * Add or replace a head item to the output |
750 | * |
751 | * Whenever possible, use more specific options like ResourceLoader modules, |
752 | * OutputPage::addLink(), OutputPage::addMeta() and OutputPage::addFeedLink() |
753 | * Fallback options for those are: OutputPage::addStyle, OutputPage::addScript(), |
754 | * OutputPage::addInlineScript() and OutputPage::addInlineStyle() |
755 | * This would be your very LAST fallback. |
756 | * |
757 | * @param string $name Item name |
758 | * @param string $value Raw HTML |
759 | * @param-taint $value exec_html |
760 | */ |
761 | public function addHeadItem( $name, $value ) { |
762 | $this->mHeadItems[$name] = $value; |
763 | } |
764 | |
765 | /** |
766 | * Add one or more head items to the output |
767 | * |
768 | * @since 1.28 |
769 | * @param string|string[] $values Raw HTML |
770 | * @param-taint $values exec_html |
771 | */ |
772 | public function addHeadItems( $values ) { |
773 | $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values ); |
774 | } |
775 | |
776 | /** |
777 | * Check if the header item $name is already set |
778 | * |
779 | * @param string $name Item name |
780 | * @return bool |
781 | */ |
782 | public function hasHeadItem( $name ) { |
783 | return isset( $this->mHeadItems[$name] ); |
784 | } |
785 | |
786 | /** |
787 | * Add a class to the <body> element |
788 | * |
789 | * @since 1.30 |
790 | * @param string|string[] $classes One or more classes to add |
791 | */ |
792 | public function addBodyClasses( $classes ) { |
793 | $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes ); |
794 | } |
795 | |
796 | /** |
797 | * Set whether the output should only contain the body of the article, |
798 | * without any skin, sidebar, etc. |
799 | * Used e.g. when calling with "action=render". |
800 | * |
801 | * @param bool $only Whether to output only the body of the article |
802 | */ |
803 | public function setArticleBodyOnly( $only ) { |
804 | $this->mArticleBodyOnly = $only; |
805 | } |
806 | |
807 | /** |
808 | * Return whether the output will contain only the body of the article |
809 | * |
810 | * @return bool |
811 | */ |
812 | public function getArticleBodyOnly() { |
813 | return $this->mArticleBodyOnly; |
814 | } |
815 | |
816 | /** |
817 | * Set an additional output property |
818 | * @since 1.21 |
819 | * |
820 | * @param string $name |
821 | * @param mixed $value |
822 | */ |
823 | public function setProperty( $name, $value ) { |
824 | $this->mProperties[$name] = $value; |
825 | } |
826 | |
827 | /** |
828 | * Get an additional output property |
829 | * @since 1.21 |
830 | * |
831 | * @param string $name |
832 | * @return mixed Property value or null if not found |
833 | */ |
834 | public function getProperty( $name ) { |
835 | return $this->mProperties[$name] ?? null; |
836 | } |
837 | |
838 | /** |
839 | * checkLastModified tells the client to use the client-cached page if |
840 | * possible. If successful, the OutputPage is disabled so that |
841 | * any future call to OutputPage->output() have no effect. |
842 | * |
843 | * Side effect: sets mLastModified for Last-Modified header |
844 | * |
845 | * @param string $timestamp |
846 | * |
847 | * @return bool True if cache-ok headers was sent. |
848 | */ |
849 | public function checkLastModified( $timestamp ) { |
850 | if ( !$timestamp || $timestamp == '19700101000000' ) { |
851 | wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP" ); |
852 | return false; |
853 | } |
854 | $config = $this->getConfig(); |
855 | if ( !$config->get( MainConfigNames::CachePages ) ) { |
856 | wfDebug( __METHOD__ . ": CACHE DISABLED" ); |
857 | return false; |
858 | } |
859 | |
860 | $timestamp = wfTimestamp( TS_MW, $timestamp ); |
861 | $modifiedTimes = [ |
862 | 'page' => $timestamp, |
863 | 'user' => $this->getUser()->getTouched(), |
864 | 'epoch' => $config->get( MainConfigNames::CacheEpoch ) |
865 | ]; |
866 | if ( $config->get( MainConfigNames::UseCdn ) ) { |
867 | $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, $this->getCdnCacheEpoch( |
868 | time(), |
869 | $config->get( MainConfigNames::CdnMaxAge ) |
870 | ) ); |
871 | } |
872 | $this->getHookRunner()->onOutputPageCheckLastModified( $modifiedTimes, $this ); |
873 | |
874 | $maxModified = max( $modifiedTimes ); |
875 | $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified ); |
876 | |
877 | $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' ); |
878 | if ( $clientHeader === false ) { |
879 | wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' ); |
880 | return false; |
881 | } |
882 | |
883 | # IE sends sizes after the date like this: |
884 | # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 |
885 | # this breaks strtotime(). |
886 | $clientHeader = preg_replace( '/;.*$/', '', $clientHeader ); |
887 | |
888 | // E_STRICT system time warnings |
889 | AtEase::suppressWarnings(); |
890 | $clientHeaderTime = strtotime( $clientHeader ); |
891 | AtEase::restoreWarnings(); |
892 | if ( !$clientHeaderTime ) { |
893 | wfDebug( __METHOD__ |
894 | . ": unable to parse the client's If-Modified-Since header: $clientHeader" ); |
895 | return false; |
896 | } |
897 | $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime ); |
898 | |
899 | # Make debug info |
900 | $info = ''; |
901 | foreach ( $modifiedTimes as $name => $value ) { |
902 | if ( $info !== '' ) { |
903 | $info .= ', '; |
904 | } |
905 | $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value ); |
906 | } |
907 | |
908 | wfDebug( __METHOD__ . ": client sent If-Modified-Since: " . |
909 | wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' ); |
910 | wfDebug( __METHOD__ . ": effective Last-Modified: " . |
911 | wfTimestamp( TS_ISO_8601, $maxModified ), 'private' ); |
912 | if ( $clientHeaderTime < $maxModified ) { |
913 | wfDebug( __METHOD__ . ": STALE, $info", 'private' ); |
914 | return false; |
915 | } |
916 | |
917 | # Not modified |
918 | # Give a 304 Not Modified response code and disable body output |
919 | wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' ); |
920 | ini_set( 'zlib.output_compression', 0 ); |
921 | $this->getRequest()->response()->statusHeader( 304 ); |
922 | $this->sendCacheControl(); |
923 | $this->disable(); |
924 | |
925 | // Don't output a compressed blob when using ob_gzhandler; |
926 | // it's technically against HTTP spec and seems to confuse |
927 | // Firefox when the response gets split over two packets. |
928 | wfResetOutputBuffers( false ); |
929 | |
930 | return true; |
931 | } |
932 | |
933 | /** |
934 | * @param int $reqTime Time of request (eg. now) |
935 | * @param int $maxAge Cache TTL in seconds |
936 | * @return int Timestamp |
937 | */ |
938 | private function getCdnCacheEpoch( $reqTime, $maxAge ) { |
939 | // Ensure Last-Modified is never more than $wgCdnMaxAge in the past, |
940 | // because even if the wiki page content hasn't changed since, static |
941 | // resources may have changed (skin HTML, interface messages, urls, etc.) |
942 | // and must roll-over in a timely manner (T46570) |
943 | return $reqTime - $maxAge; |
944 | } |
945 | |
946 | /** |
947 | * Override the last modified timestamp |
948 | * |
949 | * @param string $timestamp New timestamp, in a format readable by |
950 | * wfTimestamp() |
951 | */ |
952 | public function setLastModified( $timestamp ) { |
953 | $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp ); |
954 | } |
955 | |
956 | /** |
957 | * Set the robot policy for the page: <http://www.robotstxt.org/meta.html> |
958 | * |
959 | * @param string $policy The literal string to output as the contents of |
960 | * the meta tag. Will be parsed according to the spec and output in |
961 | * standardized form. |
962 | */ |
963 | public function setRobotPolicy( $policy ) { |
964 | $policy = Article::formatRobotPolicy( $policy ); |
965 | |
966 | if ( isset( $policy['index'] ) ) { |
967 | $this->setIndexPolicy( $policy['index'] ); |
968 | } |
969 | if ( isset( $policy['follow'] ) ) { |
970 | $this->setFollowPolicy( $policy['follow'] ); |
971 | } |
972 | } |
973 | |
974 | /** |
975 | * Get the current robot policy for the page as a string in the form |
976 | * <index policy>,<follow policy>. |
977 | * |
978 | * @return string |
979 | */ |
980 | public function getRobotPolicy() { |
981 | return "{$this->mIndexPolicy},{$this->mFollowPolicy}"; |
982 | } |
983 | |
984 | /** |
985 | * Format an array of robots options as a string of directives. |
986 | * |
987 | * @return string The robots policy options. |
988 | */ |
989 | private function formatRobotsOptions(): string { |
990 | $options = $this->mRobotsOptions; |
991 | // Check if options array has any non-integer keys. |
992 | if ( count( array_filter( array_keys( $options ), 'is_string' ) ) > 0 ) { |
993 | // Robots meta tags can have directives that are single strings or |
994 | // have parameters that should be formatted like <directive>:<setting>. |
995 | // If the options keys are strings, format them accordingly. |
996 | // https://developers.google.com/search/docs/advanced/robots/robots_meta_tag |
997 | array_walk( $options, static function ( &$value, $key ) { |
998 | $value = is_string( $key ) ? "{$key}:{$value}" : "{$value}"; |
999 | } ); |
1000 | } |
1001 | return implode( ',', $options ); |
1002 | } |
1003 | |
1004 | /** |
1005 | * Set the robots policy with options for the page. |
1006 | * |
1007 | * @since 1.38 |
1008 | * @param array $options An array of key-value pairs or a string |
1009 | * to populate the robots meta tag content attribute as a string. |
1010 | */ |
1011 | public function setRobotsOptions( array $options = [] ): void { |
1012 | $this->mRobotsOptions = array_merge( $this->mRobotsOptions, $options ); |
1013 | } |
1014 | |
1015 | /** |
1016 | * Get the robots policy content attribute for the page |
1017 | * as a string in the form <index policy>,<follow policy>,<options>. |
1018 | * |
1019 | * @return string |
1020 | */ |
1021 | private function getRobotsContent(): string { |
1022 | $robotOptionString = $this->formatRobotsOptions(); |
1023 | $robotArgs = ( $this->mIndexPolicy === 'index' && |
1024 | $this->mFollowPolicy === 'follow' ) ? |
1025 | [] : |
1026 | [ |
1027 | $this->mIndexPolicy, |
1028 | $this->mFollowPolicy, |
1029 | ]; |
1030 | if ( $robotOptionString ) { |
1031 | $robotArgs[] = $robotOptionString; |
1032 | } |
1033 | return implode( ',', $robotArgs ); |
1034 | } |
1035 | |
1036 | /** |
1037 | * Set the index policy for the page, but leave the follow policy un- |
1038 | * touched. |
1039 | * |
1040 | * @param string $policy Either 'index' or 'noindex'. |
1041 | */ |
1042 | public function setIndexPolicy( $policy ) { |
1043 | $policy = trim( $policy ); |
1044 | if ( in_array( $policy, [ 'index', 'noindex' ] ) ) { |
1045 | $this->mIndexPolicy = $policy; |
1046 | } |
1047 | } |
1048 | |
1049 | /** |
1050 | * Get the current index policy for the page as a string. |
1051 | * |
1052 | * @return string |
1053 | */ |
1054 | public function getIndexPolicy() { |
1055 | return $this->mIndexPolicy; |
1056 | } |
1057 | |
1058 | /** |
1059 | * Set the follow policy for the page, but leave the index policy un- |
1060 | * touched. |
1061 | * |
1062 | * @param string $policy Either 'follow' or 'nofollow'. |
1063 | */ |
1064 | public function setFollowPolicy( $policy ) { |
1065 | $policy = trim( $policy ); |
1066 | if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) { |
1067 | $this->mFollowPolicy = $policy; |
1068 | } |
1069 | } |
1070 | |
1071 | /** |
1072 | * Get the current follow policy for the page as a string. |
1073 | * |
1074 | * @return string |
1075 | */ |
1076 | public function getFollowPolicy() { |
1077 | return $this->mFollowPolicy; |
1078 | } |
1079 | |
1080 | /** |
1081 | * "HTML title" means the contents of "<title>". |
1082 | * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file. |
1083 | * |
1084 | * @param string|Message $name |
1085 | */ |
1086 | public function setHTMLTitle( $name ) { |
1087 | if ( $name instanceof Message ) { |
1088 | $this->mHTMLtitle = $name->setContext( $this->getContext() )->text(); |
1089 | } else { |
1090 | $this->mHTMLtitle = $name; |
1091 | } |
1092 | } |
1093 | |
1094 | /** |
1095 | * Return the "HTML title", i.e. the content of the "<title>" tag. |
1096 | * |
1097 | * @return string |
1098 | */ |
1099 | public function getHTMLTitle() { |
1100 | return $this->mHTMLtitle; |
1101 | } |
1102 | |
1103 | /** |
1104 | * Set $mRedirectedFrom, the page which redirected us to the current page. |
1105 | * |
1106 | * @param PageReference $t |
1107 | */ |
1108 | public function setRedirectedFrom( PageReference $t ) { |
1109 | $this->mRedirectedFrom = $t; |
1110 | } |
1111 | |
1112 | /** |
1113 | * "Page title" means the contents of \<h1\>. It is stored as a valid HTML |
1114 | * fragment. This function allows good tags like \<sup\> in the \<h1\> tag, |
1115 | * but not bad tags like \<script\>. This function automatically sets |
1116 | * \<title\> to the same content as \<h1\> but with all tags removed. Bad |
1117 | * tags that were escaped in \<h1\> will still be escaped in \<title\>, and |
1118 | * good tags like \<i\> will be dropped entirely. |
1119 | * |
1120 | * @param string|Message $name The page title, either as HTML string or |
1121 | * as a message which will be formatted with FORMAT_TEXT to yield HTML. |
1122 | * Passing a Message is deprecated, since 1.41; please use |
1123 | * ::setPageTitleMsg() for that case instead. |
1124 | * @param-taint $name tainted |
1125 | * Phan-taint-check gets very confused by $name being either a string or a Message |
1126 | */ |
1127 | public function setPageTitle( $name ) { |
1128 | if ( $name instanceof Message ) { |
1129 | // T343849: passing a Message is deprecated and a warning will |
1130 | // eventually be emitted here. |
1131 | $name = $name->setContext( $this->getContext() )->text(); |
1132 | } |
1133 | $this->setPageTitleInternal( $name ); |
1134 | } |
1135 | |
1136 | /** |
1137 | * "Page title" means the contents of \<h1\>. This message takes a |
1138 | * Message, which will be formatted with FORMAT_ESCAPED to yield |
1139 | * HTML. Raw parameters to the message may contain some HTML |
1140 | * tags; see ::setPageTitle() and Sanitizer::removeSomeTags() for |
1141 | * details. This function automatically sets \<title\> to the |
1142 | * same content as \<h1\> but with all tags removed. Bad tags from |
1143 | * "raw" parameters that were escaped in \<h1\> will still be |
1144 | * escaped in \<title\>, and good tags like \<i\> will be dropped |
1145 | * entirely. |
1146 | * |
1147 | * @param Message $msg The page title, as a message which will be |
1148 | * formatted with FORMAT_ESCAPED to yield HTML. |
1149 | * @since 1.41 |
1150 | */ |
1151 | public function setPageTitleMsg( Message $msg ): void { |
1152 | $this->setPageTitleInternal( |
1153 | $msg->setContext( $this->getContext() )->escaped() |
1154 | ); |
1155 | } |
1156 | |
1157 | private function setPageTitleInternal( string $name ): void { |
1158 | # change "<script>foo&bar</script>" to "<script>foo&bar</script>" |
1159 | # but leave "<i>foobar</i>" alone |
1160 | $nameWithTags = Sanitizer::removeSomeTags( $name ); |
1161 | $this->mPageTitle = $nameWithTags; |
1162 | |
1163 | # change "<i>foo&bar</i>" to "foo&bar" |
1164 | $this->setHTMLTitle( |
1165 | $this->msg( 'pagetitle' )->plaintextParams( Sanitizer::stripAllTags( $nameWithTags ) ) |
1166 | ->inContentLanguage() |
1167 | ); |
1168 | } |
1169 | |
1170 | /** |
1171 | * Return the "page title", i.e. the content of the \<h1\> tag. |
1172 | * |
1173 | * @return string |
1174 | */ |
1175 | public function getPageTitle() { |
1176 | return $this->mPageTitle; |
1177 | } |
1178 | |
1179 | /** |
1180 | * Same as page title but only contains name of the page, not any other text. |
1181 | * |
1182 | * @since 1.32 |
1183 | * @param string $html Page title text. |
1184 | * @see OutputPage::setPageTitle |
1185 | */ |
1186 | public function setDisplayTitle( $html ) { |
1187 | $this->displayTitle = $html; |
1188 | } |
1189 | |
1190 | /** |
1191 | * Returns page display title. |
1192 | * |
1193 | * Performs some normalization, but this not as strict the magic word. |
1194 | * |
1195 | * @since 1.32 |
1196 | * @return string HTML |
1197 | */ |
1198 | public function getDisplayTitle() { |
1199 | $html = $this->displayTitle; |
1200 | if ( $html === null ) { |
1201 | return htmlspecialchars( $this->getTitle()->getPrefixedText(), ENT_NOQUOTES ); |
1202 | } |
1203 | |
1204 | return Sanitizer::removeSomeTags( $html ); |
1205 | } |
1206 | |
1207 | /** |
1208 | * Returns page display title without namespace prefix if possible. |
1209 | * |
1210 | * This method is unreliable and best avoided. (T314399) |
1211 | * |
1212 | * @since 1.32 |
1213 | * @return string HTML |
1214 | */ |
1215 | public function getUnprefixedDisplayTitle() { |
1216 | $service = MediaWikiServices::getInstance(); |
1217 | $languageConverter = $service->getLanguageConverterFactory() |
1218 | ->getLanguageConverter( $service->getContentLanguage() ); |
1219 | $text = $this->getDisplayTitle(); |
1220 | |
1221 | // Create a regexp with matching groups as placeholders for the namespace, separator and main text |
1222 | $pageTitleRegexp = "/^" . str_replace( |
1223 | preg_quote( '(.+?)', '/' ), |
1224 | '(.+?)', |
1225 | preg_quote( Parser::formatPageTitle( '(.+?)', '(.+?)', '(.+?)' ), '/' ) |
1226 | ) . "$/"; |
1227 | $matches = []; |
1228 | if ( preg_match( $pageTitleRegexp, $text, $matches ) ) { |
1229 | // The regexp above could be manipulated by malicious user input, |
1230 | // sanitize the result just in case |
1231 | return Sanitizer::removeSomeTags( $matches[3] ); |
1232 | } |
1233 | |
1234 | $nsPrefix = $languageConverter->convertNamespace( |
1235 | $this->getTitle()->getNamespace() |
1236 | ) . ':'; |
1237 | $prefix = preg_quote( $nsPrefix, '/' ); |
1238 | |
1239 | return preg_replace( "/^$prefix/i", '', $text ); |
1240 | } |
1241 | |
1242 | /** |
1243 | * Set the Title object to use |
1244 | * |
1245 | * @param PageReference $t |
1246 | */ |
1247 | public function setTitle( PageReference $t ) { |
1248 | $t = Title::newFromPageReference( $t ); |
1249 | |
1250 | // @phan-suppress-next-next-line PhanUndeclaredMethod |
1251 | // @fixme Not all implementations of IContextSource have this method! |
1252 | $this->getContext()->setTitle( $t ); |
1253 | } |
1254 | |
1255 | /** |
1256 | * Replace the subtitle with $str |
1257 | * |
1258 | * @param string|Message $str New value of the subtitle. String should be safe HTML. |
1259 | */ |
1260 | public function setSubtitle( $str ) { |
1261 | $this->clearSubtitle(); |
1262 | $this->addSubtitle( $str ); |
1263 | } |
1264 | |
1265 | /** |
1266 | * Add $str to the subtitle |
1267 | * |
1268 | * @param string|Message $str String or Message to add to the subtitle. String should be safe HTML. |
1269 | * @param-taint $str exec_html |
1270 | */ |
1271 | public function addSubtitle( $str ) { |
1272 | if ( $str instanceof Message ) { |
1273 | $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse(); |
1274 | } else { |
1275 | $this->mSubtitle[] = $str; |
1276 | } |
1277 | } |
1278 | |
1279 | /** |
1280 | * Build message object for a subtitle containing a backlink to a page |
1281 | * |
1282 | * @since 1.25 |
1283 | * @param PageReference $page Title to link to |
1284 | * @param array $query Array of additional parameters to include in the link |
1285 | * @return Message |
1286 | */ |
1287 | public static function buildBacklinkSubtitle( PageReference $page, $query = [] ) { |
1288 | if ( $page instanceof PageRecord || $page instanceof Title ) { |
1289 | // Callers will typically have a PageRecord |
1290 | if ( $page->isRedirect() ) { |
1291 | $query['redirect'] = 'no'; |
1292 | } |
1293 | } elseif ( $page->getNamespace() !== NS_SPECIAL ) { |
1294 | // We don't know whether it's a redirect, so add the parameter, just to be sure. |
1295 | $query['redirect'] = 'no'; |
1296 | } |
1297 | |
1298 | $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); |
1299 | return wfMessage( 'backlinksubtitle' ) |
1300 | ->rawParams( $linkRenderer->makeLink( $page, null, [], $query ) ); |
1301 | } |
1302 | |
1303 | /** |
1304 | * Add a subtitle containing a backlink to a page |
1305 | * |
1306 | * @param PageReference $title Title to link to |
1307 | * @param array $query Array of additional parameters to include in the link |
1308 | */ |
1309 | public function addBacklinkSubtitle( PageReference $title, $query = [] ) { |
1310 | $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) ); |
1311 | } |
1312 | |
1313 | /** |
1314 | * Clear the subtitles |
1315 | */ |
1316 | public function clearSubtitle() { |
1317 | $this->mSubtitle = []; |
1318 | } |
1319 | |
1320 | /** |
1321 | * @return string |
1322 | */ |
1323 | public function getSubtitle() { |
1324 | return implode( "<br />\n\t\t\t\t", $this->mSubtitle ); |
1325 | } |
1326 | |
1327 | /** |
1328 | * Set the page as printable, i.e. it'll be displayed with all |
1329 | * print styles included |
1330 | */ |
1331 | public function setPrintable() { |
1332 | $this->mPrintable = true; |
1333 | } |
1334 | |
1335 | /** |
1336 | * Return whether the page is "printable" |
1337 | * |
1338 | * @return bool |
1339 | */ |
1340 | public function isPrintable() { |
1341 | return $this->mPrintable; |
1342 | } |
1343 | |
1344 | /** |
1345 | * Disable output completely, i.e. calling output() will have no effect |
1346 | */ |
1347 | public function disable() { |
1348 | $this->mDoNothing = true; |
1349 | } |
1350 | |
1351 | /** |
1352 | * Return whether the output will be completely disabled |
1353 | * |
1354 | * @return bool |
1355 | */ |
1356 | public function isDisabled() { |
1357 | return $this->mDoNothing; |
1358 | } |
1359 | |
1360 | /** |
1361 | * Show an "add new section" link? |
1362 | * |
1363 | * @return bool |
1364 | */ |
1365 | public function showNewSectionLink() { |
1366 | return $this->mNewSectionLink; |
1367 | } |
1368 | |
1369 | /** |
1370 | * Forcibly hide the new section link? |
1371 | * |
1372 | * @return bool |
1373 | */ |
1374 | public function forceHideNewSectionLink() { |
1375 | return $this->mHideNewSectionLink; |
1376 | } |
1377 | |
1378 | /** |
1379 | * Add or remove feed links in the page header |
1380 | * This is mainly kept for backward compatibility, see OutputPage::addFeedLink() |
1381 | * for the new version |
1382 | * @see addFeedLink() |
1383 | * |
1384 | * @param bool $show True: add default feeds, false: remove all feeds |
1385 | */ |
1386 | public function setSyndicated( $show = true ) { |
1387 | if ( $show ) { |
1388 | $this->setFeedAppendQuery( false ); |
1389 | } else { |
1390 | $this->mFeedLinks = []; |
1391 | } |
1392 | } |
1393 | |
1394 | /** |
1395 | * Return effective list of advertised feed types |
1396 | * @see addFeedLink() |
1397 | * |
1398 | * @return string[] Array of feed type names ( 'rss', 'atom' ) |
1399 | */ |
1400 | protected function getAdvertisedFeedTypes() { |
1401 | if ( $this->getConfig()->get( MainConfigNames::Feed ) ) { |
1402 | return $this->getConfig()->get( MainConfigNames::AdvertisedFeedTypes ); |
1403 | } else { |
1404 | return []; |
1405 | } |
1406 | } |
1407 | |
1408 | /** |
1409 | * Add default feeds to the page header |
1410 | * This is mainly kept for backward compatibility, see OutputPage::addFeedLink() |
1411 | * for the new version |
1412 | * @see addFeedLink() |
1413 | * |
1414 | * @param string|false $val Query to append to feed links or false to output |
1415 | * default links |
1416 | */ |
1417 | public function setFeedAppendQuery( $val ) { |
1418 | $this->mFeedLinks = []; |
1419 | |
1420 | foreach ( $this->getAdvertisedFeedTypes() as $type ) { |
1421 | $query = "feed=$type"; |
1422 | if ( is_string( $val ) ) { |
1423 | $query .= '&' . $val; |
1424 | } |
1425 | $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query ); |
1426 | } |
1427 | } |
1428 | |
1429 | /** |
1430 | * Add a feed link to the page header |
1431 | * |
1432 | * @param string $format Feed type, should be a key of $wgFeedClasses |
1433 | * @param string $href URL |
1434 | */ |
1435 | public function addFeedLink( $format, $href ) { |
1436 | if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) { |
1437 | $this->mFeedLinks[$format] = $href; |
1438 | } |
1439 | } |
1440 | |
1441 | /** |
1442 | * Should we output feed links for this page? |
1443 | * @return bool |
1444 | */ |
1445 | public function isSyndicated() { |
1446 | return count( $this->mFeedLinks ) > 0; |
1447 | } |
1448 | |
1449 | /** |
1450 | * Return URLs for each supported syndication format for this page. |
1451 | * @return array Associating format keys with URLs |
1452 | */ |
1453 | public function getSyndicationLinks() { |
1454 | return $this->mFeedLinks; |
1455 | } |
1456 | |
1457 | /** |
1458 | * Will currently always return null |
1459 | * |
1460 | * @return null |
1461 | */ |
1462 | public function getFeedAppendQuery() { |
1463 | return $this->mFeedLinksAppendQuery; |
1464 | } |
1465 | |
1466 | /** |
1467 | * Set whether the displayed content is related to the source of the |
1468 | * corresponding article on the wiki |
1469 | * Setting true will cause the change "article related" toggle to true |
1470 | * |
1471 | * @param bool $newVal |
1472 | */ |
1473 | public function setArticleFlag( $newVal ) { |
1474 | $this->mIsArticle = $newVal; |
1475 | if ( $newVal ) { |
1476 | $this->mIsArticleRelated = $newVal; |
1477 | } |
1478 | } |
1479 | |
1480 | /** |
1481 | * Return whether the content displayed page is related to the source of |
1482 | * the corresponding article on the wiki |
1483 | * |
1484 | * @return bool |
1485 | */ |
1486 | public function isArticle() { |
1487 | return $this->mIsArticle; |
1488 | } |
1489 | |
1490 | /** |
1491 | * Set whether this page is related an article on the wiki |
1492 | * Setting false will cause the change of "article flag" toggle to false |
1493 | * |
1494 | * @param bool $newVal |
1495 | */ |
1496 | public function setArticleRelated( $newVal ) { |
1497 | $this->mIsArticleRelated = $newVal; |
1498 | if ( !$newVal ) { |
1499 | $this->mIsArticle = false; |
1500 | } |
1501 | } |
1502 | |
1503 | /** |
1504 | * Return whether this page is related an article on the wiki |
1505 | * |
1506 | * @return bool |
1507 | */ |
1508 | public function isArticleRelated() { |
1509 | return $this->mIsArticleRelated; |
1510 | } |
1511 | |
1512 | /** |
1513 | * Set whether the standard copyright should be shown for the current page. |
1514 | * |
1515 | * @param bool $hasCopyright |
1516 | */ |
1517 | public function setCopyright( $hasCopyright ) { |
1518 | $this->mHasCopyright = $hasCopyright; |
1519 | } |
1520 | |
1521 | /** |
1522 | * Return whether the standard copyright should be shown for the current page. |
1523 | * By default, it is true for all articles but other pages |
1524 | * can signal it by using setCopyright( true ). |
1525 | * |
1526 | * Used by SkinTemplate to decided whether to show the copyright. |
1527 | * |
1528 | * @return bool |
1529 | */ |
1530 | public function showsCopyright() { |
1531 | return $this->isArticle() || $this->mHasCopyright; |
1532 | } |
1533 | |
1534 | /** |
1535 | * Add new language links |
1536 | * |
1537 | * @param string[] $newLinkArray Array of interwiki-prefixed (non DB key) titles |
1538 | * (e.g. 'fr:Test page') |
1539 | */ |
1540 | public function addLanguageLinks( array $newLinkArray ) { |
1541 | $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray ); |
1542 | } |
1543 | |
1544 | /** |
1545 | * Reset the language links and add new language links |
1546 | * |
1547 | * @param string[] $newLinkArray Array of interwiki-prefixed (non DB key) titles |
1548 | * (e.g. 'fr:Test page') |
1549 | */ |
1550 | public function setLanguageLinks( array $newLinkArray ) { |
1551 | $this->mLanguageLinks = $newLinkArray; |
1552 | } |
1553 | |
1554 | /** |
1555 | * Get the list of language links |
1556 | * |
1557 | * @return string[] Array of interwiki-prefixed (non DB key) titles (e.g. 'fr:Test page') |
1558 | */ |
1559 | public function getLanguageLinks() { |
1560 | return $this->mLanguageLinks; |
1561 | } |
1562 | |
1563 | /** |
1564 | * Add an array of categories, with names in the keys |
1565 | * |
1566 | * @param array $categories Mapping category name => sort key |
1567 | */ |
1568 | public function addCategoryLinks( array $categories ) { |
1569 | if ( !$categories ) { |
1570 | return; |
1571 | } |
1572 | |
1573 | $res = $this->addCategoryLinksToLBAndGetResult( $categories ); |
1574 | |
1575 | # Set all the values to 'normal'. |
1576 | $categories = array_fill_keys( array_keys( $categories ), 'normal' ); |
1577 | $pageData = []; |
1578 | |
1579 | # Mark hidden categories |
1580 | foreach ( $res as $row ) { |
1581 | if ( isset( $row->pp_value ) ) { |
1582 | $categories[$row->page_title] = 'hidden'; |
1583 | } |
1584 | // Page exists, cache results |
1585 | if ( isset( $row->page_id ) ) { |
1586 | $pageData[$row->page_title] = $row; |
1587 | } |
1588 | } |
1589 | |
1590 | # Add the remaining categories to the skin |
1591 | if ( $this->getHookRunner()->onOutputPageMakeCategoryLinks( |
1592 | $this, $categories, $this->mCategoryLinks ) |
1593 | ) { |
1594 | $services = MediaWikiServices::getInstance(); |
1595 | $linkRenderer = $services->getLinkRenderer(); |
1596 | $languageConverter = $services->getLanguageConverterFactory() |
1597 | ->getLanguageConverter( $services->getContentLanguage() ); |
1598 | foreach ( $categories as $category => $type ) { |
1599 | // array keys will cast numeric category names to ints, so cast back to string |
1600 | $category = (string)$category; |
1601 | $origcategory = $category; |
1602 | if ( array_key_exists( $category, $pageData ) ) { |
1603 | $title = Title::newFromRow( $pageData[$category] ); |
1604 | } else { |
1605 | $title = Title::makeTitleSafe( NS_CATEGORY, $category ); |
1606 | } |
1607 | if ( !$title ) { |
1608 | continue; |
1609 | } |
1610 | $languageConverter->findVariantLink( $category, $title, true ); |
1611 | |
1612 | if ( $category != $origcategory && array_key_exists( $category, $categories ) ) { |
1613 | continue; |
1614 | } |
1615 | $text = $languageConverter->convertHtml( $title->getText() ); |
1616 | $this->mCategories[$type][] = $title->getText(); |
1617 | $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) ); |
1618 | } |
1619 | } |
1620 | } |
1621 | |
1622 | /** |
1623 | * @param array $categories |
1624 | * @return IResultWrapper |
1625 | */ |
1626 | protected function addCategoryLinksToLBAndGetResult( array $categories ) { |
1627 | # Add the links to a LinkBatch |
1628 | $arr = [ NS_CATEGORY => $categories ]; |
1629 | $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory(); |
1630 | $lb = $linkBatchFactory->newLinkBatch(); |
1631 | $lb->setArray( $arr ); |
1632 | |
1633 | # Fetch existence plus the hiddencat property |
1634 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
1635 | $fields = array_merge( |
1636 | LinkCache::getSelectFields(), |
1637 | [ 'pp_value' ] |
1638 | ); |
1639 | |
1640 | $res = $dbr->newSelectQueryBuilder() |
1641 | ->select( $fields ) |
1642 | ->from( 'page' ) |
1643 | ->leftJoin( 'page_props', null, [ |
1644 | 'pp_propname' => 'hiddencat', |
1645 | 'pp_page = page_id', |
1646 | ] ) |
1647 | ->where( $lb->constructSet( 'page', $dbr ) ) |
1648 | ->caller( __METHOD__ ) |
1649 | ->fetchResultSet(); |
1650 | |
1651 | # Add the results to the link cache |
1652 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
1653 | $lb->addResultToCache( $linkCache, $res ); |
1654 | |
1655 | return $res; |
1656 | } |
1657 | |
1658 | /** |
1659 | * Reset the category links (but not the category list) and add $categories |
1660 | * |
1661 | * @param array $categories Mapping category name => sort key |
1662 | */ |
1663 | public function setCategoryLinks( array $categories ) { |
1664 | $this->mCategoryLinks = []; |
1665 | $this->addCategoryLinks( $categories ); |
1666 | } |
1667 | |
1668 | /** |
1669 | * Get the list of category links, in a 2-D array with the following format: |
1670 | * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for |
1671 | * hidden categories) and $link a HTML fragment with a link to the category |
1672 | * page |
1673 | * |
1674 | * @return string[][] |
1675 | * @return-taint none |
1676 | */ |
1677 | public function getCategoryLinks() { |
1678 | return $this->mCategoryLinks; |
1679 | } |
1680 | |
1681 | /** |
1682 | * Get the list of category names this page belongs to. |
1683 | * |
1684 | * @param string $type The type of categories which should be returned. Possible values: |
1685 | * * all: all categories of all types |
1686 | * * hidden: only the hidden categories |
1687 | * * normal: all categories, except hidden categories |
1688 | * @return string[] |
1689 | */ |
1690 | public function getCategories( $type = 'all' ) { |
1691 | if ( $type === 'all' ) { |
1692 | $allCategories = []; |
1693 | foreach ( $this->mCategories as $categories ) { |
1694 | $allCategories = array_merge( $allCategories, $categories ); |
1695 | } |
1696 | return $allCategories; |
1697 | } |
1698 | if ( !isset( $this->mCategories[$type] ) ) { |
1699 | throw new InvalidArgumentException( 'Invalid category type given: ' . $type ); |
1700 | } |
1701 | return $this->mCategories[$type]; |
1702 | } |
1703 | |
1704 | /** |
1705 | * Add an array of indicators, with their identifiers as array |
1706 | * keys and HTML contents as values. |
1707 | * |
1708 | * In case of duplicate keys, existing values are overwritten. |
1709 | * |
1710 | * @note External code which calls this method should ensure that |
1711 | * any indicators sourced from parsed wikitext are wrapped with |
1712 | * the appropriate class; see note in ::getIndicators(). |
1713 | * |
1714 | * @param string[] $indicators |
1715 | * @param-taint $indicators exec_html |
1716 | * @since 1.25 |
1717 | */ |
1718 | public function setIndicators( array $indicators ) { |
1719 | $this->mIndicators = $indicators + $this->mIndicators; |
1720 | // Keep ordered by key |
1721 | ksort( $this->mIndicators ); |
1722 | } |
1723 | |
1724 | /** |
1725 | * Get the indicators associated with this page. |
1726 | * |
1727 | * The array will be internally ordered by item keys. |
1728 | * |
1729 | * @return string[] Keys: identifiers, values: HTML contents |
1730 | * @since 1.25 |
1731 | */ |
1732 | public function getIndicators(): array { |
1733 | // Note that some -- but not all -- indicators will be wrapped |
1734 | // with a class appropriate for user-generated wikitext content |
1735 | // (usually .mw-parser-output). The exceptions would be an |
1736 | // indicator added via ::addHelpLink() below, which adds content |
1737 | // which don't come from the parser and is not user-generated; |
1738 | // and any indicators added by extensions which may call |
1739 | // OutputPage::setIndicators() directly. In the latter case the |
1740 | // caller is responsible for wrapping any parser-generated |
1741 | // indicators. |
1742 | return $this->mIndicators; |
1743 | } |
1744 | |
1745 | /** |
1746 | * Adds help link with an icon via page indicators. |
1747 | * Link target can be overridden by a local message containing a wikilink: |
1748 | * the message key is: lowercase action or special page name + '-helppage'. |
1749 | * @param string $to Target MediaWiki.org page title or encoded URL. |
1750 | * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MediaWiki.org. |
1751 | * @since 1.25 |
1752 | */ |
1753 | public function addHelpLink( $to, $overrideBaseUrl = false ) { |
1754 | $this->addModuleStyles( 'mediawiki.helplink' ); |
1755 | $text = $this->msg( 'helppage-top-gethelp' )->escaped(); |
1756 | |
1757 | if ( $overrideBaseUrl ) { |
1758 | $helpUrl = $to; |
1759 | } else { |
1760 | $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) ); |
1761 | $helpUrl = "https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded"; |
1762 | } |
1763 | |
1764 | $link = Html::rawElement( |
1765 | 'a', |
1766 | [ |
1767 | 'href' => $helpUrl, |
1768 | 'target' => '_blank', |
1769 | 'class' => 'mw-helplink', |
1770 | ], |
1771 | $text |
1772 | ); |
1773 | |
1774 | // See note in ::getIndicators() above -- unlike wikitext-generated |
1775 | // indicators which come from ParserOutput, this indicator will not |
1776 | // be wrapped. |
1777 | $this->setIndicators( [ 'mw-helplink' => $link ] ); |
1778 | } |
1779 | |
1780 | /** |
1781 | * Do not allow scripts which can be modified by wiki users to load on this page; |
1782 | * only allow scripts bundled with, or generated by, the software. |
1783 | * Site-wide styles are controlled by a config setting, since they can be |
1784 | * used to create a custom skin/theme, but not user-specific ones. |
1785 | * |
1786 | * @todo this should be given a more accurate name |
1787 | */ |
1788 | public function disallowUserJs() { |
1789 | $this->reduceAllowedModules( |
1790 | RL\Module::TYPE_SCRIPTS, |
1791 | RL\Module::ORIGIN_CORE_INDIVIDUAL |
1792 | ); |
1793 | |
1794 | // Site-wide styles are controlled by a config setting, see T73621 |
1795 | // for background on why. User styles are never allowed. |
1796 | if ( $this->getConfig()->get( MainConfigNames::AllowSiteCSSOnRestrictedPages ) ) { |
1797 | $styleOrigin = RL\Module::ORIGIN_USER_SITEWIDE; |
1798 | } else { |
1799 | $styleOrigin = RL\Module::ORIGIN_CORE_INDIVIDUAL; |
1800 | } |
1801 | $this->reduceAllowedModules( |
1802 | RL\Module::TYPE_STYLES, |
1803 | $styleOrigin |
1804 | ); |
1805 | } |
1806 | |
1807 | /** |
1808 | * Show what level of JavaScript / CSS untrustworthiness is allowed on this page |
1809 | * @see RL\Module::$origin |
1810 | * @param string $type RL\Module TYPE_ constant |
1811 | * @return int Module ORIGIN_ class constant |
1812 | */ |
1813 | public function getAllowedModules( $type ) { |
1814 | if ( $type == RL\Module::TYPE_COMBINED ) { |
1815 | return min( array_values( $this->mAllowedModules ) ); |
1816 | } else { |
1817 | return $this->mAllowedModules[$type] ?? RL\Module::ORIGIN_ALL; |
1818 | } |
1819 | } |
1820 | |
1821 | /** |
1822 | * Limit the highest level of CSS/JS untrustworthiness allowed. |
1823 | * |
1824 | * If passed the same or a higher level than the current level of untrustworthiness set, the |
1825 | * level will remain unchanged. |
1826 | * |
1827 | * @param string $type |
1828 | * @param int $level RL\Module class constant |
1829 | */ |
1830 | public function reduceAllowedModules( $type, $level ) { |
1831 | $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level ); |
1832 | } |
1833 | |
1834 | /** |
1835 | * Prepend $text to the body HTML |
1836 | * |
1837 | * @param string $text HTML |
1838 | * @param-taint $text exec_html |
1839 | */ |
1840 | public function prependHTML( $text ) { |
1841 | $this->mBodytext = $text . $this->mBodytext; |
1842 | } |
1843 | |
1844 | /** |
1845 | * Append $text to the body HTML |
1846 | * |
1847 | * @param string $text HTML |
1848 | * @param-taint $text exec_html |
1849 | */ |
1850 | public function addHTML( $text ) { |
1851 | $this->mBodytext .= $text; |
1852 | } |
1853 | |
1854 | /** |
1855 | * Shortcut for adding an Html::element via addHTML. |
1856 | * |
1857 | * @since 1.19 |
1858 | * |
1859 | * @param string $element |
1860 | * @param array $attribs |
1861 | * @param string $contents |
1862 | */ |
1863 | public function addElement( $element, array $attribs = [], $contents = '' ) { |
1864 | $this->addHTML( Html::element( $element, $attribs, $contents ) ); |
1865 | } |
1866 | |
1867 | /** |
1868 | * Clear the body HTML |
1869 | */ |
1870 | public function clearHTML() { |
1871 | $this->mBodytext = ''; |
1872 | } |
1873 | |
1874 | /** |
1875 | * Get the body HTML |
1876 | * |
1877 | * @return string HTML |
1878 | */ |
1879 | public function getHTML() { |
1880 | return $this->mBodytext; |
1881 | } |
1882 | |
1883 | /** |
1884 | * Get/set the ParserOptions object to use for wikitext parsing |
1885 | * |
1886 | * @return ParserOptions |
1887 | */ |
1888 | public function parserOptions() { |
1889 | if ( !$this->mParserOptions ) { |
1890 | if ( !$this->getUser()->isSafeToLoad() ) { |
1891 | // Context user isn't unstubbable yet, so don't try to get a |
1892 | // ParserOptions for it. And don't cache this ParserOptions |
1893 | // either. |
1894 | $po = ParserOptions::newFromAnon(); |
1895 | $po->setAllowUnsafeRawHtml( false ); |
1896 | return $po; |
1897 | } |
1898 | |
1899 | $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() ); |
1900 | $this->mParserOptions->setAllowUnsafeRawHtml( false ); |
1901 | } |
1902 | |
1903 | return $this->mParserOptions; |
1904 | } |
1905 | |
1906 | /** |
1907 | * Set the revision ID which will be seen by the wiki text parser |
1908 | * for things such as embedded {{REVISIONID}} variable use. |
1909 | * |
1910 | * @param int|null $revid A positive integer, or null |
1911 | * @return mixed Previous value |
1912 | */ |
1913 | public function setRevisionId( $revid ) { |
1914 | $val = $revid === null ? null : intval( $revid ); |
1915 | return wfSetVar( $this->mRevisionId, $val, true ); |
1916 | } |
1917 | |
1918 | /** |
1919 | * Get the displayed revision ID |
1920 | * |
1921 | * @return int|null |
1922 | */ |
1923 | public function getRevisionId() { |
1924 | return $this->mRevisionId; |
1925 | } |
1926 | |
1927 | /** |
1928 | * Set whether the revision displayed (as set in ::setRevisionId()) |
1929 | * is the latest revision of the page. |
1930 | * |
1931 | * @param bool $isCurrent |
1932 | */ |
1933 | public function setRevisionIsCurrent( bool $isCurrent ): void { |
1934 | $this->mRevisionIsCurrent = $isCurrent; |
1935 | } |
1936 | |
1937 | /** |
1938 | * Whether the revision displayed is the latest revision of the page |
1939 | * |
1940 | * @since 1.34 |
1941 | * @return bool |
1942 | */ |
1943 | public function isRevisionCurrent(): bool { |
1944 | return $this->mRevisionId == 0 || ( |
1945 | $this->mRevisionIsCurrent ?? ( |
1946 | $this->mRevisionId == $this->getTitle()->getLatestRevID() |
1947 | ) |
1948 | ); |
1949 | } |
1950 | |
1951 | /** |
1952 | * Set the timestamp of the revision which will be displayed. This is used |
1953 | * to avoid a extra DB call in SkinComponentFooter::lastModified(). |
1954 | * |
1955 | * @param string|null $timestamp |
1956 | * @return mixed Previous value |
1957 | */ |
1958 | public function setRevisionTimestamp( $timestamp ) { |
1959 | return wfSetVar( $this->mRevisionTimestamp, $timestamp, true ); |
1960 | } |
1961 | |
1962 | /** |
1963 | * Get the timestamp of displayed revision. |
1964 | * This will be null if not filled by setRevisionTimestamp(). |
1965 | * |
1966 | * @return string|null |
1967 | */ |
1968 | public function getRevisionTimestamp() { |
1969 | return $this->mRevisionTimestamp; |
1970 | } |
1971 | |
1972 | /** |
1973 | * Set the displayed file version |
1974 | * |
1975 | * @param File|null $file |
1976 | * @return mixed Previous value |
1977 | */ |
1978 | public function setFileVersion( $file ) { |
1979 | $val = null; |
1980 | if ( $file instanceof File && $file->exists() ) { |
1981 | $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ]; |
1982 | } |
1983 | return wfSetVar( $this->mFileVersion, $val, true ); |
1984 | } |
1985 | |
1986 | /** |
1987 | * Get the displayed file version |
1988 | * |
1989 | * @return array|null ('time' => MW timestamp, 'sha1' => sha1) |
1990 | */ |
1991 | public function getFileVersion() { |
1992 | return $this->mFileVersion; |
1993 | } |
1994 | |
1995 | /** |
1996 | * Get the templates used on this page |
1997 | * |
1998 | * @return array<int,array<string,int>> (namespace => dbKey => revId) |
1999 | * @since 1.18 |
2000 | */ |
2001 | public function getTemplateIds() { |
2002 | return $this->mTemplateIds; |
2003 | } |
2004 | |
2005 | /** |
2006 | * Get the files used on this page |
2007 | * |
2008 | * @return array [ dbKey => [ 'time' => MW timestamp or null, 'sha1' => sha1 or '' ] ] |
2009 | * @since 1.18 |
2010 | */ |
2011 | public function getFileSearchOptions() { |
2012 | return $this->mImageTimeKeys; |
2013 | } |
2014 | |
2015 | /** |
2016 | * Convert wikitext *in the user interface language* to HTML and |
2017 | * add it to the buffer. The result will not be |
2018 | * language-converted, as user interface messages are already |
2019 | * localized into a specific variant. Assumes that the current |
2020 | * page title will be used if optional $title is not |
2021 | * provided. Output will be tidy. |
2022 | * |
2023 | * @param string $text Wikitext in the user interface language |
2024 | * @param bool $linestart Is this the start of a line? (Defaults to true) |
2025 | * @param PageReference|null $title Optional title to use; default of `null` |
2026 | * means use current page title. |
2027 | * @since 1.32 |
2028 | */ |
2029 | public function addWikiTextAsInterface( |
2030 | $text, $linestart = true, PageReference $title = null |
2031 | ) { |
2032 | $title ??= $this->getTitle(); |
2033 | if ( !$title ) { |
2034 | throw new RuntimeException( 'Title is null' ); |
2035 | } |
2036 | $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true ); |
2037 | } |
2038 | |
2039 | /** |
2040 | * Convert wikitext *in the user interface language* to HTML and |
2041 | * add it to the buffer with a `<div class="$wrapperClass">` |
2042 | * wrapper. The result will not be language-converted, as user |
2043 | * interface messages as already localized into a specific |
2044 | * variant. The $text will be parsed in start-of-line context. |
2045 | * Output will be tidy. |
2046 | * |
2047 | * @param string $wrapperClass The class attribute value for the <div> |
2048 | * wrapper in the output HTML |
2049 | * @param string $text Wikitext in the user interface language |
2050 | * @since 1.32 |
2051 | */ |
2052 | public function wrapWikiTextAsInterface( |
2053 | $wrapperClass, $text |
2054 | ) { |
2055 | $this->addWikiTextTitleInternal( |
2056 | $text, $this->getTitle(), |
2057 | /*linestart*/true, /*interface*/true, |
2058 | $wrapperClass |
2059 | ); |
2060 | } |
2061 | |
2062 | /** |
2063 | * Convert wikitext *in the page content language* to HTML and add |
2064 | * it to the buffer. The result with be language-converted to the |
2065 | * user's preferred variant. Assumes that the current page title |
2066 | * will be used if optional $title is not provided. Output will be |
2067 | * tidy. |
2068 | * |
2069 | * @param string $text Wikitext in the page content language |
2070 | * @param bool $linestart Is this the start of a line? (Defaults to true) |
2071 | * @param PageReference|null $title Optional title to use; default of `null` |
2072 | * means use current page title. |
2073 | * @since 1.32 |
2074 | */ |
2075 | public function addWikiTextAsContent( |
2076 | $text, $linestart = true, PageReference $title = null |
2077 | ) { |
2078 | $title ??= $this->getTitle(); |
2079 | if ( !$title ) { |
2080 | throw new RuntimeException( 'Title is null' ); |
2081 | } |
2082 | $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false ); |
2083 | } |
2084 | |
2085 | /** |
2086 | * Add wikitext with a custom Title object. |
2087 | * Output is unwrapped. |
2088 | * |
2089 | * @param string $text Wikitext |
2090 | * @param PageReference $title |
2091 | * @param bool $linestart Is this the start of a line?@param |
2092 | * @param bool $interface Whether it is an interface message |
2093 | * (for example disables conversion) |
2094 | * @param string|null $wrapperClass if not empty, wraps the output in |
2095 | * a `<div class="$wrapperClass">` |
2096 | */ |
2097 | private function addWikiTextTitleInternal( |
2098 | $text, PageReference $title, $linestart, $interface, $wrapperClass = null |
2099 | ) { |
2100 | $parserOutput = $this->parseInternal( |
2101 | $text, $title, $linestart, $interface |
2102 | ); |
2103 | |
2104 | $this->addParserOutput( $parserOutput, [ |
2105 | 'enableSectionEditLinks' => false, |
2106 | 'wrapperDivClass' => $wrapperClass ?? '', |
2107 | ] ); |
2108 | } |
2109 | |
2110 | /** |
2111 | * Adds Table of Contents data to OutputPage from ParserOutput |
2112 | * @param TOCData $tocData |
2113 | * @internal For use by Article.php |
2114 | */ |
2115 | public function setTOCData( TOCData $tocData ) { |
2116 | $this->tocData = $tocData; |
2117 | } |
2118 | |
2119 | /** |
2120 | * @internal For usage in Skin::getTOCData() only. |
2121 | * @return ?TOCData Table of Contents data, or |
2122 | * null if OutputPage::setTOCData() has not been called. |
2123 | */ |
2124 | public function getTOCData(): ?TOCData { |
2125 | return $this->tocData; |
2126 | } |
2127 | |
2128 | /** |
2129 | * @internal Will be replaced by direct access to |
2130 | * ParserOutput::getOutputFlag() |
2131 | * @param string $name A flag name from ParserOutputFlags |
2132 | * @return bool |
2133 | */ |
2134 | public function getOutputFlag( string $name ): bool { |
2135 | return isset( $this->mOutputFlags[$name] ); |
2136 | } |
2137 | |
2138 | /** |
2139 | * @internal For use by ViewAction/Article only |
2140 | * @since 1.42 |
2141 | * @param Bcp47Code $lang |
2142 | */ |
2143 | public function setContentLangForJS( Bcp47Code $lang ): void { |
2144 | $this->contentLang = MediaWikiServices::getInstance()->getLanguageFactory() |
2145 | ->getLanguage( $lang ); |
2146 | } |
2147 | |
2148 | /** |
2149 | * Which language getJSVars should use |
2150 | * |
2151 | * Use of this is strongly discouraged in favour of ParserOutput::getLanguage(), |
2152 | * and should not be needed in most cases given that ParserOutput::getText() |
2153 | * already takes care of 'lang' and 'dir' attributes. |
2154 | * |
2155 | * Consider whether RequestContext::getLanguage (e.g. OutputPage::getLanguage |
2156 | * or Skin::getLanguage) or MediaWikiServices::getContentLanguage is more |
2157 | * appropiate first for your use case. |
2158 | * |
2159 | * @since 1.42 |
2160 | * @return Language |
2161 | */ |
2162 | private function getContentLangForJS(): Language { |
2163 | if ( !$this->contentLang ) { |
2164 | // If this is not set, then we're likely not on in a request that renders page content |
2165 | // (e.g. ViewAction or ApiParse), but rather a different Action or SpecialPage. |
2166 | // In that case there isn't a main ParserOutput object to represent the page or output. |
2167 | // But, the skin and frontend code mostly don't make this distinction, and so we still |
2168 | // need to return something for mw.config. |
2169 | // |
2170 | // For historical reasons, the expectation is that: |
2171 | // * on a SpecialPage, we return the language for the content area just like on a |
2172 | // page view. SpecialPage content is localised, and so this is the user language. |
2173 | // * on an Action about a WikiPage, we return the language that content would have |
2174 | // been shown in, if this were a page view. This is generally the page language |
2175 | // as stored in the database, except adapted to the current user (e.g. in case of |
2176 | // translated pages or a language variant preference) |
2177 | // |
2178 | // This mess was centralised to here in 2023 (T341244). |
2179 | $title = $this->getTitle(); |
2180 | if ( $title->isSpecialPage() ) { |
2181 | // Special pages render in the interface language, based on request context. |
2182 | // If the user's preference (or request parameter) specifies a variant, |
2183 | // the content may have been converted to the user's language variant. |
2184 | $pageLang = $this->getLanguage(); |
2185 | } else { |
2186 | wfDebug( __METHOD__ . ' has to guess ParserOutput language' ); |
2187 | // Guess what Article::getParserOutput and ParserOptions::optionsHash() would decide |
2188 | // on a page view: |
2189 | // |
2190 | // - Pages may have a custom page_lang set in the database, |
2191 | // via Title::getPageLanguage/Title::getDbPageLanguage |
2192 | // |
2193 | // - Interface messages (NS_MEDIAWIKI) render based on their subpage, |
2194 | // via Title::getPageLanguage/ContentHandler::getPageLanguage/MessageCache::figureMessage |
2195 | // |
2196 | // - Otherwise, pages are assumed to be in the wiki's default content language. |
2197 | // via Title::getPageLanguage/ContentHandler::getPageLanguage/MediaWikiServices::getContentLanguage |
2198 | $pageLang = $title->getPageLanguage(); |
2199 | } |
2200 | if ( $title->getNamespace() !== NS_MEDIAWIKI ) { |
2201 | $services = MediaWikiServices::getInstance(); |
2202 | $langConv = $services->getLanguageConverterFactory()->getLanguageConverter( $pageLang ); |
2203 | // NOTE: LanguageConverter::getPreferredVariant inspects global RequestContext. |
2204 | // This usually returns $pageLang unchanged. |
2205 | $variant = $langConv->getPreferredVariant(); |
2206 | if ( $pageLang->getCode() !== $variant ) { |
2207 | $pageLang = $services->getLanguageFactory()->getLanguage( $variant ); |
2208 | } |
2209 | } |
2210 | $this->contentLang = $pageLang; |
2211 | } |
2212 | return $this->contentLang; |
2213 | } |
2214 | |
2215 | /** |
2216 | * Add all metadata associated with a ParserOutput object, but without the actual HTML. This |
2217 | * includes categories, language links, ResourceLoader modules, effects of certain magic words, |
2218 | * and so on. It does *not* include section information. |
2219 | * |
2220 | * @since 1.24 |
2221 | * @param ParserOutput $parserOutput |
2222 | */ |
2223 | public function addParserOutputMetadata( ParserOutput $parserOutput ) { |
2224 | // T301020 This should eventually use the standard "merge ParserOutput" |
2225 | // function between $parserOutput and $this->metadata. |
2226 | $this->mLanguageLinks = |
2227 | array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() ); |
2228 | $this->addCategoryLinks( $parserOutput->getCategoryMap() ); |
2229 | |
2230 | // Parser-generated indicators get wrapped like other parser output. |
2231 | $wrapClass = $parserOutput->getWrapperDivClass(); |
2232 | $result = []; |
2233 | foreach ( $parserOutput->getIndicators() as $name => $html ) { |
2234 | if ( $html !== '' && $wrapClass !== '' ) { |
2235 | $html = Html::rawElement( 'div', [ 'class' => $wrapClass ], $html ); |
2236 | } |
2237 | $result[$name] = $html; |
2238 | } |
2239 | $this->setIndicators( $result ); |
2240 | |
2241 | $tocData = $parserOutput->getTOCData(); |
2242 | // Do not override existing TOC data if the new one is empty (T307256#8817705) |
2243 | // TODO: Invent a way to merge TOCs from multiple outputs (T327429) |
2244 | if ( $tocData !== null && ( $this->tocData === null || count( $tocData->getSections() ) > 0 ) ) { |
2245 | $this->setTOCData( $tocData ); |
2246 | } |
2247 | |
2248 | // FIXME: Best practice is for OutputPage to be an accumulator, as |
2249 | // addParserOutputMetadata() may be called multiple times, but the |
2250 | // following lines overwrite any previous data. These should |
2251 | // be migrated to an injection pattern. (T301020, T300979) |
2252 | // (Note that OutputPage::getOutputFlag() also contains this |
2253 | // information, with flags from each $parserOutput all OR'ed together.) |
2254 | $this->mNewSectionLink = $parserOutput->getNewSection(); |
2255 | $this->mHideNewSectionLink = $parserOutput->getHideNewSection(); |
2256 | $this->mNoGallery = $parserOutput->getNoGallery(); |
2257 | |
2258 | if ( !$parserOutput->isCacheable() ) { |
2259 | $this->disableClientCache(); |
2260 | } |
2261 | $this->addHeadItems( $parserOutput->getHeadItems() ); |
2262 | $this->addModules( $parserOutput->getModules() ); |
2263 | $this->addModuleStyles( $parserOutput->getModuleStyles() ); |
2264 | $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); |
2265 | $this->mPreventClickjacking = $this->mPreventClickjacking |
2266 | || $parserOutput->getPreventClickjacking(); |
2267 | $scriptSrcs = $parserOutput->getExtraCSPScriptSrcs(); |
2268 | foreach ( $scriptSrcs as $src ) { |
2269 | $this->getCSP()->addScriptSrc( $src ); |
2270 | } |
2271 | $defaultSrcs = $parserOutput->getExtraCSPDefaultSrcs(); |
2272 | foreach ( $defaultSrcs as $src ) { |
2273 | $this->getCSP()->addDefaultSrc( $src ); |
2274 | } |
2275 | $styleSrcs = $parserOutput->getExtraCSPStyleSrcs(); |
2276 | foreach ( $styleSrcs as $src ) { |
2277 | $this->getCSP()->addStyleSrc( $src ); |
2278 | } |
2279 | |
2280 | // If $wgImagePreconnect is true, and if the output contains images, give the user-agent |
2281 | // a hint about a remote hosts from which images may be served. Launched in T123582. |
2282 | if ( $this->getConfig()->get( MainConfigNames::ImagePreconnect ) && count( $parserOutput->getImages() ) ) { |
2283 | $preconnect = []; |
2284 | // Optimization: Instead of processing each image, assume that wikis either serve both |
2285 | // foreign and local from the same remote hostname (e.g. public wikis at WMF), or that |
2286 | // foreign images are common enough to be worth the preconnect (e.g. private wikis). |
2287 | $repoGroup = MediaWikiServices::getInstance()->getRepoGroup(); |
2288 | $repoGroup->forEachForeignRepo( static function ( $repo ) use ( &$preconnect ) { |
2289 | $preconnect[] = $repo->getZoneUrl( 'thumb' ); |
2290 | } ); |
2291 | // Consider both foreign and local repos. While LocalRepo by default uses a relative |
2292 | // path on the same domain, wiki farms may configure it to use a dedicated hostname. |
2293 | $preconnect[] = $repoGroup->getLocalRepo()->getZoneUrl( 'thumb' ); |
2294 | foreach ( $preconnect as $url ) { |
2295 | $host = parse_url( $url, PHP_URL_HOST ); |
2296 | // It is expected that file URLs are often path-only, without hostname (T317329). |
2297 | if ( $host ) { |
2298 | $this->addLink( [ 'rel' => 'preconnect', 'href' => '//' . $host ] ); |
2299 | break; |
2300 | } |
2301 | } |
2302 | } |
2303 | |
2304 | // Template versioning... |
2305 | foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) { |
2306 | if ( isset( $this->mTemplateIds[$ns] ) ) { |
2307 | $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns]; |
2308 | } else { |
2309 | $this->mTemplateIds[$ns] = $dbks; |
2310 | } |
2311 | } |
2312 | // File versioning... |
2313 | foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) { |
2314 | $this->mImageTimeKeys[$dbk] = $data; |
2315 | } |
2316 | |
2317 | // Enable OOUI if requested via ParserOutput |
2318 | if ( $parserOutput->getEnableOOUI() ) { |
2319 | $this->enableOOUI(); |
2320 | } |
2321 | |
2322 | // Include parser limit report |
2323 | // FIXME: This should append, rather than overwrite, or else this |
2324 | // data should be injected into the OutputPage like is done for the |
2325 | // other page-level things (like OutputPage::setTOCData()). |
2326 | if ( !$this->limitReportJSData ) { |
2327 | $this->limitReportJSData = $parserOutput->getLimitReportJSData(); |
2328 | } |
2329 | |
2330 | // Link flags are ignored for now, but may in the future be |
2331 | // used to mark individual language links. |
2332 | $linkFlags = []; |
2333 | $this->getHookRunner()->onLanguageLinks( $this->getTitle(), $this->mLanguageLinks, $linkFlags ); |
2334 | |
2335 | $this->getHookRunner()->onOutputPageParserOutput( $this, $parserOutput ); |
2336 | |
2337 | // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata |
2338 | // so that extensions may modify ParserOutput to toggle TOC. |
2339 | // This cannot be moved to addParserOutputText because that is not |
2340 | // called by EditPage for Preview. |
2341 | |
2342 | // ParserOutputFlags::SHOW_TOC is used to indicate whether the TOC |
2343 | // should be shown (or hidden) in the output. |
2344 | $this->mEnableTOC = $this->mEnableTOC || |
2345 | $parserOutput->getOutputFlag( ParserOutputFlags::SHOW_TOC ); |
2346 | // Uniform handling of all boolean flags: they are OR'ed together |
2347 | // (See ParserOutput::collectMetadata()) |
2348 | $flags = |
2349 | array_flip( $parserOutput->getAllFlags() ) + |
2350 | array_flip( ParserOutputFlags::cases() ); |
2351 | foreach ( $flags as $name => $ignore ) { |
2352 | if ( $parserOutput->getOutputFlag( $name ) ) { |
2353 | $this->mOutputFlags[$name] = true; |
2354 | } |
2355 | } |
2356 | } |
2357 | |
2358 | private function getParserOutputText( ParserOutput $parserOutput, array $poOptions = [] ): string { |
2359 | // Add default options from the skin |
2360 | $skin = $this->getSkin(); |
2361 | $skinOptions = $skin->getOptions(); |
2362 | $poOptions += [ |
2363 | 'skin' => $skin, |
2364 | 'injectTOC' => $skinOptions['toc'], |
2365 | ]; |
2366 | // Note: this path absolutely expects $parserOutput to be mutated by getText, see T353257 |
2367 | return $parserOutput->getText( $poOptions ); |
2368 | } |
2369 | |
2370 | /** |
2371 | * Add the HTML and enhancements for it (like ResourceLoader modules) associated with a |
2372 | * ParserOutput object, without any other metadata. |
2373 | * |
2374 | * @since 1.24 |
2375 | * @param ParserOutput $parserOutput |
2376 | * @param array $poOptions Options to ParserOutput::getText() |
2377 | */ |
2378 | public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) { |
2379 | $text = $this->getParserOutputText( $parserOutput, $poOptions ); |
2380 | $this->addParserOutputText( $text, $poOptions ); |
2381 | |
2382 | $this->addModules( $parserOutput->getModules() ); |
2383 | $this->addModuleStyles( $parserOutput->getModuleStyles() ); |
2384 | |
2385 | $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); |
2386 | } |
2387 | |
2388 | /** |
2389 | * Add the HTML associated with a ParserOutput object, without any metadata. |
2390 | * |
2391 | * @internal For local use only |
2392 | * @param string|ParserOutput $text |
2393 | * @param array $poOptions Options to ParserOutput::getText() |
2394 | */ |
2395 | public function addParserOutputText( $text, $poOptions = [] ) { |
2396 | if ( $text instanceof ParserOutput ) { |
2397 | wfDeprecated( __METHOD__ . ' with ParserOutput as first arg', '1.42' ); |
2398 | $text = $this->getParserOutputText( $text, $poOptions ); |
2399 | } |
2400 | $this->getHookRunner()->onOutputPageBeforeHTML( $this, $text ); |
2401 | $this->addHTML( $text ); |
2402 | } |
2403 | |
2404 | /** |
2405 | * Add everything from a ParserOutput object. |
2406 | * |
2407 | * @param ParserOutput $parserOutput |
2408 | * @param array $poOptions Options to ParserOutput::getText() |
2409 | */ |
2410 | public function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) { |
2411 | $text = $this->getParserOutputText( $parserOutput, $poOptions ); |
2412 | $this->addParserOutputMetadata( $parserOutput ); |
2413 | $this->addParserOutputText( $text, $poOptions ); |
2414 | } |
2415 | |
2416 | /** |
2417 | * Add the output of a QuickTemplate to the output buffer |
2418 | * |
2419 | * @param \QuickTemplate &$template |
2420 | */ |
2421 | public function addTemplate( &$template ) { |
2422 | $this->addHTML( $template->getHTML() ); |
2423 | } |
2424 | |
2425 | /** |
2426 | * Parse wikitext *in the page content language* and return the HTML. |
2427 | * The result will be language-converted to the user's preferred variant. |
2428 | * Output will be tidy. |
2429 | * |
2430 | * @param string $text Wikitext in the page content language |
2431 | * @param bool $linestart Is this the start of a line? (Defaults to true) |
2432 | * @return string HTML |
2433 | * @since 1.32 |
2434 | */ |
2435 | public function parseAsContent( $text, $linestart = true ) { |
2436 | return $this->parseInternal( |
2437 | $text, $this->getTitle(), $linestart, /*interface*/false |
2438 | )->getText( [ |
2439 | 'allowTOC' => false, |
2440 | 'enableSectionEditLinks' => false, |
2441 | 'wrapperDivClass' => '', |
2442 | 'userLang' => $this->getContext()->getLanguage(), |
2443 | ] ); |
2444 | } |
2445 | |
2446 | /** |
2447 | * Parse wikitext *in the user interface language* and return the HTML. |
2448 | * The result will not be language-converted, as user interface messages |
2449 | * are already localized into a specific variant. |
2450 | * Output will be tidy. |
2451 | * |
2452 | * @param string $text Wikitext in the user interface language |
2453 | * @param bool $linestart Is this the start of a line? (Defaults to true) |
2454 | * @return string HTML |
2455 | * @since 1.32 |
2456 | */ |
2457 | public function parseAsInterface( $text, $linestart = true ) { |
2458 | return $this->parseInternal( |
2459 | $text, $this->getTitle(), $linestart, /*interface*/true |
2460 | )->getText( [ |
2461 | 'allowTOC' => false, |
2462 | 'enableSectionEditLinks' => false, |
2463 | 'wrapperDivClass' => '', |
2464 | 'userLang' => $this->getContext()->getLanguage(), |
2465 | ] ); |
2466 | } |
2467 | |
2468 | /** |
2469 | * Parse wikitext *in the user interface language*, strip |
2470 | * paragraph wrapper, and return the HTML. |
2471 | * The result will not be language-converted, as user interface messages |
2472 | * are already localized into a specific variant. |
2473 | * Output will be tidy. Outer paragraph wrapper will only be stripped |
2474 | * if the result is a single paragraph. |
2475 | * |
2476 | * @param string $text Wikitext in the user interface language |
2477 | * @param bool $linestart Is this the start of a line? (Defaults to true) |
2478 | * @return string HTML |
2479 | * @since 1.32 |
2480 | */ |
2481 | public function parseInlineAsInterface( $text, $linestart = true ) { |
2482 | return Parser::stripOuterParagraph( |
2483 | $this->parseAsInterface( $text, $linestart ) |
2484 | ); |
2485 | } |
2486 | |
2487 | /** |
2488 | * Parse wikitext and return the HTML (internal implementation helper) |
2489 | * |
2490 | * @param string $text |
2491 | * @param PageReference $title The title to use |
2492 | * @param bool $linestart Is this the start of a line? |
2493 | * @param bool $interface Use interface language (instead of content language) while parsing |
2494 | * language sensitive magic words like GRAMMAR and PLURAL. This also disables |
2495 | * LanguageConverter. |
2496 | * @return ParserOutput |
2497 | */ |
2498 | private function parseInternal( $text, $title, $linestart, $interface ) { |
2499 | if ( $title === null ) { |
2500 | throw new RuntimeException( 'Empty $mTitle in ' . __METHOD__ ); |
2501 | } |
2502 | $popts = $this->parserOptions(); |
2503 | |
2504 | $oldInterface = $popts->setInterfaceMessage( (bool)$interface ); |
2505 | |
2506 | $parserOutput = MediaWikiServices::getInstance()->getParserFactory()->getInstance() |
2507 | ->parse( |
2508 | $text, $title, $popts, |
2509 | $linestart, true, $this->mRevisionId |
2510 | ); |
2511 | |
2512 | $popts->setInterfaceMessage( $oldInterface ); |
2513 | |
2514 | return $parserOutput; |
2515 | } |
2516 | |
2517 | /** |
2518 | * Set the value of the "s-maxage" part of the "Cache-control" HTTP header |
2519 | * |
2520 | * @param int $maxage Maximum cache time on the CDN, in seconds. |
2521 | */ |
2522 | public function setCdnMaxage( $maxage ) { |
2523 | $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit ); |
2524 | } |
2525 | |
2526 | /** |
2527 | * Set the value of the "s-maxage" part of the "Cache-control" HTTP header to $maxage if that is |
2528 | * lower than the current s-maxage. Either way, $maxage is now an upper limit on s-maxage, so |
2529 | * that future calls to setCdnMaxage() will no longer be able to raise the s-maxage above |
2530 | * $maxage. |
2531 | * |
2532 | * @param int $maxage Maximum cache time on the CDN, in seconds |
2533 | * @since 1.27 |
2534 | */ |
2535 | public function lowerCdnMaxage( $maxage ) { |
2536 | $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit ); |
2537 | $this->setCdnMaxage( $this->mCdnMaxage ); |
2538 | } |
2539 | |
2540 | /** |
2541 | * Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage() |
2542 | * |
2543 | * This sets and returns $minTTL if $mtime is false or null. Otherwise, |
2544 | * the TTL is higher the older the $mtime timestamp is. Essentially, the |
2545 | * TTL is 90% of the age of the object, subject to the min and max. |
2546 | * |
2547 | * @param string|int|float|bool|null $mtime Last-Modified timestamp |
2548 | * @param int $minTTL Minimum TTL in seconds [default: 1 minute] |
2549 | * @param int $maxTTL Maximum TTL in seconds [default: $wgCdnMaxAge] |
2550 | * @since 1.28 |
2551 | */ |
2552 | public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) { |
2553 | $minTTL = $minTTL ?: ExpirationAwareness::TTL_MINUTE; |
2554 | $maxTTL = $maxTTL ?: $this->getConfig()->get( MainConfigNames::CdnMaxAge ); |
2555 | |
2556 | if ( $mtime === null || $mtime === false ) { |
2557 | return; // entity does not exist |
2558 | } |
2559 | |
2560 | $age = MWTimestamp::time() - (int)wfTimestamp( TS_UNIX, $mtime ); |
2561 | $adaptiveTTL = max( 0.9 * $age, $minTTL ); |
2562 | $adaptiveTTL = min( $adaptiveTTL, $maxTTL ); |
2563 | |
2564 | $this->lowerCdnMaxage( (int)$adaptiveTTL ); |
2565 | } |
2566 | |
2567 | /** |
2568 | * Do not send nocache headers |
2569 | */ |
2570 | public function enableClientCache(): void { |
2571 | $this->mEnableClientCache = true; |
2572 | } |
2573 | |
2574 | /** |
2575 | * Force the page to send nocache headers |
2576 | * @since 1.38 |
2577 | */ |
2578 | public function disableClientCache(): void { |
2579 | $this->mEnableClientCache = false; |
2580 | } |
2581 | |
2582 | /** |
2583 | * Whether the output might become publicly cached. |
2584 | * |
2585 | * @since 1.34 |
2586 | * @return bool |
2587 | */ |
2588 | public function couldBePublicCached() { |
2589 | if ( !$this->cacheIsFinal ) { |
2590 | // - The entry point handles its own caching and/or doesn't use OutputPage. |
2591 | // (such as load.php, or MediaWiki\Rest\EntryPoint). |
2592 | // |
2593 | // - Or, we haven't finished processing the main part of the request yet |
2594 | // (e.g. Action::show, SpecialPage::execute), and the state may still |
2595 | // change via enableClientCache(). |
2596 | return true; |
2597 | } |
2598 | // e.g. various error-type pages disable all client caching |
2599 | return $this->mEnableClientCache; |
2600 | } |
2601 | |
2602 | /** |
2603 | * Set the expectation that cache control will not change after this point. |
2604 | * |
2605 | * This should be called after the main processing logic has completed |
2606 | * (e.g. Action::show or SpecialPage::execute), but may be called |
2607 | * before Skin output has started (OutputPage::output). |
2608 | * |
2609 | * @since 1.34 |
2610 | */ |
2611 | public function considerCacheSettingsFinal() { |
2612 | $this->cacheIsFinal = true; |
2613 | } |
2614 | |
2615 | /** |
2616 | * Get the list of cookie names that will influence the cache |
2617 | * |
2618 | * @return array |
2619 | */ |
2620 | public function getCacheVaryCookies() { |
2621 | if ( self::$cacheVaryCookies === null ) { |
2622 | $config = $this->getConfig(); |
2623 | self::$cacheVaryCookies = array_values( array_unique( array_merge( |
2624 | SessionManager::singleton()->getVaryCookies(), |
2625 | [ |
2626 | 'forceHTTPS', |
2627 | ], |
2628 | $config->get( MainConfigNames::CacheVaryCookies ) |
2629 | ) ) ); |
2630 | $this->getHookRunner()->onGetCacheVaryCookies( $this, self::$cacheVaryCookies ); |
2631 | } |
2632 | return self::$cacheVaryCookies; |
2633 | } |
2634 | |
2635 | /** |
2636 | * Check if the request has a cache-varying cookie header |
2637 | * If it does, it's very important that we don't allow public caching |
2638 | * |
2639 | * @return bool |
2640 | */ |
2641 | public function haveCacheVaryCookies() { |
2642 | $request = $this->getRequest(); |
2643 | foreach ( $this->getCacheVaryCookies() as $cookieName ) { |
2644 | if ( $request->getCookie( $cookieName, '', '' ) !== '' ) { |
2645 | wfDebug( __METHOD__ . ": found $cookieName" ); |
2646 | return true; |
2647 | } |
2648 | } |
2649 | wfDebug( __METHOD__ . ": no cache-varying cookies found" ); |
2650 | return false; |
2651 | } |
2652 | |
2653 | /** |
2654 | * Add an HTTP header that will have an influence on the cache |
2655 | * |
2656 | * @param string $header Header name |
2657 | */ |
2658 | public function addVaryHeader( $header ) { |
2659 | if ( !array_key_exists( $header, $this->mVaryHeader ) ) { |
2660 | $this->mVaryHeader[$header] = null; |
2661 | } |
2662 | } |
2663 | |
2664 | /** |
2665 | * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader, |
2666 | * such as Accept-Encoding or Cookie |
2667 | * |
2668 | * @return string |
2669 | */ |
2670 | public function getVaryHeader() { |
2671 | // If we vary on cookies, let's make sure it's always included here too. |
2672 | if ( $this->getCacheVaryCookies() ) { |
2673 | $this->addVaryHeader( 'Cookie' ); |
2674 | } |
2675 | |
2676 | foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $_ ) { |
2677 | $this->addVaryHeader( $header ); |
2678 | } |
2679 | return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) ); |
2680 | } |
2681 | |
2682 | /** |
2683 | * Add an HTTP Link: header |
2684 | * |
2685 | * @param string $header Header value |
2686 | */ |
2687 | public function addLinkHeader( $header ) { |
2688 | $this->mLinkHeader[] = $header; |
2689 | } |
2690 | |
2691 | /** |
2692 | * Return a Link: header. Based on the values of $mLinkHeader. |
2693 | * |
2694 | * @return string|false |
2695 | */ |
2696 | public function getLinkHeader() { |
2697 | if ( !$this->mLinkHeader ) { |
2698 | return false; |
2699 | } |
2700 | |
2701 | return 'Link: ' . implode( ',', $this->mLinkHeader ); |
2702 | } |
2703 | |
2704 | /** |
2705 | * T23672: Add Accept-Language to Vary header if there's no 'variant' parameter in GET. |
2706 | * |
2707 | * For example: |
2708 | * /w/index.php?title=Main_page will vary based on Accept-Language; but |
2709 | * /w/index.php?title=Main_page&variant=zh-cn will not. |
2710 | */ |
2711 | private function addAcceptLanguage() { |
2712 | $title = $this->getTitle(); |
2713 | if ( !$title instanceof Title ) { |
2714 | return; |
2715 | } |
2716 | |
2717 | $languageConverter = MediaWikiServices::getInstance()->getLanguageConverterFactory() |
2718 | ->getLanguageConverter( $title->getPageLanguage() ); |
2719 | if ( !$this->getRequest()->getCheck( 'variant' ) && $languageConverter->hasVariants() ) { |
2720 | $this->addVaryHeader( 'Accept-Language' ); |
2721 | } |
2722 | } |
2723 | |
2724 | /** |
2725 | * Set the prevent-clickjacking flag. |
2726 | * |
2727 | * If true, will cause an X-Frame-Options header appropriate for |
2728 | * edit pages to be sent. The header value is controlled by |
2729 | * $wgEditPageFrameOptions. This is the default for special |
2730 | * pages. If you display a CSRF-protected form on an ordinary view |
2731 | * page, then you need to call this function. |
2732 | * |
2733 | * Setting this flag to false will turn off frame-breaking. This |
2734 | * can be called from pages which do not contain any |
2735 | * CSRF-protected HTML form. |
2736 | * |
2737 | * @param bool $enable If true, will cause an X-Frame-Options header |
2738 | * appropriate for edit pages to be sent. |
2739 | * |
2740 | * @since 1.38 |
2741 | */ |
2742 | public function setPreventClickjacking( bool $enable ) { |
2743 | $this->mPreventClickjacking = $enable; |
2744 | } |
2745 | |
2746 | /** |
2747 | * Get the prevent-clickjacking flag |
2748 | * |
2749 | * @since 1.24 |
2750 | * @return bool |
2751 | */ |
2752 | public function getPreventClickjacking() { |
2753 | return $this->mPreventClickjacking; |
2754 | } |
2755 | |
2756 | /** |
2757 | * Get the X-Frame-Options header value (without the name part), or false |
2758 | * if there isn't one. This is used by Skin to determine whether to enable |
2759 | * JavaScript frame-breaking, for clients that don't support X-Frame-Options. |
2760 | * |
2761 | * @return string|false |
2762 | */ |
2763 | public function getFrameOptions() { |
2764 | $config = $this->getConfig(); |
2765 | if ( $config->get( MainConfigNames::BreakFrames ) ) { |
2766 | return 'DENY'; |
2767 | } elseif ( $this->mPreventClickjacking && $config->get( MainConfigNames::EditPageFrameOptions ) ) { |
2768 | return $config->get( MainConfigNames::EditPageFrameOptions ); |
2769 | } |
2770 | return false; |
2771 | } |
2772 | |
2773 | private function getReportTo() { |
2774 | $config = $this->getConfig(); |
2775 | |
2776 | $expiry = $config->get( MainConfigNames::ReportToExpiry ); |
2777 | |
2778 | if ( !$expiry ) { |
2779 | return false; |
2780 | } |
2781 | |
2782 | $endpoints = $config->get( MainConfigNames::ReportToEndpoints ); |
2783 | |
2784 | if ( !$endpoints ) { |
2785 | return false; |
2786 | } |
2787 | |
2788 | $output = [ 'max_age' => $expiry, 'endpoints' => [] ]; |
2789 | |
2790 | foreach ( $endpoints as $endpoint ) { |
2791 | $output['endpoints'][] = [ 'url' => $endpoint ]; |
2792 | } |
2793 | |
2794 | return json_encode( $output, JSON_UNESCAPED_SLASHES ); |
2795 | } |
2796 | |
2797 | private function getFeaturePolicyReportOnly() { |
2798 | $config = $this->getConfig(); |
2799 | |
2800 | $features = $config->get( MainConfigNames::FeaturePolicyReportOnly ); |
2801 | return implode( ';', $features ); |
2802 | } |
2803 | |
2804 | /** |
2805 | * Send cache control HTTP headers |
2806 | */ |
2807 | public function sendCacheControl() { |
2808 | $response = $this->getRequest()->response(); |
2809 | $config = $this->getConfig(); |
2810 | |
2811 | $this->addVaryHeader( 'Cookie' ); |
2812 | $this->addAcceptLanguage(); |
2813 | |
2814 | # don't serve compressed data to clients who can't handle it |
2815 | # maintain different caches for logged-in users and non-logged in ones |
2816 | $response->header( $this->getVaryHeader() ); |
2817 | |
2818 | if ( $this->mEnableClientCache ) { |
2819 | if ( !$config->get( MainConfigNames::UseCdn ) ) { |
2820 | $privateReason = 'config'; |
2821 | } elseif ( $response->hasCookies() ) { |
2822 | $privateReason = 'set-cookies'; |
2823 | // The client might use methods other than cookies to appear logged-in. |
2824 | // E.g. HTTP headers, or query parameter tokens, OAuth, etc. |
2825 | } elseif ( SessionManager::getGlobalSession()->isPersistent() ) { |
2826 | $privateReason = 'session'; |
2827 | } elseif ( $this->isPrintable() ) { |
2828 | $privateReason = 'printable'; |
2829 | } elseif ( $this->mCdnMaxage == 0 ) { |
2830 | $privateReason = 'no-maxage'; |
2831 | } elseif ( $this->haveCacheVaryCookies() ) { |
2832 | $privateReason = 'cache-vary-cookies'; |
2833 | } else { |
2834 | $privateReason = false; |
2835 | } |
2836 | |
2837 | if ( $privateReason === false ) { |
2838 | # We'll purge the proxy cache for anons explicitly, but require end user agents |
2839 | # to revalidate against the proxy on each visit. |
2840 | # IMPORTANT! The CDN needs to replace the Cache-Control header with |
2841 | # Cache-Control: s-maxage=0, must-revalidate, max-age=0 |
2842 | wfDebug( __METHOD__ . |
2843 | ": local proxy caching; {$this->mLastModified} **", 'private' ); |
2844 | # start with a shorter timeout for initial testing |
2845 | # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); |
2846 | $response->header( "Cache-Control: " . |
2847 | "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" ); |
2848 | } else { |
2849 | # We do want clients to cache if they can, but they *must* check for updates |
2850 | # on revisiting the page. |
2851 | wfDebug( __METHOD__ . ": private caching ($privateReason); {$this->mLastModified} **", 'private' ); |
2852 | |
2853 | $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); |
2854 | $response->header( "Cache-Control: private, must-revalidate, max-age=0" ); |
2855 | } |
2856 | if ( $this->mLastModified ) { |
2857 | $response->header( "Last-Modified: {$this->mLastModified}" ); |
2858 | } |
2859 | } else { |
2860 | wfDebug( __METHOD__ . ": no caching **", 'private' ); |
2861 | |
2862 | # In general, the absence of a last modified header should be enough to prevent |
2863 | # the client from using its cache. We send a few other things just to make sure. |
2864 | $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); |
2865 | $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); |
2866 | } |
2867 | } |
2868 | |
2869 | /** |
2870 | * Transfer styles and JavaScript modules from skin. |
2871 | * |
2872 | * @param Skin $sk to load modules for |
2873 | */ |
2874 | public function loadSkinModules( $sk ) { |
2875 | foreach ( $sk->getDefaultModules() as $group => $modules ) { |
2876 | if ( $group === 'styles' ) { |
2877 | foreach ( $modules as $moduleMembers ) { |
2878 | $this->addModuleStyles( $moduleMembers ); |
2879 | } |
2880 | } else { |
2881 | $this->addModules( $modules ); |
2882 | } |
2883 | } |
2884 | } |
2885 | |
2886 | /** |
2887 | * Finally, all the text has been munged and accumulated into |
2888 | * the object, let's actually output it: |
2889 | * |
2890 | * @param bool $return Set to true to get the result as a string rather than sending it |
2891 | * @return string|null |
2892 | */ |
2893 | public function output( $return = false ) { |
2894 | if ( $this->mDoNothing ) { |
2895 | return $return ? '' : null; |
2896 | } |
2897 | |
2898 | $response = $this->getRequest()->response(); |
2899 | $config = $this->getConfig(); |
2900 | |
2901 | if ( $this->mRedirect != '' ) { |
2902 | $services = MediaWikiServices::getInstance(); |
2903 | // Modern standards don't require redirect URLs to be absolute, but make it so just in case. |
2904 | // Note that this doesn't actually guarantee an absolute URL: relative-path URLs are left intact. |
2905 | $this->mRedirect = (string)$services->getUrlUtils()->expand( $this->mRedirect, PROTO_CURRENT ); |
2906 | |
2907 | $redirect = $this->mRedirect; |
2908 | $code = $this->mRedirectCode; |
2909 | $content = ''; |
2910 | |
2911 | if ( $this->getHookRunner()->onBeforePageRedirect( $this, $redirect, $code ) ) { |
2912 | if ( $code == '301' || $code == '303' ) { |
2913 | if ( !$config->get( MainConfigNames::DebugRedirects ) ) { |
2914 | $response->statusHeader( (int)$code ); |
2915 | } |
2916 | $this->mLastModified = wfTimestamp( TS_RFC2822 ); |
2917 | } |
2918 | if ( $config->get( MainConfigNames::VaryOnXFP ) ) { |
2919 | $this->addVaryHeader( 'X-Forwarded-Proto' ); |
2920 | } |
2921 | $this->sendCacheControl(); |
2922 | |
2923 | $response->header( 'Content-Type: text/html; charset=UTF-8' ); |
2924 | if ( $config->get( MainConfigNames::DebugRedirects ) ) { |
2925 | $url = htmlspecialchars( $redirect ); |
2926 | $content = "<!DOCTYPE html>\n<html>\n<head>\n" |
2927 | . "<title>Redirect</title>\n</head>\n<body>\n" |
2928 | . "<p>Location: <a href=\"$url\">$url</a></p>\n" |
2929 | . "</body>\n</html>\n"; |
2930 | |
2931 | if ( !$return ) { |
2932 | print $content; |
2933 | } |
2934 | |
2935 | } else { |
2936 | $response->header( 'Location: ' . $redirect ); |
2937 | } |
2938 | } |
2939 | |
2940 | return $return ? $content : null; |
2941 | } elseif ( $this->mStatusCode ) { |
2942 | $response->statusHeader( $this->mStatusCode ); |
2943 | } |
2944 | |
2945 | # Buffer output; final headers may depend on later processing |
2946 | ob_start(); |
2947 | |
2948 | $response->header( 'Content-type: ' . $config->get( MainConfigNames::MimeType ) . '; charset=UTF-8' ); |
2949 | $response->header( 'Content-language: ' . |
2950 | MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() ); |
2951 | |
2952 | $linkHeader = $this->getLinkHeader(); |
2953 | if ( $linkHeader ) { |
2954 | $response->header( $linkHeader ); |
2955 | } |
2956 | |
2957 | // Prevent framing, if requested |
2958 | $frameOptions = $this->getFrameOptions(); |
2959 | if ( $frameOptions ) { |
2960 | $response->header( "X-Frame-Options: $frameOptions" ); |
2961 | } |
2962 | |
2963 | // Get the Origin-Trial header values. This is used to enable Chrome Origin |
2964 | // Trials: https://github.com/GoogleChrome/OriginTrials |
2965 | $originTrials = $config->get( MainConfigNames::OriginTrials ); |
2966 | foreach ( $originTrials as $originTrial ) { |
2967 | $response->header( "Origin-Trial: $originTrial", false ); |
2968 | } |
2969 | |
2970 | $reportTo = $this->getReportTo(); |
2971 | if ( $reportTo ) { |
2972 | $response->header( "Report-To: $reportTo" ); |
2973 | } |
2974 | |
2975 | $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly(); |
2976 | if ( $featurePolicyReportOnly ) { |
2977 | $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" ); |
2978 | } |
2979 | |
2980 | if ( $this->mArticleBodyOnly ) { |
2981 | if ( $this->cspOutputMode === self::CSP_HEADERS ) { |
2982 | $this->CSP->sendHeaders(); |
2983 | } |
2984 | echo $this->mBodytext; |
2985 | } else { |
2986 | // Enable safe mode if requested (T152169) |
2987 | if ( $this->getRequest()->getBool( 'safemode' ) ) { |
2988 | $this->disallowUserJs(); |
2989 | } |
2990 | |
2991 | $sk = $this->getSkin(); |
2992 | $this->loadSkinModules( $sk ); |
2993 | |
2994 | MWDebug::addModules( $this ); |
2995 | |
2996 | // Hook that allows last minute changes to the output page, e.g. |
2997 | // adding of CSS or Javascript by extensions, adding CSP sources. |
2998 | $this->getHookRunner()->onBeforePageDisplay( $this, $sk ); |
2999 | |
3000 | if ( $this->cspOutputMode === self::CSP_HEADERS ) { |
3001 | $this->CSP->sendHeaders(); |
3002 | } |
3003 | |
3004 | try { |
3005 | $sk->outputPage(); |
3006 | } catch ( Exception $e ) { |
3007 | ob_end_clean(); // bug T129657 |
3008 | throw $e; |
3009 | } |
3010 | } |
3011 | |
3012 | try { |
3013 | // This hook allows last minute changes to final overall output by modifying output buffer |
3014 | $this->getHookRunner()->onAfterFinalPageOutput( $this ); |
3015 | } catch ( Exception $e ) { |
3016 | ob_end_clean(); // bug T129657 |
3017 | throw $e; |
3018 | } |
3019 | |
3020 | $this->sendCacheControl(); |
3021 | |
3022 | if ( $return ) { |
3023 | return ob_get_clean(); |
3024 | } else { |
3025 | ob_end_flush(); |
3026 | return null; |
3027 | } |
3028 | } |
3029 | |
3030 | /** |
3031 | * Prepare this object to display an error page; disable caching and |
3032 | * indexing, clear the current text and redirect, set the page's title |
3033 | * and optionally a custom HTML title (content of the "<title>" tag). |
3034 | * |
3035 | * @param string|Message|null $pageTitle Will be passed directly to setPageTitle() |
3036 | * @param string|Message|false $htmlTitle Will be passed directly to setHTMLTitle(); |
3037 | * optional, if not passed the "<title>" attribute will be |
3038 | * based on $pageTitle |
3039 | * @note Explicitly passing $pageTitle or $htmlTitle has been deprecated |
3040 | * since 1.41; use ::setPageTitleMsg() and ::setHTMLTitle() instead. |
3041 | */ |
3042 | public function prepareErrorPage( $pageTitle = null, $htmlTitle = false ) { |
3043 | if ( $pageTitle !== null || $htmlTitle !== false ) { |
3044 | wfDeprecated( __METHOD__ . ' with explicit arguments', '1.41' ); |
3045 | if ( $pageTitle !== null ) { |
3046 | $this->setPageTitle( $pageTitle ); |
3047 | } |
3048 | if ( $htmlTitle !== false ) { |
3049 | $this->setHTMLTitle( $htmlTitle ); |
3050 | } |
3051 | } |
3052 | $this->setRobotPolicy( 'noindex,nofollow' ); |
3053 | $this->setArticleRelated( false ); |
3054 | $this->disableClientCache(); |
3055 | $this->mRedirect = ''; |
3056 | $this->clearSubtitle(); |
3057 | $this->clearHTML(); |
3058 | } |
3059 | |
3060 | /** |
3061 | * Output a standard error page |
3062 | * |
3063 | * showErrorPage( 'titlemsg', 'pagetextmsg' ); |
3064 | * showErrorPage( 'titlemsg', 'pagetextmsg', [ 'param1', 'param2' ] ); |
3065 | * showErrorPage( 'titlemsg', $messageObject ); |
3066 | * showErrorPage( $titleMessageObject, $messageObject ); |
3067 | * |
3068 | * @param string|MessageSpecifier $title Message key (string) for page title, or a MessageSpecifier |
3069 | * @param string|MessageSpecifier $msg Message key (string) for page text, or a MessageSpecifier |
3070 | * @param array $params Message parameters; ignored if $msg is a Message object |
3071 | * @param PageReference|LinkTarget|string|null $returnto Page to show a return link to; |
3072 | * defaults to the 'returnto' URL parameter |
3073 | * @param string|null $returntoquery Query string for the return to link; |
3074 | * defaults to the 'returntoquery' URL parameter |
3075 | */ |
3076 | public function showErrorPage( |
3077 | $title, $msg, $params = [], $returnto = null, $returntoquery = null |
3078 | ) { |
3079 | if ( !$title instanceof Message ) { |
3080 | $title = $this->msg( $title ); |
3081 | } |
3082 | |
3083 | $this->prepareErrorPage(); |
3084 | $this->setPageTitleMsg( $title ); |
3085 | |
3086 | if ( $msg instanceof Message ) { |
3087 | if ( $params !== [] ) { |
3088 | trigger_error( 'Argument ignored: $params. The message parameters argument ' |
3089 | . 'is discarded when the $msg argument is a Message object instead of ' |
3090 | . 'a string.', E_USER_NOTICE ); |
3091 | } |
3092 | $this->addHTML( $msg->parseAsBlock() ); |
3093 | } else { |
3094 | $this->addWikiMsgArray( $msg, $params ); |
3095 | } |
3096 | |
3097 | $this->returnToMain( null, $returnto, $returntoquery ); |
3098 | } |
3099 | |
3100 | /** |
3101 | * Output a standard permission error page |
3102 | * |
3103 | * @param array $errors Error message keys or [key, param...] arrays |
3104 | * @param string|null $action Action that was denied or null if unknown |
3105 | */ |
3106 | public function showPermissionsErrorPage( array $errors, $action = null ) { |
3107 | $services = MediaWikiServices::getInstance(); |
3108 | $groupPermissionsLookup = $services->getGroupPermissionsLookup(); |
3109 | foreach ( $errors as $key => $error ) { |
3110 | $errors[$key] = (array)$error; |
3111 | } |
3112 | |
3113 | // For some actions (read, edit, create and upload), display a "login to do this action" |
3114 | // error if all of the following conditions are met: |
3115 | // 1. the user is not logged in as a named user, and so cannot be added to groups |
3116 | // 2. the only error is insufficient permissions (i.e. no block or something else) |
3117 | // 3. the error can be avoided simply by logging in |
3118 | |
3119 | if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] ) |
3120 | && !$this->getUser()->isNamed() && count( $errors ) == 1 && isset( $errors[0][0] ) |
3121 | && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' ) |
3122 | && ( $groupPermissionsLookup->groupHasPermission( 'user', $action ) |
3123 | || $groupPermissionsLookup->groupHasPermission( 'autoconfirmed', $action ) ) |
3124 | ) { |
3125 | $displayReturnto = null; |
3126 | |
3127 | # Due to T34276, if a user does not have read permissions, |
3128 | # $this->getTitle() will just give Special:Badtitle, which is |
3129 | # not especially useful as a returnto parameter. Use the title |
3130 | # from the request instead, if there was one. |
3131 | $request = $this->getRequest(); |
3132 | $returnto = Title::newFromText( $request->getText( 'title' ) ); |
3133 | if ( $action == 'edit' ) { |
3134 | $msg = 'whitelistedittext'; |
3135 | $displayReturnto = $returnto; |
3136 | } elseif ( $action == 'createpage' || $action == 'createtalk' ) { |
3137 | $msg = 'nocreatetext'; |
3138 | } elseif ( $action == 'upload' ) { |
3139 | $msg = 'uploadnologintext'; |
3140 | } else { # Read |
3141 | $msg = 'loginreqpagetext'; |
3142 | $displayReturnto = Title::newMainPage(); |
3143 | } |
3144 | |
3145 | $query = []; |
3146 | |
3147 | if ( $returnto ) { |
3148 | $query['returnto'] = $returnto->getPrefixedText(); |
3149 | |
3150 | if ( !$request->wasPosted() ) { |
3151 | $returntoquery = $request->getValues(); |
3152 | unset( $returntoquery['title'] ); |
3153 | unset( $returntoquery['returnto'] ); |
3154 | unset( $returntoquery['returntoquery'] ); |
3155 | $query['returntoquery'] = wfArrayToCgi( $returntoquery ); |
3156 | } |
3157 | } |
3158 | |
3159 | $title = SpecialPage::getTitleFor( 'Userlogin' ); |
3160 | $linkRenderer = $services->getLinkRenderer(); |
3161 | $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE ); |
3162 | $loginLink = $linkRenderer->makeKnownLink( |
3163 | $title, |
3164 | $this->msg( 'loginreqlink' )->text(), |
3165 | [], |
3166 | $query |
3167 | ); |
3168 | |
3169 | $this->prepareErrorPage(); |
3170 | $this->setPageTitleMsg( $this->msg( 'loginreqtitle' ) ); |
3171 | $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() ); |
3172 | |
3173 | # Don't return to a page the user can't read otherwise |
3174 | # we'll end up in a pointless loop |
3175 | if ( $displayReturnto && $this->getAuthority()->probablyCan( 'read', $displayReturnto ) ) { |
3176 | $this->returnToMain( null, $displayReturnto ); |
3177 | } |
3178 | } else { |
3179 | $this->prepareErrorPage(); |
3180 | $this->setPageTitleMsg( $this->msg( 'permissionserrors' ) ); |
3181 | $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) ); |
3182 | } |
3183 | } |
3184 | |
3185 | /** |
3186 | * Display an error page indicating that a given version of MediaWiki is |
3187 | * required to use it |
3188 | * |
3189 | * @param mixed $version The version of MediaWiki needed to use the page |
3190 | */ |
3191 | public function versionRequired( $version ) { |
3192 | $this->prepareErrorPage(); |
3193 | $this->setPageTitleMsg( |
3194 | $this->msg( 'versionrequired' )->plaintextParams( $version ) |
3195 | ); |
3196 | |
3197 | $this->addWikiMsg( 'versionrequiredtext', $version ); |
3198 | $this->returnToMain(); |
3199 | } |
3200 | |
3201 | /** |
3202 | * Format permission $status obtained from Authority for display. |
3203 | * |
3204 | * @param PermissionStatus $status |
3205 | * @param string|null $action that was denied or null if unknown |
3206 | * @return string |
3207 | */ |
3208 | public function formatPermissionStatus( PermissionStatus $status, string $action = null ): string { |
3209 | if ( $status->isGood() ) { |
3210 | return ''; |
3211 | } |
3212 | return $this->formatPermissionsErrorMessage( $status->toLegacyErrorArray(), $action ); |
3213 | } |
3214 | |
3215 | /** |
3216 | * Format a list of error messages |
3217 | * |
3218 | * @deprecated since 1.36. Use ::formatPermissionStatus instead |
3219 | * @param array $errors Array of arrays returned by PermissionManager::getPermissionErrors |
3220 | * @phan-param non-empty-array[] $errors |
3221 | * @param string|null $action Action that was denied or null if unknown |
3222 | * @return string The wikitext error-messages, formatted into a list. |
3223 | */ |
3224 | public function formatPermissionsErrorMessage( array $errors, $action = null ) { |
3225 | if ( $action == null ) { |
3226 | $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n"; |
3227 | } else { |
3228 | $action_desc = $this->msg( "action-$action" )->plain(); |
3229 | $text = $this->msg( |
3230 | 'permissionserrorstext-withaction', |
3231 | count( $errors ), |
3232 | $action_desc |
3233 | )->plain() . "\n\n"; |
3234 | } |
3235 | |
3236 | if ( count( $errors ) > 1 ) { |
3237 | $text .= '<ul class="permissions-errors">' . "\n"; |
3238 | |
3239 | foreach ( $errors as $error ) { |
3240 | $text .= '<li>'; |
3241 | $text .= $this->msg( ...$error )->plain(); |
3242 | $text .= "</li>\n"; |
3243 | } |
3244 | $text .= '</ul>'; |
3245 | } else { |
3246 | $text .= "<div class=\"permissions-errors\">\n" . |
3247 | // @phan-suppress-next-line PhanParamTooFewUnpack Elements of $errors already annotated as non-empty |
3248 | $this->msg( ...reset( $errors ) )->plain() . |
3249 | "\n</div>"; |
3250 | } |
3251 | |
3252 | return $text; |
3253 | } |
3254 | |
3255 | /** |
3256 | * Show a warning about replica DB lag |
3257 | * |
3258 | * If the lag is higher than $wgDatabaseReplicaLagCritical seconds, |
3259 | * then the warning is a bit more obvious. If the lag is |
3260 | * lower than $wgDatabaseReplicaLagWarning, then no warning is shown. |
3261 | * |
3262 | * @param int $lag Replica lag |
3263 | */ |
3264 | public function showLagWarning( $lag ) { |
3265 | $config = $this->getConfig(); |
3266 | if ( $lag >= $config->get( MainConfigNames::DatabaseReplicaLagWarning ) ) { |
3267 | $lag = floor( $lag ); // floor to avoid nano seconds to display |
3268 | $message = $lag < $config->get( MainConfigNames::DatabaseReplicaLagCritical ) |
3269 | ? 'lag-warn-normal' |
3270 | : 'lag-warn-high'; |
3271 | // For grep: mw-lag-warn-normal, mw-lag-warn-high |
3272 | $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" ); |
3273 | $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] ); |
3274 | } |
3275 | } |
3276 | |
3277 | /** |
3278 | * Output an error page |
3279 | * |
3280 | * @deprecated since 1.43 Use showErrorPage() instead |
3281 | * @param string $message Error to output. Must be escaped for HTML. |
3282 | */ |
3283 | public function showFatalError( $message ) { |
3284 | wfDeprecated( __METHOD__, '1.43' ); |
3285 | |
3286 | $this->prepareErrorPage(); |
3287 | $this->setPageTitleMsg( $this->msg( 'internalerror' ) ); |
3288 | |
3289 | $this->addHTML( $message ); |
3290 | } |
3291 | |
3292 | /** |
3293 | * Add a "return to" link pointing to a specified title |
3294 | * |
3295 | * @param LinkTarget $title Title to link |
3296 | * @param array $query Query string parameters |
3297 | * @param string|null $text Text of the link (input is not escaped) |
3298 | * @param array $options Options array to pass to Linker |
3299 | */ |
3300 | public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) { |
3301 | $linkRenderer = MediaWikiServices::getInstance() |
3302 | ->getLinkRendererFactory()->createFromLegacyOptions( $options ); |
3303 | $link = $this->msg( 'returnto' )->rawParams( |
3304 | $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped(); |
3305 | $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" ); |
3306 | } |
3307 | |
3308 | /** |
3309 | * Add a "return to" link pointing to a specified title, |
3310 | * or the title indicated in the request, or else the main page |
3311 | * |
3312 | * @param mixed|null $unused |
3313 | * @param PageReference|LinkTarget|string|null $returnto Page to return to |
3314 | * @param string|null $returntoquery Query string for the return to link |
3315 | */ |
3316 | public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) { |
3317 | $returnto ??= $this->getRequest()->getText( 'returnto' ); |
3318 | |
3319 | $returntoquery ??= $this->getRequest()->getText( 'returntoquery' ); |
3320 | |
3321 | if ( $returnto === '' ) { |
3322 | $returnto = Title::newMainPage(); |
3323 | } |
3324 | |
3325 | if ( is_object( $returnto ) ) { |
3326 | $linkTarget = TitleValue::castPageToLinkTarget( $returnto ); |
3327 | } else { |
3328 | $linkTarget = Title::newFromText( $returnto ); |
3329 | } |
3330 | |
3331 | // We don't want people to return to external interwiki. That |
3332 | // might potentially be used as part of a phishing scheme |
3333 | if ( !is_object( $linkTarget ) || $linkTarget->isExternal() ) { |
3334 | $linkTarget = Title::newMainPage(); |
3335 | } |
3336 | |
3337 | $this->addReturnTo( $linkTarget, wfCgiToArray( $returntoquery ) ); |
3338 | } |
3339 | |
3340 | /** |
3341 | * Output a standard "wait for takeover" warning |
3342 | * |
3343 | * This is useful for extensions which are hooking an action and |
3344 | * suppressing its normal output so it can be taken over with JS. |
3345 | * |
3346 | * showPendingTakeover( 'url', 'pagetextmsg' ); |
3347 | * showPendingTakeover( 'url', 'pagetextmsg', [ 'param1', 'param2' ] ); |
3348 | * showPendingTakeover( 'url', $messageObject ); |
3349 | * |
3350 | * @param string $fallbackUrl URL to redirect to if the user doesn't have JavaScript |
3351 | * or ResourceLoader available; this should ideally be to a page that provides similar |
3352 | * functionality without requiring JavaScript |
3353 | * @param string|MessageSpecifier $msg Message key (string) for page text, or a MessageSpecifier |
3354 | * @param mixed ...$params Message parameters; ignored if $msg is a Message object |
3355 | */ |
3356 | public function showPendingTakeover( |
3357 | $fallbackUrl, $msg, ...$params |
3358 | ) { |
3359 | if ( $msg instanceof Message ) { |
3360 | if ( $params !== [] ) { |
3361 | trigger_error( 'Argument ignored: $params. The message parameters argument ' |
3362 | . 'is discarded when the $msg argument is a Message object instead of ' |
3363 | . 'a string.', E_USER_NOTICE ); |
3364 | } |
3365 | $this->addHTML( $msg->parseAsBlock() ); |
3366 | } else { |
3367 | $this->addHTML( $this->msg( $msg, ...$params )->parseAsBlock() ); |
3368 | } |
3369 | |
3370 | // Redirect if the user has no JS (<noscript>) |
3371 | $escapedUrl = htmlspecialchars( $fallbackUrl ); |
3372 | $this->addHeadItem( |
3373 | 'mw-noscript-fallback', |
3374 | // https://html.spec.whatwg.org/#attr-meta-http-equiv-refresh |
3375 | // means that if $fallbackUrl contains unencoded quotation marks |
3376 | // then this will behave confusingly, but shouldn't break the page |
3377 | "<noscript><meta http-equiv=\"refresh\" content=\"0; url=$escapedUrl\"></noscript>" |
3378 | ); |
3379 | // Redirect if the user has no ResourceLoader |
3380 | $this->addScript( Html::inlineScript( |
3381 | "(window.NORLQ=window.NORLQ||[]).push(" . |
3382 | "function(){" . |
3383 | "location.href=" . json_encode( $fallbackUrl ) . ";" . |
3384 | "}" . |
3385 | ");" |
3386 | ) ); |
3387 | } |
3388 | |
3389 | private function getRlClientContext() { |
3390 | if ( !$this->rlClientContext ) { |
3391 | $query = ResourceLoader::makeLoaderQuery( |
3392 | [], // modules; not relevant |
3393 | $this->getLanguage()->getCode(), |
3394 | $this->getSkin()->getSkinName(), |
3395 | $this->getUser()->isRegistered() ? $this->getUser()->getName() : null, |
3396 | null, // version; not relevant |
3397 | ResourceLoader::inDebugMode(), |
3398 | null, // only; not relevant |
3399 | $this->isPrintable() |
3400 | ); |
3401 | $this->rlClientContext = new RL\Context( |
3402 | $this->getResourceLoader(), |
3403 | new FauxRequest( $query ) |
3404 | ); |
3405 | if ( $this->contentOverrideCallbacks ) { |
3406 | $this->rlClientContext = new RL\DerivativeContext( $this->rlClientContext ); |
3407 | $this->rlClientContext->setContentOverrideCallback( function ( $title ) { |
3408 | foreach ( $this->contentOverrideCallbacks as $callback ) { |
3409 | $content = $callback( $title ); |
3410 | if ( $content !== null ) { |
3411 | $text = ( $content instanceof TextContent ) ? $content->getText() : ''; |
3412 | if ( strpos( $text, '</script>' ) !== false ) { |
3413 | // Proactively replace this so that we can display a message |
3414 | // to the user, instead of letting it go to Html::inlineScript(), |
3415 | // where it would be considered a server-side issue. |
3416 | $content = new JavaScriptContent( |
3417 | Html::encodeJsCall( 'mw.log.error', [ |
3418 | "Cannot preview $title due to script-closing tag." |
3419 | ] ) |
3420 | ); |
3421 | } |
3422 | return $content; |
3423 | } |
3424 | } |
3425 | return null; |
3426 | } ); |
3427 | } |
3428 | } |
3429 | return $this->rlClientContext; |
3430 | } |
3431 | |
3432 | /** |
3433 | * Call this to freeze the module queue and JS config and create a formatter. |
3434 | * |
3435 | * Depending on the Skin, this may get lazy-initialised in either headElement() or |
3436 | * getBottomScripts(). See SkinTemplate::prepareQuickTemplate(). Calling this too early may |
3437 | * cause unexpected side-effects since disallowUserJs() may be called at any time to change |
3438 | * the module filters retroactively. Skins and extension hooks may also add modules until very |
3439 | * late in the request lifecycle. |
3440 | * |
3441 | * @return RL\ClientHtml |
3442 | */ |
3443 | public function getRlClient() { |
3444 | if ( !$this->rlClient ) { |
3445 | $context = $this->getRlClientContext(); |
3446 | $rl = $this->getResourceLoader(); |
3447 | $this->addModules( [ |
3448 | 'user', |
3449 | 'user.options', |
3450 | ] ); |
3451 | $this->addModuleStyles( [ |
3452 | 'site.styles', |
3453 | 'noscript', |
3454 | 'user.styles', |
3455 | ] ); |
3456 | |
3457 | // Prepare exempt modules for buildExemptModules() |
3458 | $exemptGroups = [ |
3459 | RL\Module::GROUP_SITE => [], |
3460 | RL\Module::GROUP_NOSCRIPT => [], |
3461 | RL\Module::GROUP_PRIVATE => [], |
3462 | RL\Module::GROUP_USER => [] |
3463 | ]; |
3464 | $exemptStates = []; |
3465 | $moduleStyles = $this->getModuleStyles( /*filter*/ true ); |
3466 | |
3467 | // Preload getTitleInfo for isKnownEmpty calls below and in RL\ClientHtml |
3468 | // Separate user-specific batch for improved cache-hit ratio. |
3469 | $userBatch = [ 'user.styles', 'user' ]; |
3470 | $siteBatch = array_diff( $moduleStyles, $userBatch ); |
3471 | RL\WikiModule::preloadTitleInfo( $context, $siteBatch ); |
3472 | RL\WikiModule::preloadTitleInfo( $context, $userBatch ); |
3473 | |
3474 | // Filter out modules handled by buildExemptModules() |
3475 | $moduleStyles = array_filter( $moduleStyles, |
3476 | static function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) { |
3477 | $module = $rl->getModule( $name ); |
3478 | if ( $module ) { |
3479 | $group = $module->getGroup(); |
3480 | if ( $group !== null && isset( $exemptGroups[$group] ) ) { |
3481 | // The `noscript` module is excluded from the client |
3482 | // side registry, no need to set its state either. |
3483 | // But we still output it. See T291735 |
3484 | if ( $group !== RL\Module::GROUP_NOSCRIPT ) { |
3485 | $exemptStates[$name] = 'ready'; |
3486 | } |
3487 | if ( !$module->isKnownEmpty( $context ) ) { |
3488 | // E.g. Don't output empty <styles> |
3489 | $exemptGroups[$group][] = $name; |
3490 | } |
3491 | return false; |
3492 | } |
3493 | } |
3494 | return true; |
3495 | } |
3496 | ); |
3497 | $this->rlExemptStyleModules = $exemptGroups; |
3498 | |
3499 | $config = $this->getConfig(); |
3500 | // Client preferences are controlled by the skin and specific to unregistered |
3501 | // users. See mw.user.clientPrefs for details on how this works and how to |
3502 | // handle registered users. |
3503 | $clientPrefEnabled = ( |
3504 | $this->getSkin()->getOptions()['clientPrefEnabled'] && |
3505 | !$this->getUser()->isNamed() |
3506 | ); |
3507 | $clientPrefCookiePrefix = $config->get( MainConfigNames::CookiePrefix ); |
3508 | |
3509 | $rlClient = new RL\ClientHtml( $context, [ |
3510 | 'target' => $this->getTarget(), |
3511 | // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used |
3512 | // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of |
3513 | // modules enqueued for loading on this page is filtered to just those. |
3514 | // However, to make sure we also apply the restriction to dynamic dependencies and |
3515 | // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the |
3516 | // StartupModule so that the client-side registry will not contain any restricted |
3517 | // modules either. (T152169, T185303) |
3518 | 'safemode' => ( $this->getAllowedModules( RL\Module::TYPE_COMBINED ) |
3519 | <= RL\Module::ORIGIN_CORE_INDIVIDUAL |
3520 | ) ? '1' : null, |
3521 | 'clientPrefEnabled' => $clientPrefEnabled, |
3522 | 'clientPrefCookiePrefix' => $clientPrefCookiePrefix, |
3523 | ] ); |
3524 | $rlClient->setConfig( $this->getJSVars( self::JS_VAR_EARLY ) ); |
3525 | $rlClient->setModules( $this->getModules( /*filter*/ true ) ); |
3526 | $rlClient->setModuleStyles( $moduleStyles ); |
3527 | $rlClient->setExemptStates( $exemptStates ); |
3528 | $this->rlClient = $rlClient; |
3529 | } |
3530 | return $this->rlClient; |
3531 | } |
3532 | |
3533 | /** |
3534 | * @param Skin $sk The given Skin |
3535 | * @param bool $includeStyle Unused |
3536 | * @return string The doctype, opening "<html>", and head element. |
3537 | */ |
3538 | public function headElement( Skin $sk, $includeStyle = true ) { |
3539 | $config = $this->getConfig(); |
3540 | $userdir = $this->getLanguage()->getDir(); |
3541 | $services = MediaWikiServices::getInstance(); |
3542 | $sitedir = $services->getContentLanguage()->getDir(); |
3543 | |
3544 | $pieces = []; |
3545 | $htmlAttribs = Sanitizer::mergeAttributes( Sanitizer::mergeAttributes( |
3546 | $this->getRlClient()->getDocumentAttributes(), |
3547 | $sk->getHtmlElementAttributes() |
3548 | ), [ 'class' => implode( ' ', $this->mAdditionalHtmlClasses ) ] ); |
3549 | $pieces[] = Html::htmlHeader( $htmlAttribs ); |
3550 | $pieces[] = Html::openElement( 'head' ); |
3551 | |
3552 | if ( $this->getHTMLTitle() == '' ) { |
3553 | $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() ); |
3554 | } |
3555 | |
3556 | if ( !Html::isXmlMimeType( $config->get( MainConfigNames::MimeType ) ) ) { |
3557 | // Add <meta charset="UTF-8"> |
3558 | // This should be before <title> since it defines the charset used by |
3559 | // text including the text inside <title>. |
3560 | // The spec recommends defining XHTML5's charset using the XML declaration |
3561 | // instead of meta. |
3562 | // Our XML declaration is output by Html::htmlHeader. |
3563 | // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type |
3564 | // https://html.spec.whatwg.org/multipage/semantics.html#charset |
3565 | $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] ); |
3566 | } |
3567 | |
3568 | $pieces[] = Html::element( 'title', [], $this->getHTMLTitle() ); |
3569 | $pieces[] = $this->getRlClient()->getHeadHtml( $htmlAttribs['class'] ?? null ); |
3570 | $pieces[] = $this->buildExemptModules(); |
3571 | $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) ); |
3572 | $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) ); |
3573 | |
3574 | $pieces[] = Html::closeElement( 'head' ); |
3575 | |
3576 | $skinOptions = $sk->getOptions(); |
3577 | $bodyClasses = array_merge( $this->mAdditionalBodyClasses, $skinOptions['bodyClasses'] ); |
3578 | $bodyClasses[] = 'mediawiki'; |
3579 | |
3580 | # Classes for LTR/RTL directionality support |
3581 | $bodyClasses[] = $userdir; |
3582 | $bodyClasses[] = "sitedir-$sitedir"; |
3583 | |
3584 | // See Article:showDiffPage for class to support article diff styling |
3585 | |
3586 | $underline = $services->getUserOptionsLookup()->getOption( $this->getUser(), 'underline' ); |
3587 | if ( $underline < 2 ) { |
3588 | // The following classes can be used here: |
3589 | // * mw-underline-always |
3590 | // * mw-underline-never |
3591 | $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' ); |
3592 | } |
3593 | |
3594 | // Parser feature migration class |
3595 | // The idea is that this will eventually be removed, after the wikitext |
3596 | // which requires it is cleaned up. |
3597 | $bodyClasses[] = 'mw-hide-empty-elt'; |
3598 | |
3599 | $bodyClasses[] = $sk->getPageClasses( $this->getTitle() ); |
3600 | $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() ); |
3601 | $bodyClasses[] = |
3602 | 'action-' . Sanitizer::escapeClass( $this->getContext()->getActionName() ); |
3603 | |
3604 | if ( $sk->isResponsive() ) { |
3605 | $bodyClasses[] = 'skin--responsive'; |
3606 | } |
3607 | |
3608 | $bodyAttrs = []; |
3609 | // While the implode() is not strictly needed, it's used for backwards compatibility |
3610 | // (this used to be built as a string and hooks likely still expect that). |
3611 | $bodyAttrs['class'] = implode( ' ', $bodyClasses ); |
3612 | |
3613 | // Allow skins and extensions to add body attributes they need |
3614 | // Get ones from deprecated method |
3615 | if ( method_exists( $sk, 'addToBodyAttributes' ) ) { |
3616 | /** @phan-suppress-next-line PhanUndeclaredMethod */ |
3617 | $sk->addToBodyAttributes( $this, $bodyAttrs ); |
3618 | wfDeprecated( 'Skin::addToBodyAttributes method to add body attributes', '1.35' ); |
3619 | } |
3620 | |
3621 | // Then run the hook, the recommended way of adding body attributes now |
3622 | $this->getHookRunner()->onOutputPageBodyAttributes( $this, $sk, $bodyAttrs ); |
3623 | |
3624 | $pieces[] = Html::openElement( 'body', $bodyAttrs ); |
3625 | |
3626 | return self::combineWrappedStrings( $pieces ); |
3627 | } |
3628 | |
3629 | /** |
3630 | * Get a ResourceLoader object associated with this OutputPage |
3631 | * |
3632 | * @return ResourceLoader |
3633 | */ |
3634 | public function getResourceLoader() { |
3635 | if ( $this->mResourceLoader === null ) { |
3636 | // Lazy-initialise as needed |
3637 | $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader(); |
3638 | } |
3639 | return $this->mResourceLoader; |
3640 | } |
3641 | |
3642 | /** |
3643 | * Explicitly load or embed modules on a page. |
3644 | * |
3645 | * @param array|string $modules One or more module names |
3646 | * @param string $only RL\Module TYPE_ class constant |
3647 | * @param array $extraQuery [optional] Array with extra query parameters for the request |
3648 | * @return string|WrappedStringList HTML |
3649 | */ |
3650 | public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) { |
3651 | // Apply 'origin' filters |
3652 | $modules = $this->filterModules( (array)$modules, null, $only ); |
3653 | |
3654 | return RL\ClientHtml::makeLoad( |
3655 | $this->getRlClientContext(), |
3656 | $modules, |
3657 | $only, |
3658 | $extraQuery |
3659 | ); |
3660 | } |
3661 | |
3662 | /** |
3663 | * Combine WrappedString chunks and filter out empty ones |
3664 | * |
3665 | * @param array $chunks |
3666 | * @return string|WrappedStringList HTML |
3667 | */ |
3668 | protected static function combineWrappedStrings( array $chunks ) { |
3669 | // Filter out empty values |
3670 | $chunks = array_filter( $chunks, 'strlen' ); |
3671 | return WrappedString::join( "\n", $chunks ); |
3672 | } |
3673 | |
3674 | /** |
3675 | * JS stuff to put at the bottom of the `<body>`. |
3676 | * These are legacy scripts ($this->mScripts), and user JS. |
3677 | * |
3678 | * @return string|WrappedStringList HTML |
3679 | */ |
3680 | public function getBottomScripts() { |
3681 | // Keep the hook appendage separate to preserve WrappedString objects. |
3682 | // This enables to merge them where possible. |
3683 | $extraHtml = ''; |
3684 | $this->getHookRunner()->onSkinAfterBottomScripts( $this->getSkin(), $extraHtml ); |
3685 | |
3686 | $chunks = []; |
3687 | $chunks[] = $this->getRlClient()->getBodyHtml(); |
3688 | |
3689 | // Legacy non-ResourceLoader scripts |
3690 | $chunks[] = $this->mScripts; |
3691 | |
3692 | // Keep hostname and backend time as the first variables for quick view-source access. |
3693 | // This other variables will form a very long inline blob. |
3694 | $vars = []; |
3695 | if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) { |
3696 | $vars['wgHostname'] = wfHostname(); |
3697 | } |
3698 | $elapsed = $this->getRequest()->getElapsedTime(); |
3699 | // seconds to milliseconds |
3700 | $vars['wgBackendResponseTime'] = round( $elapsed * 1000 ); |
3701 | |
3702 | $vars += $this->getJSVars( self::JS_VAR_LATE ); |
3703 | if ( $this->limitReportJSData ) { |
3704 | $vars['wgPageParseReport'] = $this->limitReportJSData; |
3705 | } |
3706 | |
3707 | $rlContext = $this->getRlClientContext(); |
3708 | $chunks[] = ResourceLoader::makeInlineScript( |
3709 | 'mw.config.set(' . $rlContext->encodeJson( $vars ) . ');' |
3710 | ); |
3711 | |
3712 | $chunks = [ self::combineWrappedStrings( $chunks ) ]; |
3713 | if ( $extraHtml !== '' ) { |
3714 | $chunks[] = $extraHtml; |
3715 | } |
3716 | |
3717 | return WrappedString::join( "\n", $chunks ); |
3718 | } |
3719 | |
3720 | /** |
3721 | * Get the javascript config vars to include on this page |
3722 | * |
3723 | * @return array Array of javascript config vars |
3724 | * @since 1.23 |
3725 | */ |
3726 | public function getJsConfigVars() { |
3727 | return $this->mJsConfigVars; |
3728 | } |
3729 | |
3730 | /** |
3731 | * Add one or more variables to be set in mw.config in JavaScript |
3732 | * |
3733 | * @param string|array $keys Key or array of key/value pairs |
3734 | * @param mixed|null $value [optional] Value of the configuration variable |
3735 | */ |
3736 | public function addJsConfigVars( $keys, $value = null ) { |
3737 | if ( is_array( $keys ) ) { |
3738 | foreach ( $keys as $key => $value ) { |
3739 | $this->mJsConfigVars[$key] = $value; |
3740 | } |
3741 | return; |
3742 | } |
3743 | |
3744 | $this->mJsConfigVars[$keys] = $value; |
3745 | } |
3746 | |
3747 | /** |
3748 | * Get an array containing the variables to be set in mw.config in JavaScript. |
3749 | * |
3750 | * Do not add things here which can be evaluated in RL\StartUpModule, |
3751 | * in other words, page-independent/site-wide variables (without state). |
3752 | * These would add a blocking HTML cost to page rendering time, and require waiting for |
3753 | * HTTP caches to expire before configuration changes take effect everywhere. |
3754 | * |
3755 | * By default, these are loaded in the HTML head and block page rendering. |
3756 | * Config variable names can be set in CORE_LATE_JS_CONFIG_VAR_NAMES, or |
3757 | * for extensions via the 'LateJSConfigVarNames' attribute, to opt-in to |
3758 | * being sent from the end of the HTML body instead, to improve page load time. |
3759 | * In JavaScript, late variables should be accessed via mw.hook('wikipage.content'). |
3760 | * |
3761 | * @param int|null $flag Return only the specified kind of variables: self::JS_VAR_EARLY or self::JS_VAR_LATE. |
3762 | * For internal use only. |
3763 | * @return array |
3764 | */ |
3765 | public function getJSVars( ?int $flag = null ) { |
3766 | $curRevisionId = 0; |
3767 | $articleId = 0; |
3768 | $canonicalSpecialPageName = false; # T23115 |
3769 | $services = MediaWikiServices::getInstance(); |
3770 | |
3771 | $title = $this->getTitle(); |
3772 | $ns = $title->getNamespace(); |
3773 | $nsInfo = $services->getNamespaceInfo(); |
3774 | $canonicalNamespace = $nsInfo->exists( $ns ) |
3775 | ? $nsInfo->getCanonicalName( $ns ) |
3776 | : $title->getNsText(); |
3777 | |
3778 | $sk = $this->getSkin(); |
3779 | // Get the relevant title so that AJAX features can use the correct page name |
3780 | // when making API requests from certain special pages (T36972). |
3781 | $relevantTitle = $sk->getRelevantTitle(); |
3782 | |
3783 | if ( $ns === NS_SPECIAL ) { |
3784 | [ $canonicalSpecialPageName, /*...*/ ] = |
3785 | $services->getSpecialPageFactory()-> |
3786 | resolveAlias( $title->getDBkey() ); |
3787 | } elseif ( $this->canUseWikiPage() ) { |
3788 | $wikiPage = $this->getWikiPage(); |
3789 | // If we already know that the latest revision ID is the same as the revision ID being viewed, |
3790 | // avoid fetching it again, as it may give inconsistent results (T339164). |
3791 | if ( $this->isRevisionCurrent() && $this->getRevisionId() ) { |
3792 | $curRevisionId = $this->getRevisionId(); |
3793 | } else { |
3794 | $curRevisionId = $wikiPage->getLatest(); |
3795 | } |
3796 | $articleId = $wikiPage->getId(); |
3797 | } |
3798 | |
3799 | // ParserOutput informs HTML/CSS via lang/dir attributes. |
3800 | // We inform JavaScript via mw.config from here. |
3801 | $lang = $this->getContentLangForJS(); |
3802 | |
3803 | // Pre-process information |
3804 | $separatorTransTable = $lang->separatorTransformTable(); |
3805 | $separatorTransTable = $separatorTransTable ?: []; |
3806 | $compactSeparatorTransTable = [ |
3807 | implode( "\t", array_keys( $separatorTransTable ) ), |
3808 | implode( "\t", $separatorTransTable ), |
3809 | ]; |
3810 | $digitTransTable = $lang->digitTransformTable(); |
3811 | $digitTransTable = $digitTransTable ?: []; |
3812 | $compactDigitTransTable = [ |
3813 | implode( "\t", array_keys( $digitTransTable ) ), |
3814 | implode( "\t", $digitTransTable ), |
3815 | ]; |
3816 | |
3817 | $user = $this->getUser(); |
3818 | |
3819 | // Internal variables for MediaWiki core |
3820 | $vars = [ |
3821 | // @internal For mediawiki.page.ready |
3822 | 'wgBreakFrames' => $this->getFrameOptions() == 'DENY', |
3823 | |
3824 | // @internal For jquery.tablesorter |
3825 | 'wgSeparatorTransformTable' => $compactSeparatorTransTable, |
3826 | 'wgDigitTransformTable' => $compactDigitTransTable, |
3827 | 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(), |
3828 | 'wgMonthNames' => $lang->getMonthNamesArray(), |
3829 | |
3830 | // @internal For debugging purposes |
3831 | 'wgRequestId' => WebRequest::getRequestId(), |
3832 | ]; |
3833 | |
3834 | // Start of supported and stable config vars (for use by extensions/gadgets). |
3835 | $vars += [ |
3836 | 'wgCanonicalNamespace' => $canonicalNamespace, |
3837 | 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName, |
3838 | 'wgNamespaceNumber' => $title->getNamespace(), |
3839 | 'wgPageName' => $title->getPrefixedDBkey(), |
3840 | 'wgTitle' => $title->getText(), |
3841 | 'wgCurRevisionId' => $curRevisionId, |
3842 | 'wgRevisionId' => (int)$this->getRevisionId(), |
3843 | 'wgArticleId' => $articleId, |
3844 | 'wgIsArticle' => $this->isArticle(), |
3845 | 'wgIsRedirect' => $title->isRedirect(), |
3846 | 'wgAction' => $this->getContext()->getActionName(), |
3847 | 'wgUserName' => $user->isAnon() ? null : $user->getName(), |
3848 | 'wgUserGroups' => $services->getUserGroupManager()->getUserEffectiveGroups( $user ), |
3849 | 'wgCategories' => $this->getCategories(), |
3850 | 'wgPageViewLanguage' => $lang->getCode(), |
3851 | 'wgPageContentLanguage' => $lang->getCode(), |
3852 | 'wgPageContentModel' => $title->getContentModel(), |
3853 | 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(), |
3854 | 'wgRelevantArticleId' => $relevantTitle->getArticleID(), |
3855 | ]; |
3856 | if ( $user->isRegistered() ) { |
3857 | $vars['wgUserId'] = $user->getId(); |
3858 | $vars['wgUserIsTemp'] = $user->isTemp(); |
3859 | $vars['wgUserEditCount'] = $user->getEditCount(); |
3860 | $userReg = $user->getRegistration(); |
3861 | $vars['wgUserRegistration'] = $userReg ? (int)wfTimestamp( TS_UNIX, $userReg ) * 1000 : null; |
3862 | $userFirstReg = $services->getUserRegistrationLookup()->getFirstRegistration( $user ); |
3863 | $vars['wgUserFirstRegistration'] = $userFirstReg ? (int)wfTimestamp( TS_UNIX, $userFirstReg ) * 1000 : null; |
3864 | // Get the revision ID of the oldest new message on the user's talk |
3865 | // page. This can be used for constructing new message alerts on |
3866 | // the client side. |
3867 | $userNewMsgRevId = $this->getLastSeenUserTalkRevId(); |
3868 | // Only occupy precious space in the <head> when it is non-null (T53640) |
3869 | // mw.config.get returns null by default. |
3870 | if ( $userNewMsgRevId ) { |
3871 | $vars['wgUserNewMsgRevisionId'] = $userNewMsgRevId; |
3872 | } |
3873 | } else { |
3874 | $tempUserCreator = $services->getTempUserCreator(); |
3875 | if ( $tempUserCreator->isEnabled() ) { |
3876 | // For logged-out users only (without a temporary account): get the user name that will |
3877 | // be used for their temporary account, if it has already been acquired. |
3878 | // This may be used in previews. |
3879 | $session = $this->getRequest()->getSession(); |
3880 | $vars['wgTempUserName'] = $tempUserCreator->getStashedName( $session ); |
3881 | } |
3882 | } |
3883 | $languageConverter = $services->getLanguageConverterFactory() |
3884 | ->getLanguageConverter( $title->getPageLanguage() ); |
3885 | if ( $languageConverter->hasVariants() ) { |
3886 | $vars['wgUserVariant'] = $languageConverter->getPreferredVariant(); |
3887 | } |
3888 | // Same test as SkinTemplate |
3889 | $vars['wgIsProbablyEditable'] = $this->getAuthority()->probablyCan( 'edit', $title ); |
3890 | $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle && |
3891 | $this->getAuthority()->probablyCan( 'edit', $relevantTitle ); |
3892 | $restrictionStore = $services->getRestrictionStore(); |
3893 | foreach ( $restrictionStore->listApplicableRestrictionTypes( $title ) as $type ) { |
3894 | // Following keys are set in $vars: |
3895 | // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload |
3896 | $vars['wgRestriction' . ucfirst( $type )] = $restrictionStore->getRestrictions( $title, $type ); |
3897 | } |
3898 | if ( $title->isMainPage() ) { |
3899 | $vars['wgIsMainPage'] = true; |
3900 | } |
3901 | |
3902 | $relevantUser = $sk->getRelevantUser(); |
3903 | if ( $relevantUser ) { |
3904 | $vars['wgRelevantUserName'] = $relevantUser->getName(); |
3905 | } |
3906 | // End of stable config vars |
3907 | |
3908 | $titleFormatter = $services->getTitleFormatter(); |
3909 | |
3910 | if ( $this->mRedirectedFrom ) { |
3911 | // @internal For skin JS |
3912 | $vars['wgRedirectedFrom'] = $titleFormatter->getPrefixedDBkey( $this->mRedirectedFrom ); |
3913 | } |
3914 | |
3915 | // Allow extensions to add their custom variables to the mw.config map. |
3916 | // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not |
3917 | // page-dependent but site-wide (without state). |
3918 | // Alternatively, you may want to use OutputPage->addJsConfigVars() instead. |
3919 | $this->getHookRunner()->onMakeGlobalVariablesScript( $vars, $this ); |
3920 | |
3921 | // Merge in variables from addJsConfigVars last |
3922 | $vars = array_merge( $vars, $this->getJsConfigVars() ); |
3923 | |
3924 | // Return only early or late vars if requested |
3925 | if ( $flag !== null ) { |
3926 | $lateVarNames = |
3927 | array_fill_keys( self::CORE_LATE_JS_CONFIG_VAR_NAMES, true ) + |
3928 | array_fill_keys( ExtensionRegistry::getInstance()->getAttribute( 'LateJSConfigVarNames' ), true ); |
3929 | foreach ( $vars as $name => $_ ) { |
3930 | // If the variable's late flag doesn't match the requested late flag, unset it |
3931 | if ( isset( $lateVarNames[ $name ] ) !== ( $flag === self::JS_VAR_LATE ) ) { |
3932 | unset( $vars[ $name ] ); |
3933 | } |
3934 | } |
3935 | } |
3936 | |
3937 | return $vars; |
3938 | } |
3939 | |
3940 | /** |
3941 | * Get the revision ID for the last user talk page revision viewed by the talk page owner. |
3942 | * |
3943 | * @return int|null |
3944 | */ |
3945 | private function getLastSeenUserTalkRevId() { |
3946 | $services = MediaWikiServices::getInstance(); |
3947 | $user = $this->getUser(); |
3948 | $userHasNewMessages = $services |
3949 | ->getTalkPageNotificationManager() |
3950 | ->userHasNewMessages( $user ); |
3951 | if ( !$userHasNewMessages ) { |
3952 | return null; |
3953 | } |
3954 | |
3955 | $timestamp = $services |
3956 | ->getTalkPageNotificationManager() |
3957 | ->getLatestSeenMessageTimestamp( $user ); |
3958 | if ( !$timestamp ) { |
3959 | return null; |
3960 | } |
3961 | |
3962 | $revRecord = $services->getRevisionLookup()->getRevisionByTimestamp( |
3963 | $user->getTalkPage(), |
3964 | $timestamp |
3965 | ); |
3966 | return $revRecord ? $revRecord->getId() : null; |
3967 | } |
3968 | |
3969 | /** |
3970 | * To make it harder for someone to slip a user a fake |
3971 | * JavaScript or CSS preview, a random token |
3972 | * is associated with the login session. If it's not |
3973 | * passed back with the preview request, we won't render |
3974 | * the code. |
3975 | * |
3976 | * @return bool |
3977 | */ |
3978 | public function userCanPreview() { |
3979 | $request = $this->getRequest(); |
3980 | if ( |
3981 | $request->getRawVal( 'action' ) !== 'submit' || |
3982 | !$request->wasPosted() |
3983 | ) { |
3984 | return false; |
3985 | } |
3986 | |
3987 | $user = $this->getUser(); |
3988 | |
3989 | if ( !$user->isRegistered() ) { |
3990 | // Anons have predictable edit tokens |
3991 | return false; |
3992 | } |
3993 | if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { |
3994 | return false; |
3995 | } |
3996 | |
3997 | $title = $this->getTitle(); |
3998 | if ( !$this->getAuthority()->probablyCan( 'edit', $title ) ) { |
3999 | return false; |
4000 | } |
4001 | |
4002 | return true; |
4003 | } |
4004 | |
4005 | /** |
4006 | * @return array Array in format "link name or number => 'link html'". |
4007 | */ |
4008 | public function getHeadLinksArray() { |
4009 | $tags = []; |
4010 | $config = $this->getConfig(); |
4011 | |
4012 | if ( $this->cspOutputMode === self::CSP_META ) { |
4013 | foreach ( $this->CSP->getDirectives() as $header => $directive ) { |
4014 | $tags["meta-csp-$header"] = Html::element( 'meta', [ |
4015 | 'http-equiv' => $header, |
4016 | 'content' => $directive, |
4017 | ] ); |
4018 | } |
4019 | } |
4020 | |
4021 | $tags['meta-generator'] = Html::element( 'meta', [ |
4022 | 'name' => 'generator', |
4023 | 'content' => 'MediaWiki ' . MW_VERSION, |
4024 | ] ); |
4025 | |
4026 | if ( $config->get( MainConfigNames::ReferrerPolicy ) !== false ) { |
4027 | // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values |
4028 | // fallbacks should come before the primary value so we need to reverse the array. |
4029 | foreach ( array_reverse( (array)$config->get( MainConfigNames::ReferrerPolicy ) ) as $i => $policy ) { |
4030 | $tags["meta-referrer-$i"] = Html::element( 'meta', [ |
4031 | 'name' => 'referrer', |
4032 | 'content' => $policy, |
4033 | ] ); |
4034 | } |
4035 | } |
4036 | |
4037 | $p = $this->getRobotsContent(); |
4038 | if ( $p ) { |
4039 | // http://www.robotstxt.org/wc/meta-user.html |
4040 | // Only show if it's different from the default robots policy |
4041 | $tags['meta-robots'] = Html::element( 'meta', [ |
4042 | 'name' => 'robots', |
4043 | 'content' => $p, |
4044 | ] ); |
4045 | } |
4046 | |
4047 | # Browser based phonenumber detection |
4048 | if ( $config->get( MainConfigNames::BrowserFormatDetection ) !== false ) { |
4049 | $tags['meta-format-detection'] = Html::element( 'meta', [ |
4050 | 'name' => 'format-detection', |
4051 | 'content' => $config->get( MainConfigNames::BrowserFormatDetection ), |
4052 | ] ); |
4053 | } |
4054 | |
4055 | foreach ( $this->mMetatags as [ $name, $val ] ) { |
4056 | $attrs = []; |
4057 | if ( strncasecmp( $name, 'http:', 5 ) === 0 ) { |
4058 | $name = substr( $name, 5 ); |
4059 | $attrs['http-equiv'] = $name; |
4060 | } elseif ( strncasecmp( $name, 'og:', 3 ) === 0 ) { |
4061 | $attrs['property'] = $name; |
4062 | } else { |
4063 | $attrs['name'] = $name; |
4064 | } |
4065 | $attrs['content'] = $val; |
4066 | $tagName = "meta-$name"; |
4067 | if ( isset( $tags[$tagName] ) ) { |
4068 | $tagName .= $val; |
4069 | } |
4070 | $tags[$tagName] = Html::element( 'meta', $attrs ); |
4071 | } |
4072 | |
4073 | foreach ( $this->mLinktags as $tag ) { |
4074 | $tags[] = Html::element( 'link', $tag ); |
4075 | } |
4076 | |
4077 | if ( $config->get( MainConfigNames::UniversalEditButton ) && $this->isArticleRelated() ) { |
4078 | if ( $this->getAuthority()->probablyCan( 'edit', $this->getTitle() ) ) { |
4079 | $msg = $this->msg( 'edit' )->text(); |
4080 | // Use mime type per https://phabricator.wikimedia.org/T21165#6946526 |
4081 | $tags['universal-edit-button'] = Html::element( 'link', [ |
4082 | 'rel' => 'alternate', |
4083 | 'type' => 'application/x-wiki', |
4084 | 'title' => $msg, |
4085 | 'href' => $this->getTitle()->getEditURL(), |
4086 | ] ); |
4087 | } |
4088 | } |
4089 | |
4090 | # Generally the order of the favicon and apple-touch-icon links |
4091 | # should not matter, but Konqueror (3.5.9 at least) incorrectly |
4092 | # uses whichever one appears later in the HTML source. Make sure |
4093 | # apple-touch-icon is specified first to avoid this. |
4094 | $appleTouchIconHref = $config->get( MainConfigNames::AppleTouchIcon ); |
4095 | # Browser look for those by default, unnessecary to set a link tag |
4096 | if ( |
4097 | $appleTouchIconHref !== false && |
4098 | $appleTouchIconHref !== '/apple-touch-icon.png' && |
4099 | $appleTouchIconHref !== '/apple-touch-icon-precomposed.png' |
4100 | ) { |
4101 | $tags['apple-touch-icon'] = Html::element( 'link', [ |
4102 | 'rel' => 'apple-touch-icon', |
4103 | 'href' => $appleTouchIconHref |
4104 | ] ); |
4105 | } |
4106 | |
4107 | $faviconHref = $config->get( MainConfigNames::Favicon ); |
4108 | # Browser look for those by default, unnessecary to set a link tag |
4109 | if ( $faviconHref !== false && $faviconHref !== '/favicon.ico' ) { |
4110 | $tags['favicon'] = Html::element( 'link', [ |
4111 | 'rel' => 'icon', |
4112 | 'href' => $faviconHref |
4113 | ] ); |
4114 | } |
4115 | |
4116 | # OpenSearch description link |
4117 | $tags['opensearch'] = Html::element( 'link', [ |
4118 | 'rel' => 'search', |
4119 | 'type' => 'application/opensearchdescription+xml', |
4120 | 'href' => wfScript( 'opensearch_desc' ), |
4121 | 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(), |
4122 | ] ); |
4123 | |
4124 | $services = MediaWikiServices::getInstance(); |
4125 | |
4126 | # Real Simple Discovery link, provides auto-discovery information |
4127 | # for the MediaWiki API (and potentially additional custom API |
4128 | # support such as WordPress or Twitter-compatible APIs for a |
4129 | # blogging extension, etc) |
4130 | $tags['rsd'] = Html::element( 'link', [ |
4131 | 'rel' => 'EditURI', |
4132 | 'type' => 'application/rsd+xml', |
4133 | // Output a protocol-relative URL here if $wgServer is protocol-relative. |
4134 | // Whether RSD accepts relative or protocol-relative URLs is completely |
4135 | // undocumented, though. |
4136 | 'href' => (string)$services->getUrlUtils()->expand( wfAppendQuery( |
4137 | wfScript( 'api' ), |
4138 | [ 'action' => 'rsd' ] ), |
4139 | PROTO_RELATIVE |
4140 | ), |
4141 | ] ); |
4142 | |
4143 | $tags = array_merge( |
4144 | $tags, |
4145 | $this->getHeadLinksCanonicalURLArray( $config ), |
4146 | $this->getHeadLinksAlternateURLsArray(), |
4147 | $this->getHeadLinksCopyrightArray( $config ), |
4148 | $this->getHeadLinksSyndicationArray( $config ), |
4149 | ); |
4150 | |
4151 | // Allow extensions to add, remove and/or otherwise manipulate these links |
4152 | // If you want only to *add* <head> links, please use the addHeadItem() |
4153 | // (or addHeadItems() for multiple items) method instead. |
4154 | // This hook is provided as a last resort for extensions to modify these |
4155 | // links before the output is sent to client. |
4156 | $this->getHookRunner()->onOutputPageAfterGetHeadLinksArray( $tags, $this ); |
4157 | |
4158 | return $tags; |
4159 | } |
4160 | |
4161 | /** |
4162 | * Canonical URL and alternate URLs |
4163 | * |
4164 | * isCanonicalUrlAction affects all requests where "setArticleRelated" is true. |
4165 | * This is typically all requests that show content (query title, curid, oldid, diff), |
4166 | * and all wikipage actions (edit, delete, purge, info, history etc.). |
4167 | * It does not apply to file pages and special pages. |
4168 | * 'history' and 'info' actions address page metadata rather than the page |
4169 | * content itself, so they may not be canonicalized to the view page url. |
4170 | * TODO: this logic should be owned by Action subclasses. |
4171 | * See T67402 |
4172 | */ |
4173 | |
4174 | /** |
4175 | * Get head links relating to the canonical URL |
4176 | * Note: There should only be one canonical URL. |
4177 | * @param Config $config |
4178 | * @return array |
4179 | */ |
4180 | private function getHeadLinksCanonicalURLArray( Config $config ) { |
4181 | $tags = []; |
4182 | $canonicalUrl = $this->mCanonicalUrl; |
4183 | |
4184 | if ( $config->get( MainConfigNames::EnableCanonicalServerLink ) ) { |
4185 | $query = []; |
4186 | $action = $this->getContext()->getActionName(); |
4187 | $isCanonicalUrlAction = in_array( $action, [ 'history', 'info' ] ); |
4188 | $services = MediaWikiServices::getInstance(); |
4189 | $languageConverterFactory = $services->getLanguageConverterFactory(); |
4190 | $isLangConversionDisabled = $languageConverterFactory->isConversionDisabled(); |
4191 | $pageLang = $this->getTitle()->getPageLanguage(); |
4192 | $pageLanguageConverter = $languageConverterFactory->getLanguageConverter( $pageLang ); |
4193 | $urlVariant = $pageLanguageConverter->getURLVariant(); |
4194 | |
4195 | if ( $canonicalUrl !== false ) { |
4196 | $canonicalUrl = (string)$services->getUrlUtils()->expand( $canonicalUrl, PROTO_CANONICAL ); |
4197 | } elseif ( $this->isArticleRelated() ) { |
4198 | if ( $isCanonicalUrlAction ) { |
4199 | $query['action'] = $action; |
4200 | } elseif ( !$isLangConversionDisabled && $urlVariant ) { |
4201 | # T54429, T108443: Making canonical URL language-variant-aware. |
4202 | $query['variant'] = $urlVariant; |
4203 | } |
4204 | $canonicalUrl = $this->getTitle()->getCanonicalURL( $query ); |
4205 | } else { |
4206 | $reqUrl = $this->getRequest()->getRequestURL(); |
4207 | $canonicalUrl = (string)$services->getUrlUtils()->expand( $reqUrl, PROTO_CANONICAL ); |
4208 | } |
4209 | } |
4210 | |
4211 | if ( $canonicalUrl !== false ) { |
4212 | $tags['link-canonical'] = Html::element( 'link', [ |
4213 | 'rel' => 'canonical', |
4214 | 'href' => $canonicalUrl |
4215 | ] ); |
4216 | } |
4217 | |
4218 | return $tags; |
4219 | } |
4220 | |
4221 | /** |
4222 | * Get head links relating to alternate URL(s) in languages including language variants |
4223 | * Output fully-qualified URL since meta alternate URLs must be fully-qualified |
4224 | * Per https://developers.google.com/search/docs/advanced/crawling/localized-versions |
4225 | * See T294716 |
4226 | * |
4227 | * @return array |
4228 | */ |
4229 | private function getHeadLinksAlternateURLsArray() { |
4230 | $tags = []; |
4231 | $languageUrls = []; |
4232 | $action = $this->getContext()->getActionName(); |
4233 | $isCanonicalUrlAction = in_array( $action, [ 'history', 'info' ] ); |
4234 | $services = MediaWikiServices::getInstance(); |
4235 | $languageConverterFactory = $services->getLanguageConverterFactory(); |
4236 | $isLangConversionDisabled = $languageConverterFactory->isConversionDisabled(); |
4237 | $pageLang = $this->getTitle()->getPageLanguage(); |
4238 | $pageLanguageConverter = $languageConverterFactory->getLanguageConverter( $pageLang ); |
4239 | |
4240 | # Language variants |
4241 | if ( |
4242 | $this->isArticleRelated() && |
4243 | !$isCanonicalUrlAction && |
4244 | $pageLanguageConverter->hasVariants() && |
4245 | !$isLangConversionDisabled |
4246 | ) { |
4247 | $variants = $pageLanguageConverter->getVariants(); |
4248 | foreach ( $variants as $variant ) { |
4249 | $bcp47 = LanguageCode::bcp47( $variant ); |
4250 | $languageUrls[$bcp47] = $this->getTitle() |
4251 | ->getFullURL( [ 'variant' => $variant ], false, PROTO_CURRENT ); |
4252 | } |
4253 | } |
4254 | |
4255 | # Alternate URLs for interlanguage links would be handeled in HTML body tag instead of |
4256 | # head tag, see T326829. |
4257 | |
4258 | if ( $languageUrls ) { |
4259 | # Force the alternate URL of page language code to be self. |
4260 | # T123901, T305540, T108443: Override mixed-variant variant link in language variant links. |
4261 | $currentUrl = $this->getTitle()->getFullURL( [], false, PROTO_CURRENT ); |
4262 | $pageLangCodeBcp47 = LanguageCode::bcp47( $pageLang->getCode() ); |
4263 | $languageUrls[$pageLangCodeBcp47] = $currentUrl; |
4264 | |
4265 | ksort( $languageUrls ); |
4266 | |
4267 | # Also add x-default link per https://support.google.com/webmasters/answer/189077?hl=en |
4268 | $languageUrls['x-default'] = $currentUrl; |
4269 | |
4270 | # Process all of language variants and interlanguage links |
4271 | foreach ( $languageUrls as $bcp47 => $languageUrl ) { |
4272 | $bcp47lowercase = strtolower( $bcp47 ); |
4273 | $tags['link-alternate-language-' . $bcp47lowercase] = Html::element( 'link', [ |
4274 | 'rel' => 'alternate', |
4275 | 'hreflang' => $bcp47, |
4276 | 'href' => $languageUrl, |
4277 | ] ); |
4278 | } |
4279 | } |
4280 | |
4281 | return $tags; |
4282 | } |
4283 | |
4284 | /** |
4285 | * Get head links relating to copyright |
4286 | * |
4287 | * @param Config $config |
4288 | * @return array |
4289 | */ |
4290 | private function getHeadLinksCopyrightArray( Config $config ) { |
4291 | $tags = []; |
4292 | |
4293 | if ( $this->copyrightUrl !== null ) { |
4294 | $copyright = $this->copyrightUrl; |
4295 | } else { |
4296 | $copyright = ''; |
4297 | if ( $config->get( MainConfigNames::RightsPage ) ) { |
4298 | $copy = Title::newFromText( $config->get( MainConfigNames::RightsPage ) ); |
4299 | |
4300 | if ( $copy ) { |
4301 | $copyright = $copy->getLocalURL(); |
4302 | } |
4303 | } |
4304 | |
4305 | if ( !$copyright && $config->get( MainConfigNames::RightsUrl ) ) { |
4306 | $copyright = $config->get( MainConfigNames::RightsUrl ); |
4307 | } |
4308 | } |
4309 | |
4310 | if ( $copyright ) { |
4311 | $tags['copyright'] = Html::element( 'link', [ |
4312 | 'rel' => 'license', |
4313 | 'href' => $copyright |
4314 | ] ); |
4315 | } |
4316 | |
4317 | return $tags; |
4318 | } |
4319 | |
4320 | /** |
4321 | * Get head links relating to syndication feeds. |
4322 | * |
4323 | * @param Config $config |
4324 | * @return array |
4325 | */ |
4326 | private function getHeadLinksSyndicationArray( Config $config ) { |
4327 | if ( !$config->get( MainConfigNames::Feed ) ) { |
4328 | return []; |
4329 | } |
4330 | |
4331 | $tags = []; |
4332 | $feedLinks = []; |
4333 | |
4334 | foreach ( $this->getSyndicationLinks() as $format => $link ) { |
4335 | # Use the page name for the title. In principle, this could |
4336 | # lead to issues with having the same name for different feeds |
4337 | # corresponding to the same page, but we can't avoid that at |
4338 | # this low a level. |
4339 | |
4340 | $feedLinks[] = $this->feedLink( |
4341 | $format, |
4342 | $link, |
4343 | # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep) |
4344 | $this->msg( |
4345 | "page-{$format}-feed", $this->getTitle()->getPrefixedText() |
4346 | )->text() |
4347 | ); |
4348 | } |
4349 | |
4350 | # Recent changes feed should appear on every page (except recentchanges, |
4351 | # that would be redundant). Put it after the per-page feed to avoid |
4352 | # changing existing behavior. It's still available, probably via a |
4353 | # menu in your browser. Some sites might have a different feed they'd |
4354 | # like to promote instead of the RC feed (maybe like a "Recent New Articles" |
4355 | # or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined. |
4356 | # If so, use it instead. |
4357 | $sitename = $config->get( MainConfigNames::Sitename ); |
4358 | $overrideSiteFeed = $config->get( MainConfigNames::OverrideSiteFeed ); |
4359 | if ( $overrideSiteFeed ) { |
4360 | foreach ( $overrideSiteFeed as $type => $feedUrl ) { |
4361 | // Note, this->feedLink escapes the url. |
4362 | $feedLinks[] = $this->feedLink( |
4363 | $type, |
4364 | $feedUrl, |
4365 | $this->msg( "site-{$type}-feed", $sitename )->text() |
4366 | ); |
4367 | } |
4368 | } elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) { |
4369 | $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); |
4370 | foreach ( $this->getAdvertisedFeedTypes() as $format ) { |
4371 | $feedLinks[] = $this->feedLink( |
4372 | $format, |
4373 | $rctitle->getLocalURL( [ 'feed' => $format ] ), |
4374 | # For grep: 'site-rss-feed', 'site-atom-feed' |
4375 | $this->msg( "site-{$format}-feed", $sitename )->text() |
4376 | ); |
4377 | } |
4378 | } |
4379 | |
4380 | # Allow extensions to change the list pf feeds. This hook is primarily for changing, |
4381 | # manipulating or removing existing feed tags. If you want to add new feeds, you should |
4382 | # use OutputPage::addFeedLink() instead. |
4383 | $this->getHookRunner()->onAfterBuildFeedLinks( $feedLinks ); |
4384 | |
4385 | $tags += $feedLinks; |
4386 | |
4387 | return $tags; |
4388 | } |
4389 | |
4390 | /** |
4391 | * Generate a "<link rel/>" for a feed. |
4392 | * |
4393 | * @param string $type Feed type |
4394 | * @param string $url URL to the feed |
4395 | * @param string $text Value of the "title" attribute |
4396 | * @return string HTML fragment |
4397 | */ |
4398 | private function feedLink( $type, $url, $text ) { |
4399 | return Html::element( 'link', [ |
4400 | 'rel' => 'alternate', |
4401 | 'type' => "application/$type+xml", |
4402 | 'title' => $text, |
4403 | 'href' => $url ] |
4404 | ); |
4405 | } |
4406 | |
4407 | /** |
4408 | * Add a local or specified stylesheet, with the given media options. |
4409 | * Internal use only. Use OutputPage::addModuleStyles() if possible. |
4410 | * |
4411 | * @param string $style URL to the file |
4412 | * @param string $media To specify a media type, 'screen', 'printable', 'handheld' or any. |
4413 | * @param string $condition For IE conditional comments, specifying an IE version |
4414 | * @param string $dir Set to 'rtl' or 'ltr' for direction-specific sheets |
4415 | */ |
4416 | public function addStyle( $style, $media = '', $condition = '', $dir = '' ) { |
4417 | $options = []; |
4418 | if ( $media ) { |
4419 | $options['media'] = $media; |
4420 | } |
4421 | if ( $condition ) { |
4422 | $options['condition'] = $condition; |
4423 | } |
4424 | if ( $dir ) { |
4425 | $options['dir'] = $dir; |
4426 | } |
4427 | $this->styles[$style] = $options; |
4428 | } |
4429 | |
4430 | /** |
4431 | * Adds inline CSS styles |
4432 | * Internal use only. Use OutputPage::addModuleStyles() if possible. |
4433 | * |
4434 | * @param mixed $style_css Inline CSS |
4435 | * @param-taint $style_css exec_html |
4436 | * @param string $flip Set to 'flip' to flip the CSS if needed |
4437 | */ |
4438 | public function addInlineStyle( $style_css, $flip = 'noflip' ) { |
4439 | if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) { |
4440 | # If wanted, and the interface is right-to-left, flip the CSS |
4441 | $style_css = CSSJanus::transform( $style_css, true, false ); |
4442 | } |
4443 | $this->mInlineStyles .= Html::inlineStyle( $style_css ); |
4444 | } |
4445 | |
4446 | /** |
4447 | * Build exempt modules and legacy non-ResourceLoader styles. |
4448 | * |
4449 | * @return string|WrappedStringList HTML |
4450 | */ |
4451 | protected function buildExemptModules() { |
4452 | $chunks = []; |
4453 | |
4454 | // Requirements: |
4455 | // - Within modules provided by the software (core, skin, extensions), |
4456 | // styles from skin stylesheets should be overridden by styles |
4457 | // from modules dynamically loaded with JavaScript. |
4458 | // - Styles from site-specific, private, and user modules should override |
4459 | // both of the above. |
4460 | // |
4461 | // The effective order for stylesheets must thus be: |
4462 | // 1. Page style modules, formatted server-side by RL\ClientHtml. |
4463 | // 2. Dynamically-loaded styles, inserted client-side by mw.loader. |
4464 | // 3. Styles that are site-specific, private or from the user, formatted |
4465 | // server-side by this function. |
4466 | // |
4467 | // The 'ResourceLoaderDynamicStyles' marker helps JavaScript know where |
4468 | // point #2 is. |
4469 | |
4470 | // Add legacy styles added through addStyle()/addInlineStyle() here |
4471 | $chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles; |
4472 | |
4473 | // Things that go after the ResourceLoaderDynamicStyles marker |
4474 | $append = []; |
4475 | $separateReq = [ 'site.styles', 'user.styles' ]; |
4476 | foreach ( $this->rlExemptStyleModules as $moduleNames ) { |
4477 | if ( $moduleNames ) { |
4478 | $append[] = $this->makeResourceLoaderLink( |
4479 | array_diff( $moduleNames, $separateReq ), |
4480 | RL\Module::TYPE_STYLES |
4481 | ); |
4482 | |
4483 | foreach ( array_intersect( $moduleNames, $separateReq ) as $name ) { |
4484 | // These require their own dedicated request in order to support "@import" |
4485 | // syntax, which is incompatible with concatenation. (T147667, T37562) |
4486 | $append[] = $this->makeResourceLoaderLink( $name, |
4487 | RL\Module::TYPE_STYLES |
4488 | ); |
4489 | } |
4490 | } |
4491 | } |
4492 | if ( $append ) { |
4493 | $chunks[] = Html::element( |
4494 | 'meta', |
4495 | [ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ] |
4496 | ); |
4497 | $chunks = array_merge( $chunks, $append ); |
4498 | } |
4499 | |
4500 | return self::combineWrappedStrings( $chunks ); |
4501 | } |
4502 | |
4503 | /** |
4504 | * @return array |
4505 | */ |
4506 | public function buildCssLinksArray() { |
4507 | $links = []; |
4508 | |
4509 | foreach ( $this->styles as $file => $options ) { |
4510 | $link = $this->styleLink( $file, $options ); |
4511 | if ( $link ) { |
4512 | $links[$file] = $link; |
4513 | } |
4514 | } |
4515 | return $links; |
4516 | } |
4517 | |
4518 | /** |
4519 | * Generate \<link\> tags for stylesheets |
4520 | * |
4521 | * @param string $style URL to the file |
4522 | * @param array $options Option, can contain 'condition', 'dir', 'media' keys |
4523 | * @return string HTML fragment |
4524 | */ |
4525 | protected function styleLink( $style, array $options ) { |
4526 | if ( isset( $options['dir'] ) && $this->getLanguage()->getDir() != $options['dir'] ) { |
4527 | return ''; |
4528 | } |
4529 | |
4530 | if ( isset( $options['media'] ) ) { |
4531 | $media = self::transformCssMedia( $options['media'] ); |
4532 | if ( $media === null ) { |
4533 | return ''; |
4534 | } |
4535 | } else { |
4536 | $media = 'all'; |
4537 | } |
4538 | |
4539 | if ( str_starts_with( $style, '/' ) || |
4540 | str_starts_with( $style, 'http:' ) || |
4541 | str_starts_with( $style, 'https:' ) |
4542 | ) { |
4543 | $url = $style; |
4544 | } else { |
4545 | $config = $this->getConfig(); |
4546 | // Append file hash as query parameter |
4547 | $url = self::transformResourcePath( |
4548 | $config, |
4549 | $config->get( MainConfigNames::StylePath ) . '/' . $style |
4550 | ); |
4551 | } |
4552 | |
4553 | $link = Html::linkedStyle( $url, $media ); |
4554 | |
4555 | if ( isset( $options['condition'] ) ) { |
4556 | $condition = htmlspecialchars( $options['condition'] ); |
4557 | $link = "<!--[if $condition]>$link<![endif]-->"; |
4558 | } |
4559 | return $link; |
4560 | } |
4561 | |
4562 | /** |
4563 | * Transform path to web-accessible static resource. |
4564 | * |
4565 | * This is used to add a validation hash as query string. |
4566 | * This aids various behaviors: |
4567 | * |
4568 | * - Put long Cache-Control max-age headers on responses for improved |
4569 | * cache performance. |
4570 | * - Get the correct version of a file as expected by the current page. |
4571 | * - Instantly get the updated version of a file after deployment. |
4572 | * |
4573 | * Avoid using this for urls included in HTML as otherwise clients may get different |
4574 | * versions of a resource when navigating the site depending on when the page was cached. |
4575 | * If changes to the url propagate, this is not a problem (e.g. if the url is in |
4576 | * an external stylesheet). |
4577 | * |
4578 | * @since 1.27 |
4579 | * @param Config $config |
4580 | * @param string $path Path-absolute URL to file (from document root, must start with "/") |
4581 | * @return string URL |
4582 | */ |
4583 | public static function transformResourcePath( Config $config, $path ) { |
4584 | $localDir = $config->get( MainConfigNames::BaseDirectory ); |
4585 | $remotePathPrefix = $config->get( MainConfigNames::ResourceBasePath ); |
4586 | if ( $remotePathPrefix === '' ) { |
4587 | // The configured base path is required to be empty string for |
4588 | // wikis in the domain root |
4589 | $remotePath = '/'; |
4590 | } else { |
4591 | $remotePath = $remotePathPrefix; |
4592 | } |
4593 | if ( !str_starts_with( $path, $remotePath ) || str_starts_with( $path, '//' ) ) { |
4594 | // - Path is outside wgResourceBasePath, ignore. |
4595 | // - Path is protocol-relative. Fixes T155310. Not supported by RelPath lib. |
4596 | return $path; |
4597 | } |
4598 | // For files in resources, extensions/ or skins/, ResourceBasePath is preferred here. |
4599 | // For other misc files in $IP, we'll fallback to that as well. There is, however, a fourth |
4600 | // supported dir/path pair in the configuration (wgUploadDirectory, wgUploadPath) |
4601 | // which is not expected to be in wgResourceBasePath on CDNs. (T155146) |
4602 | $uploadPath = $config->get( MainConfigNames::UploadPath ); |
4603 | if ( strpos( $path, $uploadPath ) === 0 ) { |
4604 | $localDir = $config->get( MainConfigNames::UploadDirectory ); |
4605 | $remotePathPrefix = $remotePath = $uploadPath; |
4606 | } |
4607 | |
4608 | $path = RelPath::getRelativePath( $path, $remotePath ); |
4609 | return self::transformFilePath( $remotePathPrefix, $localDir, $path ); |
4610 | } |
4611 | |
4612 | /** |
4613 | * Utility method for transformResourceFilePath(). |
4614 | * |
4615 | * Caller is responsible for ensuring the file exists. Emits a PHP warning otherwise. |
4616 | * |
4617 | * @since 1.27 |
4618 | * @param string $remotePathPrefix URL path prefix that points to $localPath |
4619 | * @param string $localPath File directory exposed at $remotePath |
4620 | * @param string $file Path to target file relative to $localPath |
4621 | * @return string URL |
4622 | */ |
4623 | public static function transformFilePath( $remotePathPrefix, $localPath, $file ) { |
4624 | // This MUST match the equivalent logic in CSSMin::remapOne() |
4625 | $localFile = "$localPath/$file"; |
4626 | $url = "$remotePathPrefix/$file"; |
4627 | if ( is_file( $localFile ) ) { |
4628 | $hash = md5_file( $localFile ); |
4629 | if ( $hash === false ) { |
4630 | wfLogWarning( __METHOD__ . ": Failed to hash $localFile" ); |
4631 | $hash = ''; |
4632 | } |
4633 | $url .= '?' . substr( $hash, 0, 5 ); |
4634 | } |
4635 | return $url; |
4636 | } |
4637 | |
4638 | /** |
4639 | * Transform "media" attribute based on request parameters |
4640 | * |
4641 | * @param string $media Current value of the "media" attribute |
4642 | * @return string|null Modified value of the "media" attribute, or null to disable |
4643 | * this stylesheet |
4644 | */ |
4645 | public static function transformCssMedia( $media ) { |
4646 | global $wgRequest; |
4647 | |
4648 | if ( $wgRequest->getBool( 'printable' ) ) { |
4649 | // When browsing with printable=yes, apply "print" media styles |
4650 | // as if they are screen styles (no media, media=""). |
4651 | if ( $media === 'print' ) { |
4652 | return ''; |
4653 | } |
4654 | |
4655 | // https://www.w3.org/TR/css3-mediaqueries/#syntax |
4656 | // |
4657 | // This regex will not attempt to understand a comma-separated media_query_list |
4658 | // Example supported values for $media: |
4659 | // |
4660 | // 'screen', 'only screen', 'screen and (min-width: 982px)' ), |
4661 | // |
4662 | // Example NOT supported value for $media: |
4663 | // |
4664 | // '3d-glasses, screen, print and resolution > 90dpi' |
4665 | // |
4666 | // If it's a "printable" request, we disable all screen stylesheets. |
4667 | $screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i'; |
4668 | if ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) { |
4669 | return null; |
4670 | } |
4671 | } |
4672 | |
4673 | return $media; |
4674 | } |
4675 | |
4676 | /** |
4677 | * Add a wikitext-formatted message to the output. |
4678 | * |
4679 | * @param string|MessageSpecifier $name Message key |
4680 | * @param mixed ...$args Message parameters. Unlike wfMessage(), this method only accepts |
4681 | * variadic parameters (they can't be passed as a single array parameter). |
4682 | */ |
4683 | public function addWikiMsg( $name, ...$args ) { |
4684 | $this->addWikiMsgArray( $name, $args ); |
4685 | } |
4686 | |
4687 | /** |
4688 | * Add a wikitext-formatted message to the output. |
4689 | * |
4690 | * @param string|MessageSpecifier $name Message key |
4691 | * @param array $args Message parameters. Unlike wfMessage(), this method only accepts |
4692 | * the parameters as an array (they can't be passed as variadic parameters), |
4693 | * or just a single parameter (this only works by accident, don't rely on it). |
4694 | */ |
4695 | public function addWikiMsgArray( $name, $args ) { |
4696 | $this->addHTML( $this->msg( $name, $args )->parseAsBlock() ); |
4697 | } |
4698 | |
4699 | /** |
4700 | * This function takes a number of message/argument specifications, wraps them in |
4701 | * some overall structure, and then parses the result and adds it to the output. |
4702 | * |
4703 | * In the $wrap, $1 is replaced with the first message, $2 with the second, |
4704 | * and so on. The subsequent arguments may be either |
4705 | * 1) strings, in which case they are message names, or |
4706 | * 2) arrays, in which case, within each array, the first element is the message |
4707 | * name, and subsequent elements are the parameters to that message. |
4708 | * |
4709 | * Don't use this for messages that are not in the user's interface language. |
4710 | * |
4711 | * For example: |
4712 | * |
4713 | * $wgOut->wrapWikiMsg( "<div class='customclass'>\n$1\n</div>", 'some-msg-key' ); |
4714 | * |
4715 | * Is equivalent to: |
4716 | * |
4717 | * $wgOut->addWikiTextAsInterface( "<div class='customclass'>\n" |
4718 | * . wfMessage( 'some-msg-key' )->plain() . "\n</div>" ); |
4719 | * |
4720 | * The newline after the opening div is needed in some wikitext. See T21226. |
4721 | * |
4722 | * @param string $wrap |
4723 | * @param mixed ...$msgSpecs |
4724 | */ |
4725 | public function wrapWikiMsg( $wrap, ...$msgSpecs ) { |
4726 | $s = $wrap; |
4727 | foreach ( $msgSpecs as $n => $spec ) { |
4728 | if ( is_array( $spec ) ) { |
4729 | $args = $spec; |
4730 | $name = array_shift( $args ); |
4731 | } else { |
4732 | $args = []; |
4733 | $name = $spec; |
4734 | } |
4735 | $s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s ); |
4736 | } |
4737 | $this->addWikiTextAsInterface( $s ); |
4738 | } |
4739 | |
4740 | /** |
4741 | * Whether the output has a table of contents when the ToC is |
4742 | * rendered inline. |
4743 | * @return bool |
4744 | * @since 1.22 |
4745 | */ |
4746 | public function isTOCEnabled() { |
4747 | return $this->mEnableTOC; |
4748 | } |
4749 | |
4750 | /** |
4751 | * Helper function to setup the PHP implementation of OOUI to use in this request. |
4752 | * |
4753 | * @since 1.26 |
4754 | * @param string|null $skinName Ignored since 1.41 |
4755 | * @param string|null $dir Ignored since 1.41 |
4756 | */ |
4757 | public static function setupOOUI( $skinName = null, $dir = null ) { |
4758 | if ( !self::$oouiSetupDone ) { |
4759 | self::$oouiSetupDone = true; |
4760 | $context = RequestContext::getMain(); |
4761 | $skinName = $context->getSkinName(); |
4762 | $dir = $context->getLanguage()->getDir(); |
4763 | $themes = RL\OOUIFileModule::getSkinThemeMap(); |
4764 | $theme = $themes[$skinName] ?? $themes['default']; |
4765 | // For example, 'OOUI\WikimediaUITheme'. |
4766 | $themeClass = "OOUI\\{$theme}Theme"; |
4767 | Theme::setSingleton( new $themeClass() ); |
4768 | Element::setDefaultDir( $dir ); |
4769 | } |
4770 | } |
4771 | |
4772 | /** |
4773 | * Notify of a change in global skin or language which would necessitate |
4774 | * reinitialization of OOUI global static data. |
4775 | * @internal |
4776 | */ |
4777 | public static function resetOOUI() { |
4778 | if ( self::$oouiSetupDone ) { |
4779 | self::$oouiSetupDone = false; |
4780 | self::setupOOUI(); |
4781 | } |
4782 | } |
4783 | |
4784 | /** |
4785 | * Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with |
4786 | * MediaWiki and this OutputPage instance. |
4787 | * |
4788 | * @since 1.25 |
4789 | */ |
4790 | public function enableOOUI() { |
4791 | self::setupOOUI(); |
4792 | $this->addModuleStyles( [ |
4793 | 'oojs-ui-core.styles', |
4794 | 'oojs-ui.styles.indicators', |
4795 | 'mediawiki.widgets.styles', |
4796 | 'oojs-ui-core.icons', |
4797 | ] ); |
4798 | } |
4799 | |
4800 | /** |
4801 | * Get (and set if not yet set) the CSP nonce. |
4802 | * |
4803 | * This value needs to be included in any <script> tags on the |
4804 | * page. |
4805 | * |
4806 | * @return string|false Nonce or false to mean don't output nonce |
4807 | * @since 1.32 |
4808 | * @deprecated Since 1.35 use getCSP()->getNonce() instead |
4809 | */ |
4810 | public function getCSPNonce() { |
4811 | wfDeprecated( __METHOD__, '1.35' ); |
4812 | return $this->CSP->getNonce(); |
4813 | } |
4814 | |
4815 | /** |
4816 | * Get the ContentSecurityPolicy object |
4817 | * |
4818 | * @since 1.35 |
4819 | * @return ContentSecurityPolicy |
4820 | */ |
4821 | public function getCSP() { |
4822 | return $this->CSP; |
4823 | } |
4824 | |
4825 | /** |
4826 | * Sets the output mechanism for content security policies (HTTP headers or meta tags). |
4827 | * Defaults to HTTP headers; in most cases this should not be changed. |
4828 | * |
4829 | * Meta mode should not be used together with setArticleBodyOnly() as meta tags and other |
4830 | * headers are not output when that flag is set. |
4831 | * |
4832 | * @param string $mode One of the CSP_* constants |
4833 | * @phan-param 'headers'|'meta' $mode |
4834 | * @return void |
4835 | * @see self::CSP_HEADERS |
4836 | * @see self::CSP_META |
4837 | */ |
4838 | public function setCspOutputMode( string $mode ): void { |
4839 | $this->cspOutputMode = $mode; |
4840 | } |
4841 | |
4842 | /** |
4843 | * The final bits that go to the bottom of a page |
4844 | * HTML document including the closing tags |
4845 | * |
4846 | * @internal |
4847 | * @since 1.37 |
4848 | * @param Skin $skin |
4849 | * @return string |
4850 | */ |
4851 | public function tailElement( $skin ) { |
4852 | $tail = [ |
4853 | MWDebug::getDebugHTML( $skin ), |
4854 | $this->getBottomScripts(), |
4855 | MWDebug::getHTMLDebugLog(), |
4856 | Html::closeElement( 'body' ), |
4857 | Html::closeElement( 'html' ), |
4858 | ]; |
4859 | |
4860 | return WrappedStringList::join( "\n", $tail ); |
4861 | } |
4862 | } |
4863 | |
4864 | /** @deprecated class alias since 1.41 */ |
4865 | class_alias( OutputPage::class, 'OutputPage' ); |