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