Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
22.00% |
97 / 441 |
|
4.76% |
2 / 42 |
CRAP | |
0.00% |
0 / 1 |
MobileFrontendHooks | |
22.00% |
97 / 441 |
|
4.76% |
2 / 42 |
9986.03 | |
0.00% |
0 / 1 |
getDefaultMobileSkin | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onRequestContextCreateSkin | |
70.83% |
17 / 24 |
|
0.00% |
0 / 1 |
8.22 | |||
onSkinAddFooterLinks | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
onSkinAfterBottomScripts | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onBeforeDisplayNoArticleText | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
onOutputPageBeforeHTML | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
56 | |||
onOutputPageBodyAttributes | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
onBeforePageRedirect | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
onDifferenceEngineViewHeader | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
onResourceLoaderSiteStylesModulePages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
onResourceLoaderSiteModulePages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
onGetCacheVaryCookies | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getResourceLoaderMFConfigVars | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
getWikibaseStaticConfigVars | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
shouldMobileFormatSpecialPages | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
onSpecialPage_initList | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
onListDefinedTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onChangeTagsListActive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addDefinedTags | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onRecentChange_save | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onManualLogEntryBeforePublish | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onTaggableObjectCreation | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
onAbuseFilterGenerateUserVars | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
onAbuseFilterBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSpecialPageBeforeExecute | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
onPostLoginRedirect | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
onBeforePageDisplay | |
86.57% |
58 / 67 |
|
0.00% |
0 / 1 |
17.70 | |||
onAfterBuildFeedLinks | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onUserGetDefaultOptions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onGetPreferences | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
onCentralAuthLoginRedirectData | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onCentralAuthSilentLoginRedirect | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
setTagline | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findTagline | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
onOutputPageParserOutput | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
onHTMLFileCache__useFileCache | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onLoginFormValidErrorMessages | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
onMakeGlobalVariablesScript | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
hasEditNoticesFeatureConflict | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
onTitleSquidURLs | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
onAuthChangeFormFields | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
onAPIQuerySiteInfoGeneralInfo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | use MediaWiki\Api\Hook\APIQuerySiteInfoGeneralInfoHook; |
6 | use MediaWiki\Auth\AuthenticationRequest; |
7 | use MediaWiki\Auth\AuthManager; |
8 | use MediaWiki\Cache\Hook\HTMLFileCache__useFileCacheHook; |
9 | use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook; |
10 | use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook; |
11 | use MediaWiki\ChangeTags\Taggable; |
12 | use MediaWiki\Config\Config; |
13 | use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook; |
14 | use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder; |
15 | use MediaWiki\Extension\Gadgets\GadgetRepo; |
16 | use MediaWiki\Hook\AfterBuildFeedLinksHook; |
17 | use MediaWiki\Hook\BeforePageDisplayHook; |
18 | use MediaWiki\Hook\BeforePageRedirectHook; |
19 | use MediaWiki\Hook\GetCacheVaryCookiesHook; |
20 | use MediaWiki\Hook\LoginFormValidErrorMessagesHook; |
21 | use MediaWiki\Hook\MakeGlobalVariablesScriptHook; |
22 | use MediaWiki\Hook\ManualLogEntryBeforePublishHook; |
23 | use MediaWiki\Hook\OutputPageBeforeHTMLHook; |
24 | use MediaWiki\Hook\OutputPageBodyAttributesHook; |
25 | use MediaWiki\Hook\OutputPageParserOutputHook; |
26 | use MediaWiki\Hook\PostLoginRedirectHook; |
27 | use MediaWiki\Hook\RecentChange_saveHook; |
28 | use MediaWiki\Hook\RequestContextCreateSkinHook; |
29 | use MediaWiki\Hook\SkinAddFooterLinksHook; |
30 | use MediaWiki\Hook\SkinAfterBottomScriptsHook; |
31 | use MediaWiki\Hook\TitleSquidURLsHook; |
32 | use MediaWiki\Html\Html; |
33 | use MediaWiki\MediaWikiServices; |
34 | use MediaWiki\Output\OutputPage; |
35 | use MediaWiki\Page\Hook\BeforeDisplayNoArticleTextHook; |
36 | use MediaWiki\Parser\ParserOutput; |
37 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
38 | use MediaWiki\ResourceLoader as RL; |
39 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderSiteModulePagesHook; |
40 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderSiteStylesModulePagesHook; |
41 | use MediaWiki\ResourceLoader\ResourceLoader; |
42 | use MediaWiki\SpecialPage\Hook\AuthChangeFormFieldsHook; |
43 | use MediaWiki\SpecialPage\Hook\SpecialPage_initListHook; |
44 | use MediaWiki\SpecialPage\Hook\SpecialPageBeforeExecuteHook; |
45 | use MediaWiki\SpecialPage\SpecialPage; |
46 | use MediaWiki\Title\Title; |
47 | use MediaWiki\User\Hook\UserGetDefaultOptionsHook; |
48 | use MediaWiki\User\User; |
49 | use MobileFrontend\Api\ApiParseExtender; |
50 | use MobileFrontend\ContentProviders\DefaultContentProvider; |
51 | use MobileFrontend\Features\FeaturesManager; |
52 | use MobileFrontend\Hooks\HookRunner; |
53 | use MobileFrontend\Models\MobilePage; |
54 | use MobileFrontend\Transforms\LazyImageTransform; |
55 | use MobileFrontend\Transforms\MakeSectionsTransform; |
56 | |
57 | /** |
58 | * Hook handlers for MobileFrontend extension |
59 | * |
60 | * If your hook changes the behaviour of the Minerva skin, you are in the wrong place. |
61 | * Any changes relating to Minerva should go into Minerva.hooks.php |
62 | */ |
63 | class MobileFrontendHooks implements |
64 | APIQuerySiteInfoGeneralInfoHook, |
65 | AuthChangeFormFieldsHook, |
66 | RequestContextCreateSkinHook, |
67 | BeforeDisplayNoArticleTextHook, |
68 | OutputPageBeforeHTMLHook, |
69 | OutputPageBodyAttributesHook, |
70 | ResourceLoaderSiteStylesModulePagesHook, |
71 | ResourceLoaderSiteModulePagesHook, |
72 | SkinAfterBottomScriptsHook, |
73 | SkinAddFooterLinksHook, |
74 | BeforePageRedirectHook, |
75 | DifferenceEngineViewHeaderHook, |
76 | GetCacheVaryCookiesHook, |
77 | SpecialPage_initListHook, |
78 | ListDefinedTagsHook, |
79 | ChangeTagsListActiveHook, |
80 | RecentChange_saveHook, |
81 | SpecialPageBeforeExecuteHook, |
82 | PostLoginRedirectHook, |
83 | BeforePageDisplayHook, |
84 | GetPreferencesHook, |
85 | OutputPageParserOutputHook, |
86 | HTMLFileCache__useFileCacheHook, |
87 | LoginFormValidErrorMessagesHook, |
88 | AfterBuildFeedLinksHook, |
89 | MakeGlobalVariablesScriptHook, |
90 | TitleSquidURLsHook, |
91 | UserGetDefaultOptionsHook, |
92 | ManualLogEntryBeforePublishHook |
93 | { |
94 | private const MOBILE_PREFERENCES_SECTION = 'rendering/mobile'; |
95 | public const MOBILE_PREFERENCES_SPECIAL_PAGES = 'mobile-specialpages'; |
96 | public const MOBILE_PREFERENCES_EDITOR = 'mobile-editor'; |
97 | public const MOBILE_PREFERENCES_FONTSIZE = 'mf-font-size'; |
98 | public const MOBILE_PREFERENCES_EXPAND_SECTIONS = 'mf-expand-sections'; |
99 | private const ENABLE_SPECIAL_PAGE_OPTIMISATIONS = '1'; |
100 | // This should always be kept in sync with `@width-breakpoint-tablet` |
101 | // in mediawiki.skin.variables.less |
102 | private const DEVICE_WIDTH_TABLET = '720px'; |
103 | |
104 | /** |
105 | * Obtain the default mobile skin |
106 | * |
107 | * @param Config $config |
108 | * @throws SkinException If a factory function isn't registered for the skin name |
109 | * @return Skin |
110 | */ |
111 | protected static function getDefaultMobileSkin( Config $config ): Skin { |
112 | $defaultSkin = $config->get( 'DefaultMobileSkin' ); |
113 | |
114 | if ( !$defaultSkin ) { |
115 | $defaultSkin = $config->get( 'DefaultSkin' ); |
116 | } |
117 | |
118 | $factory = MediaWikiServices::getInstance()->getSkinFactory(); |
119 | return $factory->makeSkin( Skin::normalizeKey( $defaultSkin ) ); |
120 | } |
121 | |
122 | /** |
123 | * RequestContextCreateSkin hook handler |
124 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/RequestContextCreateSkin |
125 | * |
126 | * @param IContextSource $context The RequestContext object the skin is being created for. |
127 | * @param Skin|null|string &$skin A variable reference you may set a Skin instance or string |
128 | * key on to override the skin that will be used for the context. |
129 | * @return bool |
130 | */ |
131 | public function onRequestContextCreateSkin( $context, &$skin ) { |
132 | $services = MediaWikiServices::getInstance(); |
133 | |
134 | /** @var MobileContext $mobileContext */ |
135 | $mobileContext = $services->getService( 'MobileFrontend.Context' ); |
136 | $config = $services->getService( 'MobileFrontend.Config' ); |
137 | |
138 | $mobileContext->doToggling(); |
139 | if ( !$mobileContext->shouldDisplayMobileView() ) { |
140 | return true; |
141 | } |
142 | |
143 | // Handle any X-Analytics header values in the request by adding them |
144 | // as log items. X-Analytics header values are serialized key=value |
145 | // pairs, separated by ';', used for analytics purposes. |
146 | $xanalytics = $mobileContext->getRequest()->getHeader( 'X-Analytics' ); |
147 | if ( $xanalytics ) { |
148 | $xanalytics_arr = explode( ';', $xanalytics ); |
149 | if ( count( $xanalytics_arr ) > 1 ) { |
150 | foreach ( $xanalytics_arr as $xanalytics_item ) { |
151 | $mobileContext->addAnalyticsLogItemFromXAnalytics( $xanalytics_item ); |
152 | } |
153 | } else { |
154 | $mobileContext->addAnalyticsLogItemFromXAnalytics( $xanalytics ); |
155 | } |
156 | } |
157 | |
158 | // log whether user is using beta/stable |
159 | $mobileContext->logMobileMode(); |
160 | |
161 | // Allow overriding of skin by useskin e.g. useskin=vector&useformat=mobile or by |
162 | // setting the mobileskin preferences (api only currently) |
163 | $userOption = $services->getUserOptionsLookup()->getOption( |
164 | $context->getUser(), 'mobileskin' |
165 | ); |
166 | $userSkin = $context->getRequest()->getVal( 'useskin', $userOption ); |
167 | if ( $userSkin && Skin::normalizeKey( $userSkin ) === $userSkin ) { |
168 | $skin = $services->getSkinFactory()->makeSkin( $userSkin ); |
169 | } else { |
170 | $skin = self::getDefaultMobileSkin( $config ); |
171 | } |
172 | |
173 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
174 | $hookRunner->onRequestContextCreateSkinMobile( $mobileContext, $skin ); |
175 | |
176 | return false; |
177 | } |
178 | |
179 | /** |
180 | * Update the footer |
181 | * @param Skin $skin |
182 | * @param string $key the current key for the current group (row) of footer links. |
183 | * e.g. `info` or `places`. |
184 | * @param array &$footerLinks an empty array that can be populated with new links. |
185 | * keys should be strings and will be used for generating the ID of the footer item |
186 | * and value should be an HTML string. |
187 | */ |
188 | public function onSkinAddFooterLinks( Skin $skin, string $key, array &$footerLinks ) { |
189 | /** @var MobileContext $context */ |
190 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
191 | |
192 | if ( $key === 'places' ) { |
193 | if ( $context->shouldDisplayMobileView() ) { |
194 | $terms = MobileFrontendSkinHooks::getTermsLink( $skin ); |
195 | if ( $terms ) { |
196 | $footerLinks['terms-use'] = $terms; |
197 | } |
198 | $footerLinks['desktop-toggle'] = MobileFrontendSkinHooks::getDesktopViewLink( $skin, $context ); |
199 | } else { |
200 | // If desktop site append a mobile view link |
201 | $footerLinks['mobileview'] = |
202 | MobileFrontendSkinHooks::getMobileViewLink( $skin, $context ); |
203 | } |
204 | } |
205 | } |
206 | |
207 | /** |
208 | * SkinAfterBottomScripts hook handler |
209 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinAfterBottomScripts |
210 | * |
211 | * Adds an inline script for lazy loading the images in Grade C browsers. |
212 | * |
213 | * @param Skin $skin |
214 | * @param string &$html bottomScripts text. Append to $text to add additional |
215 | * text/scripts after the stock bottom scripts. |
216 | */ |
217 | public function onSkinAfterBottomScripts( $skin, &$html ) { |
218 | $services = MediaWikiServices::getInstance(); |
219 | /** @var MobileContext $context */ |
220 | $context = $services->getService( 'MobileFrontend.Context' ); |
221 | /** @var FeaturesManager $featureManager */ |
222 | $featureManager = $services->getService( 'MobileFrontend.FeaturesManager' ); |
223 | |
224 | // TODO: We may want to enable the following script on Desktop Minerva... |
225 | // ... when Minerva is widely used. |
226 | if ( $context->shouldDisplayMobileView() && |
227 | $featureManager->isFeatureAvailableForCurrentUser( 'MFLazyLoadImages' ) |
228 | ) { |
229 | $html .= Html::inlineScript( ResourceLoader::filter( 'minify-js', |
230 | LazyImageTransform::gradeCImageSupport() |
231 | ) ); |
232 | } |
233 | } |
234 | |
235 | /** |
236 | * BeforeDisplayNoArticleText hook handler |
237 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText |
238 | * |
239 | * @param Article $article The (empty) article |
240 | * @return bool This hook can abort |
241 | */ |
242 | public function onBeforeDisplayNoArticleText( $article ) { |
243 | /** @var MobileContext $context */ |
244 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
245 | $displayMobileView = $context->shouldDisplayMobileView(); |
246 | |
247 | $title = $article->getTitle(); |
248 | |
249 | // if the page is a userpage |
250 | // @todo: Upstream to core (T248347). |
251 | if ( $displayMobileView && |
252 | $title->inNamespaces( NS_USER ) && |
253 | !$title->isSubpage() |
254 | ) { |
255 | $out = $article->getContext()->getOutput(); |
256 | $userpagetext = ExtMobileFrontend::blankUserPageHTML( $out, $title ); |
257 | if ( $userpagetext ) { |
258 | // Replace the default message with ours |
259 | $out->addHTML( $userpagetext ); |
260 | return false; |
261 | } |
262 | } |
263 | |
264 | return true; |
265 | } |
266 | |
267 | /** |
268 | * OutputPageBeforeHTML hook handler |
269 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBeforeHTML |
270 | * |
271 | * Applies MobileFormatter to mobile viewed content |
272 | * |
273 | * @param OutputPage $out the OutputPage object to which wikitext is added |
274 | * @param string &$text the HTML to be wrapped inside the #mw-content-text element |
275 | */ |
276 | public function onOutputPageBeforeHTML( $out, &$text ) { |
277 | // This hook can be executed more than once per page view if the page content is composed from |
278 | // multiple sources! Anything that doesn't depend on $text should use onBeforePageDisplay. |
279 | |
280 | $services = MediaWikiServices::getInstance(); |
281 | /** @var MobileContext $context */ |
282 | $context = $services->getService( 'MobileFrontend.Context' ); |
283 | $title = $out->getTitle(); |
284 | $config = $services->getService( 'MobileFrontend.Config' ); |
285 | $displayMobileView = $context->shouldDisplayMobileView(); |
286 | |
287 | if ( !$title ) { |
288 | return; |
289 | } |
290 | |
291 | $options = $config->get( 'MFMobileFormatterOptions' ); |
292 | $excludeNamespaces = $options['excludeNamespaces'] ?? []; |
293 | // Perform a few extra changes if we are in mobile mode |
294 | $namespaceAllowed = !$title->inNamespaces( $excludeNamespaces ); |
295 | |
296 | $provider = new DefaultContentProvider( $text ); |
297 | $originalProviderClass = DefaultContentProvider::class; |
298 | ( new HookRunner( $services->getHookContainer() ) )->onMobileFrontendContentProvider( |
299 | $provider, $out |
300 | ); |
301 | |
302 | $isParse = ApiParseExtender::isParseAction( |
303 | $context->getRequest()->getText( 'action' ) |
304 | ); |
305 | |
306 | if ( get_class( $provider ) === $originalProviderClass ) { |
307 | // This line is important to avoid the default content provider running unnecessarily |
308 | // on desktop views. |
309 | $useContentProvider = $displayMobileView; |
310 | $runMobileFormatter = $displayMobileView && ( |
311 | // T245160 - don't run the mobile formatter on old revisions. |
312 | // Note if not the default content provider we ignore this requirement. |
313 | $title->getLatestRevID() > 0 || |
314 | // Always allow the formatter in ApiParse |
315 | $isParse |
316 | ); |
317 | } else { |
318 | // When a custom content provider is enabled, always use it. |
319 | $useContentProvider = true; |
320 | $runMobileFormatter = $displayMobileView; |
321 | } |
322 | |
323 | if ( $namespaceAllowed && $useContentProvider ) { |
324 | $text = ExtMobileFrontend::domParseWithContentProvider( |
325 | $provider, $out, $runMobileFormatter |
326 | ); |
327 | } |
328 | } |
329 | |
330 | /** |
331 | * Modifies the `<body>` element's attributes. |
332 | * |
333 | * By default, the `class` attribute is set to the output's "bodyClassName" |
334 | * property. |
335 | * |
336 | * @param OutputPage $out |
337 | * @param Skin $skin |
338 | * @param string[] &$bodyAttrs |
339 | */ |
340 | public function onOutputPageBodyAttributes( $out, $skin, &$bodyAttrs ): void { |
341 | /** @var \MobileFrontend\Amc\UserMode $userMode */ |
342 | $userMode = MediaWikiServices::getInstance()->getService( 'MobileFrontend.AMC.UserMode' ); |
343 | /** @var MobileContext $context */ |
344 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
345 | $isMobile = $context->shouldDisplayMobileView(); |
346 | |
347 | // FIXME: This can be removed when existing references have been updated. |
348 | if ( $isMobile && !$userMode->isEnabled() ) { |
349 | $bodyAttrs['class'] .= ' mw-mf-amc-disabled'; |
350 | } |
351 | |
352 | if ( $isMobile ) { |
353 | // Add a class to the body so that TemplateStyles (which can only |
354 | // access html and body) and gadgets have something to check for. |
355 | // @stable added in 1.38 |
356 | $bodyAttrs['class'] .= ' mw-mf'; |
357 | } |
358 | } |
359 | |
360 | /** |
361 | * BeforePageRedirect hook handler |
362 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageRedirect |
363 | * |
364 | * Ensures URLs are handled properly for select special pages. |
365 | * @param OutputPage $out |
366 | * @param string &$redirect URL string, modifiable |
367 | * @param string &$code HTTP code (eg '301' or '302'), modifiable |
368 | */ |
369 | public function onBeforePageRedirect( $out, &$redirect, &$code ) { |
370 | /** @var MobileContext $context */ |
371 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
372 | $shouldDisplayMobileView = $context->shouldDisplayMobileView(); |
373 | if ( !$shouldDisplayMobileView ) { |
374 | return; |
375 | } |
376 | |
377 | // T45123: force mobile URLs only for local redirects |
378 | if ( $context->isLocalUrl( $redirect ) ) { |
379 | $out->addVaryHeader( 'X-Subdomain' ); |
380 | $redirect = $context->getMobileUrl( $redirect ); |
381 | } |
382 | } |
383 | |
384 | /** |
385 | * DifferenceEngineViewHeader hook handler |
386 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/DifferenceEngineViewHeader |
387 | * |
388 | * Redirect Diff page to mobile version if appropriate |
389 | * |
390 | * @param DifferenceEngine $diff DifferenceEngine object that's calling |
391 | */ |
392 | public function onDifferenceEngineViewHeader( $diff ) { |
393 | $services = MediaWikiServices::getInstance(); |
394 | /** @var MobileContext $context */ |
395 | $context = $services->getService( 'MobileFrontend.Context' ); |
396 | if ( !$context->shouldDisplayMobileView() ) { |
397 | // this code should only apply to mobile view. |
398 | return; |
399 | } |
400 | /** @var FeaturesManager $featureManager */ |
401 | $featureManager = $services->getService( 'MobileFrontend.FeaturesManager' ); |
402 | |
403 | $oldRevRecord = $diff->getOldRevision(); |
404 | $newRevRecord = $diff->getNewRevision(); |
405 | |
406 | $otherParams = $diff->getContext()->getRequest()->getValues(); |
407 | $title = $context->getTitle(); |
408 | $output = $context->getOutput(); |
409 | // On diff pages on mobile if not specified default to diff only mode. |
410 | if ( !isset( $otherParams['diffonly'] ) ) { |
411 | $otherParams['diffonly'] = '1'; |
412 | $output->redirect( $title->getFullUrl( $otherParams ) ); |
413 | } |
414 | } |
415 | |
416 | /** |
417 | * ResourceLoaderSiteStylesModulePages hook handler |
418 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderSiteStylesModulePages |
419 | * |
420 | * @param string $skin |
421 | * @param array &$pages to sort modules from. |
422 | */ |
423 | public function onResourceLoaderSiteStylesModulePages( $skin, array &$pages ): void { |
424 | $ctx = MobileContext::singleton(); |
425 | $services = MediaWikiServices::getInstance(); |
426 | $config = $services->getService( 'MobileFrontend.Config' ); |
427 | // Use Mobile.css instead of MediaWiki:Common.css on mobile views. |
428 | if ( $ctx->shouldDisplayMobileView() && $config->get( 'MFCustomSiteModules' ) ) { |
429 | unset( $pages['MediaWiki:Common.css'] ); |
430 | unset( $pages['MediaWiki:Print.css'] ); |
431 | if ( $config->get( 'MFSiteStylesRenderBlocking' ) ) { |
432 | $pages['MediaWiki:Mobile.css'] = [ 'type' => 'style' ]; |
433 | } |
434 | } |
435 | } |
436 | |
437 | /** |
438 | * ResourceLoaderSiteModulePages hook handler |
439 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderSiteModulePages |
440 | * |
441 | * @param string $skin |
442 | * @param array &$pages to sort modules from. |
443 | */ |
444 | public function onResourceLoaderSiteModulePages( $skin, array &$pages ): void { |
445 | $ctx = MobileContext::singleton(); |
446 | $services = MediaWikiServices::getInstance(); |
447 | $config = $services->getService( 'MobileFrontend.Config' ); |
448 | // Use Mobile.js instead of MediaWiki:Common.js and MediaWiki:<skinname.js> on mobile views. |
449 | if ( $ctx->shouldDisplayMobileView() && $config->get( 'MFCustomSiteModules' ) ) { |
450 | unset( $pages['MediaWiki:Common.js'] ); |
451 | $pages['MediaWiki:Mobile.js'] = [ 'type' => 'script' ]; |
452 | if ( !$config->get( 'MFSiteStylesRenderBlocking' ) ) { |
453 | $pages['MediaWiki:Mobile.css'] = [ 'type' => 'style' ]; |
454 | } |
455 | } |
456 | } |
457 | |
458 | /** |
459 | * GetCacheVaryCookies hook handler |
460 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetCacheVaryCookies |
461 | * |
462 | * @param OutputPage $out |
463 | * @param array &$cookies array of cookies name, add a value to it |
464 | * if you want to add a cookie that have to vary cache options |
465 | */ |
466 | public function onGetCacheVaryCookies( $out, &$cookies ) { |
467 | /** @var MobileContext $context */ |
468 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
469 | $hasMobileUrl = $context->hasMobileDomain(); |
470 | |
471 | // Enables mobile cookies on wikis w/o mobile domain |
472 | $cookies[] = MobileContext::USEFORMAT_COOKIE_NAME; |
473 | // Don't redirect to mobile if user had explicitly opted out of it |
474 | $cookies[] = MobileContext::STOP_MOBILE_REDIRECT_COOKIE_NAME; |
475 | |
476 | if ( $context->shouldDisplayMobileView() || !$hasMobileUrl ) { |
477 | // beta cookie |
478 | $cookies[] = MobileContext::OPTIN_COOKIE_NAME; |
479 | } |
480 | } |
481 | |
482 | /** |
483 | * Generate config for usage inside MobileFrontend |
484 | * This should be used for variables which: |
485 | * - vary with the html |
486 | * - variables that should work cross skin including anonymous users |
487 | * - used for both, stable and beta mode (don't use |
488 | * MobileContext::isBetaGroupMember in this function - T127860) |
489 | * |
490 | * @return array |
491 | */ |
492 | public static function getResourceLoaderMFConfigVars() { |
493 | $vars = []; |
494 | $config = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Config' ); |
495 | $mfScriptPath = $config->get( 'MFScriptPath' ); |
496 | $pageProps = $config->get( 'MFQueryPropModules' ); |
497 | $searchParams = $config->get( 'MFSearchAPIParams' ); |
498 | // Avoid API warnings and allow integration with optional extensions. |
499 | if ( $mfScriptPath || ExtensionRegistry::getInstance()->isLoaded( 'PageImages' ) ) { |
500 | $pageProps[] = 'pageimages'; |
501 | $searchParams = array_merge_recursive( $searchParams, [ |
502 | 'piprop' => 'thumbnail', |
503 | 'pithumbsize' => MobilePage::SMALL_IMAGE_WIDTH, |
504 | 'pilimit' => 50, |
505 | ] ); |
506 | } |
507 | |
508 | // Get the licensing agreement that is displayed in the uploading interface. |
509 | $vars += [ |
510 | // Page.js |
511 | 'wgMFMobileFormatterHeadings' => $config->get( 'MFMobileFormatterOptions' )['headings'], |
512 | // extendSearchParams |
513 | 'wgMFSearchAPIParams' => $searchParams, |
514 | 'wgMFQueryPropModules' => $pageProps, |
515 | // SearchGateway.js |
516 | 'wgMFSearchGenerator' => $config->get( 'MFSearchGenerator' ), |
517 | // PhotoListGateway.js, SearchGateway.js |
518 | 'wgMFThumbnailSizes' => [ |
519 | 'tiny' => MobilePage::TINY_IMAGE_WIDTH, |
520 | 'small' => MobilePage::SMALL_IMAGE_WIDTH, |
521 | ], |
522 | 'wgMFEnableJSConsoleRecruitment' => $config->get( 'MFEnableJSConsoleRecruitment' ), |
523 | // Browser.js |
524 | 'wgMFDeviceWidthTablet' => self::DEVICE_WIDTH_TABLET, |
525 | // toggle.js |
526 | 'wgMFCollapseSectionsByDefault' => $config->get( 'MFCollapseSectionsByDefault' ), |
527 | // extendSearchParams.js |
528 | 'wgMFTrackBlockNotices' => $config->get( 'MFTrackBlockNotices' ), |
529 | ]; |
530 | return $vars; |
531 | } |
532 | |
533 | /** |
534 | * @param MobileContext $context |
535 | * @param Config $config |
536 | * @return array |
537 | */ |
538 | private static function getWikibaseStaticConfigVars( |
539 | MobileContext $context, Config $config |
540 | ) { |
541 | $features = array_keys( $config->get( 'MFDisplayWikibaseDescriptions' ) ); |
542 | $result = [ 'wgMFDisplayWikibaseDescriptions' => [] ]; |
543 | /** @var FeaturesManager $featureManager */ |
544 | $featureManager = MediaWikiServices::getInstance() |
545 | ->getService( 'MobileFrontend.FeaturesManager' ); |
546 | |
547 | $descriptionsEnabled = $featureManager->isFeatureAvailableForCurrentUser( |
548 | 'MFEnableWikidataDescriptions' |
549 | ); |
550 | |
551 | foreach ( $features as $feature ) { |
552 | $result['wgMFDisplayWikibaseDescriptions'][$feature] = $descriptionsEnabled && |
553 | $context->shouldShowWikibaseDescriptions( $feature, $config ); |
554 | } |
555 | |
556 | return $result; |
557 | } |
558 | |
559 | /** |
560 | * Should special pages be replaced with mobile formatted equivalents? |
561 | * |
562 | * @param User $user for which we need to make the decision based on user prefs |
563 | * @return bool whether special pages should be substituted with |
564 | * mobile friendly equivalents |
565 | */ |
566 | public static function shouldMobileFormatSpecialPages( $user ) { |
567 | $services = MediaWikiServices::getInstance(); |
568 | $config = $services->getService( 'MobileFrontend.Config' ); |
569 | $enabled = $config->get( 'MFEnableMobilePreferences' ); |
570 | |
571 | if ( !$enabled ) { |
572 | return true; |
573 | } |
574 | if ( !$user->isSafeToLoad() ) { |
575 | // if not isSafeToLoad |
576 | // assume an anonymous session |
577 | // (see I2a6ef640d328106c88331da7c53785486e16a353) |
578 | return true; |
579 | } |
580 | |
581 | $userOption = $services->getUserOptionsLookup()->getOption( |
582 | $user, |
583 | self::MOBILE_PREFERENCES_SPECIAL_PAGES, |
584 | self::ENABLE_SPECIAL_PAGE_OPTIMISATIONS |
585 | ); |
586 | |
587 | return $userOption === self::ENABLE_SPECIAL_PAGE_OPTIMISATIONS; |
588 | } |
589 | |
590 | /** |
591 | * Hook for SpecialPage_initList in SpecialPageFactory. |
592 | * |
593 | * @param array &$list list of special page classes |
594 | */ |
595 | public function onSpecialPage_initList( &$list ) { |
596 | $services = MediaWikiServices::getInstance(); |
597 | /** @var MobileContext $context */ |
598 | $context = $services->getService( 'MobileFrontend.Context' ); |
599 | $user = $context->getUser(); |
600 | /** @var FeaturesManager $featureManager */ |
601 | $featureManager = $services->getService( 'MobileFrontend.FeaturesManager' ); |
602 | |
603 | // Perform substitutions of pages that are unsuitable for mobile |
604 | // FIXME: Upstream these changes to core. |
605 | if ( $context->shouldDisplayMobileView() && |
606 | self::shouldMobileFormatSpecialPages( $user ) |
607 | ) { |
608 | |
609 | if ( $user->isSafeToLoad() && |
610 | !$featureManager->isFeatureAvailableForCurrentUser( 'MFUseDesktopSpecialWatchlistPage' ) |
611 | ) { |
612 | // Replace the standard watchlist view with our custom one |
613 | $list['Watchlist'] = [ |
614 | 'class' => SpecialMobileWatchlist::class, |
615 | 'services' => [ |
616 | 'ConnectionProvider', |
617 | ], |
618 | ]; |
619 | $list['EditWatchlist'] = SpecialMobileEditWatchlist::class; |
620 | } |
621 | } |
622 | } |
623 | |
624 | /** |
625 | * ListDefinedTags hook handler |
626 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/ListDefinedTags |
627 | * |
628 | * @param array &$tags The list of tags. Add your extension's tags to this array. |
629 | */ |
630 | public function onListDefinedTags( &$tags ) { |
631 | $this->addDefinedTags( $tags ); |
632 | } |
633 | |
634 | /** |
635 | * ChangeTagsListActive hook handler |
636 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsListActive |
637 | * |
638 | * @param array &$tags The list of tags. Add your extension's tags to this array. |
639 | */ |
640 | public function onChangeTagsListActive( &$tags ) { |
641 | $this->addDefinedTags( $tags ); |
642 | } |
643 | |
644 | /** |
645 | * @param array &$tags |
646 | */ |
647 | public function addDefinedTags( &$tags ) { |
648 | $tags[] = 'mobile edit'; |
649 | $tags[] = 'mobile web edit'; |
650 | } |
651 | |
652 | /** |
653 | * RecentChange_save hook handler that tags mobile changes |
654 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/RecentChange_save |
655 | * |
656 | * @param RecentChange $recentChange |
657 | */ |
658 | public function onRecentChange_save( $recentChange ) { |
659 | self::onTaggableObjectCreation( $recentChange ); |
660 | } |
661 | |
662 | /** |
663 | * ManualLogEntryBeforePublish hook handler that tags actions logged when user uses mobile mode |
664 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/ManualLogEntryBeforePublish |
665 | * |
666 | * @param ManualLogEntry $logEntry |
667 | */ |
668 | public function onManualLogEntryBeforePublish( $logEntry ): void { |
669 | self::onTaggableObjectCreation( $logEntry ); |
670 | } |
671 | |
672 | /** |
673 | * @param Taggable $taggable Object to tag |
674 | */ |
675 | public static function onTaggableObjectCreation( Taggable $taggable ) { |
676 | /** @var MobileContext $context */ |
677 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
678 | $userAgent = $context->getRequest()->getHeader( "User-agent" ); |
679 | if ( $context->shouldDisplayMobileView() ) { |
680 | $taggable->addTags( [ 'mobile edit' ] ); |
681 | // Tag as mobile web edit specifically, if it isn't coming from the apps |
682 | if ( strpos( $userAgent, 'WikipediaApp/' ) !== 0 ) { |
683 | $taggable->addTags( [ 'mobile web edit' ] ); |
684 | } |
685 | } |
686 | } |
687 | |
688 | /** |
689 | * AbuseFilter-generateUserVars hook handler that adds a user_mobile variable. |
690 | * Altering the variables generated for a specific user |
691 | * |
692 | * @see hooks.txt in AbuseFilter extension |
693 | * @param VariableHolder $vars object to add vars to |
694 | * @param User $user |
695 | * @param RecentChange|null $rc If the variables should be generated for an RC entry, this |
696 | * is the entry. Null if it's for the current action being filtered. |
697 | */ |
698 | public static function onAbuseFilterGenerateUserVars( $vars, $user, RecentChange $rc = null ) { |
699 | if ( !$rc ) { |
700 | /** @var MobileContext $context */ |
701 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
702 | $vars->setVar( 'user_mobile', $context->shouldDisplayMobileView() ); |
703 | } else { |
704 | $dbr = MediaWikiServices::getInstance() |
705 | ->getConnectionProvider() |
706 | ->getReplicaDatabase(); |
707 | |
708 | $tags = ChangeTags::getTags( $dbr, $rc->getAttribute( 'rc_id' ) ); |
709 | $val = (bool)array_intersect( $tags, [ 'mobile edit', 'mobile web edit' ] ); |
710 | $vars->setVar( 'user_mobile', $val ); |
711 | } |
712 | } |
713 | |
714 | /** |
715 | * AbuseFilter-builder hook handler that adds user_mobile variable to list |
716 | * of valid vars |
717 | * |
718 | * @param array &$builder Array in AbuseFilter::getBuilderValues to add to. |
719 | */ |
720 | public static function onAbuseFilterBuilder( &$builder ) { |
721 | $builder['vars']['user_mobile'] = 'user-mobile'; |
722 | } |
723 | |
724 | /** |
725 | * Invocation of hook SpecialPageBeforeExecute |
726 | * |
727 | * We use this hook to ensure that login/account creation pages |
728 | * are redirected to HTTPS if they are not accessed via HTTPS and |
729 | * $wgSecureLogin == true - but only when using the |
730 | * mobile site. |
731 | * |
732 | * @param SpecialPage $special |
733 | * @param string $subpage subpage name |
734 | */ |
735 | public function onSpecialPageBeforeExecute( $special, $subpage ) { |
736 | $services = MediaWikiServices::getInstance(); |
737 | /** @var MobileContext $context */ |
738 | $context = $services->getService( 'MobileFrontend.Context' ); |
739 | |
740 | $isMobileView = $context->shouldDisplayMobileView(); |
741 | $taglines = $context->getConfig()->get( 'MFSpecialPageTaglines' ); |
742 | $name = $special->getName(); |
743 | |
744 | if ( $isMobileView ) { |
745 | $out = $special->getOutput(); |
746 | // FIXME: mobile.special.styles should be replaced with mediawiki.special module |
747 | $out->addModuleStyles( |
748 | [ 'mobile.special.styles' ] |
749 | ); |
750 | // FIXME: Should be moved to MediaWiki core module. |
751 | if ( $name === 'Userlogin' || $name === 'CreateAccount' ) { |
752 | $out->addModules( 'mobile.special.userlogin.scripts' ); |
753 | } |
754 | if ( array_key_exists( $name, $taglines ) ) { |
755 | self::setTagline( $out, $out->msg( $taglines[$name] )->parse() ); |
756 | } |
757 | } |
758 | } |
759 | |
760 | /** |
761 | * PostLoginRedirect hook handler |
762 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/PostLoginRedirect |
763 | * |
764 | * Used here to handle watchlist actions made by anons to be handled after |
765 | * login or account creation redirect. |
766 | * |
767 | * @inheritDoc |
768 | */ |
769 | public function onPostLoginRedirect( &$returnTo, &$returnToQuery, &$type ) { |
770 | $services = MediaWikiServices::getInstance(); |
771 | /** @var MobileContext $context */ |
772 | $context = $services->getService( 'MobileFrontend.Context' ); |
773 | if ( !$context->shouldDisplayMobileView() ) { |
774 | return; |
775 | } |
776 | |
777 | // If 'watch' is set from the login form, watch the requested article |
778 | $campaign = $context->getRequest()->getVal( 'campaign' ); |
779 | |
780 | // The user came from one of the drawers that prompted them to login. |
781 | // We must watch the article per their original intent. |
782 | if ( $campaign === 'mobile_watchPageActionCta' || |
783 | wfArrayToCgi( $returnToQuery ) === 'article_action=watch' |
784 | ) { |
785 | $title = Title::newFromText( $returnTo ); |
786 | // protect against watching special pages (these cannot be watched!) |
787 | if ( $title !== null && !$title->isSpecialPage() ) { |
788 | $services->getWatchlistManager()->addWatch( $context->getAuthority(), $title ); |
789 | } |
790 | } |
791 | } |
792 | |
793 | /** |
794 | * BeforePageDisplay hook handler |
795 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay |
796 | * |
797 | * @param OutputPage $out |
798 | * @param Skin $skin Skin object that will be used to generate the page, added in 1.13. |
799 | */ |
800 | public function onBeforePageDisplay( $out, $skin ): void { |
801 | $context = MobileContext::singleton(); |
802 | $services = MediaWikiServices::getInstance(); |
803 | $config = $services->getService( 'MobileFrontend.Config' ); |
804 | $mfEnableXAnalyticsLogging = $config->get( 'MFEnableXAnalyticsLogging' ); |
805 | $mfNoIndexPages = $config->get( 'MFNoindexPages' ); |
806 | $isCanonicalLinkHandledByCore = $config->get( 'EnableCanonicalServerLink' ); |
807 | $hasMobileUrl = $context->hasMobileDomain(); |
808 | $displayMobileView = $context->shouldDisplayMobileView(); |
809 | |
810 | $title = $skin->getTitle(); |
811 | |
812 | // an canonical/alternate link is only useful, if the mobile and desktop URL are different |
813 | // and $wgMFNoindexPages needs to be true |
814 | if ( $hasMobileUrl && $mfNoIndexPages ) { |
815 | $link = false; |
816 | |
817 | if ( !$displayMobileView ) { |
818 | // add alternate link to desktop sites - bug T91183 |
819 | $desktopUrl = $title->getFullURL(); |
820 | $link = [ |
821 | 'rel' => 'alternate', |
822 | 'media' => 'only screen and (max-width: ' . self::DEVICE_WIDTH_TABLET . ')', |
823 | 'href' => $context->getMobileUrl( $desktopUrl ), |
824 | ]; |
825 | } elseif ( !$isCanonicalLinkHandledByCore ) { |
826 | $link = [ |
827 | 'rel' => 'canonical', |
828 | 'href' => $title->getFullURL(), |
829 | ]; |
830 | } |
831 | |
832 | if ( $link ) { |
833 | $out->addLink( $link ); |
834 | } |
835 | } |
836 | |
837 | // set the vary header to User-Agent, if mobile frontend auto detects, if the mobile |
838 | // view should be delivered and the same url is used for desktop and mobile devices |
839 | // Bug: T123189 |
840 | if ( |
841 | $config->get( 'MFVaryOnUA' ) && |
842 | $config->get( 'MFAutodetectMobileView' ) && |
843 | !$hasMobileUrl |
844 | ) { |
845 | $out->addVaryHeader( 'User-Agent' ); |
846 | } |
847 | |
848 | // Set X-Analytics HTTP response header if necessary |
849 | if ( $displayMobileView ) { |
850 | $analyticsHeader = ( $mfEnableXAnalyticsLogging ? $context->getXAnalyticsHeader() : false ); |
851 | if ( $analyticsHeader ) { |
852 | $resp = $out->getRequest()->response(); |
853 | $resp->header( $analyticsHeader ); |
854 | } |
855 | |
856 | // in mobile view: always add vary header |
857 | $out->addVaryHeader( 'Cookie' ); |
858 | |
859 | if ( $config->get( 'MFEnableManifest' ) ) { |
860 | $out->addLink( |
861 | [ |
862 | 'rel' => 'manifest', |
863 | 'href' => wfAppendQuery( |
864 | wfScript( 'api' ), |
865 | [ 'action' => 'webapp-manifest' ] |
866 | ) |
867 | ] |
868 | ); |
869 | } |
870 | |
871 | // In mobile mode, MediaWiki:Common.css/MediaWiki:Common.js is not loaded. |
872 | // We load MediaWiki:Mobile.css/js instead |
873 | // We load mobile.init so that lazy loading images works on all skins |
874 | $out->addModules( [ 'mobile.init' ] ); |
875 | $out->addModuleStyles( [ 'mobile.init.styles' ] ); |
876 | |
877 | $fontSize = $services->getUserOptionsLookup()->getOption( |
878 | $context->getUser(), self::MOBILE_PREFERENCES_FONTSIZE |
879 | ) ?? 'small'; |
880 | $expandSections = $services->getUserOptionsLookup()->getOption( |
881 | $context->getUser(), self::MOBILE_PREFERENCES_EXPAND_SECTIONS |
882 | ) ?? '0'; |
883 | |
884 | /** @var \MobileFrontend\Amc\UserMode $userMode */ |
885 | $userMode = MediaWikiServices::getInstance()->getService( 'MobileFrontend.AMC.UserMode' ); |
886 | $amc = !$userMode->isEnabled() ? '0' : '1'; |
887 | $context->getOutput()->addHtmlClasses( [ |
888 | 'mf-expand-sections-clientpref-' . $expandSections, |
889 | 'mf-font-size-clientpref-' . $fontSize, |
890 | 'mw-mf-amc-clientpref-' . $amc |
891 | ] ); |
892 | |
893 | // Allow modifications in mobile only mode |
894 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
895 | $hookRunner->onBeforePageDisplayMobile( $out, $skin ); |
896 | } |
897 | |
898 | // T204691 |
899 | $theme = $config->get( 'MFManifestThemeColor' ); |
900 | if ( $theme && $displayMobileView ) { |
901 | $out->addMeta( 'theme-color', $theme ); |
902 | } |
903 | |
904 | if ( $displayMobileView ) { |
905 | // Adds inline script to allow opening of sections while JS is still loading |
906 | $out->prependHTML( MakeSectionsTransform::interimTogglingSupport() ); |
907 | } |
908 | } |
909 | |
910 | /** |
911 | * AfterBuildFeedLinks hook handler. Remove all feed links in mobile view. |
912 | * |
913 | * @param array &$tags Added feed links |
914 | */ |
915 | public function onAfterBuildFeedLinks( &$tags ) { |
916 | $services = MediaWikiServices::getInstance(); |
917 | /** @var MobileContext $context */ |
918 | $context = $services->getService( 'MobileFrontend.Context' ); |
919 | $config = $services->getService( 'MobileFrontend.Config' ); |
920 | if ( $context->shouldDisplayMobileView() && !$config->get( 'MFRSSFeedLink' ) ) { |
921 | $tags = []; |
922 | } |
923 | } |
924 | |
925 | /** |
926 | * Register default preferences for MobileFrontend |
927 | * |
928 | * @param array &$defaultUserOptions Reference to default options array |
929 | */ |
930 | public function onUserGetDefaultOptions( &$defaultUserOptions ) { |
931 | $config = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Config' ); |
932 | if ( $config->get( 'MFEnableMobilePreferences' ) ) { |
933 | $defaultUserOptions += [ |
934 | self::MOBILE_PREFERENCES_SPECIAL_PAGES => self::ENABLE_SPECIAL_PAGE_OPTIMISATIONS, |
935 | ]; |
936 | } |
937 | } |
938 | |
939 | /** |
940 | * GetPreferences hook handler |
941 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences |
942 | * |
943 | * @param User $user User whose preferences are being modified |
944 | * @param array &$preferences Preferences description array, to be fed to an HTMLForm object |
945 | */ |
946 | public function onGetPreferences( $user, &$preferences ) { |
947 | $config = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Config' ); |
948 | $definition = [ |
949 | 'type' => 'api', |
950 | 'default' => '', |
951 | ]; |
952 | $preferences[SpecialMobileWatchlist::FILTER_OPTION_NAME] = $definition; |
953 | $preferences[SpecialMobileWatchlist::VIEW_OPTION_NAME] = $definition; |
954 | $preferences[MobileContext::USER_MODE_PREFERENCE_NAME] = $definition; |
955 | $preferences[self::MOBILE_PREFERENCES_EDITOR] = $definition; |
956 | $preferences[self::MOBILE_PREFERENCES_FONTSIZE] = $definition; |
957 | $preferences[self::MOBILE_PREFERENCES_EXPAND_SECTIONS] = $definition; |
958 | |
959 | if ( $config->get( 'MFEnableMobilePreferences' ) ) { |
960 | $preferences[ self::MOBILE_PREFERENCES_SPECIAL_PAGES ] = [ |
961 | 'type' => 'check', |
962 | 'label-message' => 'mobile-frontend-special-pages-pref', |
963 | 'help-message' => 'mobile-frontend-special-pages-pref', |
964 | // The following messages are generated here: |
965 | // * prefs-mobile |
966 | 'section' => self::MOBILE_PREFERENCES_SECTION |
967 | ]; |
968 | } |
969 | } |
970 | |
971 | /** |
972 | * CentralAuthLoginRedirectData hook handler |
973 | * Saves mobile host so that the CentralAuth wiki could redirect back properly |
974 | * |
975 | * @see CentralAuthHooks::doCentralLoginRedirect in CentralAuth extension |
976 | * @param \MediaWiki\Extension\CentralAuth\User\CentralAuthUser $centralUser |
977 | * @param array &$data Redirect data |
978 | */ |
979 | public static function onCentralAuthLoginRedirectData( $centralUser, &$data ) { |
980 | /** @var MobileContext $context */ |
981 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
982 | $server = $context->getConfig()->get( 'Server' ); |
983 | if ( $context->shouldDisplayMobileView() ) { |
984 | $data['mobileServer'] = $context->getMobileUrl( $server ); |
985 | } |
986 | } |
987 | |
988 | /** |
989 | * CentralAuthSilentLoginRedirect hook handler |
990 | * Points redirects from CentralAuth wiki to mobile domain if user has logged in from it |
991 | * @see SpecialCentralLogin in CentralAuth extension |
992 | * @param \MediaWiki\Extension\CentralAuth\User\CentralAuthUser $centralUser |
993 | * @param string &$url to redirect to |
994 | * @param array $info token information |
995 | */ |
996 | public static function onCentralAuthSilentLoginRedirect( $centralUser, &$url, $info ) { |
997 | if ( isset( $info['mobileServer'] ) ) { |
998 | $mobileUrlParsed = wfParseUrl( $info['mobileServer'] ); |
999 | $urlParsed = wfParseUrl( $url ); |
1000 | $urlParsed['host'] = $mobileUrlParsed['host']; |
1001 | $url = wfAssembleUrl( $urlParsed ); |
1002 | } |
1003 | } |
1004 | |
1005 | /** |
1006 | * Sets a tagline for a given page that can be displayed by the skin. |
1007 | * |
1008 | * @param OutputPage $outputPage |
1009 | * @param string $desc |
1010 | */ |
1011 | private static function setTagline( OutputPage $outputPage, $desc ) { |
1012 | $outputPage->setProperty( 'wgMFDescription', $desc ); |
1013 | } |
1014 | |
1015 | /** |
1016 | * Finds the wikidata tagline associated with the page |
1017 | * |
1018 | * @param ParserOutput $po |
1019 | * @param callable $fallbackWikibaseDescriptionFunc A fallback to provide Wikibase description. |
1020 | * Function takes wikibase_item as a first and only argument |
1021 | * @return ?string the tagline as a string, or else null if none is found |
1022 | */ |
1023 | public static function findTagline( ParserOutput $po, $fallbackWikibaseDescriptionFunc ) { |
1024 | $desc = $po->getPageProperty( 'wikibase-shortdesc' ); |
1025 | $item = $po->getPageProperty( 'wikibase_item' ); |
1026 | if ( $desc === null && $item && $fallbackWikibaseDescriptionFunc ) { |
1027 | return $fallbackWikibaseDescriptionFunc( $item ); |
1028 | } |
1029 | return $desc; |
1030 | } |
1031 | |
1032 | /** |
1033 | * OutputPageParserOutput hook handler |
1034 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput |
1035 | * |
1036 | * @param OutputPage $outputPage the OutputPage object to which wikitext is added |
1037 | * @param ParserOutput $po |
1038 | */ |
1039 | public function onOutputPageParserOutput( $outputPage, $po ): void { |
1040 | $services = MediaWikiServices::getInstance(); |
1041 | /** @var MobileContext $context */ |
1042 | $context = $services->getService( 'MobileFrontend.Context' ); |
1043 | $config = $services->getService( 'MobileFrontend.Config' ); |
1044 | /** @var FeaturesManager $featureManager */ |
1045 | $featureManager = $services->getService( 'MobileFrontend.FeaturesManager' ); |
1046 | $title = $outputPage->getTitle(); |
1047 | $descriptionsEnabled = !$title->isMainPage() && |
1048 | $title->getNamespace() === NS_MAIN && |
1049 | $featureManager->isFeatureAvailableForCurrentUser( |
1050 | 'MFEnableWikidataDescriptions' |
1051 | ) && $context->shouldShowWikibaseDescriptions( 'tagline', $config ); |
1052 | |
1053 | // Only set the tagline if the feature has been enabled and the article is in the main namespace |
1054 | if ( $context->shouldDisplayMobileView() && $descriptionsEnabled ) { |
1055 | $desc = self::findTagline( $po, static function ( $item ) { |
1056 | return ExtMobileFrontend::getWikibaseDescription( $item ); |
1057 | } ); |
1058 | if ( $desc ) { |
1059 | self::setTagline( $outputPage, $desc ); |
1060 | } |
1061 | } |
1062 | } |
1063 | |
1064 | /** |
1065 | * HTMLFileCache::useFileCache hook handler |
1066 | * Disables file caching for mobile pageviews |
1067 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/HTMLFileCache::useFileCache |
1068 | * |
1069 | * @param IContextSource $context |
1070 | * @return bool |
1071 | */ |
1072 | public function onHTMLFileCache__useFileCache( $context ) { |
1073 | /** @var MobileContext $context */ |
1074 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
1075 | return !$context->shouldDisplayMobileView(); |
1076 | } |
1077 | |
1078 | /** |
1079 | * LoginFormValidErrorMessages hook handler to promote MF specific error message be valid. |
1080 | * |
1081 | * @param array &$messages Array of already added messages |
1082 | */ |
1083 | public function onLoginFormValidErrorMessages( array &$messages ) { |
1084 | $messages = array_merge( $messages, |
1085 | [ |
1086 | // watchstart sign up CTA |
1087 | 'mobile-frontend-watchlist-signup-action', |
1088 | // Watchlist and watchstar sign in CTA |
1089 | 'mobile-frontend-watchlist-purpose', |
1090 | // Edit button sign in CTA |
1091 | 'mobile-frontend-edit-login-action', |
1092 | // Edit button sign-up CTA |
1093 | 'mobile-frontend-edit-signup-action', |
1094 | 'mobile-frontend-donate-image-login-action', |
1095 | // default message |
1096 | 'mobile-frontend-generic-login-new', |
1097 | ] |
1098 | ); |
1099 | } |
1100 | |
1101 | /** |
1102 | * Handler for MakeGlobalVariablesScript hook. |
1103 | * For values that depend on the current page, user or request state. |
1104 | * |
1105 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/MakeGlobalVariablesScript |
1106 | * @param array &$vars Variables to be added into the output |
1107 | * @param OutputPage $out OutputPage instance calling the hook |
1108 | */ |
1109 | public function onMakeGlobalVariablesScript( &$vars, $out ): void { |
1110 | $services = MediaWikiServices::getInstance(); |
1111 | /** @var \MobileFrontend\Amc\UserMode $userMode */ |
1112 | $userMode = $services->getService( 'MobileFrontend.AMC.UserMode' ); |
1113 | /** @var FeaturesManager $featureManager */ |
1114 | $featureManager = $services->getService( 'MobileFrontend.FeaturesManager' ); |
1115 | $config = $services->getService( 'MobileFrontend.Config' ); |
1116 | |
1117 | // If the device is a mobile, Remove the category entry. |
1118 | /** @var MobileContext $context */ |
1119 | $context = $services->getService( 'MobileFrontend.Context' ); |
1120 | if ( $context->shouldDisplayMobileView() ) { |
1121 | /** @var \MobileFrontend\Amc\Outreach $outreach */ |
1122 | $outreach = $services->getService( 'MobileFrontend.AMC.Outreach' ); |
1123 | unset( $vars['wgCategories'] ); |
1124 | $vars['wgMFMode'] = $context->isBetaGroupMember() ? 'beta' : 'stable'; |
1125 | $vars['wgMFAmc'] = $userMode->isEnabled(); |
1126 | $vars['wgMFAmcOutreachActive'] = $outreach->isCampaignActive(); |
1127 | $vars['wgMFAmcOutreachUserEligible'] = $outreach->isUserEligible(); |
1128 | $vars['wgMFLazyLoadImages'] = |
1129 | $featureManager->isFeatureAvailableForCurrentUser( 'MFLazyLoadImages' ); |
1130 | $vars['wgMFEditNoticesFeatureConflict'] = self::hasEditNoticesFeatureConflict( |
1131 | $config, $context->getUser() |
1132 | ); |
1133 | } |
1134 | // Needed by mobile.startup and mobile.special.watchlist.scripts. |
1135 | // Needs to know if in beta mode or not and needs to load for Minerva desktop as well. |
1136 | // Ideally this would be inside ResourceLoaderFileModuleWithMFConfig but |
1137 | // sessions are not allowed there. |
1138 | $vars += self::getWikibaseStaticConfigVars( $context, $config ); |
1139 | } |
1140 | |
1141 | /** |
1142 | * Check if a conflicting edit notices gadget is enabled for the current user |
1143 | * |
1144 | * @param Config $config |
1145 | * @param User $user |
1146 | * @return bool |
1147 | */ |
1148 | public static function hasEditNoticesFeatureConflict( Config $config, User $user ) { |
1149 | $gadgetName = $config->get( 'MFEditNoticesConflictingGadgetName' ); |
1150 | if ( !$gadgetName ) { |
1151 | return false; |
1152 | } |
1153 | |
1154 | $extensionRegistry = ExtensionRegistry::getInstance(); |
1155 | if ( $extensionRegistry->isLoaded( 'Gadgets' ) ) { |
1156 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
1157 | $gadgetsRepo = GadgetRepo::singleton(); |
1158 | $match = array_search( $gadgetName, $gadgetsRepo->getGadgetIds(), true ); |
1159 | if ( $match !== false ) { |
1160 | try { |
1161 | return $gadgetsRepo->getGadget( $gadgetName ) |
1162 | ->isEnabled( $user ); |
1163 | } catch ( \InvalidArgumentException $e ) { |
1164 | return false; |
1165 | } |
1166 | } |
1167 | } |
1168 | return false; |
1169 | } |
1170 | |
1171 | /** |
1172 | * Handler for TitleSquidURLs hook to add copies of the cache purge |
1173 | * URLs which are transformed according to the wgMobileUrlTemplate, so |
1174 | * that both mobile and non-mobile URL variants get purged. |
1175 | * |
1176 | * @see * https://www.mediawiki.org/wiki/Manual:Hooks/TitleSquidURLs |
1177 | * @param Title $title the article title |
1178 | * @param array &$urls the set of URLs to purge |
1179 | */ |
1180 | public function onTitleSquidURLs( $title, &$urls ) { |
1181 | /** @var MobileContext $context */ |
1182 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
1183 | foreach ( $urls as $url ) { |
1184 | $newUrl = $context->getMobileUrl( $url ); |
1185 | if ( $newUrl !== false && $newUrl !== $url ) { |
1186 | $urls[] = $newUrl; |
1187 | } |
1188 | } |
1189 | } |
1190 | |
1191 | /** |
1192 | * Handler for the AuthChangeFormFields hook to add a logo on top of |
1193 | * the login screen. This is the AuthManager equivalent of changeUserLoginCreateForm. |
1194 | * @param AuthenticationRequest[] $requests AuthenticationRequest objects array |
1195 | * @param array $fieldInfo Field description as given by AuthenticationRequest::mergeFieldInfo |
1196 | * @param array &$formDescriptor A form descriptor suitable for the HTMLForm constructor |
1197 | * @param string $action One of the AuthManager::ACTION_* constants |
1198 | */ |
1199 | public function onAuthChangeFormFields( |
1200 | $requests, $fieldInfo, &$formDescriptor, $action |
1201 | ) { |
1202 | $services = MediaWikiServices::getInstance(); |
1203 | /** @var MobileContext $context */ |
1204 | $context = $services->getService( 'MobileFrontend.Context' ); |
1205 | $config = $services->getService( 'MobileFrontend.Config' ); |
1206 | $logos = RL\SkinModule::getAvailableLogos( $config ); |
1207 | $mfLogo = $logos['icon'] ?? false; |
1208 | |
1209 | // do nothing in desktop mode |
1210 | if ( |
1211 | $context->shouldDisplayMobileView() && $mfLogo |
1212 | && in_array( $action, [ AuthManager::ACTION_LOGIN, AuthManager::ACTION_CREATE ], true ) |
1213 | ) { |
1214 | $logoHtml = Html::rawElement( 'div', [ 'class' => 'mw-mf-watermark' ], |
1215 | Html::element( 'img', [ 'src' => $mfLogo, 'alt' => '' ] ) ); |
1216 | $formDescriptor = [ |
1217 | 'mfLogo' => [ |
1218 | 'type' => 'info', |
1219 | 'default' => $logoHtml, |
1220 | 'raw' => true, |
1221 | ], |
1222 | ] + $formDescriptor; |
1223 | } |
1224 | } |
1225 | |
1226 | /** |
1227 | * Add the base mobile site URL to the siteinfo API output. |
1228 | * @param ApiQuerySiteinfo $module |
1229 | * @param array &$result Api result array |
1230 | */ |
1231 | public function onAPIQuerySiteInfoGeneralInfo( $module, &$result ) { |
1232 | global $wgCanonicalServer; |
1233 | /** @var MobileContext $context */ |
1234 | $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
1235 | $result['mobileserver'] = $context->getMobileUrl( $wgCanonicalServer ); |
1236 | } |
1237 | } |