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