* @file
*/
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Linker\LinkTarget;
+
/**
* This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
* them based on index. The textual names of the namespaces are handled by Language.php.
/** @var int[]|null Valid namespaces cache */
private $validNamespaces = null;
- /** @var Config */
- private $config;
+ /** @var ServiceOptions */
+ private $options;
/**
- * @param Config $config
+ * TODO Make this const when HHVM support is dropped (T192166)
+ *
+ * @since 1.34
+ * @var array
*/
- public function __construct( Config $config ) {
- $this->config = $config;
+ public static $constructorOptions = [
+ 'AllowImageMoving',
+ 'CanonicalNamespaceNames',
+ 'CapitalLinkOverrides',
+ 'CapitalLinks',
+ 'ContentNamespaces',
+ 'ExtraNamespaces',
+ 'ExtraSignatureNamespaces',
+ 'NamespaceContentModels',
+ 'NamespaceProtection',
+ 'NamespacesWithSubpages',
+ 'NonincludableNamespaces',
+ 'RestrictionLevels',
+ ];
+
+ /**
+ * @param ServiceOptions $options
+ */
+ public function __construct( ServiceOptions $options ) {
+ $options->assertRequiredOptions( self::$constructorOptions );
+ $this->options = $options;
}
/**
* @return bool
*/
public function isMovable( $index ) {
- $result = !( $index < NS_MAIN ||
- ( $index == NS_FILE && !$this->config->get( 'AllowImageMoving' ) ) );
+ $result = $index >= NS_MAIN &&
+ ( $index != NS_FILE || $this->options->get( 'AllowImageMoving' ) );
/**
* @since 1.20
: $index + 1;
}
+ /**
+ * @param LinkTarget $target
+ * @return LinkTarget Talk page for $target
+ * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
+ */
+ public function getTalkPage( LinkTarget $target ) : LinkTarget {
+ if ( $this->isTalk( $target->getNamespace() ) ) {
+ return $target;
+ }
+ return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDbKey() );
+ }
+
/**
* Get the subject namespace index for a given namespace
* Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
: $index;
}
+ /**
+ * @param LinkTarget $target
+ * @return LinkTarget Subject page for $target
+ */
+ public function getSubjectPage( LinkTarget $target ) : LinkTarget {
+ if ( $this->isSubject( $target->getNamespace() ) ) {
+ return $target;
+ }
+ return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDbKey() );
+ }
+
/**
* Get the associated namespace.
* For talk namespaces, returns the subject (non-talk) namespace
* For subject (non-talk) namespaces, returns the talk namespace
*
* @param int $index Namespace index
- * @return int|null If no associated namespace could be found
+ * @return int
+ * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL)
*/
public function getAssociated( $index ) {
$this->isMethodValidFor( $index, __METHOD__ );
if ( $this->isSubject( $index ) ) {
return $this->getTalk( $index );
- } elseif ( $this->isTalk( $index ) ) {
- return $this->getSubject( $index );
- } else {
- return null;
}
+ return $this->getSubject( $index );
+ }
+
+ /**
+ * @param LinkTarget $target
+ * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk
+ * page
+ * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
+ */
+ public function getAssociatedPage( LinkTarget $target ) : LinkTarget {
+ return new TitleValue(
+ $this->getAssociated( $target->getNamespace() ), $target->getDbKey() );
}
/**
public function getCanonicalNamespaces() {
if ( $this->canonicalNamespaces === null ) {
$this->canonicalNamespaces =
- [ NS_MAIN => '' ] + $this->config->get( 'CanonicalNamespaceNames' );
+ [ NS_MAIN => '' ] + $this->options->get( 'CanonicalNamespaceNames' );
$this->canonicalNamespaces +=
ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
- if ( is_array( $this->config->get( 'ExtraNamespaces' ) ) ) {
- $this->canonicalNamespaces += $this->config->get( 'ExtraNamespaces' );
+ if ( is_array( $this->options->get( 'ExtraNamespaces' ) ) ) {
+ $this->canonicalNamespaces += $this->options->get( 'ExtraNamespaces' );
}
Hooks::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces ] );
}
* The input *must* be converted to lower case first
*
* @param string $name Namespace name
- * @return int
+ * @return int|null
*/
public function getCanonicalIndex( $name ) {
if ( $this->namespaceIndexes === false ) {
}
/**
- * Returns an array of the namespaces (by integer id) that exist on the
- * wiki. Used primarily by the api in help documentation.
+ * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by
+ * the API in help documentation. The array is sorted numerically and omits negative namespaces.
* @return array
*/
public function getValidNamespaces() {
* @return bool
*/
public function isContent( $index ) {
- return $index == NS_MAIN || in_array( $index, $this->config->get( 'ContentNamespaces' ) );
+ return $index == NS_MAIN || in_array( $index, $this->options->get( 'ContentNamespaces' ) );
}
/**
*/
public function wantSignatures( $index ) {
return $this->isTalk( $index ) ||
- in_array( $index, $this->config->get( 'ExtraSignatureNamespaces' ) );
+ in_array( $index, $this->options->get( 'ExtraSignatureNamespaces' ) );
}
/**
* @return bool
*/
public function hasSubpages( $index ) {
- return !empty( $this->config->get( 'NamespacesWithSubpages' )[$index] );
+ return !empty( $this->options->get( 'NamespacesWithSubpages' )[$index] );
}
/**
* @return array Array of namespace indices
*/
public function getContentNamespaces() {
- $contentNamespaces = $this->config->get( 'ContentNamespaces' );
+ $contentNamespaces = $this->options->get( 'ContentNamespaces' );
if ( !is_array( $contentNamespaces ) || $contentNamespaces === [] ) {
return [ NS_MAIN ];
} elseif ( !in_array( NS_MAIN, $contentNamespaces ) ) {
if ( in_array( $index, $this->alwaysCapitalizedNamespaces ) ) {
return true;
}
- $overrides = $this->config->get( 'CapitalLinkOverrides' );
+ $overrides = $this->options->get( 'CapitalLinkOverrides' );
if ( isset( $overrides[$index] ) ) {
// CapitalLinkOverrides is explicitly set
return $overrides[$index];
}
// Default to the global setting
- return $this->config->get( 'CapitalLinks' );
+ return $this->options->get( 'CapitalLinks' );
}
/**
* @return bool
*/
public function isNonincludable( $index ) {
- $namespaces = $this->config->get( 'NonincludableNamespaces' );
+ $namespaces = $this->options->get( 'NonincludableNamespaces' );
return $namespaces && in_array( $index, $namespaces );
}
* @return null|string Default model name for the given namespace, if set
*/
public function getNamespaceContentModel( $index ) {
- return $this->config->get( 'NamespaceContentModels' )[$index] ?? null;
+ return $this->options->get( 'NamespaceContentModels' )[$index] ?? null;
}
/**
* Determine which restriction levels it makes sense to use in a namespace,
* optionally filtered by a user's rights.
*
+ * @todo Move this to PermissionManager and remove the dependency here on permissions-related
+ * config settings.
+ *
* @param int $index Index to check
* @param User|null $user User to check
* @return array
*/
public function getRestrictionLevels( $index, User $user = null ) {
- if ( !isset( $this->config->get( 'NamespaceProtection' )[$index] ) ) {
+ if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
// All levels are valid if there's no namespace restriction.
// But still filter by user, if necessary
- $levels = $this->config->get( 'RestrictionLevels' );
+ $levels = $this->options->get( 'RestrictionLevels' );
if ( $user ) {
$levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
$right = $level;
return $levels;
}
- // First, get the list of groups that can edit this namespace.
- $namespaceGroups = [];
- $combine = 'array_merge';
- foreach ( (array)$this->config->get( 'NamespaceProtection' )[$index] as $right ) {
+ // $wgNamespaceProtection can require one or more rights to edit the namespace, which
+ // may be satisfied by membership in multiple groups each giving a subset of those rights.
+ // A restriction level is redundant if, for any one of the namespace rights, all groups
+ // giving that right also give the restriction level's right. Or, conversely, a
+ // restriction level is not redundant if, for every namespace right, there's at least one
+ // group giving that right without the restriction level's right.
+ //
+ // First, for each right, get a list of groups with that right.
+ $namespaceRightGroups = [];
+ foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
if ( $right == 'sysop' ) {
$right = 'editprotected'; // BC
}
$right = 'editsemiprotected'; // BC
}
if ( $right != '' ) {
- $namespaceGroups = call_user_func( $combine, $namespaceGroups,
- User::getGroupsWithPermission( $right ) );
- $combine = 'array_intersect';
+ $namespaceRightGroups[$right] = User::getGroupsWithPermission( $right );
}
}
- // Now, keep only those restriction levels where there is at least one
- // group that can edit the namespace but would be blocked by the
- // restriction.
+ // Now, go through the protection levels one by one.
$usableLevels = [ '' ];
- foreach ( $this->config->get( 'RestrictionLevels' ) as $level ) {
+ foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
$right = $level;
if ( $right == 'sysop' ) {
$right = 'editprotected'; // BC
if ( $right == 'autoconfirmed' ) {
$right = 'editsemiprotected'; // BC
}
- if ( $right != '' && ( !$user || $user->isAllowed( $right ) ) &&
- array_diff( $namespaceGroups, User::getGroupsWithPermission( $right ) )
+
+ if ( $right != '' &&
+ !isset( $namespaceRightGroups[$right] ) &&
+ ( !$user || $user->isAllowed( $right ) )
) {
+ // Do any of the namespace rights imply the restriction right? (see explanation above)
+ foreach ( $namespaceRightGroups as $groups ) {
+ if ( !array_diff( $groups, User::getGroupsWithPermission( $right ) ) ) {
+ // Yes, this one does.
+ continue 2;
+ }
+ }
+ // No, keep the restriction level
$usableLevels[] = $level;
}
}