use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Block\BlockRestriction;
use MediaWiki\Block\Restriction\Restriction;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\MediaWikiServices;
class Block {
return false;
}
+
+ /**
+ * Checks if a block applies to a particular namespace
+ *
+ * @since 1.33
+ *
+ * @param int $ns
+ * @return bool
+ */
+ public function appliesToNamespace( $ns ) {
+ if ( $this->isSitewide() ) {
+ return true;
+ }
+
+ // Blocks do not apply to virtual namespaces.
+ if ( $ns < 0 ) {
+ return false;
+ }
+
+ $restriction = $this->findRestriction( NamespaceRestriction::TYPE, $ns );
+
+ return (bool)$restriction;
+ }
+
+ /**
+ * Checks if a block applies to a particular page
+ *
+ * This check does not consider whether `$this->prevents( 'editownusertalk' )`
+ * returns false, as the identity of the user making the hypothetical edit
+ * isn't known here (particularly in the case of IP hardblocks, range
+ * blocks, and auto-blocks).
+ *
+ * @since 1.33
+ *
+ * @param int $pageId
+ * @return bool
+ */
+ public function appliesToPage( $pageId ) {
+ if ( $this->isSitewide() ) {
+ return true;
+ }
+
+ // If the pageId is not over zero, the block cannot apply to it.
+ if ( $pageId <= 0 ) {
+ return false;
+ }
+
+ $restriction = $this->findRestriction( PageRestriction::TYPE, $pageId );
+
+ return (bool)$restriction;
+ }
+
+ /**
+ * Find Restriction by type and value.
+ *
+ * @param string $type
+ * @param int $value
+ * @return Restriction|null
+ */
+ private function findRestriction( $type, $value ) {
+ $restrictions = $this->getRestrictions();
+ foreach ( $restrictions as $restriction ) {
+ if ( $restriction->getType() !== $type ) {
+ continue;
+ }
+
+ if ( $restriction->getValue() === $value ) {
+ return $restriction;
+ }
+ }
+
+ return null;
+ }
}
namespace MediaWiki\Block;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\Restriction\Restriction;
use Wikimedia\Rdbms\IResultWrapper;
class BlockRestriction {
+ /**
+ * Map of all of the restriction types.
+ */
+ private static $types = [
+ PageRestriction::TYPE_ID => PageRestriction::class,
+ NamespaceRestriction::TYPE_ID => NamespaceRestriction::class,
+ ];
+
/**
* Retrieves the restrictions from the database by block id.
*
* @return Restriction|null
*/
private static function rowToRestriction( \stdClass $row ) {
- switch ( $row->ir_type ) {
- case PageRestriction::TYPE_ID:
- return PageRestriction::newFromRow( $row );
- default:
- return null;
+ if ( array_key_exists( (int)$row->ir_type, self::$types ) ) {
+ $class = self::$types[ (int)$row->ir_type ];
+ return call_user_func( [ $class, 'newFromRow' ], $row );
}
+
+ return null;
}
}
abstract class AbstractRestriction implements Restriction {
+ /**
+ * @var string
+ */
+ const TYPE = '';
+
+ /**
+ * @var int
+ */
+ const TYPE_ID = 0;
+
/**
* @var int
*/
$this->value = (int)$value;
}
+ /**
+ * {@inheritdoc}
+ */
+ public static function getType() {
+ return static::TYPE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getTypeId() {
+ return static::TYPE_ID;
+ }
+
/**
* {@inheritdoc}
*/
--- /dev/null
+<?php
+/**
+ * A Block restriction object of type 'Namespace'.
+ *
+ * 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\Block\Restriction;
+
+class NamespaceRestriction extends AbstractRestriction {
+
+ /**
+ * {@inheritdoc}
+ */
+ const TYPE = 'ns';
+
+ /**
+ * {@inheritdoc}
+ */
+ const TYPE_ID = 2;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function matches( \Title $title ) {
+ return $this->getValue() === $title->getNamespace();
+ }
+
+}
class PageRestriction extends AbstractRestriction {
- const TYPE = 'page';
- const TYPE_ID = 1;
-
/**
- * @var \Title
+ * {@inheritdoc}
*/
- protected $title;
+ const TYPE = 'page';
/**
* {@inheritdoc}
*/
- public function matches( \Title $title ) {
- return $title->equals( $this->getTitle() );
- }
+ const TYPE_ID = 1;
/**
- * {@inheritdoc}
+ * @var \Title
*/
- public function getType() {
- return self::TYPE;
- }
+ protected $title;
/**
* {@inheritdoc}
*/
- public function getTypeId() {
- return self::TYPE_ID;
+ public function matches( \Title $title ) {
+ return $title->equals( $this->getTitle() );
}
/**
* @since 1.33
* @return string
*/
- public function getType();
+ public static function getType();
/**
* Gets the id of the type of restriction. This id is used in the database.
* @since 1.33
* @return string
*/
- public function getTypeId();
+ public static function getTypeId();
/**
* Creates a new Restriction from a database row.
// Special handling for a user's own talk page. The block is not aware
// of the user, so this must be done here.
if ( $title->equals( $this->getTalkPage() ) ) {
- // If the block is sitewide, then whatever is set is what is honored.
if ( $block->isSitewide() ) {
+ // If the block is sitewide, whatever is set is what is honored.
+ // This must be checked here, because Block::appliesToPage will
+ // return true for a sitewide block.
$blocked = $block->prevents( 'editownusertalk' );
} else {
- // If the block is partial, ignore 'editownusertalk' unless
- // there is a restriction on the user talk namespace.
- // TODO: To be implemented with Namespace restrictions
- $blocked = $block->appliesToTitle( $title );
+ // The page restrictions always take precedence over the namespace
+ // restrictions. If the user is explicity blocked from their own
+ // talk page, nothing can change that.
+ $blocked = $block->appliesToPage( $title->getArticleID() );
+
+ // If the block applies to the user talk namespace, then whatever is
+ // set is what is honored.
+ if ( !$blocked && $block->appliesToNamespace( NS_USER_TALK ) ) {
+ $blocked = $block->prevents( 'editownusertalk' );
+ }
+
+ // If another type of restriction is added, it should be checked
+ // here.
}
} else {
$blocked = $block->appliesToTitle( $title );
use MediaWiki\Block\BlockRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
/**
* @group Database
$pageFoo = $this->getExistingTestPage( 'Foo' );
$pageBar = $this->getExistingTestPage( 'Bar' );
+ $pageJohn = $this->getExistingTestPage( 'User:John' );
$pageRestriction = new PageRestriction( $block->getId(), $pageFoo->getId() );
- BlockRestriction::insert( [ $pageRestriction ] );
+ $namespaceRestriction = new NamespaceRestriction( $block->getId(), NS_USER );
+ BlockRestriction::insert( [ $pageRestriction, $namespaceRestriction ] );
$this->assertTrue( $block->appliesToTitle( $pageFoo->getTitle() ) );
$this->assertFalse( $block->appliesToTitle( $pageBar->getTitle() ) );
+ $this->assertTrue( $block->appliesToTitle( $pageJohn->getTitle() ) );
+
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::appliesToNamespace
+ * @covers Block::appliesToPage
+ */
+ public function testAppliesToReturnsTrueOnSitewideBlock() {
+ $user = $this->getTestUser()->getUser();
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => true,
+ 'sitewide' => true
+ ] );
+
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ $block->insert();
+
+ $title = $this->getExistingTestPage()->getTitle();
+
+ $this->assertTrue( $block->appliesToPage( $title->getArticleID() ) );
+ $this->assertTrue( $block->appliesToNamespace( NS_MAIN ) );
+ $this->assertTrue( $block->appliesToNamespace( NS_USER_TALK ) );
+
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::appliesToPage
+ */
+ public function testAppliesToPageOnPartialPageBlock() {
+ $user = $this->getTestUser()->getUser();
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => true,
+ 'sitewide' => false
+ ] );
+
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ $block->insert();
+
+ $title = $this->getExistingTestPage()->getTitle();
+
+ $pageRestriction = new PageRestriction(
+ $block->getId(),
+ $title->getArticleID()
+ );
+ BlockRestriction::insert( [ $pageRestriction ] );
+
+ $this->assertTrue( $block->appliesToPage( $title->getArticleID() ) );
+
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::appliesToNamespace
+ */
+ public function testAppliesToNamespaceOnPartialNamespaceBlock() {
+ $user = $this->getTestUser()->getUser();
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => true,
+ 'sitewide' => false
+ ] );
+
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ $block->insert();
+
+ $namespaceRestriction = new NamespaceRestriction( $block->getId(), NS_MAIN );
+ BlockRestriction::insert( [ $namespaceRestriction ] );
+
+ $this->assertTrue( $block->appliesToNamespace( NS_MAIN ) );
+ $this->assertFalse( $block->appliesToNamespace( NS_USER ) );
$block->delete();
}
namespace MediaWiki\Tests\Block;
use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\Restriction\Restriction;
$pageBar = $this->getExistingTestPage( 'Bar' );
BlockRestriction::insert( [
- new PageRestriction( $block->getId(), $pageFoo->getId() ),
- new PageRestriction( $block->getId(), $pageBar->getId() ),
+ new PageRestriction( $block->getId(), $pageFoo->getId() ),
+ new PageRestriction( $block->getId(), $pageBar->getId() ),
+ new NamespaceRestriction( $block->getId(), NS_USER ),
] );
$restrictions = BlockRestriction::loadByBlockId( $block->getId() );
- $this->assertCount( 2, $restrictions );
+ $this->assertCount( 3, $restrictions );
}
/**
// valid type
$this->insertRestriction( $block->getId(), PageRestriction::TYPE_ID, $pageFoo->getId() );
+ $this->insertRestriction( $block->getId(), NamespaceRestriction::TYPE_ID, NS_USER );
// invalid type
$this->insertRestriction( $block->getId(), 9, $pageBar->getId() );
+ $this->insertRestriction( $block->getId(), 10, NS_FILE );
$restrictions = BlockRestriction::loadByBlockId( $block->getId() );
- $this->assertCount( 1, $restrictions );
+ $this->assertCount( 2, $restrictions );
}
/**
* @covers ::resultToRestrictions
* @covers ::rowToRestriction
*/
- public function testMappingRestrictionObject() {
+ public function testMappingPageRestrictionObject() {
$block = $this->insertBlock();
$title = 'Lady Macbeth';
$page = $this->getExistingTestPage( $title );
+ // Test Page Restrictions.
BlockRestriction::insert( [
- new PageRestriction( $block->getId(), $page->getId() ),
+ new PageRestriction( $block->getId(), $page->getId() ),
] );
$restrictions = BlockRestriction::loadByBlockId( $block->getId() );
$this->assertEquals( $pageRestriction->getTitle()->getText(), $title );
}
+ /**
+ * @covers ::loadByBlockId
+ * @covers ::resultToRestrictions
+ * @covers ::rowToRestriction
+ */
+ public function testMappingNamespaceRestrictionObject() {
+ $block = $this->insertBlock();
+
+ BlockRestriction::insert( [
+ new NamespaceRestriction( $block->getId(), NS_USER ),
+ ] );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+
+ list( $namespaceRestriction ) = $restrictions;
+ $this->assertInstanceOf( NamespaceRestriction::class, $namespaceRestriction );
+ $this->assertEquals( $block->getId(), $namespaceRestriction->getBlockId() );
+ $this->assertSame( NS_USER, $namespaceRestriction->getValue() );
+ $this->assertEquals( $namespaceRestriction->getType(), NamespaceRestriction::TYPE );
+ }
+
/**
* @covers ::insert
*/
new \stdClass(),
new PageRestriction( $block->getId(), $pageFoo->getId() ),
new PageRestriction( $block->getId(), $pageBar->getId() ),
+ new NamespaceRestriction( $block->getId(), NS_USER )
];
$result = BlockRestriction::insert( $restrictions );
$pageFoo = $this->getExistingTestPage( 'Foo' );
$pageBar = $this->getExistingTestPage( 'Bar' );
- $namespace = $this->createMock( Restriction::class );
- $namespace->method( 'toRow' )
- ->willReturn( [
- 'ir_ipb_id' => $block->getId(),
- 'ir_type' => 2,
- 'ir_value' => 0,
- ] );
-
$invalid = $this->createMock( Restriction::class );
$invalid->method( 'toRow' )
->willReturn( [
new \stdClass(),
new PageRestriction( $block->getId(), $pageFoo->getId() ),
new PageRestriction( $block->getId(), $pageBar->getId() ),
- $namespace,
+ new NamespaceRestriction( $block->getId(), NS_USER ),
$invalid,
];
$this->assertTrue( $result );
$restrictions = BlockRestriction::loadByBlockId( $block->getId() );
- $this->assertCount( 2, $restrictions );
+ $this->assertCount( 3, $restrictions );
}
/**
BlockRestriction::update( [
new \stdClass(),
new PageRestriction( $block->getId(), $pageBar->getId() ),
+ new NamespaceRestriction( $block->getId(), NS_USER ),
] );
$db = wfGetDb( DB_REPLICA );
[ 'ir_ipb_id' => $block->getId() ]
);
- $this->assertEquals( 1, $result->numRows() );
+ $this->assertEquals( 2, $result->numRows() );
$row = $result->fetchObject();
$this->assertEquals( $block->getId(), $row->ir_ipb_id );
$this->assertEquals( $pageBar->getId(), $row->ir_value );
$block = $this->insertBlock();
$page = $this->getExistingTestPage( 'Foo' );
BlockRestriction::insert( [
- new PageRestriction( $block->getId(), $page->getId() ),
+ new PageRestriction( $block->getId(), $page->getId() ),
] );
BlockRestriction::update( [
],
true
],
+ [
+ [
+ new NamespaceRestriction( 1, NS_USER ),
+ ],
+ [
+ new NamespaceRestriction( 1, NS_USER ),
+ ],
+ true
+ ],
+ [
+ [
+ new NamespaceRestriction( 1, NS_USER ),
+ ],
+ [
+ new NamespaceRestriction( 1, NS_TALK ),
+ ],
+ false
+ ],
];
}
new \stdClass(),
new PageRestriction( 1, 1 ),
new PageRestriction( 1, 2 ),
+ new NamespaceRestriction( 1, NS_USER ),
];
- $result = BlockRestriction::setBlockId( 2, $restrictions );
-
$this->assertSame( 1, $restrictions[1]->getBlockId() );
$this->assertSame( 1, $restrictions[2]->getBlockId() );
- $this->assertSame( 2, $result[0]->getBlockId() );
- $this->assertSame( 2, $result[1]->getBlockId() );
+ $this->assertSame( 1, $restrictions[3]->getBlockId() );
+
+ $result = BlockRestriction::setBlockId( 2, $restrictions );
+
+ foreach ( $result as $restriction ) {
+ $this->assertSame( 2, $restriction->getBlockId() );
+ }
}
protected function insertBlock() {
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Block\Restriction;
+
+use MediaWiki\Block\Restriction\NamespaceRestriction;
+
+/**
+ * @group Database
+ * @group Blocking
+ * @covers \MediaWiki\Block\Restriction\AbstractRestriction
+ * @covers \MediaWiki\Block\Restriction\NamespaceRestriction
+ */
+class NamespaceRestrictionTest extends RestrictionTestCase {
+
+ public function testMatches() {
+ $class = $this->getClass();
+ $page = $this->getExistingTestPage( 'Saturn' );
+ $restriction = new $class( 1, NS_MAIN );
+ $this->assertTrue( $restriction->matches( $page->getTitle() ) );
+
+ $page = $this->getExistingTestPage( 'Talk:Saturn' );
+ $this->assertFalse( $restriction->matches( $page->getTitle() ) );
+ }
+
+ public function testGetType() {
+ $class = $this->getClass();
+ $restriction = new $class( 1, 2 );
+ $this->assertEquals( 'ns', $restriction->getType() );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getClass() {
+ return NamespaceRestriction::class;
+ }
+}
define( 'NS_UNITTEST_TALK', 5601 );
use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\MediaWikiServices;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\TestingAccessWrapper;
);
$restrictions[] = new PageRestriction( 0, $page->getId() );
}
+ foreach ( $options['namespaceRestrictions'] ?? [] as $ns ) {
+ $restrictions[] = new NamespaceRestriction( 0, $ns );
+ }
$block = new Block( [
'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
'blockAllowsUTEdit' => false,
]
],
+ 'Partial namespace block, not allowing user talk' => [ self::USER_TALK_PAGE, true, [
+ 'allowUsertalk' => false,
+ 'namespaceRestrictions' => [ NS_USER_TALK ],
+ ] ],
+ 'Partial namespace block, not allowing user talk' => [ self::USER_TALK_PAGE, false, [
+ 'allowUsertalk' => true,
+ 'namespaceRestrictions' => [ NS_USER_TALK ],
+ ] ],
];
}