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