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