Revert "Revert "Revert "End migration mode"""

This reverts commit 8f5a057027.

Reason for revert: I was wrong, this was not deployed and it's causing
lots of other errors. Reverting for real this time, and leaving it
alone.

Change-Id: Idb8f3ca8fa4cbc2fe4a3359d45a7fd0127dace9d
This commit is contained in:
Catrope 2022-03-30 00:09:00 +00:00
parent 8f5a057027
commit becf14ef61
16 changed files with 1338 additions and 109 deletions

View File

@ -29,6 +29,30 @@ project are noted at:
<https://www.mediawiki.org/wiki/Reading/Web/Coding_conventions>
URL query parameters
--------------------
- `useskinversion`: Like `useskin` but for overriding the Vector skin version
user preference and configuration. E.g.,
http://localhost:8181?useskin=vector&useskinversion=2.
Skin preferences
----------------
Vector defines skin-specific user preferences. These are exposed on
Special:Preferences when the `VectorShowSkinPreferences` configuration is
enabled. The user's preference state for skin preferences is used for skin
previews and any other operation unless specified otherwise.
### Version
Vector defines a "version" preference to enable users who prefer the December
2019 version of Vector to continue to do so without any visible changes. This
version is called "Legacy Vector." The related preference defaults are
configurable via the configurations prefixed with `VectorDefaultSkinVersion`.
Version preference and configuration may be overridden by the `useskinversion`
URL query parameter.
### Pre-commit tests
A pre-commit hook is installed when executing `npm install`. By default, it runs

View File

@ -43,6 +43,20 @@ final class Constants {
*/
public const SERVICE_FEATURE_MANAGER = 'Vector.FeatureManager';
// These are tightly coupled to skin.json's config.
/**
* @var string
*/
public const CONFIG_KEY_SHOW_SKIN_PREFERENCES = 'VectorShowSkinPreferences';
/**
* @var string
*/
public const CONFIG_KEY_DEFAULT_SKIN_VERSION = 'VectorDefaultSkinVersion';
/**
* @var string
*/
public const CONFIG_KEY_DEFAULT_SKIN_VERSION_FOR_EXISTING_ACCOUNTS =
'VectorDefaultSkinVersionForExistingAccounts';
/**
* @var string
*/

View File

@ -22,14 +22,31 @@
namespace Vector\FeatureManagement\Requirements;
use MediaWiki\User\UserOptionsLookup;
use User;
use Vector\Constants;
use Vector\FeatureManagement\Requirement;
use WebRequest;
use Vector\SkinVersionLookup;
/**
* Checks if the current skin is modern Vector.
* Retrieve the skin version for the request and compare it with `Constants::SKIN_VERSION_LATEST`.
* This requirement is met if the two are equal.
*
* Skin version is evaluated in the following order:
*
* - `useskinversion` URL query parameter override. See `README.md`.
*
* - User preference. The `User` object for new and existing accounts are updated by hook according
* to the `VectorDefaultSkinVersionForNewAccounts` and
* `VectorDefaultSkinVersionForExistingAccounts` config values. See the `Vector\Hooks` class and
* `skin.json`.
*
* If the skin version is evaluated prior to `User` preference hook invocations, an incorrect
* version may be returned as only query parameter and site configuration will be known.
*
* - Site configuration default. The default is controlled by the `VectorDefaultSkinVersion` config
* value. This is used for anonymous users and as a fallback configuration. See `skin.json`.
*
* This majority of this class is taken from Stephen Niedzielski's `Vector\SkinVersionLookup` class,
* which was introduced in `d1072d0fdfb1`.
*
* @unstable
*
@ -39,31 +56,17 @@ use WebRequest;
final class LatestSkinVersionRequirement implements Requirement {
/**
* @var WebRequest
* @var SkinVersionLookup
*/
private $request;
/**
* @var User
*/
private $user;
/**
* @var UserOptionsLookup
*/
private $userOptionsLookup;
private $skinVersionLookup;
/**
* This constructor accepts all dependencies needed to obtain the skin version.
*
* @param WebRequest $request
* @param User $user
* @param UserOptionsLookup $userOptionsLookup
* @param SkinVersionLookup $skinVersionLookup
*/
public function __construct( WebRequest $request, User $user, UserOptionsLookup $userOptionsLookup ) {
$this->request = $request;
$this->user = $user;
$this->userOptionsLookup = $userOptionsLookup;
public function __construct( SkinVersionLookup $skinVersionLookup ) {
$this->skinVersionLookup = $skinVersionLookup;
}
/**
@ -78,14 +81,6 @@ final class LatestSkinVersionRequirement implements Requirement {
* @throws \ConfigException
*/
public function isMet(): bool {
$useSkin = $this->request->getVal( 'useskin' );
$user = $this->user;
if ( !$useSkin && $user->isSafeToLoad() ) {
$useSkin = $this->userOptionsLookup->getOption(
$user,
Constants::PREF_KEY_SKIN
);
}
return $useSkin === Constants::SKIN_NAME_MODERN;
return $this->skinVersionLookup->getVersion() === Constants::SKIN_VERSION_LATEST;
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Vector\HTMLForm\Fields;
use Vector\Constants;
/**
* The field on Special:Preferences (and Special:GlobalPreferences) that allows the user to
* enable/disable the legacy version of the Vector skin. Per
* https://phabricator.wikimedia.org/T242381, the field is a checkbox, that, when checked, enables
* the legacy version of the Vector skin.
*
* `HTMLLegacySkinVersionField` adapts the boolean storage type of a checkbox field to the string
* storage type of the Vector skin version preference (e.g. see `Constants::SKIN_VERSION_LEGACY`).
*
* However, we cannot extend `HTMLCheckField` to inherit the behavior of a checkbox field.
* `HTMLCheckField::loadDataFromRequest` returns boolean values. Returning non-boolean values in
* `HTMLLegacySkinVersionField::loadDataFromRequest` would violate Liskov's Substitution Principle.
* Like `HTMLExpiryField`, `HTMLLegacySkinVersionField` proxies to a private instance of
* `HTMLCheckField`, adapting parameter and return values where necessary.
*
* @package Vector\HTMLForm\Fields
* @internal
*/
final class HTMLLegacySkinVersionField extends \HTMLFormField {
/**
* @var \HTMLCheckField
*/
private $checkField;
/**
* @inheritDoc
*/
public function __construct( $params ) {
/**
* HTMLCheckField must be given a boolean as the 'default' value.
* Since MW 1.38.0-wmf.9, we could be given a boolean or a string.
* @see T296068
*/
$params['default'] = $params['default'] === true ||
$params['default'] === Constants::SKIN_VERSION_LEGACY;
parent::__construct( $params );
$this->checkField = new \HTMLCheckField( $params );
}
// BEGIN ADAPTER
/** @inheritDoc */
public function getInputHTML( $value ) {
return $this->checkField->getInputHTML( $value === Constants::SKIN_VERSION_LEGACY );
}
/** @inheritDoc */
public function getInputOOUI( $value ) {
return $this->checkField->getInputOOUI( (string)( $value === Constants::SKIN_VERSION_LEGACY ) );
}
/**
* @inheritDoc
*
* @return string If the checkbox is checked, then `Constants::SKIN_VERSION_LEGACY`;
* `Constants::SKIN_VERSION_LATEST` otherwise
*/
public function loadDataFromRequest( $request ) {
return $this->checkField->loadDataFromRequest( $request )
? Constants::SKIN_VERSION_LEGACY
: Constants::SKIN_VERSION_LATEST;
}
// END ADAPTER
/** @inheritDoc */
public function getLabel() {
return $this->checkField->getLabel();
}
// Note well that we can't invoke the following methods of `HTMLCheckField` directly because
// they're protected and `HTMLSkinVectorField` doesn't extend `HTMLCheckField`.
/** @inheritDoc */
protected function getLabelAlignOOUI() {
// See \HTMLCheckField::getLabelAlignOOUI
return 'inline';
}
/** @inheritDoc */
protected function needsLabel() {
// See \HTMLCheckField::needsLabel
return false;
}
}

View File

@ -3,6 +3,7 @@
namespace Vector;
use Config;
use HTMLForm;
use MediaWiki\MediaWikiServices;
use OutputPage;
use ResourceLoaderContext;
@ -11,6 +12,7 @@ use Skin;
use SkinTemplate;
use Title;
use User;
use Vector\HTMLForm\Fields\HTMLLegacySkinVersionField;
/**
* Presentation hook handlers for Vector skin.
@ -417,6 +419,70 @@ class Hooks {
}
}
/**
* Add Vector preferences to the user's Special:Preferences page directly underneath skins
* provided that $wgVectorSkinMigrationMode is not enabled.
*
* @param User $user User whose preferences are being modified.
* @param array[] &$prefs Preferences description array, to be fed to a HTMLForm object.
*/
public static function onGetPreferences( User $user, array &$prefs ) {
if ( !self::getConfig( Constants::CONFIG_KEY_SHOW_SKIN_PREFERENCES ) ) {
// Do not add Vector skin specific preferences.
return;
}
// If migration mode was enabled, and the skin version is set to modern,
// switch over the skin.
if ( self::isMigrationMode() && !self::isSkinVersionLegacy() ) {
MediaWikiServices::getInstance()->getUserOptionsManager()->setOption(
$user,
Constants::PREF_KEY_SKIN,
Constants::SKIN_NAME_MODERN
);
}
// Preferences to add.
$vectorPrefs = [
Constants::PREF_KEY_SKIN_VERSION => [
'class' => HTMLLegacySkinVersionField::class,
// The checkbox title.
'label-message' => 'prefs-vector-enable-vector-1-label',
// Show a little informational snippet underneath the checkbox.
'help-message' => 'prefs-vector-enable-vector-1-help',
// The tab location and title of the section to insert the checkbox. The bit after the slash
// indicates that a prefs-skin-prefs string will be provided.
'section' => 'rendering/skin/skin-prefs',
'default' => self::isSkinVersionLegacy(),
// Only show this section when the Vector skin is checked. The JavaScript client also uses
// this state to determine whether to show or hide the whole section.
// If migration mode is enabled, the section is always hidden.
'hide-if' => self::isMigrationMode() ? [ '!==', 'skin', '0' ] :
[ '!==', 'skin', Constants::SKIN_NAME_LEGACY ],
],
Constants::PREF_KEY_SIDEBAR_VISIBLE => [
'type' => 'api',
'default' => self::getConfig( Constants::CONFIG_KEY_DEFAULT_SIDEBAR_VISIBLE_FOR_AUTHORISED_USER )
],
];
// Seek the skin preference section to add Vector preferences just below it.
$skinSectionIndex = array_search(
Constants::PREF_KEY_SKIN, array_keys( $prefs )
);
if ( $skinSectionIndex !== false ) {
// Skin preference section found. Inject Vector skin-specific preferences just below it.
// This pattern can be found in Popups too. See T246162.
$vectorSectionIndex = $skinSectionIndex + 1;
$prefs = array_slice( $prefs, 0, $vectorSectionIndex, true )
+ $vectorPrefs
+ array_slice( $prefs, $vectorSectionIndex, null, true );
} else {
// Skin preference section not found. Just append Vector skin-specific preferences.
$prefs += $vectorPrefs;
}
}
/**
* Adds MediaWiki:Vector.css as the skin style that controls classic Vector.
*
@ -441,6 +507,62 @@ class Hooks {
}
}
/**
* Hook executed on user's Special:Preferences form save. This is used to convert the boolean
* presentation of skin version to a version string. That is, a single preference change by the
* user may trigger two writes: a boolean followed by a string.
*
* @param array &$formData Form data submitted by user
* @param HTMLForm $form A preferences form
* @param User $user Logged-in user
* @param bool &$result Variable defining is form save successful
* @param array $oldPreferences
*/
public static function onPreferencesFormPreSave(
array &$formData,
HTMLForm $form,
User $user,
&$result,
$oldPreferences
) {
$userManager = MediaWikiServices::getInstance()->getUserOptionsManager();
$skinVersion = $formData[ Constants::PREF_KEY_SKIN_VERSION ] ?? '';
$skin = $formData[ Constants::PREF_KEY_SKIN ] ?? '';
$isVectorEnabled = self::isVectorSkin( $skin );
if (
self::isMigrationMode() &&
$skin === Constants::SKIN_NAME_LEGACY &&
$skinVersion === Constants::SKIN_VERSION_LATEST
) {
// Mismatch between skin and version. Use skin.
$userManager->setOption(
$user,
Constants::PREF_KEY_SKIN_VERSION,
Constants::SKIN_VERSION_LEGACY
);
}
if ( !$isVectorEnabled && array_key_exists( Constants::PREF_KEY_SKIN_VERSION, $oldPreferences ) ) {
// The setting was cleared. However, this is likely because a different skin was chosen and
// the skin version preference was hidden.
$userManager->setOption(
$user,
Constants::PREF_KEY_SKIN_VERSION,
$oldPreferences[ Constants::PREF_KEY_SKIN_VERSION ]
);
}
}
/**
* Check whether we can start migrating users to use skin preference.
*
* @return bool
*/
private static function isMigrationMode(): bool {
return self::getConfig( 'VectorSkinMigrationMode' );
}
/**
* Called one time when initializing a users preferences for a newly created account.
*
@ -450,12 +572,23 @@ class Hooks {
public static function onLocalUserCreated( User $user, $isAutoCreated ) {
$default = self::getConfig( Constants::CONFIG_KEY_DEFAULT_SKIN_VERSION_FOR_NEW_ACCOUNTS );
$optionsManager = MediaWikiServices::getInstance()->getUserOptionsManager();
// Permanently set the default preference. The user can later change this preference, however,
// self::onLocalUserCreated() will not be executed for that account again.
$optionsManager->setOption(
$user,
Constants::PREF_KEY_SKIN,
$default === Constants::SKIN_VERSION_LEGACY ?
Constants::SKIN_NAME_LEGACY : Constants::SKIN_NAME_MODERN
Constants::PREF_KEY_SKIN_VERSION,
$default
);
// Also set the skin key if migration mode is enabled.
if ( self::isMigrationMode() ) {
$optionsManager->setOption(
$user,
Constants::PREF_KEY_SKIN,
$default === Constants::SKIN_VERSION_LEGACY ?
Constants::SKIN_NAME_LEGACY : Constants::SKIN_NAME_MODERN
);
}
}
/**
@ -598,7 +731,7 @@ class Hooks {
}
/**
* Get a configuration variable.
* Get a configuration variable such as `Constants::CONFIG_KEY_SHOW_SKIN_PREFERENCES`.
*
* @param string $name Name of configuration option.
* @return mixed Value configured.
@ -624,7 +757,7 @@ class Hooks {
* @param string $skinName hint that can be used to detect modern vector.
* @return bool
*/
private static function isSkinVersionLegacy( $skinName ): bool {
private static function isSkinVersionLegacy( $skinName = '' ): bool {
if ( $skinName === Constants::SKIN_NAME_MODERN ) {
return false;
}

View File

@ -28,6 +28,7 @@ use Vector\FeatureManagement\FeatureManager;
use Vector\FeatureManagement\Requirements\DynamicConfigRequirement;
use Vector\FeatureManagement\Requirements\LatestSkinVersionRequirement;
use Vector\FeatureManagement\Requirements\OverridableConfigRequirement;
use Vector\SkinVersionLookup;
return [
Constants::SERVICE_CONFIG => static function ( MediaWikiServices $services ) {
@ -50,9 +51,12 @@ return [
$featureManager->registerRequirement(
new LatestSkinVersionRequirement(
$context->getRequest(),
$context->getUser(),
$services->getUserOptionsLookup()
new SkinVersionLookup(
$context->getRequest(),
$context->getUser(),
$services->getService( Constants::SERVICE_CONFIG ),
$services->getUserOptionsLookup()
)
)
);
@ -193,7 +197,6 @@ return [
Constants::FEATURE_TABLE_OF_CONTENTS,
[
Constants::REQUIREMENT_FULLY_INITIALISED,
Constants::REQUIREMENT_LATEST_SKIN_VERSION,
Constants::REQUIREMENT_TABLE_OF_CONTENTS
]
);

View File

@ -37,7 +37,6 @@ use Title;
/**
* Skin subclass for Vector that may be the new or old version of Vector.
* IMPORTANT: DO NOT put new code here.
*
* @ingroup Skins
* Skins extending SkinVector are not supported
@ -45,12 +44,33 @@ use Title;
* @package Vector
* @internal
*
* @todo
* # Migration Plan (please remove stages when done)
*
* Stage 1:
* In future when we are ready to transition to two separate skins in this order:
* - Use $wgSkipSkins to hide vector-2022.
* - Remove skippable field from the `vector-2022` skin version. This will defer the code to the
* configuration option wgSkipSkins
* - Set $wgVectorSkinMigrationMode = true and unset the Vector entry in wgSkipSkins
* - for one wiki, to trial run. This will expose Vector in preferences. The new Vector will show
* as Vector (2022) to begin with and the skin version preference will be hidden.
* - Check VectorPrefDiffInstrumentation instrumentation is still working.
*
* Stage 2:
* - Set $wgVectorSkinMigrationMode = true for all wikis and update skin preference labels
* (See Iebe60b560069c8cfcdeed3f5986b8be35501dcbc). This will hide the skin version
* preference, and update the skin preference instead.
* - We will set $wgDefaultSkin = 'vector-2022'; for desktop improvements wikis.
* - Run script that updates prefs table, migrating any rows where skin=vector AND
* skinversion = 2 to skin=vector22, skinversion=2
*
* Stage 3:
* - Move all modern code into SkinVector22.
* - Move legacy skin code from SkinVector to SkinVectorLegacy.
* - Update skin.json `vector` key to point to SkinVectorLegacy.
* - SkinVector left as alias if necessary.
*/
abstract class SkinVector extends SkinMustache {
class SkinVector extends SkinMustache {
/** @var null|array for caching purposes */
private $languages;
/** @var int */
@ -153,7 +173,22 @@ abstract class SkinVector extends SkinMustache {
return '';
}
abstract protected function isLegacy(): bool;
/**
* Whether the legacy version of the skin is being used.
*
* @return bool
*/
protected function isLegacy(): bool {
$options = $this->getOptions();
if ( $options['name'] === Constants::SKIN_NAME_MODERN ) {
return false;
}
$isLatestSkinFeatureEnabled = MediaWikiServices::getInstance()
->getService( Constants::SERVICE_FEATURE_MANAGER )
->isFeatureEnabled( Constants::FEATURE_LATEST_SKIN );
return !$isLatestSkinFeatureEnabled;
}
/**
* Calls getLanguages with caching.
@ -390,6 +425,36 @@ abstract class SkinVector extends SkinMustache {
Hooks::onSkinTemplateNavigation( $skin, $content_navigation );
}
/**
* Updates modules for use in legacy Vector skin.
* Do not repeat this pattern. Will be addressed in T291098.
* @inheritDoc
*/
public function getDefaultModules() {
// FIXME: Do not repeat this pattern. Will be addressed in T291098.
if ( $this->isLegacy() ) {
$this->options['scripts'] = SkinVectorLegacy::getScriptsOption();
$this->options['styles'] = SkinVectorLegacy::getStylesOption();
} else {
$this->options['scripts'] = SkinVector22::getScriptsOption();
$this->options['styles'] = SkinVector22::getStylesOption();
}
return parent::getDefaultModules();
}
/**
* Updates HTML generation for use in legacy Vector skin.
* Do not repeat this pattern. Will be addressed in T291098.
*
* @inheritDoc
*/
public function generateHTML() {
if ( $this->isLegacy() ) {
$this->options['template'] = SkinVectorLegacy::getTemplateOption();
}
return parent::generateHTML();
}
/**
* @inheritDoc
*/
@ -560,14 +625,16 @@ abstract class SkinVector extends SkinMustache {
] );
if ( $skin->getUser()->isRegistered() ) {
$migrationMode = $this->getConfig()->get( 'VectorSkinMigrationMode' );
$query = $migrationMode ? 'useskin=vector&' : '';
// Note: This data is also passed to legacy template where it is unused.
$optOutUrl = [
'text' => $this->msg( 'vector-opt-out' )->text(),
'href' => SpecialPage::getTitleFor(
'Preferences',
false,
'mw-prefsection-rendering-skin'
)->getLinkURL( 'useskin=vector&wprov=' . self::OPT_OUT_LINK_TRACKING_CODE ),
$migrationMode ? 'mw-prefsection-rendering-skin' : 'mw-prefsection-rendering-skin-skin-prefs'
)->getLinkURL( $query . 'wprov=' . self::OPT_OUT_LINK_TRACKING_CODE ),
'title' => $this->msg( 'vector-opt-out-tooltip' )->text(),
'active' => false,
];

View File

@ -9,12 +9,16 @@ namespace Vector;
*/
class SkinVector22 extends SkinVector {
/**
* Updates the constructor to conditionally disable table of contents in article
* body. Note, the constructor can only check feature flags that do not vary on
* whether the user is logged in e.g. features with the 'default' key set.
* @inheritDoc
* Updates the constructor to conditionally disable table of contents in article body.
*/
public function __construct( array $options ) {
public function __construct( $options = [] ) {
$options += [
'template' => self::getTemplateOption(),
'scripts' => self::getScriptsOption(),
'styles' => self::getStylesOption(),
];
$options['toc'] = !$this->isTableOfContentsVisibleInSidebar();
parent::__construct( $options );
}
@ -35,12 +39,40 @@ class SkinVector22 extends SkinVector {
}
/**
* Temporary function while we deprecate SkinVector class.
* Temporary static function while we deprecate SkinVector class.
*
* @return bool
* @return string
*/
protected function isLegacy(): bool {
return false;
public static function getTemplateOption() {
return 'skin';
}
/**
* Temporary static function while we deprecate SkinVector class.
*
* @return array
*/
public static function getScriptsOption() {
return [
'skins.vector.user',
'skins.vector.js',
'skins.vector.es6',
];
}
/**
* Temporary static function while we deprecate SkinVector class.
*
* @return array
*/
public static function getStylesOption() {
return [
'mediawiki.ui.button',
'skins.vector.styles',
'skins.vector.user.styles',
'skins.vector.icons',
'mediawiki.ui.icon',
];
}
/**

View File

@ -9,11 +9,45 @@ namespace Vector;
*/
class SkinVectorLegacy extends SkinVector {
/**
* Whether or not the legacy version of the skin is being used.
*
* @return bool
* @inheritDoc
*/
protected function isLegacy(): bool {
return true;
public function __construct( $options = [] ) {
$options += [
'template' => self::getTemplateOption(),
'scripts' => self::getScriptsOption(),
'styles' => self::getStylesOption(),
];
parent::__construct( $options );
}
/**
* Temporary static function while we deprecate SkinVector class.
*
* @return string
*/
public static function getTemplateOption() {
return 'skin-legacy';
}
/**
* Temporary static function while we deprecate SkinVector class.
*
* @return array
*/
public static function getScriptsOption() {
return [
'skins.vector.legacy.js',
];
}
/**
* Temporary static function while we deprecate SkinVector class.
*
* @return array
*/
public static function getStylesOption() {
return [
'skins.vector.styles.legacy',
];
}
}

View File

@ -0,0 +1,176 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @since 1.35
*/
namespace Vector;
use Config;
use MediaWiki\User\UserOptionsLookup;
use User;
use WebRequest;
/**
* Given initial dependencies, retrieve the current skin version. This class does no parsing, just
* the lookup.
*
* Skin version is evaluated in the following order:
*
* - useskinversion URL query parameter override. See readme.
*
* - User preference. The User object for new accounts is updated (persisted as a user preference)
* by hook according to VectorDefaultSkinVersionForNewAccounts. See Hooks and skin.json. The user
* may then change the preference at will.
*
* - Site configuration default. The default is controlled by VectorDefaultSkinVersion and
* VectorDefaultSkinVersionForExistingAccounts based on login state. The former is used
* for anonymous users and as a fallback configuration, the latter is logged in users (existing
* accounts). See skin.json.
*
* @unstable
*
* @package Vector
* @internal
*/
final class SkinVersionLookup {
/**
* @var WebRequest
*/
private $request;
/**
* @var User
*/
private $user;
/**
* @var Config
*/
private $config;
/**
* @var UserOptionsLookup
*/
private $userOptionsLookup;
/**
* This constructor accepts all dependencies needed to obtain the skin version. The dependencies
* are lazily evaluated, not cached, meaning they always return the current results.
*
* @param WebRequest $request
* @param User $user
* @param Config $config
* @param UserOptionsLookup $userOptionsLookup
*/
public function __construct(
WebRequest $request,
User $user,
Config $config,
UserOptionsLookup $userOptionsLookup
) {
$this->request = $request;
$this->user = $user;
$this->config = $config;
$this->userOptionsLookup = $userOptionsLookup;
}
/**
* Whether or not the legacy skin is being used.
*
* @return bool
* @throws \ConfigException
*/
public function isLegacy(): bool {
return $this->getVersion() === Constants::SKIN_VERSION_LEGACY;
}
/**
* The skin version as a string. E.g., `Constants::SKIN_VERSION_LATEST`,
* `Constants::SKIN_VERSION_LATEST`, or maybe 'beta'. Note: it's likely someone will put arbitrary
* strings in the query parameter which means this function returns those strings as is.
*
* @return string
* @throws \ConfigException
*/
public function getVersion(): string {
$migrationMode = $this->config->get( 'VectorSkinMigrationMode' );
$useSkin = $this->request->getVal(
Constants::QUERY_PARAM_SKIN
);
// In migration mode, the useskin parameter is the source of truth.
if ( $migrationMode ) {
if ( $useSkin ) {
return $useSkin === Constants::SKIN_NAME_LEGACY ?
Constants::SKIN_VERSION_LEGACY :
Constants::SKIN_VERSION_LATEST;
}
}
// [[phab:T299971]]
if ( $useSkin === Constants::SKIN_NAME_MODERN ) {
return Constants::SKIN_VERSION_LATEST;
}
// If skin key is not vector, then version should be considered legacy.
// If skin is "Vector" invoke additional skin versioning detection.
// Obtain the skin version from the 1) `useskinversion` URL query parameter override, 2) the
// user preference, 3) the configured default for logged in users, 4) or the site default.
//
// The latter two configurations cannot be set by `Hooks::onUserGetDefaultOptions()` as user
// sessions are unavailable at that time so it's not possible to determine whether the
// preference is for a logged in user or an anonymous user. Since new users are known to have
// had their user preferences initialized in `Hooks::onLocalUserCreated()`, that means all
// subsequent requests to `User->getOption()` that do not have a preference set are either
// existing accounts or anonymous users. Login state makes the distinction.
$skin = $this->userOptionsLookup->getOption(
$this->user,
Constants::PREF_KEY_SKIN
);
if ( $skin === Constants::SKIN_NAME_MODERN ) {
return Constants::SKIN_VERSION_LATEST;
}
$skinVersionPref = $this->userOptionsLookup->getOption(
$this->user,
Constants::PREF_KEY_SKIN_VERSION,
$this->config->get(
$this->user->isRegistered()
? Constants::CONFIG_KEY_DEFAULT_SKIN_VERSION_FOR_EXISTING_ACCOUNTS
: Constants::CONFIG_KEY_DEFAULT_SKIN_VERSION
)
);
// If we are in migration mode...
if ( $migrationMode ) {
// ... we must check the skin version preference for logged in users.
// No need to check for anons as wgDefaultSkin has already been consulted at this point.
if (
$this->user->isRegistered() &&
$skin === Constants::SKIN_NAME_LEGACY &&
$skinVersionPref === Constants::SKIN_VERSION_LATEST
) {
return Constants::SKIN_VERSION_LATEST;
}
return Constants::SKIN_VERSION_LEGACY;
}
return (string)$this->request->getVal(
Constants::QUERY_PARAM_SKIN_VERSION,
$skinVersionPref
);
}
}

View File

@ -30,7 +30,6 @@
{
"name": "vector-2022",
"templateDirectory": "includes/templates",
"template": "skin",
"responsive": true,
"link": {
"text-wrapper": {
@ -41,18 +40,6 @@
"skin-vector",
"skin-vector-search-vue"
],
"scripts": [
"skins.vector.user",
"skins.vector.js",
"skins.vector.es6"
],
"styles": [
"mediawiki.ui.button",
"skins.vector.styles",
"skins.vector.user.styles",
"skins.vector.icons",
"mediawiki.ui.icon"
],
"messages": [
"tooltip-p-logo",
"vector-opt-out-tooltip",
@ -74,12 +61,11 @@
]
},
"vector": {
"class": "Vector\\SkinVectorLegacy",
"class": "Vector\\SkinVector",
"@args": "See SkinVector::__construct for more detail.",
"args": [
{
"name": "vector",
"template": "skin-legacy",
"templateDirectory": "includes/templates",
"responsive": true,
"link": {
@ -87,12 +73,6 @@
"tag": "span"
}
},
"scripts": [
"skins.vector.legacy.js"
],
"styles": [
"skins.vector.styles.legacy"
],
"messages": [
"tooltip-p-logo",
"vector-opt-out-tooltip",
@ -138,6 +118,8 @@
"ResourceLoaderSiteModulePages": "Vector\\Hooks::onResourceLoaderSiteModulePages",
"ResourceLoaderSiteStylesModulePages": "Vector\\Hooks::onResourceLoaderSiteStylesModulePages",
"SkinPageReadyConfig": "Vector\\Hooks::onSkinPageReadyConfig",
"GetPreferences": "Vector\\Hooks::onGetPreferences",
"PreferencesFormPreSave": "Vector\\Hooks::onPreferencesFormPreSave",
"LocalUserCreated": "Vector\\Hooks::onLocalUserCreated",
"OutputPageBodyAttributes": "Vector\\Hooks::onOutputPageBodyAttributes",
"MakeGlobalVariablesScript": "Vector\\Hooks::onMakeGlobalVariablesScript"
@ -407,7 +389,15 @@
},
"VectorResponsive": {
"value": false,
"description": "@var boolean turn Vector-2022 into a responsive skin by applying a view port and disabling the min-width"
"description": "@var boolean When wgVectorDefaultSkinVersion is set to 2, turn Vector into a responsive skin by applying a view port and disabling the min-width"
},
"VectorShowSkinPreferences": {
"value": true,
"description": "@var boolean Show skin-specific user preferences on the Special:Preferences appearance tab when true and hide them otherwise."
},
"VectorDefaultSkinVersion": {
"value": "1",
"description": "@var string:['2'|'1'] The version ('2' for latest, '1' for legacy) of the Vector skin to use for anonymous users and as a fallback. The value is _not_ persisted."
},
"VectorDefaultSkinVersionForExistingAccounts": {
"value": "1",
@ -417,6 +407,10 @@
"value": "1",
"description": "@var string:['2'|'1'] The version ('2' for latest, '1' for legacy) of the Vector skin to **set** for newly created user accounts. **The value is persisted as a user preference.** This configuration is not used for preexisting accounts (see VectorDefaultSkinVersionForExistingAccounts) and only ever executed once at new account creation time."
},
"VectorSkinMigrationMode": {
"value": true,
"description": "@internal. For usage to fulfil [[phab:T291098]]"
},
"VectorWvuiSearchOptions": {
"value": {
"showThumbnail": true,

View File

@ -0,0 +1,122 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\Skins\Vector\Tests\Integration;
use Vector\Constants;
use Vector\HTMLForm\Fields\HTMLLegacySkinVersionField;
/**
* @group Vector
* @coversDefaultClass \Vector\HTMLForm\Fields\HTMLLegacySkinVersionField
*/
class HTMLLegacySkinVersionFieldTest extends \MediaWikiIntegrationTestCase {
public function provideDefault() {
return [
[ false, '0' ],
[ false, false ],
[ true, '1' ],
[ true, true ],
];
}
/**
* @dataProvider provideDefault
* @covers ::__construct
*/
public function testConstructValidatesDefault( $expected, $default ) {
$field = new HTMLLegacySkinVersionField( [
'default' => $default,
'fieldname' => 'VectorSkinVersion',
] );
$this->assertSame(
$expected,
$field->getDefault()
);
}
public function provideGetInput() {
yield [ Constants::SKIN_VERSION_LEGACY, true ];
yield [ Constants::SKIN_VERSION_LATEST, false ];
}
/**
* @dataProvider provideGetInput
* @covers ::getInputHTML
* @covers ::getInputOOUI
*/
public function testGetInput( $skinVersionValue, $checkValue ) {
$params = [
'fieldname' => 'VectorSkinVersion',
'class' => HTMLLegacySkinVersionField::class,
'section' => 'rendering/skin/skin-prefs',
'label-message' => 'prefs-vector-enable-vector-1-label',
'help-message' => 'prefs-vector-enable-vector-1-help',
'default' => true,
'hide-if' => [ '!==', 'skin', Constants::SKIN_NAME_LEGACY ],
];
$skinVersionField = new HTMLLegacySkinVersionField( $params );
$checkField = new \HTMLCheckField( $params );
$this->assertSame(
$skinVersionField->getInputHTML( $skinVersionValue ),
$checkField->getInputHTML( $checkValue ),
'::getInputHTML matches HTMLCheckField::getInputHTML with mapped value'
);
$this->assertEquals(
$skinVersionField->getInputOOUI( $skinVersionValue ),
$checkField->getInputOOUI( $checkValue ),
'::getInputOOUI matches HTMLCheckField::getInputOOUI with mapped value'
);
}
public function provideLoadDataFromRequest() {
// User just load the form, fallback to default.
yield [ null, false, Constants::SKIN_VERSION_LEGACY ];
// User submit the form, load from request.
yield [ null, true, Constants::SKIN_VERSION_LATEST ];
yield [ true, true, Constants::SKIN_VERSION_LEGACY ];
// In normal request you can't get the 'false' value though.
yield [ false, true, Constants::SKIN_VERSION_LATEST ];
}
/**
* @dataProvider provideLoadDataFromRequest
* @covers ::loadDataFromRequest
*/
public function testLoadDataFromRequest( $wpVectorSkinVersion, $wasPosted, $expectedResult ) {
$skinVerionField = new HTMLLegacySkinVersionField( [
'fieldname' => 'VectorSkinVersion',
'default' => true,
] );
$requestData = [ 'wpVectorSkinVersion' => $wpVectorSkinVersion ];
if ( $wasPosted ) {
// Check field would load data from requst if it pass the isSubmitAttempt() check.
$requestData['wpEditToken'] = 'ABC123';
}
$request = new \FauxRequest( $requestData, $wasPosted );
$this->assertSame( $expectedResult, $skinVerionField->loadDataFromRequest( $request ) );
}
}

View File

@ -3,13 +3,11 @@ namespace MediaWiki\Skins\Vector\Tests\Integration;
use Exception;
use HashConfig;
use MediaWiki\MediaWikiServices;
use MediaWikiIntegrationTestCase;
use ReflectionMethod;
use RequestContext;
use Title;
use Vector\SkinVector22;
use Vector\SkinVectorLegacy;
use Vector\SkinVector;
use Wikimedia\TestingAccessWrapper;
/**
@ -17,16 +15,16 @@ use Wikimedia\TestingAccessWrapper;
* @package MediaWiki\Skins\Vector\Tests\Unit
* @group Vector
* @group Skins
*
* @coversDefaultClass \Vector\SkinVector
*/
class SkinVectorTest extends MediaWikiIntegrationTestCase {
/**
* @return SkinVectorLegacy
* @return SkinVector
*/
private function provideVectorTemplateObject() {
$skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
$template = $skinFactory->makeSkin( 'vector' );
return $template;
return new SkinVector( [ 'name' => 'vector' ] );
}
/**
@ -118,7 +116,7 @@ class SkinVectorTest extends MediaWikiIntegrationTestCase {
}
/**
* @covers \Vector\SkinVector::getTocData
* @covers ::getTocData
* @dataProvider provideGetTOCData
*/
public function testGetTocData(
@ -130,7 +128,7 @@ class SkinVectorTest extends MediaWikiIntegrationTestCase {
'wgVectorTableOfContentsCollapseAtCount' => $configValue
] );
$skinVector = new SkinVectorLegacy( [ 'name' => 'vector-2022' ] );
$skinVector = new SkinVector( [ 'name' => 'vector-2022' ] );
$openSkinVector = TestingAccessWrapper::newFromObject( $skinVector );
$data = $openSkinVector->getTocData( $tocData );
@ -148,7 +146,7 @@ class SkinVectorTest extends MediaWikiIntegrationTestCase {
}
/**
* @covers \Vector\SkinVector::getTemplateData
* @covers ::getTemplateData
*/
public function testGetTemplateData() {
$title = Title::newFromText( 'SkinVector' );
@ -372,7 +370,7 @@ class SkinVectorTest extends MediaWikiIntegrationTestCase {
/**
* @dataProvider providerLanguageAlertRequirements
* @covers \Vector\SkinVector::shouldLanguageAlertBeInSidebar
* @covers ::shouldLanguageAlertBeInSidebar
* @param array $requirements
* @param Title $title
* @param array $getLanguagesCached
@ -396,7 +394,7 @@ class SkinVectorTest extends MediaWikiIntegrationTestCase {
] ) );
$this->installMockMwServices( $config );
$mockSkinVector = $this->getMockBuilder( SkinVector22::class )
$mockSkinVector = $this->getMockBuilder( SkinVector::class )
->disableOriginalConstructor()
->onlyMethods( [ 'getTitle', 'getLanguagesCached','isLanguagesInContentAt', 'shouldHideLanguages' ] )
->getMock();
@ -410,7 +408,7 @@ class SkinVectorTest extends MediaWikiIntegrationTestCase {
->willReturn( $shouldHideLanguages );
$shouldLanguageAlertBeInSidebarMethod = new ReflectionMethod(
SkinVector22::class,
SkinVector::class,
'shouldLanguageAlertBeInSidebar'
);
$shouldLanguageAlertBeInSidebarMethod->setAccessible( true );

View File

@ -0,0 +1,368 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @since 1.35
*/
namespace MediaWiki\Skins\Vector\Tests\Integration;
use HashConfig;
use MediaWiki\User\UserOptionsLookup;
use Vector\Constants;
use Vector\SkinVersionLookup;
/**
* @group Vector
* @coversDefaultClass \Vector\SkinVersionLookup
*/
class SkinVersionLookupTest extends \MediaWikiIntegrationTestCase {
/**
* @covers ::isLegacy
* @covers ::getVersion
*/
public function testRequest() {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->willReturnCallback( static function ( $key ) {
if ( $key === Constants::QUERY_PARAM_SKIN ) {
return null;
} else {
return 'alpha';
}
} );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( false );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersion' => '2',
'VectorDefaultSkinVersionForExistingAccounts' => '1'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, 'beta', [
'skin' => Constants::SKIN_NAME_LEGACY,
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
'alpha',
$skinVersionLookup->getVersion(),
'Query parameter is the first priority.'
);
$this->assertSame(
false,
$skinVersionLookup->isLegacy(),
'Version is non-Legacy.'
);
}
/**
* @covers ::getVersion
* @covers ::isLegacy
*/
public function testUserPreference() {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->willReturnCallback( static function ( $key ) {
if ( $key === Constants::QUERY_PARAM_SKIN ) {
return null;
} else {
return 'beta';
}
} );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( false );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersion' => '2',
'VectorDefaultSkinVersionForExistingAccounts' => '1'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, 'beta', [
'skin' => Constants::SKIN_NAME_LEGACY,
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
'beta',
$skinVersionLookup->getVersion(),
'User preference is the second priority.'
);
$this->assertSame(
false,
$skinVersionLookup->isLegacy(),
'Version is non-Legacy.'
);
}
/**
* @covers ::getVersion
* @covers ::isLegacy
*/
public function testConfigRegistered() {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->willReturnCallback( static function ( $key ) {
if ( $key === Constants::QUERY_PARAM_SKIN ) {
return null;
} else {
return '1';
}
} );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( true );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersion' => '2',
'VectorDefaultSkinVersionForExistingAccounts' => '1'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, '1', [
'skin' => Constants::SKIN_NAME_LEGACY,
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
'1',
$skinVersionLookup->getVersion(),
'Config is the third priority and distinguishes logged in users from anonymous users.'
);
$this->assertSame(
true,
$skinVersionLookup->isLegacy(),
'Version is Legacy.'
);
}
public function providerAnonUserMigrationMode() {
return [
// When no query string just return DefaultSkin version.
[
Constants::SKIN_NAME_LEGACY,
null,
Constants::SKIN_VERSION_LEGACY,
],
[
Constants::SKIN_NAME_MODERN,
null,
Constants::SKIN_VERSION_LATEST,
],
// When useskin=vector return legacy Vector version.
[
Constants::SKIN_NAME_LEGACY,
Constants::SKIN_NAME_LEGACY,
Constants::SKIN_VERSION_LEGACY,
],
[
Constants::SKIN_NAME_MODERN,
Constants::SKIN_NAME_LEGACY,
Constants::SKIN_VERSION_LEGACY,
],
// When useskin=vector-2022 return modern Vector.
[
Constants::SKIN_NAME_MODERN,
Constants::SKIN_NAME_MODERN,
Constants::SKIN_VERSION_LATEST,
],
[
Constants::SKIN_NAME_LEGACY,
Constants::SKIN_NAME_MODERN,
Constants::SKIN_VERSION_LATEST,
],
];
}
/**
* @covers ::getVersion
* @dataProvider providerAnonUserMigrationMode
*/
public function testVectorAnonUserMigrationModeWithUseSkinVector(
string $defaultSkin,
$useSkin,
$expectedVersion
) {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->with( 'useskin' )
->willReturn( $useSkin );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( false );
$config = new HashConfig( [
'DefaultSkin' => $defaultSkin,
'VectorSkinMigrationMode' => true,
'VectorDefaultSkinVersion' => '2',
'VectorDefaultSkinVersionForExistingAccounts' => '2'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, '2', [
'skin' => $defaultSkin,
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
$expectedVersion,
$skinVersionLookup->getVersion(),
'useskin=vector query string yields legacy skin in migration mode'
);
}
/**
* @covers ::getVersion
*/
public function testVectorRegisteredUserMigrationMode() {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->willReturn( null );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( true );
$config = new HashConfig( [
'DefaultSkin' => 'vector',
'VectorSkinMigrationMode' => true,
'VectorDefaultSkinVersion' => '1',
'VectorDefaultSkinVersionForExistingAccounts' => '1'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, '2', [
'skin' => Constants::SKIN_NAME_LEGACY
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
'2',
$skinVersionLookup->getVersion(),
'If legacy skin is set with skin version modern, then the user gets modern skin still'
);
}
/**
* @covers ::getVersion
*/
public function testSkin22() {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->willReturn( '1' );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( true );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersion' => '1',
'VectorDefaultSkinVersionForExistingAccounts' => '1'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, '1', [
'skin' => Constants::SKIN_NAME_MODERN
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
'2',
$skinVersionLookup->getVersion(),
'Using the modern skin always returns 2. Ignores skinversion query string.'
);
}
/**
* @covers ::getVersion
* @covers ::isLegacy
*/
public function testConfigAnon() {
$request = $this->getMockBuilder( \WebRequest::class )->getMock();
$request
->method( 'getVal' )
->willReturnCallback( static function ( $key ) {
if ( $key === Constants::QUERY_PARAM_SKIN ) {
return null;
} else {
return '2';
}
} );
$user = $this->createMock( \User::class );
$user
->method( 'isRegistered' )
->willReturn( false );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersion' => '2',
'VectorDefaultSkinVersionForExistingAccounts' => '1'
] );
$userOptionsLookup = $this->getUserOptionsLookupMock( $user, '2', [
'skin' => Constants::SKIN_NAME_LEGACY,
] );
$skinVersionLookup = new SkinVersionLookup( $request, $user, $config, $userOptionsLookup );
$this->assertSame(
'2',
$skinVersionLookup->getVersion(),
'Config is the third priority and distinguishes anonymous users from logged in users.'
);
$this->assertSame(
false,
$skinVersionLookup->isLegacy(),
'Version is non-Legacy.'
);
}
/**
* @param User $user
* @param array $returnVal
* @param array $lookup values
* @return UserOptionsLookup
*/
private function getUserOptionsLookupMock( $user, $returnVal, $lookup = [] ) {
$mock = $this->createMock( UserOptionsLookup::class );
$mock->method( 'getOption' )
->willReturnCallback( static function ( $user, $key ) use ( $returnVal, $lookup ) {
return $lookup[ $key ] ?? $returnVal;
} );
return $mock;
}
}

View File

@ -7,6 +7,7 @@
namespace MediaWiki\Skins\Vector\Tests\Integration;
use HashConfig;
use HTMLForm;
use MediaWiki\User\UserOptionsManager;
use MediaWikiIntegrationTestCase;
use ReflectionMethod;
@ -17,6 +18,8 @@ use User;
use Vector\Constants;
use Vector\FeatureManagement\FeatureManager;
use Vector\Hooks;
use Vector\HTMLForm\Fields\HTMLLegacySkinVersionField;
use Vector\SkinVector;
use Vector\SkinVector22;
use Vector\SkinVectorLegacy;
@ -288,6 +291,21 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
);
}
/**
* @covers ::onGetPreferences
*/
public function testOnGetPreferencesShowPreferencesDisabled() {
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorShowSkinPreferences' => false,
] );
$this->setService( 'Vector.Config', $config );
$prefs = [];
Hooks::onGetPreferences( $this->getTestUser()->getUser(), $prefs );
$this->assertSame( [], $prefs, 'No preferences are added.' );
}
private function setFeatureLatestSkinVersionIsEnabled( $isEnabled ) {
$featureManager = new FeatureManager();
$featureManager->registerSimpleRequirement( Constants::REQUIREMENT_LATEST_SKIN_VERSION, $isEnabled );
@ -298,6 +316,48 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
$this->setService( Constants::SERVICE_FEATURE_MANAGER, $featureManager );
}
/**
* @covers ::onGetPreferences
*/
public function testOnGetPreferencesShowPreferencesEnabledSkinSectionFoundLegacy() {
$isLegacy = true;
$this->setFeatureLatestSkinVersionIsEnabled( !$isLegacy );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorShowSkinPreferences' => true,
'VectorDefaultSidebarVisibleForAuthorisedUser' => true,
] );
$this->setService( 'Vector.Config', $config );
$prefs = [
'foo' => [],
'skin' => [],
'bar' => []
];
Hooks::onGetPreferences( $this->getTestUser()->getUser(), $prefs );
$this->assertEquals(
[
'foo' => [],
'skin' => [],
'VectorSkinVersion' => [
'class' => HTMLLegacySkinVersionField::class,
'label-message' => 'prefs-vector-enable-vector-1-label',
'help-message' => 'prefs-vector-enable-vector-1-help',
'section' => self::SKIN_PREFS_SECTION,
'default' => $isLegacy,
'hide-if' => self::HIDE_IF,
],
'VectorSidebarVisible' => [
'type' => 'api',
'default' => true
],
'bar' => [],
],
$prefs,
'Preferences are inserted directly after skin.'
);
}
/**
* @covers ::getVectorResourceLoaderConfig
* @dataProvider provideGetVectorResourceLoaderConfig
@ -328,11 +388,118 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
);
}
/**
* @covers ::onGetPreferences
*/
public function testOnGetPreferencesShowPreferencesEnabledSkinSectionMissingLegacy() {
$isLegacy = false;
$this->setFeatureLatestSkinVersionIsEnabled( !$isLegacy );
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSidebarVisibleForAuthorisedUser' => true,
'VectorShowSkinPreferences' => true,
] );
$this->setService( 'Vector.Config', $config );
$prefs = [
'foo' => [],
'bar' => []
];
Hooks::onGetPreferences( $this->getTestUser()->getUser(), $prefs );
$this->assertEquals(
[
'foo' => [],
'bar' => [],
'VectorSkinVersion' => [
'class' => HTMLLegacySkinVersionField::class,
'label-message' => 'prefs-vector-enable-vector-1-label',
'help-message' => 'prefs-vector-enable-vector-1-help',
'section' => self::SKIN_PREFS_SECTION,
'default' => $isLegacy,
'hide-if' => self::HIDE_IF,
],
'VectorSidebarVisible' => [
'type' => 'api',
'default' => true
],
],
$prefs,
'Preferences are appended.'
);
}
/**
* @covers ::onPreferencesFormPreSave
*/
public function testOnPreferencesFormPreSaveVectorEnabledLegacyNewPreference() {
$formData = [
'skin' => 'vector',
'VectorSkinVersion' => Constants::SKIN_VERSION_LEGACY,
];
$form = $this->createMock( HTMLForm::class );
$user = $this->createMock( User::class );
$userOptionsManager = $this->createMock( UserOptionsManager::class );
$userOptionsManager->expects( $this->never() )
->method( 'setOption' );
$this->setService( 'UserOptionsManager', $userOptionsManager );
$result = true;
$oldPreferences = [];
Hooks::onPreferencesFormPreSave( $formData, $form, $user, $result, $oldPreferences );
}
/**
* @covers ::onPreferencesFormPreSave
*/
public function testOnPreferencesFormPreSaveVectorDisabledNoOldPreference() {
$formData = [
'VectorSkinVersion' => Constants::SKIN_VERSION_LATEST,
];
$form = $this->createMock( HTMLForm::class );
$user = $this->createMock( User::class );
$userOptionsManager = $this->createMock( UserOptionsManager::class );
$userOptionsManager->expects( $this->never() )
->method( 'setOption' );
$this->setService( 'UserOptionsManager', $userOptionsManager );
$result = true;
$oldPreferences = [];
Hooks::onPreferencesFormPreSave( $formData, $form, $user, $result, $oldPreferences );
}
/**
* @covers ::onPreferencesFormPreSave
*/
public function testOnPreferencesFormPreSaveVectorDisabledOldPreference() {
$formData = [
'VectorSkinVersion' => Constants::SKIN_VERSION_LATEST,
];
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorShowSkinPreferences' => false,
] );
$form = $this->createMock( HTMLForm::class );
$user = $this->createMock( User::class );
$userOptionsManager = $this->createMock( UserOptionsManager::class );
$userOptionsManager->expects( $this->once() )
->method( 'setOption' )
->with( $user, 'VectorSkinVersion', 'old' );
$this->setService( 'Vector.Config', $config );
$this->setService( 'UserOptionsManager', $userOptionsManager );
$result = true;
$oldPreferences = [
'VectorSkinVersion' => 'old',
];
Hooks::onPreferencesFormPreSave( $formData, $form, $user, $result, $oldPreferences );
}
/**
* @covers ::onLocalUserCreated
*/
public function testOnLocalUserCreatedLegacy() {
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersionForNewAccounts' => Constants::SKIN_VERSION_LEGACY,
] );
$this->setService( 'Vector.Config', $config );
@ -341,7 +508,7 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
$userOptionsManager = $this->createMock( UserOptionsManager::class );
$userOptionsManager->expects( $this->once() )
->method( 'setOption' )
->with( $user, 'skin', Constants::SKIN_NAME_LEGACY );
->with( $user, 'VectorSkinVersion', Constants::SKIN_VERSION_LEGACY );
$this->setService( 'UserOptionsManager', $userOptionsManager );
$isAutoCreated = false;
Hooks::onLocalUserCreated( $user, $isAutoCreated );
@ -352,6 +519,7 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
*/
public function testOnLocalUserCreatedLatest() {
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersionForNewAccounts' => Constants::SKIN_VERSION_LATEST,
] );
$this->setService( 'Vector.Config', $config );
@ -360,7 +528,7 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
$userOptionsManager = $this->createMock( UserOptionsManager::class );
$userOptionsManager->expects( $this->once() )
->method( 'setOption' )
->with( $user, 'skin', Constants::SKIN_NAME_MODERN );
->with( $user, 'VectorSkinVersion', Constants::SKIN_VERSION_LATEST );
$this->setService( 'UserOptionsManager', $userOptionsManager );
$isAutoCreated = false;
Hooks::onLocalUserCreated( $user, $isAutoCreated );
@ -373,7 +541,7 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase {
$this->setMwGlobals( [
'wgVectorUseIconWatch' => true
] );
$skin = new SkinVector22( [ 'name' => 'vector' ] );
$skin = new SkinVector( [ 'name' => 'vector' ] );
$skin->getContext()->setTitle( Title::newFromText( 'Foo' ) );
$contentNavWatch = [
'actions' => [

View File

@ -20,9 +20,11 @@
namespace Vector\FeatureManagement\Tests;
use HashConfig;
use MediaWiki\User\UserOptionsLookup;
use User;
use Vector\FeatureManagement\Requirements\LatestSkinVersionRequirement;
use Vector\SkinVersionLookup;
use WebRequest;
/**
@ -34,33 +36,38 @@ class LatestSkinVersionRequirementTest extends \MediaWikiUnitTestCase {
public function provideIsMet() {
// $version, $expected, $msg
yield 'not met' => [ 'vector', null, false, '"1" isn\'t considered latest.' ];
yield 'met' => [ 'vector-2022', null, true, '"2" is considered latest.' ];
yield 'met (useskin override)' => [ 'vector', 'vector-2022', true, 'useskin overrides' ];
yield 'not met (useskin override)' => [ 'vector-2022', 'vector', false, 'useskin overrides' ];
yield 'not met' => [ '1', false, '"1" isn\'t considered latest.' ];
yield 'met' => [ '2', true, '"2" is considered latest.' ];
}
/**
* @dataProvider provideIsMet
* @covers ::isMet
*/
public function testIsMet( $skin, $useSkin, $expected, $msg ) {
public function testIsMet( $version, $expected, $msg ) {
$config = new HashConfig( [
'VectorSkinMigrationMode' => false,
'VectorDefaultSkinVersionForExistingAccounts' => $version
] );
$user = $this->createMock( User::class );
$user->method( 'isRegistered' )->willReturn( true );
$user->method( 'isSafeToLoad' )->willReturn( true );
$userOptionsLookup = $this->createMock( UserOptionsLookup::class );
$userOptionsLookup->method( 'getOption' )
->willReturn( $skin );
->will( $this->returnArgument( 2 ) );
$request = $this->createMock( WebRequest::class );
$request->method( 'getVal' )
->willReturn( $useSkin );
->will( $this->returnArgument( 1 ) );
$requirement = new LatestSkinVersionRequirement(
$request,
$user,
$userOptionsLookup
new SkinVersionLookup(
$request,
$user,
$config,
$userOptionsLookup
)
);
$this->assertSame( $expected, $requirement->isMet(), $msg );