Constraint/SelfRedirectConstraint.php 0000666 00000005123 15133477336 0014042 0 ustar 00 <?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\EditPage\Constraint;
use Content;
use MediaWiki\Linker\LinkTarget;
use StatusValue;
/**
* Verify the page does not redirect to itself unless
* - the user is okay with a self redirect, or
* - the page already redirected to itself before the edit
*
* @since 1.36
* @internal
*/
class SelfRedirectConstraint implements IEditConstraint {
/** @var bool */
private $allowSelfRedirect;
/** @var Content */
private $newContent;
/** @var Content */
private $originalContent;
/** @var LinkTarget */
private $title;
/** @var string|null */
private $result;
/**
* @param bool $allowSelfRedirect
* @param Content $newContent
* @param Content $originalContent
* @param LinkTarget $title
*/
public function __construct(
bool $allowSelfRedirect,
Content $newContent,
Content $originalContent,
LinkTarget $title
) {
$this->allowSelfRedirect = $allowSelfRedirect;
$this->newContent = $newContent;
$this->originalContent = $originalContent;
$this->title = $title;
}
public function checkConstraint(): string {
if ( !$this->allowSelfRedirect
&& $this->newContent->isRedirect()
&& $this->newContent->getRedirectTarget()->equals( $this->title )
) {
// T29683 If the page already redirects to itself, don't warn.
$currentTarget = $this->originalContent->getRedirectTarget();
if ( !$currentTarget || !$currentTarget->equals( $this->title ) ) {
$this->result = self::CONSTRAINT_FAILED;
return self::CONSTRAINT_FAILED;
}
}
$this->result = self::CONSTRAINT_PASSED;
return self::CONSTRAINT_PASSED;
}
public function getLegacyStatus(): StatusValue {
$statusValue = StatusValue::newGood();
if ( $this->result === self::CONSTRAINT_FAILED ) {
$statusValue->fatal( 'selfredirect' );
$statusValue->value = self::AS_SELF_REDIRECT;
}
return $statusValue;
}
}
Constraint/ChangeTagsConstraint.php 0000666 00000004327 15133477336 0013500 0 ustar 00 <?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\EditPage\Constraint;
use ChangeTags;
use MediaWiki\Permissions\Authority;
use StatusValue;
/**
* Verify user can add change tags
*
* @since 1.36
* @internal
* @author DannyS712
*/
class ChangeTagsConstraint implements IEditConstraint {
/** @var Authority */
private $performer;
/** @var array */
private $tags;
/** @var StatusValue|string */
private $result;
/**
* @param Authority $performer
* @param string[] $tags
*/
public function __construct(
Authority $performer,
array $tags
) {
$this->performer = $performer;
$this->tags = $tags;
}
public function checkConstraint(): string {
if ( !$this->tags ) {
$this->result = self::CONSTRAINT_PASSED;
return self::CONSTRAINT_PASSED;
}
// TODO inject a service once canAddTagsAccompanyingChange is moved to a
// service as part of T245964
$changeTagStatus = ChangeTags::canAddTagsAccompanyingChange(
$this->tags,
$this->performer,
false
);
if ( $changeTagStatus->isOK() ) {
$this->result = self::CONSTRAINT_PASSED;
return self::CONSTRAINT_PASSED;
}
$this->result = $changeTagStatus; // The same status object is returned
return self::CONSTRAINT_FAILED;
}
public function getLegacyStatus(): StatusValue {
if ( $this->result === self::CONSTRAINT_PASSED ) {
$statusValue = StatusValue::newGood();
} else {
$statusValue = $this->result;
$statusValue->value = self::AS_CHANGE_TAG_ERROR;
}
return $statusValue;
}
}
Constraint/EditFilterMergedContentHookConstraint.php 0000666 00000012313 15133477336 0017021 0 ustar 00 <?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\EditPage\Constraint;
use ApiMessage;
use Content;
use IContextSource;
use Language;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Html\Html;
use MediaWiki\Status\Status;
use MediaWiki\User\User;
use Message;
use StatusValue;
/**
* Verify `EditFilterMergedContent` hook
*
* @since 1.36
* @author DannyS712
* @internal
*/
class EditFilterMergedContentHookConstraint implements IEditConstraint {
/** @var HookRunner */
private $hookRunner;
/** @var Content */
private $content;
/** @var IContextSource */
private $hookContext;
/** @var string */
private $summary;
/** @var bool */
private $minorEdit;
/** @var Language */
private $language;
/** @var User */
private $hookUser;
/** @var Status */
private $status;
/** @var string */
private $hookError = '';
/**
* @param HookContainer $hookContainer
* @param Content $content
* @param IContextSource $hookContext NOTE: This should only be passed to the hook.
* @param string $summary
* @param bool $minorEdit
* @param Language $language
* @param User $hookUser NOTE: This should only be passed to the hook.
*/
public function __construct(
HookContainer $hookContainer,
Content $content,
IContextSource $hookContext,
string $summary,
bool $minorEdit,
Language $language,
User $hookUser
) {
$this->hookRunner = new HookRunner( $hookContainer );
$this->content = $content;
$this->hookContext = $hookContext;
$this->summary = $summary;
$this->minorEdit = $minorEdit;
$this->language = $language;
$this->hookUser = $hookUser;
$this->status = Status::newGood();
}
public function checkConstraint(): string {
$hookResult = $this->hookRunner->onEditFilterMergedContent(
$this->hookContext,
$this->content,
$this->status,
$this->summary,
$this->hookUser,
$this->minorEdit
);
if ( !$hookResult ) {
// Error messages etc. could be handled within the hook...
if ( $this->status->isGood() ) {
$this->status->fatal( 'hookaborted' );
// Not setting $this->hookError here is a hack to allow the hook
// to cause a return to the edit page without $this->hookError
// being set. This is used by ConfirmEdit to display a captcha
// without any error message cruft.
} else {
if ( !$this->status->getErrors() ) {
// Provide a fallback error message if none was set
$this->status->fatal( 'hookaborted' );
}
$this->hookError = $this->formatStatusErrors( $this->status );
}
// Use the existing $status->value if the hook set it
if ( !$this->status->value ) {
// T273354: Should be AS_HOOK_ERROR_EXPECTED to display error message
$this->status->value = self::AS_HOOK_ERROR_EXPECTED;
}
return self::CONSTRAINT_FAILED;
}
if ( !$this->status->isOK() ) {
// ...or the hook could be expecting us to produce an error
// FIXME this sucks, we should just use the Status object throughout
if ( !$this->status->getErrors() ) {
// Provide a fallback error message if none was set
$this->status->fatal( 'hookaborted' );
}
$this->hookError = $this->formatStatusErrors( $this->status );
$this->status->value = self::AS_HOOK_ERROR_EXPECTED;
return self::CONSTRAINT_FAILED;
}
return self::CONSTRAINT_PASSED;
}
public function getLegacyStatus(): StatusValue {
// This returns a Status instead of a StatusValue since a Status object is
// used in the hook
return $this->status;
}
/**
* TODO this is really ugly. The constraint shouldn't know that the status
* will be used as wikitext, with is what the hookError represents, rather
* than just the error code. This needs a big refactor to remove the hook
* error string and just rely on the status object entirely.
*
* @internal
* @return string
*/
public function getHookError(): string {
return $this->hookError;
}
/**
* Wrap status errors in error boxes for increased visibility.
* @param Status $status
* @return string
*/
private function formatStatusErrors( Status $status ): string {
$ret = '';
foreach ( $status->getErrors() as $rawError ) {
// XXX: This interface is ugly, but it seems to be the only convenient way to convert a message specifier
// as used in Status to a Message without all the cruft that Status::getMessage & friends add.
$msg = Message::newFromSpecifier( ApiMessage::create( $rawError ) );
$ret .= Html::errorBox( "\n" . $msg->inLanguage( $this->language )->plain() . "\n" );
}
return $ret;
}
}
Constraint/ImageRedirectConstraint.php 0000666 00000004547 15133477336 0014204 0 ustar 00 <?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\EditPage\Constraint;
use Content;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Permissions\Authority;
use StatusValue;
/**
* Verify user permissions:
* If creating a redirect in the file namespace, must have upload rights
*
* @since 1.36
* @internal
* @author DannyS712
*/
class ImageRedirectConstraint implements IEditConstraint {
/** @var Content */
private $newContent;
/** @var LinkTarget */
private $title;
/** @var Authority */
private $performer;
/** @var string|null */
private $result;
/**
* @param Content $newContent
* @param LinkTarget $title
* @param Authority $performer
*/
public function __construct(
Content $newContent,
LinkTarget $title,
Authority $performer
) {
$this->newContent = $newContent;
$this->title = $title;
$this->performer = $performer;
}
public function checkConstraint(): string {
// Check isn't simple enough to just repeat when getting the status
if ( $this->title->getNamespace() === NS_FILE &&
$this->newContent->isRedirect() &&
!$this->performer->isAllowed( 'upload' )
) {
$this->result = self::CONSTRAINT_FAILED;
return self::CONSTRAINT_FAILED;
}
$this->result = self::CONSTRAINT_PASSED;
return self::CONSTRAINT_PASSED;
}
public function getLegacyStatus(): StatusValue {
$statusValue = StatusValue::newGood();
if ( $this->result === self::CONSTRAINT_FAILED ) {
$errorCode = $this->performer->getUser()->isRegistered() ?
self::AS_IMAGE_REDIRECT_LOGGED :
self::AS_IMAGE_REDIRECT_ANON;
$statusValue->setResult( false, $errorCode );
}
return $statusValue;
}
}
Constraint/ChangeTagsConstraintTest.php 0000666 00000004312 15133511265 0014320 0 ustar 00 <?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
*/
use MediaWiki\EditPage\Constraint\ChangeTagsConstraint;
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
/**
* Tests the ChangeTagsConstraint
*
* @author DannyS712
*
* @covers \MediaWiki\EditPage\Constraint\ChangeTagsConstraint
* @group Database
*/
class ChangeTagsConstraintTest extends MediaWikiIntegrationTestCase {
use EditConstraintTestTrait;
use MockAuthorityTrait;
protected function setUp(): void {
parent::setUp();
$this->tablesUsed = array_merge(
$this->tablesUsed,
[ 'change_tag', 'change_tag_def', 'logging' ]
);
}
public function testPass() {
$tagName = 'tag-for-constraint-test-pass';
ChangeTags::defineTag( $tagName );
$constraint = new ChangeTagsConstraint(
$this->mockRegisteredUltimateAuthority(),
[ $tagName ]
);
$this->assertConstraintPassed( $constraint );
}
public function testNoTags() {
// Early return for no tags being added
$constraint = new ChangeTagsConstraint(
$this->mockRegisteredUltimateAuthority(),
[]
);
$this->assertConstraintPassed( $constraint );
}
public function testFailure() {
$tagName = 'tag-for-constraint-test-fail';
ChangeTags::defineTag( $tagName );
$constraint = new ChangeTagsConstraint(
$this->mockRegisteredAuthorityWithoutPermissions( [ 'applychangetags' ] ),
[ $tagName ]
);
$this->assertConstraintFailed(
$constraint,
IEditConstraint::AS_CHANGE_TAG_ERROR
);
}
}
Constraint/EditFilterMergedContentHookConstraintTest.php 0000666 00000007263 15133511265 0017657 0 ustar 00 <?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
*/
use MediaWiki\EditPage\Constraint\EditFilterMergedContentHookConstraint;
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Status\Status;
use MediaWiki\User\User;
use Wikimedia\TestingAccessWrapper;
/**
* Tests the EditFilterMergedContentHookConstraint
*
* @author DannyS712
*
* @covers \MediaWiki\EditPage\Constraint\EditFilterMergedContentHookConstraint
* @todo Make this a unit test when Message will no longer use the global state.
*/
class EditFilterMergedContentHookConstraintTest extends MediaWikiIntegrationTestCase {
use EditConstraintTestTrait;
private function getConstraint( $hookResult ) {
$hookContainer = $this->createMock( HookContainer::class );
$hookContainer->expects( $this->once() )
->method( 'run' )
->with(
'EditFilterMergedContent',
$this->anything() // Not worrying about the hook call here
)
->willReturn( $hookResult );
$language = $this->createMock( Language::class );
$language->method( 'getCode' )
->willReturn( 'en' );
$constraint = new EditFilterMergedContentHookConstraint(
$hookContainer,
$this->getMockForAbstractClass( Content::class ),
$this->createMock( RequestContext::class ),
'EditSummaryGoesHere',
true, // Minor edit
$language,
$this->createMock( User::class )
);
return $constraint;
}
public function testPass() {
$constraint = $this->getConstraint( true );
$this->assertConstraintPassed( $constraint );
$this->assertSame( '', $constraint->getHookError() );
}
public function testFailure_goodStatus() {
// Code path 1: Hook returns false, but status is still good
// Status has no value set, falls back to AS_HOOK_ERROR_EXPECTED
$constraint = $this->getConstraint( false );
$this->assertConstraintFailed( $constraint, IEditConstraint::AS_HOOK_ERROR_EXPECTED );
}
public function testFailure_badStatus() {
// Code path 2: Hook returns false, status is bad
// To avoid using the real Status::getWikiText, which can use global state, etc.,
// replace the status object with a mock
$constraint = $this->getConstraint( false );
$mockStatus = $this->getMockBuilder( Status::class )
->onlyMethods( [ 'isGood', 'getWikiText' ] )
->getMock();
$mockStatus->method( 'isGood' )->willReturn( false );
$mockStatus->method( 'getWikiText' )->willReturn( 'WIKITEXT' );
$mockStatus->value = 12345;
TestingAccessWrapper::newFromObject( $constraint )->status = $mockStatus;
$this->assertConstraintFailed(
$constraint,
12345 // Value is set in hook (or in this case in the mock)
);
}
public function testFailure_notOKStatus() {
// Code path 3: Hook returns true, but status is not okay
$constraint = $this->getConstraint( true );
$status = Status::newGood();
$status->setOK( false );
TestingAccessWrapper::newFromObject( $constraint )->status = $status;
$this->assertConstraintFailed(
$constraint,
IEditConstraint::AS_HOOK_ERROR_EXPECTED
);
}
}
Constraint/SelfRedirectConstraintTest.php 0000666 00000004677 15133511710 0014700 0 ustar 00 <?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
*/
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\EditPage\Constraint\SelfRedirectConstraint;
use MediaWiki\Title\Title;
/**
* Tests the SelfRedirectConstraint
*
* @author DannyS712
*
* @covers \MediaWiki\EditPage\Constraint\SelfRedirectConstraint
*/
class SelfRedirectConstraintTest extends MediaWikiUnitTestCase {
use EditConstraintTestTrait;
private function getContent( $title, $isSelfRedirect ) {
$content = $this->createMock( Content::class );
$contentRedirectTarget = $this->createMock( Title::class );
// No $this->once() since only called for the new content
$content->method( 'isRedirect' )
->willReturn( true );
$content->expects( $this->once() )
->method( 'getRedirectTarget' )
->willReturn( $contentRedirectTarget );
$contentRedirectTarget->expects( $this->once() )
->method( 'equals' )
->with( $title )
->willReturn( $isSelfRedirect );
return $content;
}
public function testPass() {
// New content is a self redirect, but so is existing content, so no warning
$title = $this->createMock( Title::class );
$constraint = new SelfRedirectConstraint(
false, // $allowSelfRedirect
$this->getContent( $title, true ),
$this->getContent( $title, true ),
$title
);
$this->assertConstraintPassed( $constraint );
}
public function testFailure() {
// New content is a self redirect, but existing content is not
$title = $this->createMock( Title::class );
$constraint = new SelfRedirectConstraint(
false, // $allowSelfRedirect
$this->getContent( $title, true ),
$this->getContent( $title, false ),
$title
);
$this->assertConstraintFailed(
$constraint,
IEditConstraint::AS_SELF_REDIRECT
);
}
}
Constraint/ImageRedirectConstraintTest.php 0000666 00000005027 15133511710 0015017 0 ustar 00 <?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
*/
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\EditPage\Constraint\ImageRedirectConstraint;
use MediaWiki\Permissions\Authority;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
use MediaWiki\Title\Title;
/**
* Tests the ImageRedirectConstraint
*
* @author DannyS712
*
* @covers \MediaWiki\EditPage\Constraint\ImageRedirectConstraint
*/
class ImageRedirectConstraintTest extends MediaWikiUnitTestCase {
use EditConstraintTestTrait;
use MockAuthorityTrait;
/**
* @param Authority $performer
* @return ImageRedirectConstraint
*/
private function getConstraint( Authority $performer ) {
$content = $this->createMock( Content::class );
$content->method( 'isRedirect' )->willReturn( true );
$title = $this->createMock( Title::class );
$title->method( 'getNamespace' )->willReturn( NS_FILE );
return new ImageRedirectConstraint(
$content,
$title,
$performer
);
}
public function testPass() {
$constraint = $this->getConstraint( $this->mockRegisteredUltimateAuthority() );
$this->assertConstraintPassed( $constraint );
}
/**
* @dataProvider provideTestFailure
* @param Authority $performer
* @param int $expectedValue
*/
public function testFailure( Authority $performer, int $expectedValue ) {
$constraint = $this->getConstraint( $performer );
$this->assertConstraintFailed( $constraint, $expectedValue );
}
public function provideTestFailure() {
yield 'Anonymous user' => [
'performer' => $this->mockAnonAuthorityWithoutPermissions( [ 'upload' ] ),
'expectedValue' => IEditConstraint::AS_IMAGE_REDIRECT_ANON
];
yield 'Registered user' => [
'performer' => $this->mockRegisteredAuthorityWithoutPermissions( [ 'upload' ] ),
'expectedValue' => IEditConstraint::AS_IMAGE_REDIRECT_LOGGED
];
}
}