| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| ); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' ) ); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(). | ||
| } | ||
|
|
||
| } |