@@ -0,0 +1,238 @@
<?php

namespace Wikibase\Api;

use ApiBase;
use ApiMain;
use ApiResult;
use UsageException;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdParsingException;
use Wikibase\Repo\Interactors\RedirectCreationException;
use Wikibase\Repo\Interactors\RedirectCreationInteractor;
use Wikibase\Repo\WikibaseRepo;

/**
* API module for creating entity redirects.
*
* @since 0.5
*
* @licence GNU GPL v2+
* @author Daniel Kinzler
*/
class CreateRedirectModule extends ApiBase {

/**
* @var EntityIdParser
*/
private $idParser;

/**
* @var RedirectCreationInteractor
*/
private $createRedirectInteractor;

/**
* @var ApiErrorReporter
*/
private $errorReporter;

/**
* @param ApiMain $mainModule
* @param string $moduleName
* @param string $modulePrefix
*
* @see ApiBase::__construct
*/
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
parent::__construct( $mainModule, $moduleName, $modulePrefix );

$errorReporter = new ApiErrorReporter(
$this,
WikibaseRepo::getDefaultInstance()->getExceptionLocalizer(),
$this->getLanguage()
);

$this->setServices(
WikibaseRepo::getDefaultInstance()->getEntityIdParser(),
$errorReporter,
new RedirectCreationInteractor(
WikibaseRepo::getDefaultInstance()->getEntityRevisionLookup( 'uncached' ),
WikibaseRepo::getDefaultInstance()->getEntityStore(),
WikibaseRepo::getDefaultInstance()->getEntityPermissionChecker(),
WikibaseRepo::getDefaultInstance()->getSummaryFormatter(),
$this->getUser()
)
);
}

public function setServices(
EntityIdParser $idParser,
ApiErrorReporter $errorReporter,
RedirectCreationInteractor $createRedirectInteractor
) {
$this->idParser = $idParser;
$this->errorReporter = $errorReporter;

$this->createRedirectInteractor = $createRedirectInteractor;
}

/**
* @see ApiBase::execute()
*/
public function execute() {
wfProfileIn( __METHOD__ );

$params = $this->extractRequestParams();

try {
$fromId = $this->idParser->parse( $params['from'] );
$toId = $this->idParser->parse( $params['to'] );

$this->createRedirect( $fromId, $toId, $this->getResult() );
} catch ( EntityIdParsingException $ex ) {
$this->errorReporter->dieException( $ex, 'invalid-entity-id' );
} catch ( RedirectCreationException $ex ) {
$this->handleRedirectCreationException( $ex );
}

wfProfileOut( __METHOD__ );
}

/**
* @param EntityId $fromId
* @param EntityId $toId
* @param ApiResult $result The result object to report the result to.
*
* @throws RedirectCreationException
*/
private function createRedirect( EntityId $fromId, EntityId $toId, ApiResult $result ) {
$this->createRedirectInteractor->createRedirect( $fromId, $toId );

$result->addValue( null, 'success', 1 );
$result->addValue( null, 'redirect', $toId->getSerialization() );
}

/**
* @param RedirectCreationException $ex
*
* @throws UsageException always
*/
private function handleRedirectCreationException( RedirectCreationException $ex ) {
$cause = $ex->getPrevious();

if ( $cause ) {
$this->errorReporter->dieException( $cause, $ex->getErrorCode() );
} else {
$this->errorReporter->dieError( $ex->getMessage(), $ex->getErrorCode() );
}
}

/**
* Returns a list of all possible errors returned by the module
* @return array in the format of array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... )
*/
public function getPossibleErrors() {
$errors = array();

foreach ( $this->createRedirectInteractor->getErrorCodeInfo() as $code => $info ) {
$errors[$code] = $info;
}

return array_merge( parent::getPossibleErrors(), $errors );
}

/**
* @see ApiBase::isWriteMode()
*/
public function isWriteMode() {
return true;
}

/**
* @see ApiBase::needsToken()
*/
public function needsToken() {
return true;
}

/**
* @return string The empty string to indicate we need a token, but no salt.
*/
public function getTokenSalt() {
return '';
}

/**
* @see ApiBase::mustBePosted()
*/
public function mustBePosted() {
return true;
}

/**
* Returns an array of allowed parameters (parameter name) => (default
* value) or (parameter name) => (array with PARAM_* constants as keys)
* Don't call this function directly: use getFinalParams() to allow
* hooks to modify parameters as needed.
* @return array|bool
*/
public function getAllowedParams() {
return array(
'from' => array(
ApiBase::PARAM_TYPE => 'string',
),
'to' => array(
ApiBase::PARAM_TYPE => 'string',
),
'token' => array(
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => 'true',
),
'bot' => array(
ApiBase::PARAM_TYPE => 'boolean',
ApiBase::PARAM_DFLT => false,
)
);
}

/**
* Get final parameter descriptions, after hooks have had a chance to tweak it as
* needed.
*
* @return array|bool False on no parameter descriptions
*/
public function getParamDescription() {
return array(
'from' => array( 'Entity ID to make a redirect' ),
'to' => array( 'Entity ID to point the redirect to' ),
'token' => array( 'A "edittoken" token previously obtained through the token module' ),
'bot' => array( 'Mark this edit as bot',
'This URL flag will only be respected if the user belongs to the group "bot".'
),
);
}

/**
* Returns the description string for this module
* @return mixed string or array of strings
*/
public function getDescription() {
return array(
'API module for creating Entity redirects.'
);
}

/**
* Returns usage examples for this module. Return false if no examples are available.
* @return bool|string|array
*/
protected function getExamples() {
return array(
'api.php?action=wbcreateredirect&from=Q11&to=Q12'
=> 'Turn Q11 into a redirect to Q12',
);
}

}
@@ -0,0 +1,164 @@
<?php

namespace Wikibase\Test\Api;

use Status;
use User;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\Property;
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\Lib\Store\EntityRedirect;
use Wikibase\Lib\Store\UnresolvedRedirectException;
use Wikibase\Repo\Interactors\RedirectCreationException;
use Wikibase\Repo\Interactors\RedirectCreationInteractor;
use Wikibase\Repo\WikibaseRepo;
use Wikibase\Summary;
use Wikibase\Test\MockRepository;

/**
* @covers Wikibase\Repo\Interactors\RedirectCreationInteractor
*
* @group API
* @group Wikibase
* @group WikibaseAPI
* @group WikibaseRepo
*
* @licence GNU GPL v2+
* @author Daniel Kinzler
*/
class RedirectCreationInteractorTest extends \PHPUnit_Framework_TestCase {

/**
* @var MockRepository
*/
private $repo = null;

public function setUp() {
parent::setUp();

$this->repo = new MockRepository();

// empty item
$item = Item::newEmpty();
$item->setId( new ItemId( 'Q11' ) );
$this->repo->putEntity( $item );

// non-empty item
$item->setLabel( 'en', 'Foo' );
$item->setId( new ItemId( 'Q12' ) );
$this->repo->putEntity( $item );

// a property
$prop = Property::newEmpty();
$prop->setId( new PropertyId( 'P11' ) );
$this->repo->putEntity( $prop );

// another property
$prop->setId( new PropertyId( 'P12' ) );
$this->repo->putEntity( $prop );

// redirect
$redirect = new EntityRedirect( new ItemId( 'Q22' ), new ItemId( 'Q12' ) );
$this->repo->putRedirect( $redirect );
}

private function getPermissionCheckers() {
$permissionChecker = $this->getMock( 'Wikibase\EntityPermissionChecker' );

$permissionChecker->expects( $this->any() )
->method( 'getPermissionStatusForEntityId' )
->will( $this->returnCallback( function( User $user, $permission, EntityId $id ) {
if ( $user->getName() === 'UserWithoutPermission' && $permission === 'edit' ) {
return Status::newFatal( 'permissiondenied' );
} else {
return Status::newGood();
}
} ) );

return $permissionChecker;
}

/**
* @param User $user
*
* @return RedirectCreationInteractor
*/
private function newInteractor( User $user = null ) {
if ( !$user ) {
$user = $GLOBALS['wgUser'];
}

$summaryFormatter = WikibaseRepo::getDefaultInstance()->getSummaryFormatter();

$interactor = new RedirectCreationInteractor(
$this->repo,
$this->repo,
$this->getPermissionCheckers(),
$summaryFormatter,
$user
);

return $interactor;
}

public function createRedirectProvider_success() {
return array(
'redirect empty entity' => array( new ItemId( 'Q11' ), new ItemId( 'Q12' ) ),
'update redirect' => array( new ItemId( 'Q22' ), new ItemId( 'Q11' ) ),
);
}

/**
* @dataProvider createRedirectProvider_success
*/
public function testCreateRedirect_success( EntityId $fromId, EntityId $toId ) {
$interactor = $this->newInteractor();

$interactor->createRedirect( $fromId, $toId );

try {
$this->repo->getEntity( $fromId );
$this->fail( 'getEntity( ' . $fromId->getSerialization() . ' ) did not throw an UnresolvedRedirectException' );
} catch ( UnresolvedRedirectException $ex ) {
$this->assertEquals( $toId->getSerialization(), $ex->getRedirectTargetId()->getSerialization() );
}
}

public function createRedirectProvider_failure() {
return array(
'source not found' => array( new ItemId( 'Q77' ), new ItemId( 'Q12' ), 'no-such-entity' ),
'target not found' => array( new ItemId( 'Q11' ), new ItemId( 'Q77' ), 'no-such-entity' ),
'target is a redirect' => array( new ItemId( 'Q11' ), new ItemId( 'Q22' ), 'target-is-redirect' ),
'target is incompatible' => array( new ItemId( 'Q11' ), new PropertyId( 'P11' ), 'target-is-incompatible' ),

'source not empty' => array( new ItemId( 'Q12' ), new ItemId( 'Q11' ), 'not-empty' ),
'can\'t redirect' => array( new PropertyId( 'P11' ), new PropertyId( 'P12' ), 'cant-redirect' ),
);
}

/**
* @dataProvider createRedirectProvider_failure
*/
public function testCreateRedirect_failure( EntityId $fromId, EntityId $toId, $expectedCode ) {
$interactor = $this->newInteractor();

try {
$interactor->createRedirect( $fromId, $toId );
$this->fail( 'createRedirect not fail with error ' . $expectedCode . ' as expected!' );
} catch ( RedirectCreationException $ex ) {
$this->assertEquals( $expectedCode, $ex->getErrorCode() );
}
}

public function testSetRedirect_noPermission() {
$this->setExpectedException( 'Wikibase\Repo\Interactors\RedirectCreationException' );

$user = User::newFromName( 'UserWithoutPermission' );

$interactor = $this->newInteractor( $user );
$interactor->createRedirect( new ItemId( 'Q11' ), new ItemId( 'Q12' ) );
}

}
@@ -0,0 +1,216 @@
<?php

namespace Wikibase\Test\Api;

use ApiMain;
use FauxRequest;
use Language;
use Status;
use UsageException;
use User;
use Wikibase\Api\ApiErrorReporter;
use Wikibase\Api\CreateRedirectModule;
use Wikibase\DataModel\Entity\BasicEntityIdParser;
use Wikibase\DataModel\Entity\EntityId;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\ItemId;
use Wikibase\DataModel\Entity\Property;
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\Lib\Store\EntityRedirect;
use Wikibase\Repo\Interactors\RedirectCreationInteractor;
use Wikibase\Repo\WikibaseRepo;
use Wikibase\Test\MockRepository;

/**
* @covers Wikibase\Api\CreateRedirectModule
*
* @group API
* @group Wikibase
* @group WikibaseAPI
* @group WikibaseRepo
*
* @licence GNU GPL v2+
* @author Daniel Kinzler
*/
class CreateRedirectModuleTest extends \PHPUnit_Framework_TestCase {

/**
* @var MockRepository
*/
private $repo = null;

public function setUp() {
parent::setUp();

$this->repo = new MockRepository();

// empty item
$item = Item::newEmpty();
$item->setId( new ItemId( 'Q11' ) );
$this->repo->putEntity( $item );

// non-empty item
$item->setLabel( 'en', 'Foo' );
$item->setId( new ItemId( 'Q12' ) );
$this->repo->putEntity( $item );

// a property
$prop = Property::newEmpty();
$prop->setId( new PropertyId( 'P11' ) );
$this->repo->putEntity( $prop );

// another property
$prop->setId( new PropertyId( 'P12' ) );
$this->repo->putEntity( $prop );

// redirect
$redirect = new EntityRedirect( new ItemId( 'Q22' ), new ItemId( 'Q12' ) );
$this->repo->putRedirect( $redirect );
}

private function getPermissionCheckers() {
$permissionChecker = $this->getMock( 'Wikibase\EntityPermissionChecker' );

$permissionChecker->expects( $this->any() )
->method( 'getPermissionStatusForEntityId' )
->will( $this->returnCallback( function( User $user, $permission, EntityId $id ) {
if ( $user->getName() === 'UserWithoutPermission' && $permission === 'edit' ) {
return Status::newFatal( 'permissiondenied' );
} else {
return Status::newGood();
}
} ) );

return $permissionChecker;
}

/**
* @param array $params
* @param User $user
*
* @return CreateRedirectModule
*/
private function newApiModule( $params, User $user = null ) {
if ( !$user ) {
$user = $GLOBALS['wgUser'];
}

$request = new FauxRequest( $params, true );
$main = new ApiMain( $request );
$main->getContext()->setUser( $user );

$module = new CreateRedirectModule( $main, 'wbcreateredirect' );

$idParser = new BasicEntityIdParser();

$errorReporter = new ApiErrorReporter(
$module,
WikibaseRepo::getDefaultInstance()->getExceptionLocalizer(),
Language::factory( 'en' )
);

$summaryFormatter = WikibaseRepo::getDefaultInstance()->getSummaryFormatter();

$module->setServices(
$idParser,
$errorReporter,
new RedirectCreationInteractor(
$this->repo,
$this->repo,
$this->getPermissionCheckers(),
$summaryFormatter,
$user
)
);

return $module;
}

private function callApiModule( $params, User $user = null ) {
global $wgUser;

if ( !isset( $params['token'] ) ) {
$params['token'] = $wgUser->getToken();
}

$module = $this->newApiModule( $params, $user );

$module->execute();
$result = $module->getResult();

return $result->getData();
}

private function assertSuccess( $result ) {
$this->assertArrayHasKey( 'success', $result );
$this->assertEquals( 1, $result['success'] );
}

public function setRedirectProvider_success() {
return array(
'redirect empty entity' => array( 'Q11', 'Q12' ),
'update redirect' => array( 'Q22', 'Q11' ),
);
}

/**
* @dataProvider setRedirectProvider_success
*/
public function testSetRedirect_success( $from, $to ) {
$params = array( 'from' => $from, 'to' => $to );
$result = $this->callApiModule( $params );

$this->assertSuccess( $result );
}

public function setRedirectProvider_failure() {
return array(
'bad source id' => array( 'xyz', 'Q12', 'invalid-entity-id' ),
'bad target id' => array( 'Q11', 'xyz', 'invalid-entity-id' ),

'source not found' => array( 'Q77', 'Q12', 'no-such-entity' ),
'target not found' => array( 'Q11', 'Q77', 'no-such-entity' ),
'target is a redirect' => array( 'Q11', 'Q22', 'target-is-redirect' ),
'target is incompatible' => array( 'Q11', 'P11', 'target-is-incompatible' ),

'source not empty' => array( 'Q12', 'Q11', 'not-empty' ),
'can\'t redirect' => array( 'P11', 'P12', 'cant-redirect' ),
);
}

/**
* @dataProvider setRedirectProvider_failure
*/
public function testSetRedirect_failure( $from, $to, $expectedCode ) {
$params = array( 'from' => $from, 'to' => $to );

try {
$this->callApiModule( $params );
$this->fail( 'API did not fail with error ' . $expectedCode . ' as expected!' );
} catch ( UsageException $ex ) {
$this->assertEquals( $expectedCode, $ex->getCodeString() );
}
}

public function testSetRedirect_noPermission() {
$this->setExpectedException( 'UsageException' );

$user = User::newFromName( 'UserWithoutPermission' );

$params = array( 'from' => 'Q11', 'to' => 'Q12' );
$this->callApiModule( $params, $user );
}

public function testModuleFlags() {
$module = $this->newApiModule( array() );

$this->isTrue( $module->mustBePosted(), 'mustBePosted' );
$this->isTrue( $module->isWriteMode(), 'isWriteMode' );
$this->isTrue( $module->needsToken(), 'needsToken' );
$this->isTrue( $module->getTokenSalt(), 'getTokenSalt' );

//NOTE: Would be nice to test the token check directly, but that is done via
// ApiMain::execute, which is bypassed by callApiModule().
}

}