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