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