Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
27.37% covered (danger)
27.37%
101 / 369
8.70% covered (danger)
8.70%
4 / 46
CRAP
0.00% covered (danger)
0.00%
0 / 1
MobileFrontendHooks
27.37% covered (danger)
27.37%
101 / 369
8.70% covered (danger)
8.70%
4 / 46
8886.33
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 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 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 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 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 / 10
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    }
373
374    /**
375     * BeforePageRedirect hook handler
376     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageRedirect
377     *
378     * Ensures URLs are handled properly for select special pages.
379     * @param OutputPage $out
380     * @param string &$redirect URL string, modifiable
381     * @param string &$code HTTP code (eg '301' or '302'), modifiable
382     */
383    public function onBeforePageRedirect( $out, &$redirect, &$code ) {
384        $shouldDisplayMobileView = $this->mobileContext->shouldDisplayMobileView();
385        if ( !$shouldDisplayMobileView ) {
386            return;
387        }
388
389        // T45123: force mobile URLs only for local redirects
390        if ( $this->mobileContext->isLocalUrl( $redirect ) ) {
391            $redirect = $this->mobileContext->getMobileUrl( $redirect );
392        }
393    }
394
395    /**
396     * This hook is called early on api.php requests.
397     *
398     * @param ApiMain &$main
399     * @return void
400     */
401    public function onApiBeforeMain( &$main ) {
402        // T390929: api.php varies on MobileContext::shouldDisplayMobileView(),
403        // based on skin involvement (e.g. api.php?action=parse&useskin),
404        // and various extension hooks (e.g. URLs returned by query modules).
405        //
406        // NOTE: This OutputPage object is discarded and replaced for ApiHelp responses,
407        // such as the /w/api.php landing page. This is fine because those are personalised
408        // and uncachable (Cache-Control: private).
409        $mobileHeader = $this->config->get( 'MFMobileHeader' );
410        if ( $mobileHeader ) {
411            $main->getOutput()->addVaryHeader( $mobileHeader );
412        }
413    }
414
415    /**
416     * This hook is called early on load.php requests.
417     *
418     * @param RL\Context $context
419     * @param string[] &$extraHeaders
420     */
421    public function onResourceLoaderBeforeResponse( RL\Context $context, array &$extraHeaders ): void {
422        // T390929: load.php varies when $wgMFCustomSiteModules is enabled,
423        // through the onResourceLoaderSiteStylesModulePages and onResourceLoaderSiteModulePages
424        // hooks in this file.
425        global $wgMFMobileHeader, $wgMFCustomSiteModules;
426        if ( $wgMFMobileHeader
427            && $wgMFCustomSiteModules
428            && array_intersect( $context->getModules(), [ 'startup', 'site', 'site.styles' ] )
429        ) {
430            $extraHeaders[] = "Vary: $wgMFMobileHeader";
431        }
432    }
433
434    /**
435     * This hook is called early on index.php requests.
436     *
437     * @see https://www.mediawiki.org/wiki/Manual:Hooks/MediaWikiPerformActionHook
438     *
439     *
440     * @param OutputPage $output Context output
441     * @param Article $article Article on which the action will be performed
442     * @param Title $title Title on which the action will be performed
443     * @param User $user Context user
444     * @param WebRequest $request Context request
445     * @param ActionEntryPoint $entryPoint
446     * @return void
447     */
448    public function onMediaWikiPerformAction( $output, $article, $title, $user,
449        $request, $entryPoint
450    ) {
451        // T390929: index.php varies on MobileContext::shouldDisplayMobileView,
452        // especially in onRequestContextCreateSkin and onBeforePageRedirect.
453        $mobileHeader = $this->config->get( 'MFMobileHeader' );
454        if ( $mobileHeader ) {
455            $output->addVaryHeader( $mobileHeader );
456        }
457
458        // Set Diff page to diff-only mode for mobile view
459        // this code should only apply to mobile view.
460        if ( $this->mobileContext->shouldDisplayMobileView() ) {
461            // Default to diff-only mode on mobile diff pages if not specified.
462            if ( $request->getCheck( 'diff' ) && !$request->getCheck( 'diffonly' ) ) {
463                $request->setVal( 'diffonly', 'true' );
464            }
465        }
466    }
467
468    /**
469     * ResourceLoaderSiteStylesModulePages hook handler
470     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderSiteStylesModulePages
471     *
472     * @param string $skin
473     * @param array &$pages to sort modules from.
474     */
475    public function onResourceLoaderSiteStylesModulePages( $skin, array &$pages ): void {
476        $ctx = $this->mobileContext;
477        // Use Mobile.css instead of MediaWiki:Common.css on mobile views.
478        if ( $ctx->shouldDisplayMobileView() && $this->config->get( 'MFCustomSiteModules' ) ) {
479            unset( $pages['MediaWiki:Common.css'] );
480            unset( $pages['MediaWiki:Print.css'] );
481            if ( $this->config->get( 'MFSiteStylesRenderBlocking' ) ) {
482                $pages['MediaWiki:Mobile.css'] = [ 'type' => 'style' ];
483            }
484        }
485    }
486
487    /**
488     * ResourceLoaderSiteModulePages hook handler
489     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderSiteModulePages
490     *
491     * @param string $skin
492     * @param array &$pages to sort modules from.
493     */
494    public function onResourceLoaderSiteModulePages( $skin, array &$pages ): void {
495        $ctx = $this->mobileContext;
496        // Use Mobile.js instead of MediaWiki:Common.js and MediaWiki:<skinname.js> on mobile views.
497        if ( $ctx->shouldDisplayMobileView() && $this->config->get( 'MFCustomSiteModules' ) ) {
498            unset( $pages['MediaWiki:Common.js'] );
499            $pages['MediaWiki:Mobile.js'] = [ 'type' => 'script' ];
500            if ( !$this->config->get( 'MFSiteStylesRenderBlocking' ) ) {
501                $pages['MediaWiki:Mobile.css'] = [ 'type' => 'style' ];
502            }
503        }
504    }
505
506    /**
507     * GetCacheVaryCookies hook handler
508     * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetCacheVaryCookies
509     *
510     * @param OutputPage $out
511     * @param array &$cookies array of cookies name, add a value to it
512     *                        if you want to add a cookie that have to vary cache options
513     */
514    public function onGetCacheVaryCookies( $out, &$cookies ) {
515        // Enables mobile cookies on wikis w/o mobile domain
516        $cookies[] = MobileContext::USEFORMAT_COOKIE_NAME;
517        // Don't redirect to mobile if user had explicitly opted out of it
518        $cookies[] = MobileContext::STOP_MOBILE_REDIRECT_COOKIE_NAME;
519    }
520
521    /**
522     * Generate config for usage inside MobileFrontend
523     * This should be used for variables which:
524     *  - vary with the html
525     *  - should work cross skin including anonymous users.
526     *
527     * @return array
528     */
529    public static function getResourceLoaderMFConfigVars() {
530        $vars = [];
531        $config = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Config' );
532
533        // Get the licensing agreement that is displayed in the uploading interface.
534        $vars += [
535            'wgMFEnableJSConsoleRecruitment' => $config->get( 'MFEnableJSConsoleRecruitment' ),
536            // Browser.js
537            'wgMFDeviceWidthTablet' => self::DEVICE_WIDTH_TABLET,
538            // src/mobile.editor.overlay
539            'wgMFTrackBlockNotices' => $config->get( 'MFTrackBlockNotices' ),
540        ];
541        return $vars;
542    }
543
544    /**
545     * @param MobileContext $context
546     * @return array
547     */
548    private function getWikibaseStaticConfigVars(
549        MobileContext $context
550    ) {
551        $features = array_keys( $this->config->get( 'MFDisplayWikibaseDescriptions' ) );
552        $result = [ 'wgMFDisplayWikibaseDescriptions' => [] ];
553        $descriptionsEnabled = $this->featuresManager->isFeatureAvailableForCurrentUser(
554            'MFEnableWikidataDescriptions'
555        );
556
557        foreach ( $features as $feature ) {
558            $result['wgMFDisplayWikibaseDescriptions'][$feature] = $descriptionsEnabled &&
559                $context->shouldShowWikibaseDescriptions( $feature, $this->config );
560        }
561
562        return $result;
563    }
564
565    /**
566     * Should special pages be replaced with mobile formatted equivalents?
567     *
568     * @internal
569     * @param User $user for which we need to make the decision based on user prefs
570     * @return bool whether special pages should be substituted with
571     *   mobile friendly equivalents
572     */
573    public function shouldMobileFormatSpecialPages( $user ) {
574        $enabled = $this->config->get( 'MFEnableMobilePreferences' );
575
576        if ( !$enabled ) {
577            return true;
578        }
579        if ( !$user->isSafeToLoad() ) {
580            // if not isSafeToLoad
581            // assume an anonymous session
582            // (see I2a6ef640d328106c88331da7c53785486e16a353)
583            return true;
584        }
585
586        $userOption = $this->userOptionsLookup->getOption(
587            $user,
588            self::MOBILE_PREFERENCES_SPECIAL_PAGES,
589            self::ENABLE_SPECIAL_PAGE_OPTIMISATIONS
590        );
591
592        return $userOption === self::ENABLE_SPECIAL_PAGE_OPTIMISATIONS;
593    }
594
595    /**
596     * Hook for SpecialPage_initList in SpecialPageFactory.
597     *
598     * @param array &$list list of special page classes
599     */
600    public function onSpecialPage_initList( &$list ) {
601        $user = $this->mobileContext->getUser();
602
603        // Perform substitutions of pages that are unsuitable for mobile
604        // FIXME: Upstream these changes to core.
605        if (
606            $this->mobileContext->shouldDisplayMobileView() &&
607            $this->shouldMobileFormatSpecialPages( $user ) &&
608            $user->isSafeToLoad()
609        ) {
610            if (
611                !$this->featuresManager->isFeatureAvailableForCurrentUser( 'MFUseDesktopSpecialEditWatchlistPage' )
612            ) {
613                $list['EditWatchlist'] = [
614                    'class' => SpecialMobileEditWatchlist::class,
615                    'services' => [
616                        'HookContainer',
617                        'RepoGroup',
618                        'WatchedItemStore',
619                    ],
620                ];
621            }
622        }
623    }
624
625    /**
626     * ListDefinedTags hook handler
627     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ListDefinedTags
628     *
629     * @param array &$tags The list of tags. Add your extension's tags to this array.
630     */
631    public function onListDefinedTags( &$tags ) {
632        $this->addDefinedTags( $tags );
633    }
634
635    /**
636     * ChangeTagsListActive hook handler
637     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsListActive
638     *
639     * @param array &$tags The list of tags. Add your extension's tags to this array.
640     */
641    public function onChangeTagsListActive( &$tags ) {
642        $this->addDefinedTags( $tags );
643    }
644
645    /**
646     * @param array &$tags
647     */
648    public function addDefinedTags( &$tags ) {
649        $tags[] = 'mobile edit';
650        $tags[] = 'mobile web edit';
651    }
652
653    /**
654     * RecentChange_save hook handler that tags mobile changes
655     * @see https://www.mediawiki.org/wiki/Manual:Hooks/RecentChange_save
656     *
657     * @param RecentChange $recentChange
658     */
659    public function onRecentChange_save( $recentChange ) {
660        self::onTaggableObjectCreation( $recentChange );
661    }
662
663    /**
664     * ManualLogEntryBeforePublish hook handler that tags actions logged when user uses mobile mode
665     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ManualLogEntryBeforePublish
666     *
667     * @param ManualLogEntry $logEntry
668     */
669    public function onManualLogEntryBeforePublish( $logEntry ): void {
670        self::onTaggableObjectCreation( $logEntry );
671    }
672
673    /**
674     * @param Taggable $taggable Object to tag
675     */
676    public static function onTaggableObjectCreation( Taggable $taggable ) {
677        /** @var MobileContext $context */
678        $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
679        $userAgent = $context->getRequest()->getHeader( "User-agent" );
680        if ( $context->shouldDisplayMobileView() ) {
681            $taggable->addTags( [ 'mobile edit' ] );
682            // Tag as mobile web edit specifically, if it isn't coming from the apps
683            if ( strpos( $userAgent, 'WikipediaApp/' ) !== 0 ) {
684                $taggable->addTags( [ 'mobile web edit' ] );
685            }
686        }
687    }
688
689    /**
690     * AbuseFilter-generateUserVars hook handler that adds a user_mobile variable.
691     * Altering the variables generated for a specific user
692     *
693     * @see hooks.txt in AbuseFilter extension
694     * @param VariableHolder $vars object to add vars to
695     * @param User $user
696     * @param RecentChange|null $rc If the variables should be generated for an RC entry, this
697     *  is the entry. Null if it's for the current action being filtered.
698     */
699    public static function onAbuseFilterGenerateUserVars( $vars, $user, ?RecentChange $rc = null ) {
700        $services = MediaWikiServices::getInstance();
701
702        if ( !$rc ) {
703            /** @var MobileContext $context */
704            $context = $services->getService( 'MobileFrontend.Context' );
705            $vars->setVar( 'user_mobile', $context->shouldDisplayMobileView() );
706        } else {
707
708            $dbr = $services->getConnectionProvider()->getReplicaDatabase();
709
710            $tags = $services->getChangeTagsStore()->getTags( $dbr, $rc->getAttribute( 'rc_id' ) );
711            $val = (bool)array_intersect( $tags, [ 'mobile edit', 'mobile web edit' ] );
712            $vars->setVar( 'user_mobile', $val );
713        }
714    }
715
716    /**
717     * AbuseFilter-builder hook handler that adds user_mobile variable to list
718     *  of valid vars
719     *
720     * @param array &$builder Array in AbuseFilter::getBuilderValues to add to.
721     */
722    public static function onAbuseFilterBuilder( &$builder ) {
723        $builder['vars']['user_mobile'] = 'user-mobile';
724    }
725
726    /**
727     * Invocation of hook SpecialPageBeforeExecute
728     *
729     * We use this hook to ensure that login/account creation pages
730     * are redirected to HTTPS if they are not accessed via HTTPS and
731     * $wgSecureLogin == true - but only when using the
732     * mobile site.
733     *
734     * @param SpecialPage $special
735     * @param string $subpage subpage name
736     */
737    public function onSpecialPageBeforeExecute( $special, $subpage ) {
738        $isMobileView = $this->mobileContext->shouldDisplayMobileView();
739        $taglines = $this->mobileContext->getConfig()->get( 'MFSpecialPageTaglines' );
740        $name = $special->getName();
741
742        if ( $isMobileView ) {
743            $out = $special->getOutput();
744            // FIXME: mobile.special.styles should be replaced with mediawiki.special module
745            $out->addModuleStyles(
746                [ 'mobile.special.styles' ]
747            );
748            // FIXME: Should be moved to MediaWiki core module.
749            if ( $name === 'Userlogin' || $name === 'CreateAccount' ) {
750                $out->addModules( 'mobile.special.userlogin.scripts' );
751            }
752            if ( array_key_exists( $name, $taglines ) ) {
753                self::setTagline( $out, $out->msg( $taglines[$name] )->parse() );
754            }
755        }
756    }
757
758    /**
759     * PostLoginRedirect hook handler
760     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PostLoginRedirect
761     *
762     * Used here to handle watchlist actions made by anons to be handled after
763     * login or account creation redirect.
764     *
765     * @inheritDoc
766     */
767    public function onPostLoginRedirect( &$returnTo, &$returnToQuery, &$type ) {
768        $context = $this->mobileContext;
769
770        if ( !$context->shouldDisplayMobileView() ) {
771            return;
772        }
773
774        // If 'watch' is set from the login form, watch the requested article
775        $campaign = $context->getRequest()->getRawVal( 'campaign' );
776
777        // The user came from one of the drawers that prompted them to login.
778        // We must watch the article per their original intent.
779        if ( $campaign === 'mobile_watchPageActionCta' ||
780            wfArrayToCgi( $returnToQuery ) === 'article_action=watch'
781        ) {
782            $title = Title::newFromText( $returnTo );
783            // protect against watching special pages (these cannot be watched!)
784            if ( $title !== null && !$title->isSpecialPage() ) {
785                $this->watchlistManager->addWatch( $context->getAuthority(), $title );
786            }
787        }
788    }
789
790    /**
791     * BeforePageDisplay hook handler
792     * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
793     *
794     * @param OutputPage $out
795     * @param Skin $skin Skin object that will be used to generate the page, added in 1.13.
796     */
797    public function onBeforePageDisplay( $out, $skin ): void {
798        $context = $this->mobileContext;
799        $mfEnableXAnalyticsLogging = $this->config->get( 'MFEnableXAnalyticsLogging' );
800        $mfNoIndexPages = $this->config->get( 'MFNoindexPages' );
801        $isCanonicalLinkHandledByCore = $this->config->get( 'EnableCanonicalServerLink' );
802        $hasMobileUrl = $context->hasMobileDomain();
803        $displayMobileView = $context->shouldDisplayMobileView();
804
805        $title = $skin->getTitle();
806
807        // an canonical/alternate link is only useful, if the mobile and desktop URL are different
808        // and $wgMFNoindexPages needs to be true
809        if ( $hasMobileUrl && $mfNoIndexPages ) {
810            $link = false;
811
812            if ( !$displayMobileView ) {
813                // add alternate link to desktop sites - bug T91183
814                $desktopUrl = $title->getFullURL();
815                $link = [
816                    'rel' => 'alternate',
817                    'media' => 'only screen and (max-width: ' . self::DEVICE_WIDTH_TABLET . ')',
818                    'href' => $context->getMobileUrl( $desktopUrl ),
819                ];
820            } elseif ( !$isCanonicalLinkHandledByCore ) {
821                $link = [
822                    'rel' => 'canonical',
823                    'href' => $title->getFullURL(),
824                ];
825            }
826
827            if ( $link ) {
828                $out->addLink( $link );
829            }
830        }
831
832        // set the vary header to User-Agent, if mobile frontend auto detects, if the mobile
833        // view should be delivered and the same url is used for desktop and mobile devices
834        // Bug: T123189
835        if (
836            $this->config->get( 'MFVaryOnUA' ) &&
837            $this->config->get( 'MFAutodetectMobileView' ) &&
838            !$hasMobileUrl
839        ) {
840            $out->addVaryHeader( 'User-Agent' );
841        }
842
843        // Set X-Analytics HTTP response header if necessary
844        if ( $displayMobileView ) {
845            $analyticsHeader = ( $mfEnableXAnalyticsLogging ? $context->getXAnalyticsHeader() : false );
846            if ( $analyticsHeader ) {
847                $resp = $out->getRequest()->response();
848                $resp->header( $analyticsHeader );
849            }
850
851            // in mobile view: always add vary header
852            $out->addVaryHeader( 'Cookie' );
853
854            $out->addLink(
855                [
856                    'rel' => 'manifest',
857                    'href' => wfAppendQuery(
858                        wfScript( 'api' ),
859                        [ 'action' => 'webapp-manifest' ]
860                    )
861                ]
862            );
863
864            // In mobile mode, MediaWiki:Common.css/MediaWiki:Common.js is not loaded.
865            // We load MediaWiki:Mobile.css/js instead
866            // We load mobile.init so that lazy loading images works on all skins
867            $out->addModules( [ 'mobile.init' ] );
868            $out->addModuleStyles( [ 'mobile.init.styles' ] );
869
870            $fontSize = $this->userOptionsLookup->getOption(
871                $context->getUser(), self::MOBILE_PREFERENCES_FONTSIZE
872            ) ?? 'small';
873            $expandSections = $this->userOptionsLookup->getOption(
874                $context->getUser(), self::MOBILE_PREFERENCES_EXPAND_SECTIONS
875            ) ? '1' : '0';
876
877            /** @var \MobileFrontend\Amc\UserMode $userMode */
878            $userMode = MediaWikiServices::getInstance()->getService( 'MobileFrontend.AMC.UserMode' );
879            $amc = !$userMode->isEnabled() ? '0' : '1';
880            $context->getOutput()->addHtmlClasses( [
881                'mf-expand-sections-clientpref-' . $expandSections,
882                'mf-font-size-clientpref-' . $fontSize,
883                'mw-mf-amc-clientpref-' . $amc
884            ] );
885        }
886
887        // T204691
888        $theme = $this->config->get( 'MFManifestThemeColor' );
889        if ( $theme && $displayMobileView ) {
890            $out->addMeta( 'theme-color', $theme );
891        }
892
893        if ( $displayMobileView ) {
894            // Adds inline script to allow opening of sections while JS is still loading
895            $out->prependHTML( MakeSectionsTransform::interimTogglingSupport() );
896        }
897    }
898
899    /**
900     * AfterBuildFeedLinks hook handler. Remove all feed links in mobile view.
901     *
902     * @param array &$tags Added feed links
903     */
904    public function onAfterBuildFeedLinks( &$tags ) {
905        if ( $this->mobileContext->shouldDisplayMobileView() ) {
906            $tags = [];
907        }
908    }
909
910    /**
911     * Register default preferences for MobileFrontend
912     *
913     * @param array &$defaultUserOptions Reference to default options array
914     */
915    public function onUserGetDefaultOptions( &$defaultUserOptions ) {
916        if ( $this->config->get( 'MFEnableMobilePreferences' ) ) {
917            $defaultUserOptions += [
918                self::MOBILE_PREFERENCES_SPECIAL_PAGES => self::ENABLE_SPECIAL_PAGE_OPTIMISATIONS,
919            ];
920        }
921    }
922
923    /**
924     * GetPreferences hook handler
925     * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
926     *
927     * @param User $user User whose preferences are being modified
928     * @param array &$preferences Preferences description array, to be fed to an HTMLForm object
929     */
930    public function onGetPreferences( $user, &$preferences ) {
931        $definition = [
932            'type' => 'api',
933            'default' => '',
934        ];
935        $preferences[self::MOBILE_PREFERENCES_EDITOR] = $definition;
936        $preferences[self::MOBILE_PREFERENCES_FONTSIZE] = $definition;
937        $preferences[self::MOBILE_PREFERENCES_EXPAND_SECTIONS] = $definition;
938
939        if ( $this->config->get( 'MFEnableMobilePreferences' ) ) {
940            $preferences[ self::MOBILE_PREFERENCES_SPECIAL_PAGES ] = [
941                'type' => 'check',
942                'label-message' => 'mobile-frontend-special-pages-pref',
943                'help-message' => 'mobile-frontend-special-pages-pref',
944                // The following messages are generated here:
945                // * prefs-mobile
946                'section' => self::MOBILE_PREFERENCES_SECTION
947            ];
948        }
949    }
950
951    /**
952     * CentralAuthLoginRedirectData hook handler
953     * Saves mobile host so that the CentralAuth wiki could redirect back properly
954     *
955     * @see CentralAuthHooks::doCentralLoginRedirect in CentralAuth extension
956     * @param \MediaWiki\Extension\CentralAuth\User\CentralAuthUser $centralUser
957     * @param array &$data Redirect data
958     */
959    public static function onCentralAuthLoginRedirectData( $centralUser, &$data ) {
960        /** @var MobileContext $context */
961        $context = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
962        $server = $context->getConfig()->get( 'Server' );
963        if ( $context->shouldDisplayMobileView() ) {
964            $data['mobileServer'] = $context->getMobileUrl( $server );
965        }
966    }
967
968    /**
969     * CentralAuthSilentLoginRedirect hook handler
970     * Points redirects from CentralAuth wiki to mobile domain if user has logged in from it
971     * @see SpecialCentralLogin in CentralAuth extension
972     * @param \MediaWiki\Extension\CentralAuth\User\CentralAuthUser $centralUser
973     * @param string &$url to redirect to
974     * @param array $info token information
975     */
976    public static function onCentralAuthSilentLoginRedirect( $centralUser, &$url, $info ) {
977        if ( isset( $info['mobileServer'] ) ) {
978            $urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
979            $mobileUrlParsed = $urlUtils->parse( $info['mobileServer'] );
980            $urlParsed = $urlUtils->parse( $url );
981            $urlParsed['host'] = $mobileUrlParsed['host'] ?? '';
982            $url = UrlUtils::assemble( $urlParsed );
983        }
984    }
985
986    /**
987     * Sets a tagline for a given page that can be displayed by the skin.
988     *
989     * @param OutputPage $outputPage
990     * @param string $desc
991     */
992    private static function setTagline( OutputPage $outputPage, $desc ) {
993        $outputPage->setProperty( 'wgMFDescription', $desc );
994    }
995
996    /**
997     * Finds the wikidata tagline associated with the page
998     *
999     * @param ParserOutput $po
1000     * @param callable $fallbackWikibaseDescriptionFunc A fallback to provide Wikibase description.
1001     * Function takes wikibase_item as a first and only argument
1002     * @return ?string the tagline as a string, or else null if none is found
1003     */
1004    public static function findTagline( ParserOutput $po, $fallbackWikibaseDescriptionFunc ) {
1005        $desc = $po->getPageProperty( 'wikibase-shortdesc' );
1006        $item = $po->getPageProperty( 'wikibase_item' );
1007        if ( $desc === null && $item && $fallbackWikibaseDescriptionFunc ) {
1008            return $fallbackWikibaseDescriptionFunc( $item );
1009        }
1010        return $desc;
1011    }
1012
1013    /**
1014     * OutputPageParserOutput hook handler
1015     * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput
1016     *
1017     * @param OutputPage $outputPage the OutputPage object to which wikitext is added
1018     * @param ParserOutput $po
1019     */
1020    public function onOutputPageParserOutput( $outputPage, $po ): void {
1021        $title = $outputPage->getTitle();
1022        $descriptionsEnabled = !$title->isMainPage() &&
1023            $title->getNamespace() === NS_MAIN &&
1024            $this->featuresManager->isFeatureAvailableForCurrentUser(
1025                'MFEnableWikidataDescriptions'
1026            ) && $this->mobileContext->shouldShowWikibaseDescriptions( 'tagline', $this->config );
1027
1028        // Only set the tagline if the feature has been enabled and the article is in the main namespace
1029        if ( $this->mobileContext->shouldDisplayMobileView() && $descriptionsEnabled ) {
1030            $desc = self::findTagline( $po, static function ( $item ) {
1031                return ExtMobileFrontend::getWikibaseDescription( $item );
1032            } );
1033            if ( $desc ) {
1034                self::setTagline( $outputPage, $desc );
1035            }
1036        }
1037    }
1038
1039    /**
1040     * ArticleParserOptions hook handler
1041     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleParserOptions
1042     *
1043     * @param Article $article
1044     * @param ParserOptions $parserOptions
1045     */
1046    public function onArticleParserOptions( Article $article, ParserOptions $parserOptions ) {
1047        // while the parser is actively being migrated, we rely on the ParserMigration extension for using Parsoid
1048        if ( ExtensionRegistry::getInstance()->isLoaded( 'ParserMigration' ) ) {
1049            $context = $this->mobileContext;
1050            $oracle = MediaWikiServices::getInstance()->getService( 'ParserMigration.Oracle' );
1051
1052            $shouldUseParsoid =
1053                $oracle->shouldUseParsoid( $context->getUser(), $context->getRequest(), $article->getTitle() );
1054
1055            // set the collapsible sections parser flag so that section content is wrapped in a div for easier targeting
1056            // only if we're in mobile view and parsoid is enabled
1057            if ( $context->shouldDisplayMobileView() && $shouldUseParsoid ) {
1058                $parserOptions->setCollapsibleSections();
1059            }
1060        }
1061    }
1062
1063    /**
1064     * HTMLFileCache::useFileCache hook handler
1065     * Disables file caching for mobile pageviews
1066     * @see https://www.mediawiki.org/wiki/Manual:Hooks/HTMLFileCache::useFileCache
1067     *
1068     * @param IContextSource $context
1069     * @return bool
1070     */
1071    public function onHTMLFileCache__useFileCache( $context ) {
1072        return !$this->mobileContext->shouldDisplayMobileView();
1073    }
1074
1075    /**
1076     * LoginFormValidErrorMessages hook handler to promote MF specific error message be valid.
1077     *
1078     * @param array &$messages Array of already added messages
1079     */
1080    public function onLoginFormValidErrorMessages( array &$messages ) {
1081        $messages = array_merge( $messages,
1082            [
1083                // watchstart sign up CTA
1084                'mobile-frontend-watchlist-signup-action',
1085                // Watchlist and watchstar sign in CTA
1086                'mobile-frontend-watchlist-purpose',
1087                // Edit button sign in CTA
1088                'mobile-frontend-edit-login-action',
1089                // Edit button sign-up CTA
1090                'mobile-frontend-edit-signup-action',
1091                'mobile-frontend-donate-image-login-action',
1092                // default message
1093                'mobile-frontend-generic-login-new',
1094            ]
1095        );
1096    }
1097
1098    /**
1099     * Handler for MakeGlobalVariablesScript hook.
1100     * For values that depend on the current page, user or request state.
1101     *
1102     * @see https://www.mediawiki.org/wiki/Manual:Hooks/MakeGlobalVariablesScript
1103     * @param array &$vars Variables to be added into the output
1104     * @param OutputPage $out OutputPage instance calling the hook
1105     */
1106    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
1107        $services = MediaWikiServices::getInstance();
1108        /** @var \MobileFrontend\Amc\UserMode $userMode */
1109        $userMode = $services->getService( 'MobileFrontend.AMC.UserMode' );
1110
1111        // If the device is a mobile, Remove the category entry.
1112        $context = $this->mobileContext;
1113        if ( $context->shouldDisplayMobileView() ) {
1114            unset( $vars['wgCategories'] );
1115            $vars['wgMFMode'] = 'stable';
1116            $vars['wgMFAmc'] = $userMode->isEnabled();
1117            $vars['wgMFLazyLoadImages'] =
1118                $this->featuresManager->isFeatureAvailableForCurrentUser( 'MFLazyLoadImages' );
1119            $vars['wgMFEditNoticesFeatureConflict'] = $this->hasEditNoticesFeatureConflict(
1120                $this->config, $context->getUser()
1121            );
1122        }
1123        // Needed by mobile.startup and mobile.special.watchlist.scripts.
1124        // Needs to know if in beta mode or not and needs to load for Minerva desktop as well.
1125        // Ideally this would be inside ResourceLoaderFileModuleWithMFConfig but
1126        // sessions are not allowed there.
1127        $vars += $this->getWikibaseStaticConfigVars( $context );
1128    }
1129
1130    /**
1131     * Check if a conflicting edit notices gadget is enabled for the current user
1132     *
1133     * @param Config $config
1134     * @param User $user
1135     * @return bool
1136     */
1137    private function hasEditNoticesFeatureConflict( Config $config, User $user ): bool {
1138        $gadgetName = $config->get( 'MFEditNoticesConflictingGadgetName' );
1139        if ( !$gadgetName ) {
1140            return false;
1141        }
1142
1143        if ( $this->gadgetRepo ) {
1144            $match = array_search( $gadgetName, $this->gadgetRepo->getGadgetIds(), true );
1145            if ( $match !== false ) {
1146                try {
1147                    return $this->gadgetRepo->getGadget( $gadgetName )
1148                        ->isEnabled( $user );
1149                } catch ( \InvalidArgumentException ) {
1150                    return false;
1151                }
1152            }
1153        }
1154        return false;
1155    }
1156
1157    /**
1158     * Handler for TitleSquidURLs hook to add copies of the cache purge
1159     * URLs which are transformed according to the wgMobileUrlCallback, so
1160     * that both mobile and non-mobile URL variants get purged.
1161     *
1162     * @see * https://www.mediawiki.org/wiki/Manual:Hooks/TitleSquidURLs
1163     * @param Title $title the article title
1164     * @param array &$urls the set of URLs to purge
1165     */
1166    public function onTitleSquidURLs( $title, &$urls ) {
1167        foreach ( $urls as $url ) {
1168            $newUrl = $this->mobileContext->getMobileUrl( $url );
1169            if ( $newUrl !== false && $newUrl !== $url ) {
1170                $urls[] = $newUrl;
1171            }
1172        }
1173    }
1174
1175    /**
1176     * Handler for the AuthChangeFormFields hook to add a logo on top of
1177     * the login screen. This is the AuthManager equivalent of changeUserLoginCreateForm.
1178     * @param AuthenticationRequest[] $requests AuthenticationRequest objects array
1179     * @param array $fieldInfo Field description as given by AuthenticationRequest::mergeFieldInfo
1180     * @param array &$formDescriptor A form descriptor suitable for the HTMLForm constructor
1181     * @param string $action One of the AuthManager::ACTION_* constants
1182     */
1183    public function onAuthChangeFormFields(
1184        $requests, $fieldInfo, &$formDescriptor, $action
1185    ) {
1186        $logos = RL\SkinModule::getAvailableLogos( $this->config );
1187        $mfLogo = $logos['icon'] ?? false;
1188
1189        // do nothing in desktop mode
1190        if (
1191            $this->mobileContext->shouldDisplayMobileView() && $mfLogo
1192            && in_array( $action, [ AuthManager::ACTION_LOGIN, AuthManager::ACTION_CREATE ], true )
1193        ) {
1194            $logoHtml = Html::rawElement( 'div', [ 'class' => 'mw-mf-watermark' ],
1195                Html::element( 'img', [ 'src' => $mfLogo, 'alt' => '' ] ) );
1196            $formDescriptor = [
1197                'mfLogo' => [
1198                    'type' => 'info',
1199                    'default' => $logoHtml,
1200                    'raw' => true,
1201                ],
1202            ] + $formDescriptor;
1203        }
1204    }
1205
1206    /**
1207     * Add the base mobile site URL to the siteinfo API output.
1208     * @param ApiQuerySiteinfo $module
1209     * @param array &$result Api result array
1210     */
1211    public function onAPIQuerySiteInfoGeneralInfo( $module, &$result ) {
1212        global $wgCanonicalServer;
1213        $result['mobileserver'] = $this->mobileContext->getMobileUrl( $wgCanonicalServer );
1214    }
1215}