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