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