1<?php 2 3namespace Vector; 4 5use Config; 6use HTMLForm; 7use MediaWiki\MediaWikiServices; 8use OutputPage; 9use ResourceLoaderContext; 10use Skin; 11use SkinTemplate; 12use SkinVector; 13use Title; 14use User; 15use Vector\HTMLForm\Fields\HTMLLegacySkinVersionField; 16 17/** 18 * Presentation hook handlers for Vector skin. 19 * 20 * Hook handler method names should be in the form of: 21 * on<HookName>() 22 * @package Vector 23 * @internal 24 */ 25class Hooks { 26 /** 27 * Passes config variables to Vector (modern) ResourceLoader module. 28 * @param ResourceLoaderContext $context 29 * @param Config $config 30 * @return array 31 */ 32 public static function getVectorResourceLoaderConfig( 33 ResourceLoaderContext $context, 34 Config $config 35 ) { 36 return [ 37 'wgVectorSearchHost' => $config->get( 'VectorSearchHost' ), 38 ]; 39 } 40 41 /** 42 * Passes config variables to skins.vector.search ResourceLoader module. 43 * @param ResourceLoaderContext $context 44 * @param Config $config 45 * @return array 46 */ 47 public static function getVectorWvuiSearchResourceLoaderConfig( 48 ResourceLoaderContext $context, 49 Config $config 50 ) { 51 return $config->get( 'VectorWvuiSearchOptions' ); 52 } 53 54 /** 55 * SkinPageReadyConfig hook handler 56 * 57 * Replace searchModule provided by skin. 58 * 59 * @since 1.35 60 * @param ResourceLoaderContext $context 61 * @param mixed[] &$config Associative array of configurable options 62 * @return void This hook must not abort, it must return no value 63 */ 64 public static function onSkinPageReadyConfig( 65 ResourceLoaderContext $context, 66 array &$config 67 ) { 68 // It's better to exit before any additional check 69 if ( $context->getSkin() !== 'vector' ) { 70 return; 71 } 72 73 // Tell the `mediawiki.page.ready` module not to wire up search. 74 // This allows us to use $wgVectorUseWvuiSearch to decide to load 75 // the historic jquery autocomplete search or the new Vue implementation. 76 // ResourceLoaderContext has no knowledge of legacy / modern Vector 77 // and from its point of view they are the same thing. 78 // Please see the modules `skins.vector.js` and `skins.vector.legacy.js` 79 // for the wire up of search. 80 // The related method self::getVectorResourceLoaderConfig handles which 81 // search to load. 82 $config['search'] = false; 83 } 84 85 /** 86 * Add icon class to an existing navigation item inside a menu hook. 87 * See self::onSkinTemplateNavigation. 88 * @param array $item 89 * @return array 90 */ 91 private static function navigationLinkToIcon( array $item ) { 92 if ( !isset( $item['class'] ) ) { 93 $item['class'] = ''; 94 } 95 $item['class'] = rtrim( 'icon ' . $item['class'], ' ' ); 96 return $item; 97 } 98 99 /** 100 * Upgrades Vector's watch action to a watchstar. 101 * 102 * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation 103 * @param SkinTemplate $sk 104 * @param array &$content_navigation 105 */ 106 public static function onSkinTemplateNavigation( $sk, &$content_navigation ) { 107 $title = $sk->getRelevantTitle(); 108 if ( 109 $sk->getConfig()->get( 'VectorUseIconWatch' ) && 110 $sk->getSkinName() === 'vector' && 111 $title && $title->canExist() 112 ) { 113 $key = null; 114 if ( isset( $content_navigation['actions']['watch'] ) ) { 115 $key = 'watch'; 116 } 117 if ( isset( $content_navigation['actions']['unwatch'] ) ) { 118 $key = 'unwatch'; 119 } 120 121 // Promote watch link from actions to views and add an icon 122 if ( $key !== null ) { 123 $content_navigation['views'][$key] = self::navigationLinkToIcon( 124 $content_navigation['actions'][$key] 125 ); 126 unset( $content_navigation['actions'][$key] ); 127 } 128 } 129 } 130 131 /** 132 * Add Vector preferences to the user's Special:Preferences page directly underneath skins. 133 * 134 * @param User $user User whose preferences are being modified. 135 * @param array[] &$prefs Preferences description array, to be fed to a HTMLForm object. 136 */ 137 public static function onGetPreferences( User $user, array &$prefs ) { 138 if ( !self::getConfig( Constants::CONFIG_KEY_SHOW_SKIN_PREFERENCES ) ) { 139 // Do not add Vector skin specific preferences. 140 return; 141 } 142 143 // Preferences to add. 144 $vectorPrefs = [ 145 Constants::PREF_KEY_SKIN_VERSION => [ 146 'class' => HTMLLegacySkinVersionField::class, 147 // The checkbox title. 148 'label-message' => 'prefs-vector-enable-vector-1-label', 149 // Show a little informational snippet underneath the checkbox. 150 'help-message' => 'prefs-vector-enable-vector-1-help', 151 // The tab location and title of the section to insert the checkbox. The bit after the slash 152 // indicates that a prefs-skin-prefs string will be provided. 153 'section' => 'rendering/skin/skin-prefs', 154 'default' => self::isSkinVersionLegacy(), 155 // Only show this section when the Vector skin is checked. The JavaScript client also uses 156 // this state to determine whether to show or hide the whole section. 157 'hide-if' => [ '!==', 'wpskin', Constants::SKIN_NAME ], 158 ], 159 Constants::PREF_KEY_SIDEBAR_VISIBLE => [ 160 'type' => 'api', 161 'default' => self::getConfig( Constants::CONFIG_KEY_DEFAULT_SIDEBAR_VISIBLE_FOR_AUTHORISED_USER ) 162 ], 163 ]; 164 165 // Seek the skin preference section to add Vector preferences just below it. 166 $skinSectionIndex = array_search( 'skin', array_keys( $prefs ) ); 167 if ( $skinSectionIndex !== false ) { 168 // Skin preference section found. Inject Vector skin-specific preferences just below it. 169 // This pattern can be found in Popups too. See T246162. 170 $vectorSectionIndex = $skinSectionIndex + 1; 171 $prefs = array_slice( $prefs, 0, $vectorSectionIndex, true ) 172 + $vectorPrefs 173 + array_slice( $prefs, $vectorSectionIndex, null, true ); 174 } else { 175 // Skin preference section not found. Just append Vector skin-specific preferences. 176 $prefs += $vectorPrefs; 177 } 178 } 179 180 /** 181 * Hook executed on user's Special:Preferences form save. This is used to convert the boolean 182 * presentation of skin version to a version string. That is, a single preference change by the 183 * user may trigger two writes: a boolean followed by a string. 184 * 185 * @param array $formData Form data submitted by user 186 * @param HTMLForm $form A preferences form 187 * @param User $user Logged-in user 188 * @param bool &$result Variable defining is form save successful 189 * @param array $oldPreferences 190 */ 191 public static function onPreferencesFormPreSave( 192 array $formData, 193 HTMLForm $form, 194 User $user, 195 &$result, 196 $oldPreferences 197 ) { 198 $isVectorEnabled = ( $formData[ 'skin' ] ?? '' ) === Constants::SKIN_NAME; 199 200 if ( !$isVectorEnabled && array_key_exists( Constants::PREF_KEY_SKIN_VERSION, $oldPreferences ) ) { 201 // The setting was cleared. However, this is likely because a different skin was chosen and 202 // the skin version preference was hidden. 203 $user->setOption( 204 Constants::PREF_KEY_SKIN_VERSION, 205 $oldPreferences[ Constants::PREF_KEY_SKIN_VERSION ] 206 ); 207 } 208 } 209 210 /** 211 * Called one time when initializing a users preferences for a newly created account. 212 * 213 * @param User $user Newly created user object. 214 * @param bool $isAutoCreated 215 */ 216 public static function onLocalUserCreated( User $user, $isAutoCreated ) { 217 $default = self::getConfig( Constants::CONFIG_KEY_DEFAULT_SKIN_VERSION_FOR_NEW_ACCOUNTS ); 218 // Permanently set the default preference. The user can later change this preference, however, 219 // self::onLocalUserCreated() will not be executed for that account again. 220 $user->setOption( Constants::PREF_KEY_SKIN_VERSION, $default ); 221 } 222 223 /** 224 * Called when OutputPage::headElement is creating the body tag to allow skins 225 * and extensions to add attributes they might need to the body of the page. 226 * 227 * @param OutputPage $out 228 * @param Skin $sk 229 * @param string[] &$bodyAttrs 230 */ 231 public static function onOutputPageBodyAttributes( OutputPage $out, Skin $sk, &$bodyAttrs ) { 232 if ( !$sk instanceof SkinVector ) { 233 return; 234 } 235 236 // As of 2020/08/13, this CSS class is referred to by the following deployed extensions: 237 // 238 // - VisualEditor 239 // - CodeMirror 240 // - WikimediaEvents 241 // 242 // See https://codesearch.wmcloud.org/deployed/?q=skin-vector-legacy for an up-to-date 243 // list. 244 if ( self::isSkinVersionLegacy() ) { 245 $bodyAttrs['class'] .= ' skin-vector-legacy'; 246 } 247 248 // Determine the search widget treatment to send to the user 249 if ( VectorServices::getFeatureManager()->isFeatureEnabled( Constants::FEATURE_USE_WVUI_SEARCH ) ) { 250 $bodyAttrs['class'] .= ' skin-vector-search-vue'; 251 } 252 253 $config = $sk->getConfig(); 254 // Should we disable the max-width styling? 255 if ( !self::isSkinVersionLegacy() && $sk->getTitle() && self::shouldDisableMaxWidth( 256 $config->get( 'VectorMaxWidthOptions' ), 257 $sk->getTitle(), 258 $out->getRequest()->getValues() 259 ) ) { 260 $bodyAttrs['class'] .= ' skin-vector-disable-max-width'; 261 } 262 } 263 264 /** 265 * Per the $options configuration (for use with $wgVectorMaxWidthOptions) 266 * determine whether max-width should be disabled on the page. 267 * For the main page: Check the value of $options['exclude']['mainpage'] 268 * For all other pages, the following will happen: 269 * - the array $options['include'] of canonical page names will be checked 270 * against the current page. If a page has been listed there, function will return false 271 * (max-width will not be disabled) 272 * Max width is disabled if: 273 * 1) The current namespace is listed in array $options['exclude']['namespaces'] 274 * OR 275 * 2) The query string matches one of the name and value pairs $exclusions['querystring']. 276 * Note the wildcard "*" for a value, will match all query string values for the given 277 * query string parameter. 278 * 279 * @internal only for use inside tests. 280 * @param array $options 281 * @param Title $title 282 * @param array $requestValues 283 * @return bool 284 */ 285 public static function shouldDisableMaxWidth( array $options, Title $title, array $requestValues ) { 286 $canonicalTitle = $title->getRootTitle(); 287 288 $inclusions = $options['include'] ?? []; 289 $exclusions = $options['exclude'] ?? []; 290 291 if ( $title->isMainPage() ) { 292 // only one check to make 293 return $exclusions['mainpage'] ?? false; 294 } elseif ( $canonicalTitle->isSpecialPage() ) { 295 $canonicalTitle->fixSpecialName(); 296 } 297 298 // 299 // Check the inclusions based on the canonical title 300 // The inclusions are checked first as these trump any exclusions. 301 // 302 // Now we have the canonical title and the inclusions link we look for any matches. 303 foreach ( $inclusions as $titleText ) { 304 $includedTitle = Title::newFromText( $titleText ); 305 306 if ( $canonicalTitle->equals( $includedTitle ) ) { 307 return false; 308 } 309 } 310 311 // 312 // Check the exclusions 313 // If nothing matches the exclusions to determine what should happen 314 // 315 $excludeNamespaces = $exclusions['namespaces'] ?? []; 316 // Max width is disabled on certain namespaces 317 if ( $title->inNamespaces( $excludeNamespaces ) ) { 318 return true; 319 } 320 $excludeQueryString = $exclusions['querystring'] ?? []; 321 322 foreach ( $excludeQueryString as $param => $excludedParamValue ) { 323 $paramValue = $requestValues[$param] ?? false; 324 if ( $paramValue ) { 325 if ( $excludedParamValue === '*' ) { 326 // check wildcard 327 return true; 328 } elseif ( $paramValue === $excludedParamValue ) { 329 // Check if the excluded param value matches 330 return true; 331 } 332 } 333 } 334 335 return false; 336 } 337 338 /** 339 * NOTE: Please use ResourceLoaderGetConfigVars hook instead if possible 340 * for adding config to the page. 341 * Adds config variables to JS that depend on current page/request. 342 * 343 * Adds a config flag that can disable saving the VectorSidebarVisible 344 * user preference when the sidebar menu icon is clicked. 345 * 346 * @param array &$vars Array of variables to be added into the output. 347 * @param OutputPage $out OutputPage instance calling the hook 348 */ 349 public static function onMakeGlobalVariablesScript( &$vars, OutputPage $out ) { 350 if ( !$out->getSkin() instanceof SkinVector ) { 351 return; 352 } 353 354 $user = $out->getUser(); 355 356 if ( $user->isRegistered() && self::isSkinVersionLegacy() ) { 357 $vars[ 'wgVectorDisableSidebarPersistence' ] = 358 self::getConfig( 359 Constants::CONFIG_KEY_DISABLE_SIDEBAR_PERSISTENCE 360 ); 361 } 362 } 363 364 /** 365 * Get a configuration variable such as `Constants::CONFIG_KEY_SHOW_SKIN_PREFERENCES`. 366 * 367 * @param string $name Name of configuration option. 368 * @return mixed Value configured. 369 * @throws \ConfigException 370 */ 371 private static function getConfig( $name ) { 372 return self::getServiceConfig()->get( $name ); 373 } 374 375 /** 376 * @return \Config 377 */ 378 private static function getServiceConfig() { 379 return MediaWikiServices::getInstance()->getService( Constants::SERVICE_CONFIG ); 380 } 381 382 /** 383 * Gets whether the current skin version is the legacy version. 384 * 385 * @see VectorServices::getFeatureManager 386 * 387 * @return bool 388 */ 389 private static function isSkinVersionLegacy(): bool { 390 return !VectorServices::getFeatureManager()->isFeatureEnabled( Constants::FEATURE_LATEST_SKIN ); 391 } 392} 393