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