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