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