Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2751118
FeatureManager.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
FeatureManager.php
View Options
<?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\FeatureManagement
;
use
MediaWiki\Context\IContextSource
;
use
MediaWiki\Skins\Vector\ConfigHelper
;
use
MediaWiki\Skins\Vector\Constants
;
use
MediaWiki\Skins\Vector\FeatureManagement\Requirements\SimpleRequirement
;
use
MediaWiki\User\Options\UserOptionsLookup
;
use
RuntimeException
;
use
Wikimedia\Assert\Assert
;
/**
* A simple feature manager.
*
* NOTE: This API hasn't settled. It may change at any time without warning. Please don't bind to
* it unless you absolutely need to.
*
* @unstable
*
* @package MediaWiki\Skins\Vector\FeatureManagement
* @internal
* @final
*/
class
FeatureManager
{
/**
* A map of feature name to the array of requirements (referenced by name). A feature is only
* considered enabled when all of its requirements are met.
*
* See FeatureManager::registerFeature for additional detail.
*
* @var Array<string,string[]>
*/
private
$features
=
[];
/**
* A map of requirement name to the Requirement instance that represents it.
*
* The names of requirements are assumed to be static for the lifetime of the request. Therefore
* we can use them to look up Requirement instances quickly.
*
* @var Array<string,Requirement>
*/
private
$requirements
=
[];
private
UserOptionsLookup
$userOptionsLookup
;
private
IContextSource
$context
;
public
function
__construct
(
UserOptionsLookup
$userOptionsLookup
,
IContextSource
$context
)
{
$this
->
userOptionsLookup
=
$userOptionsLookup
;
$this
->
context
=
$context
;
}
/**
* Register a feature and its requirements.
*
* Essentially, a "feature" is a friendly (hopefully) name for some component, however big or
* small, that has some requirements. A feature manager allows us to decouple the component's
* logic from its requirements, allowing them to vary independently. Moreover, the use of
* friendly names wherever possible allows us to define a common language with our non-technical
* colleagues.
*
* ```php
* $featureManager->registerFeature( 'featureA', 'requirementA' );
* ```
*
* defines the "featureA" feature, which is enabled when the "requirementA" requirement is met.
*
* ```php
* $featureManager->registerFeature( 'featureB', [ 'requirementA', 'requirementB' ] );
* ```
*
* defines the "featureB" feature, which is enabled when the "requirementA" and "requirementB"
* requirements are met. Note well that the feature is only enabled when _all_ requirements are
* met, i.e. the requirements are evaluated in order and logically `AND`ed together.
*
* @param string $feature The name of the feature
* @param string|array $requirements The feature's requirements. As above, you can define a
* feature that requires a single requirement via the shorthand
*
* ```php
* $featureManager->registerFeature( 'feature', 'requirementA' );
* // Equivalent to $featureManager->registerFeature( 'feature', [ 'requirementA' ] );
* ```
*
* @throws \LogicException If the feature is already registered
* @throws \Wikimedia\Assert\ParameterAssertionException If the feature's requirements aren't
* the name of a single requirement or a list of requirements
* @throws \InvalidArgumentException If the feature references a requirement that isn't
* registered
*/
public
function
registerFeature
(
string
$feature
,
$requirements
)
{
//
// Validation
if
(
array_key_exists
(
$feature
,
$this
->
features
)
)
{
throw
new
\LogicException
(
sprintf
(
'Feature "%s" is already registered.'
,
$feature
)
);
}
Assert
::
parameterType
(
'string|array'
,
$requirements
,
'requirements'
);
$requirements
=
(
array
)
$requirements
;
Assert
::
parameterElementType
(
'string'
,
$requirements
,
'requirements'
);
foreach
(
$requirements
as
$name
)
{
if
(
!
array_key_exists
(
$name
,
$this
->
requirements
)
)
{
throw
new
\InvalidArgumentException
(
sprintf
(
'Feature "%s" references requirement "%s", which hasn
\'
t been registered'
,
$feature
,
$name
)
);
}
}
// Mutation
$this
->
features
[
$feature
]
=
$requirements
;
}
/**
* Gets user's preference value
*
* If user preference is not set or did not appear in config
* set it to default value we go back to defualt suffix value
* that will ensure that the feature will be enabled when requirements are met
*
* @param string $preferenceKey User preference key
* @return string
*/
public
function
getUserPreferenceValue
(
$preferenceKey
)
{
return
$this
->
userOptionsLookup
->
getOption
(
$this
->
context
->
getUser
(),
$preferenceKey
// For client preferences, this should be the same as `preferenceKey`
// in 'resources/skins.vector.js/clientPreferences.json'
);
}
/**
* Return a list of classes that should be added to the body tag
*
* @return array
*/
public
function
getFeatureBodyClass
()
{
return
array_map
(
function
(
$featureName
)
{
// switch to lower case and switch from camel case to hyphens
$featureClass
=
ltrim
(
strtolower
(
preg_replace
(
'/[A-Z]([A-Z](?![a-z]))*/'
,
'-$0'
,
$featureName
)
),
'-'
);
$prefix
=
'vector-feature-'
.
$featureClass
.
'-'
;
// some features (eg night mode) will require request context to determine status
$request
=
$this
->
context
->
getRequest
();
$config
=
$this
->
context
->
getConfig
();
$title
=
$this
->
context
->
getTitle
();
// Client side preferences
switch
(
$featureName
)
{
// This feature has 3 possible states: 0, 1, 2 and -excluded.
// It persists for all users.
case
CONSTANTS
::
FEATURE_FONT_SIZE
:
if
(
ConfigHelper
::
shouldDisable
(
$config
->
get
(
'VectorFontSizeConfigurableOptions'
),
$request
,
$title
)
)
{
return
$prefix
.
'clientpref--excluded'
;
}
$suffixEnabled
=
'clientpref-'
.
$this
->
getUserPreferenceValue
(
CONSTANTS
::
PREF_KEY_FONT_SIZE
);
$suffixDisabled
=
'clientpref-0'
;
break
;
// This feature has 4 possible states: day, night, os and -excluded.
// It persists for all users.
case
CONSTANTS
::
PREF_NIGHT_MODE
:
// if night mode is disabled for the page, add the exclude class instead and return early
if
(
ConfigHelper
::
shouldDisable
(
$config
->
get
(
'VectorNightModeOptions'
),
$request
,
$title
)
)
{
// The additional "-" prefix, makes this an invalid client preference for anonymous users.
return
'skin-theme-clientpref--excluded'
;
}
$prefix
=
''
;
$valueRequest
=
$request
->
getRawVal
(
'vectornightmode'
);
// If night mode query string is used, hardcode pref value to the night mode value
// NOTE: The query string parameter only works for logged in users.
// IF you have set a cookie locally this will be overriden.
$value
=
$valueRequest
!==
null
?
self
::
resolveNightModeQueryValue
(
$valueRequest
)
:
$this
->
getUserPreferenceValue
(
CONSTANTS
::
PREF_KEY_NIGHT_MODE
);
$suffixEnabled
=
'clientpref-'
.
$value
;
$suffixDisabled
=
'clientpref-day'
;
// Must be hardcoded to 'skin-theme-' to be consistent with Minerva
// So that editors can target the same class across skins
$prefix
.=
'skin-theme-'
;
break
;
// These features persist for all users and have two valid states: 0 and 1.
case
CONSTANTS
::
FEATURE_LIMITED_WIDTH
:
case
CONSTANTS
::
FEATURE_TOC_PINNED
:
case
CONSTANTS
::
FEATURE_APPEARANCE_PINNED
:
$suffixEnabled
=
'clientpref-1'
;
$suffixDisabled
=
'clientpref-0'
;
break
;
// These features only persist for logged in users so do not contain the clientpref suffix.
// These features have two valid states: enabled and disabled. In future it would be nice if these
// were 0 and 1 so that the features.js module cannot be applied to server side only flags.
case
CONSTANTS
::
FEATURE_MAIN_MENU_PINNED
:
case
CONSTANTS
::
FEATURE_PAGE_TOOLS_PINNED
:
// Server side only feature flags.
// Note these classes are fixed and cannot be changed at runtime by JavaScript,
// only via modification to LocalSettings.php.
case
Constants
::
FEATURE_NIGHT_MODE
:
case
Constants
::
FEATURE_LIMITED_WIDTH_CONTENT
:
case
Constants
::
FEATURE_LANGUAGE_IN_HEADER
:
case
Constants
::
FEATURE_LANGUAGE_IN_MAIN_PAGE_HEADER
:
case
Constants
::
FEATURE_STICKY_HEADER
:
$suffixEnabled
=
'enabled'
;
$suffixDisabled
=
'disabled'
;
break
;
default
:
throw
new
RuntimeException
(
"Feature $featureName has no associated feature class."
);
}
return
$this
->
isFeatureEnabled
(
$featureName
)
?
$prefix
.
$suffixEnabled
:
$prefix
.
$suffixDisabled
;
},
array_keys
(
$this
->
features
)
);
}
/**
* Gets whether the feature's requirements are met.
*
* @param string $feature
* @return bool
*
* @throws \InvalidArgumentException If the feature isn't registered
*/
public
function
isFeatureEnabled
(
string
$feature
):
bool
{
if
(
!
array_key_exists
(
$feature
,
$this
->
features
)
)
{
throw
new
\InvalidArgumentException
(
"The feature
\"
{$feature}
\"
isn't registered."
);
}
$requirements
=
$this
->
features
[
$feature
];
foreach
(
$requirements
as
$name
)
{
if
(
!
$this
->
requirements
[
$name
]->
isMet
()
)
{
return
false
;
}
}
return
true
;
}
/**
* Register a complex {@see Requirement}.
*
* A complex requirement is one that depends on object that may or may not be fully loaded
* while the application is booting, e.g. see `User::isSafeToLoad`.
*
* Such requirements are expected to be registered during a hook that is run early on in the
* application lifecycle, e.g. the `BeforePerformAction` and `APIBeforeMain` hooks.
*
* @param Requirement $requirement
*
* @throws \LogicException If the requirement has already been registered
*/
public
function
registerRequirement
(
Requirement
$requirement
)
{
$name
=
$requirement
->
getName
();
if
(
array_key_exists
(
$name
,
$this
->
requirements
)
)
{
throw
new
\LogicException
(
"The requirement
\"
{$name}
\"
is already registered."
);
}
$this
->
requirements
[
$name
]
=
$requirement
;
}
/**
* Register a {@see SimpleRequirement}.
*
* A requirement is some condition of the application state that a feature requires to be true
* or false.
*
* @param string $name The name of the requirement
* @param bool $isMet Whether the requirement is met
*
* @throws \LogicException If the requirement has already been registered
*/
public
function
registerSimpleRequirement
(
string
$name
,
bool
$isMet
)
{
$this
->
registerRequirement
(
new
SimpleRequirement
(
$name
,
$isMet
)
);
}
/**
* Gets whether the requirement is met.
*
* @param string $name The name of the requirement
* @return bool
*
* @throws \InvalidArgumentException If the requirement isn't registered
*/
public
function
isRequirementMet
(
string
$name
):
bool
{
if
(
!
array_key_exists
(
$name
,
$this
->
requirements
)
)
{
throw
new
\InvalidArgumentException
(
"Requirement
\"
{$name}
\"
isn't registered."
);
}
return
$this
->
requirements
[
$name
]->
isMet
();
}
/**
* Converts "1", "2", and "0" to equivalent values.
*
* @return string
*/
private
static
function
resolveNightModeQueryValue
(
string
$value
)
{
switch
(
$value
)
{
case
'day'
:
case
'night'
:
case
'os'
:
return
$value
;
case
'1'
:
return
'night'
;
case
'2'
:
return
'os'
;
default
:
return
'day'
;
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Jul 3, 17:54 (1 d, 1 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d1/65/6646b21de42668015d4a528ca6fa
Default Alt Text
FeatureManager.php (11 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment