Daniel Kinzler has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/366590 )
Change subject: Refactor EditEntity interactor ...................................................................... Refactor EditEntity interactor This change allows the EditEntity interactor to be constructed without the new EntityDocument. That way, EditEntity can be used to load and track the base revision before modifying it to create the new EntityDocument. Change-Id: Idb3c17fd19494f0f29e9496cb1d52bdf3287cb69 Depends-On: I5aaeffca3df8b4209276a82605e132f02d0128f6 --- M repo/includes/Api/EntitySavingHelper.php M repo/includes/EditEntity.php M repo/includes/EditEntityFactory.php M repo/includes/Specials/SpecialWikibaseRepoPage.php M repo/includes/UpdateRepo/UpdateRepoJob.php M repo/tests/phpunit/includes/EditEntityTest.php 6 files changed, 94 insertions(+), 80 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase refs/changes/90/366590/1 diff --git a/repo/includes/Api/EntitySavingHelper.php b/repo/includes/Api/EntitySavingHelper.php index 51d488a..845e2b5 100644 --- a/repo/includes/Api/EntitySavingHelper.php +++ b/repo/includes/Api/EntitySavingHelper.php @@ -338,13 +338,14 @@ $editEntityHandler = $this->editEntityFactory->newEditEntity( $user, - $entity, + $entity->getId(), $this->baseRevisionId ); $token = $this->evaluateTokenParam( $params ); $status = $editEntityHandler->attemptSave( + $entity, $summary, $this->entitySavingFlags | $flags, $token diff --git a/repo/includes/EditEntity.php b/repo/includes/EditEntity.php index 6fe10e3..b51c76e 100644 --- a/repo/includes/EditEntity.php +++ b/repo/includes/EditEntity.php @@ -64,11 +64,11 @@ private $entityPatcher; /** - * The modified entity we are trying to save + * The ID of the entity to edit. May be null if a new entity is being created. * - * @var EntityDocument|null + * @var EntityId|null */ - private $newEntity = null; + private $entityId = null; /** * @var EntityRevision|null @@ -169,7 +169,8 @@ * @param EntityPermissionChecker $permissionChecker * @param EntityDiffer $entityDiffer * @param EntityPatcher $entityPatcher - * @param EntityDocument $newEntity the new entity object + * @param EntityId|null $entityId the ID of the entity being edited. + * May be null when creating a new entity. * @param User $user the user performing the edit * @param EditFilterHookRunner $editFilterHookRunner * @param int|bool $baseRevId the base revision ID for conflict checking. @@ -190,13 +191,13 @@ EntityPermissionChecker $permissionChecker, EntityDiffer $entityDiffer, EntityPatcher $entityPatcher, - EntityDocument $newEntity, + EntityId $entityId = null, User $user, EditFilterHookRunner $editFilterHookRunner, $baseRevId = false, IContextSource $context = null ) { - $this->newEntity = $newEntity; + $this->entityId = $entityId; if ( is_string( $baseRevId ) ) { $baseRevId = (int)$baseRevId; @@ -229,13 +230,13 @@ } /** - * Returns the new entity object to be saved. May be different from the entity supplied - * to the constructor in case the entity was patched to resolve edit conflicts. + * Returns the ID of the entity being edited. + * May be null if a new entity is to be created. * - * @return EntityDocument + * @return null|EntityId */ - public function getNewEntity() { - return $this->newEntity; + public function getEntityId() { + return $this->entityId; } /** @@ -245,7 +246,7 @@ */ private function getTitle() { if ( $this->title === null ) { - $id = $this->newEntity->getId(); + $id = $this->getEntityId(); if ( $id !== null ) { $this->title = $this->titleLookup->getTitleForId( $id ); @@ -262,7 +263,7 @@ */ public function getLatestRevision() { if ( $this->latestRev === null ) { - $id = $this->newEntity->getId(); + $id = $this->getEntityId(); if ( $id !== null ) { // NOTE: It's important to remember this, if someone calls clear() on @@ -285,7 +286,7 @@ // Don't do negative caching: We call this to see whether the entity yet exists // before creating. if ( $this->latestRevId === 0 ) { - $id = $this->newEntity->getId(); + $id = $this->getEntityId(); if ( $this->latestRev !== null ) { $this->latestRevId = $this->latestRev->getRevisionId(); @@ -308,7 +309,7 @@ * @return bool */ private function isNew() { - return $this->newEntity->getId() === null || $this->getLatestRevisionId() === 0; + return $this->getEntityId() === null || $this->getLatestRevisionId() === 0; } /** @@ -333,7 +334,7 @@ * @return EntityRevision|null * @throws MWException */ - private function getBaseRevision() { + public function getBaseRevision() { if ( $this->baseRev === null ) { $baseRevId = $this->getBaseRevisionId(); @@ -342,7 +343,7 @@ } elseif ( $baseRevId === $this->getLatestRevisionId() ) { $this->baseRev = $this->getLatestRevision(); } else { - $id = $this->newEntity->getId(); + $id = $this->getEntityId(); $this->baseRev = $this->entityRevisionLookup->getEntityRevision( $id, $baseRevId ); if ( $this->baseRev === null ) { @@ -407,32 +408,33 @@ /** * Attempts to fix an edit conflict by patching the intended change into the latest revision after - * checking for conflicts. This modifies $this->newEntity but does not write anything to the - * database. Saving of the new content may still fail. + * checking for conflicts. * - * @return bool True if the conflict could be resolved, false otherwise + * @param EntityDocument $newEntity + * + * @throws MWException + * @return null|EntityDocument The patched Entity, or null if patching failed. */ - public function fixEditConflict() { + private function fixEditConflict( EntityDocument $newEntity ) { $baseRev = $this->getBaseRevision(); $latestRev = $this->getLatestRevision(); if ( !$latestRev ) { - wfLogWarning( 'Failed to load latest revision of entity ' . $this->newEntity->getId() . '! ' + wfLogWarning( 'Failed to load latest revision of entity ' . $newEntity->getId() . '! ' . 'This may indicate entries missing from thw wb_entities_per_page table.' ); - return false; + return null; } // calculate patch against base revision // NOTE: will fail if $baseRev or $base are null, which they may be if // this gets called at an inappropriate time. The data flow in this class // should be improved. - $patch = $this->entityDiffer->diffEntities( $baseRev->getEntity(), $this->newEntity ); + $patch = $this->entityDiffer->diffEntities( $baseRev->getEntity(), $newEntity ); if ( $patch->isEmpty() ) { // we didn't technically fix anything, but if there is nothing to change, // so just keep the current content as it is. - $this->newEntity = $latestRev->getEntity()->copy(); - return true; + return $latestRev->getEntity()->copy(); } // apply the patch( base -> new ) to the latest revision. @@ -446,28 +448,26 @@ if ( $conflicts > 0 ) { // patch doesn't apply cleanly - if ( $this->userWasLastToEdit( $this->user, $this->newEntity->getId(), $this->getBaseRevisionId() ) ) { + if ( $this->userWasLastToEdit( $this->user, $newEntity->getId(), $this->getBaseRevisionId() ) ) { // it's a self-conflict if ( $cleanPatch->count() === 0 ) { // patch collapsed, possibly because of diff operation change from base to latest - return false; + return null; } else { // we still have a working patch, try to apply $this->status->warning( 'wikibase-self-conflict-patched' ); } } else { // there are unresolvable conflicts. - return false; + return null; } } else { // can apply cleanly $this->status->warning( 'wikibase-conflict-patched' ); } - // remember the patched entity as the actual new entity to save - $this->newEntity = $patchedLatest; - - return true; + // return the patched entity + return $patchedLatest; } /** @@ -492,12 +492,14 @@ * Checks the necessary permissions to perform this edit. * Per default, the 'edit' permission is checked. * Use addRequiredPermission() to check more permissions. + * + * @param EntityDocument $newEntity */ - public function checkEditPermissions() { + private function checkEditPermissions( EntityDocument $newEntity ) { $permissionStatus = $this->permissionChecker->getPermissionStatusForEntity( $this->user, EntityPermissionChecker::ACTION_EDIT, - $this->newEntity + $newEntity ); $this->status->merge( $permissionStatus ); @@ -548,20 +550,20 @@ /** * Attempts to save the new entity content, chile first checking for permissions, edit conflicts, etc. * + * @param EntityDocument $newEntity * @param string $summary The edit summary. - * @param int $flags The EDIT_XXX flags as used by WikiPage::doEditContent(). + * @param int $flags The EDIT_XXX flags as used by WikiPage::doEditContent(). * Additionally, the EntityContent::EDIT_XXX constants can be used. * @param string|bool $token Edit token to check, or false to disable the token check. * Null will fail the token text, as will the empty string. - * @param bool|null $watch Whether the user wants to watch the entity. + * @param bool|null $watch Whether the user wants to watch the entity. * Set to null to apply default according to getWatchDefault(). - * + * @return Status + * @throws MWException * @throws ReadOnlyError - * @return Status Indicates success and provides detailed warnings or error messages. See - * getStatus() for more details. * @see WikiPage::doEditContent */ - public function attemptSave( $summary, $flags, $token, $watch = null ) { + public function attemptSave( EntityDocument $newEntity, $summary, $flags, $token, $watch = null ) { if ( wfReadOnly() ) { throw new ReadOnlyError(); } @@ -583,7 +585,7 @@ return $this->status; } - $this->checkEditPermissions(); + $this->checkEditPermissions( $newEntity ); $this->checkRateLimits(); // modifies $this->status @@ -598,7 +600,7 @@ $this->getLatestRevision(); $this->getLatestRevisionId(); - $this->applyPreSaveChecks(); // modifies $this->status + $this->applyPreSaveChecks( $newEntity ); // modifies $this->status if ( !$this->status->isOK() ) { $this->errorType |= self::PRECONDITION_FAILED; @@ -609,7 +611,7 @@ return $this->status; } - $hookStatus = $this->editFilterHookRunner->run( $this->newEntity, $this->user, $summary ); + $hookStatus = $this->editFilterHookRunner->run( $newEntity, $this->user, $summary ); if ( !$hookStatus->isOK() ) { $this->errorType |= self::FILTERED; } @@ -622,13 +624,14 @@ try { $entityRevision = $this->entityStore->saveEntity( - $this->newEntity, + $newEntity, $summary, $this->user, $flags | EDIT_AUTOSUMMARY, $this->doesCheckForEditConflicts() ? $this->getLatestRevisionId() : false ); + $this->entityId = $newEntity->getId(); $editStatus = Status::newGood( [ 'revision' => $entityRevision ] ); } catch ( StorageException $ex ) { $editStatus = $ex->getStatus(); @@ -655,9 +658,9 @@ return $this->status; } - private function applyPreSaveChecks() { + private function applyPreSaveChecks( EntityDocument $newEntity ) { if ( $this->hasEditConflict() ) { - if ( !$this->fixEditConflict() ) { + if ( !$this->fixEditConflict( $newEntity ) ) { $this->status->fatal( 'edit-conflict' ); $this->errorType |= self::EDIT_CONFLICT_ERROR; @@ -750,7 +753,7 @@ } // keep current state - return !$this->isNew() && $this->entityStore->isWatching( $this->user, $this->newEntity->getId() ); + return !$this->isNew() && $this->entityStore->isWatching( $this->user, $this->getEntityId() ); } /** @@ -768,7 +771,7 @@ throw new MWException( 'Title not yet known!' ); } - $this->entityStore->updateWatchlist( $this->user, $this->newEntity->getId(), $watch ); + $this->entityStore->updateWatchlist( $this->user, $this->getEntityId(), $watch ); } } diff --git a/repo/includes/EditEntityFactory.php b/repo/includes/EditEntityFactory.php index d88a6c5..b815e8d 100644 --- a/repo/includes/EditEntityFactory.php +++ b/repo/includes/EditEntityFactory.php @@ -5,6 +5,7 @@ use IContextSource; use User; use Wikibase\DataModel\Entity\EntityDocument; +use Wikibase\DataModel\Entity\EntityId; use Wikibase\DataModel\Services\Diff\EntityDiffer; use Wikibase\DataModel\Services\Diff\EntityPatcher; use Wikibase\Lib\Store\EntityRevisionLookup; @@ -91,7 +92,7 @@ /** * @param User $user the user performing the edit - * @param EntityDocument $entity the new entity object + * @param EntityId|null $entityId the id of the entity to edit * @param int|bool $baseRevId the base revision ID for conflict checking. * Defaults to false, disabling conflict checks. * `true` can be used to set the base revision to the latest revision: @@ -101,7 +102,7 @@ * * @return EditEntity */ - public function newEditEntity( User $user, EntityDocument $entity, $baseRevId = false ) { + public function newEditEntity( User $user, EntityId $entityId = null, $baseRevId = false ) { return new EditEntity( $this->titleLookup, $this->entityRevisionLookup, @@ -109,7 +110,7 @@ $this->permissionChecker, $this->entityDiffer, $this->entityPatcher, - $entity, + $entityId, $user, $this->editFilterHookRunner, $baseRevId, diff --git a/repo/includes/Specials/SpecialWikibaseRepoPage.php b/repo/includes/Specials/SpecialWikibaseRepoPage.php index 9c953d4..6383a19 100644 --- a/repo/includes/Specials/SpecialWikibaseRepoPage.php +++ b/repo/includes/Specials/SpecialWikibaseRepoPage.php @@ -143,11 +143,12 @@ ) { $editEntity = $this->editEntityFactory->newEditEntity( $this->getUser(), - $entity, + $entity->getId(), $baseRev ); $status = $editEntity->attemptSave( + $entity, $this->summaryFormatter->formatSummary( $summary ), $flags, $token diff --git a/repo/includes/UpdateRepo/UpdateRepoJob.php b/repo/includes/UpdateRepo/UpdateRepoJob.php index f40f0f6..80f9bcf 100644 --- a/repo/includes/UpdateRepo/UpdateRepoJob.php +++ b/repo/includes/UpdateRepo/UpdateRepoJob.php @@ -153,8 +153,9 @@ $summaryString = $this->summaryFormatter->formatSummary( $summary ); - $editEntity = $this->editEntityFactory->newEditEntity( $user, $item, true ); + $editEntity = $this->editEntityFactory->newEditEntity( $user, $item->getId(), true ); $status = $editEntity->attemptSave( + $item, $summaryString, EDIT_UPDATE, false, diff --git a/repo/tests/phpunit/includes/EditEntityTest.php b/repo/tests/phpunit/includes/EditEntityTest.php index abf7bcc..af46969 100644 --- a/repo/tests/phpunit/includes/EditEntityTest.php +++ b/repo/tests/phpunit/includes/EditEntityTest.php @@ -23,6 +23,7 @@ use Wikibase\Lib\Tests\MockRepository; use Wikibase\Repo\Hooks\EditFilterHookRunner; use Wikibase\Repo\Store\EntityPermissionChecker; +use Wikimedia\TestingAccessWrapper; /** * @covers Wikibase\EditEntity @@ -124,7 +125,7 @@ /** * @param MockRepository $mockRepository - * @param EntityDocument $entity + * @param EntityId $entityId * @param EntityTitleStoreLookup $titleLookup * @param User|null $user * @param bool $baseRevId @@ -135,7 +136,7 @@ */ private function makeEditEntity( MockRepository $mockRepository, - EntityDocument $entity, + EntityId $entityId = null, EntityTitleStoreLookup $titleLookup, User $user = null, $baseRevId = false, @@ -161,7 +162,7 @@ $permissionChecker, new EntityDiffer(), new EntityPatcher(), - $entity, + $entityId, $user, $editFilterHookRunner, $baseRevId, @@ -323,19 +324,24 @@ // save entity ---------------------------------- $titleLookup = $this->getEntityTitleLookup(); - $editEntity = $this->makeEditEntity( $repo, $item, $titleLookup, $user, $baseRevisionId ); + $editEntity = $this->makeEditEntity( $repo, $item->getId(), $titleLookup, $user, $baseRevisionId ); + $newEntity = $item; + + if ( $baseRevisionId > 0 ) { + $baseRevision = $editEntity->getBaseRevision(); + $this->assertSame( $baseRevisionId, $baseRevision->getRevisionId() ); + $this->assertEquals( $entityId, $baseRevision->getEntity()->getId() ); + } $conflict = $editEntity->hasEditConflict(); $this->assertEquals( $expectedConflict, $conflict, 'hasEditConflict()' ); if ( $conflict ) { - $fixed = $editEntity->fixEditConflict(); - $this->assertEquals( $expectedFix, $fixed, 'fixEditConflict()' ); + $newEntity = TestingAccessWrapper::newFromObject( $editEntity )->fixEditConflict( $item ); + $this->assertEquals( $expectedFix, $newEntity !== null, 'fixEditConflict()' ); } if ( $expectedData !== null ) { - /** @var Item $newEntity */ - $newEntity = $editEntity->getNewEntity(); $data = $this->fingerprintToPartialArray( $newEntity->getFingerprint() ); foreach ( $expectedData as $key => $expectedValue ) { @@ -378,7 +384,7 @@ $entity->setLabel( 'en', 'Trust' ); $titleLookup = $this->getEntityTitleLookup(); - $editEntity = $this->makeEditEntity( $repo, $entity, $titleLookup, $user, $baseRevId ); + $editEntity = $this->makeEditEntity( $repo, $entity->getId(), $titleLookup, $user, $baseRevId ); $editEntity->getLatestRevision(); // make sure EditEntity has page and revision $this->assertEquals( $baseRevId !== false, $editEntity->doesCheckForEditConflicts(), 'doesCheckForEditConflicts()' ); @@ -392,7 +398,7 @@ // now try to save the original edit. The conflict should still be detected $token = $user->getEditToken(); - $status = $editEntity->attemptSave( "Testing", EDIT_UPDATE, $token ); + $status = $editEntity->attemptSave( $entity, "Testing", EDIT_UPDATE, $token ); $id = $entity->getId()->getSerialization(); @@ -427,14 +433,14 @@ $this->getEntityPermissionChecker( $permissions ), new EntityDiffer(), new EntityPatcher(), - new Item(), + null, $this->getUser( 'EditEntityTestUser' ), $this->getMockEditFitlerHookRunner(), false, $context ); - $editEntity->checkEditPermissions(); + TestingAccessWrapper::newFromObject( $editEntity )->checkEditPermissions( new Item() ); $editEntity->showErrorPage(); $html = $context->getOutput()->getHTML(); @@ -478,8 +484,8 @@ $item = $this->prepareItemForPermissionCheck( $user, $repo, $create ); $titleLookup = $this->getEntityTitleLookup(); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup, $user, false, $permissions ); - $edit->checkEditPermissions(); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup, $user, false, $permissions ); + TestingAccessWrapper::newFromObject( $edit )->checkEditPermissions( $item ); $this->assertEquals( $expectedOK, $edit->getStatus()->isOK() ); $this->assertNotEquals( $expectedOK, $edit->hasError( EditEntity::PERMISSION_ERROR ) ); @@ -496,9 +502,9 @@ $item = $this->prepareItemForPermissionCheck( $user, $repo, $create ); $token = $user->getEditToken(); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup, $user, false, $permissions ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup, $user, false, $permissions ); - $edit->attemptSave( "testing", ( $item->getId() === null ? EDIT_NEW : EDIT_UPDATE ), $token ); + $edit->attemptSave( $item, "testing", ( $item->getId() === null ? EDIT_NEW : EDIT_UPDATE ), $token ); $this->assertEquals( $expectedOK, $edit->getStatus()->isOK(), var_export( $edit->getStatus()->getErrorsArray(), true ) ); $this->assertNotEquals( $expectedOK, $edit->hasError( EditEntity::PERMISSION_ERROR ) ); @@ -672,8 +678,8 @@ $item->setLabel( 'en', $label ); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup, $user ); - $edit->attemptSave( "testing", ( $item->getId() === null ? EDIT_NEW : EDIT_UPDATE ), false ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup, $user ); + $edit->attemptSave( $item, "testing", ( $item->getId() === null ? EDIT_NEW : EDIT_UPDATE ), false ); $this->assertEquals( $expectedOK, $edit->getStatus()->isOK(), var_export( $edit->getStatus()->getErrorsArray(), true ) ); $this->assertNotEquals( $expectedOK, $edit->hasError( EditEntity::RATE_LIMIT ) ); @@ -713,7 +719,7 @@ $item = new Item(); $titleLookup = $this->getEntityTitleLookup(); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup, $user ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup, $user ); // check valid token -------------------- if ( $token === true ) { @@ -769,8 +775,8 @@ } $titleLookup = $this->getEntityTitleLookup(); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup, $user ); - $status = $edit->attemptSave( "testing", $new ? EDIT_NEW : EDIT_UPDATE, false, $watch ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup, $user ); + $status = $edit->attemptSave( $item, "testing", $new ? EDIT_NEW : EDIT_UPDATE, false, $watch ); $this->assertTrue( $status->isOK(), "edit failed: " . $status->getWikiText() ); // sanity @@ -785,15 +791,15 @@ $isNew = new ReflectionMethod( EditEntity::class, 'isNew' ); $isNew->setAccessible( true ); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup ); $this->assertTrue( $isNew->invoke( $edit ), 'New entity: No id' ); $repo->assignFreshId( $item ); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup ); $this->assertTrue( $isNew->invoke( $edit ), "New entity: Has an id, but doesn't exist, yet" ); $repo->saveEntity( $item, 'testIsNew', $this->getUser( 'EditEntityTestUser1' ) ); - $edit = $this->makeEditEntity( $repo, $item, $titleLookup ); + $edit = $this->makeEditEntity( $repo, $item->getId(), $titleLookup ); $this->assertFalse( $isNew->invoke( $edit ), "Entity exists" ); } @@ -810,7 +816,7 @@ public function testEditFilterHookRunnerInteraction( Status $hookReturnStatus ) { $edit = $this->makeEditEntity( $this->getMockRepository(), - new Item(), + null, $this->getEntityTitleLookup(), null, false, @@ -820,6 +826,7 @@ $user = $this->getUser( 'EditEntityTestUser' ); $saveStatus = $edit->attemptSave( + new Item(), 'some Summary', EDIT_MINOR, $user->getEditToken() -- To view, visit https://gerrit.wikimedia.org/r/366590 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Idb3c17fd19494f0f29e9496cb1d52bdf3287cb69 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Wikibase Gerrit-Branch: master Gerrit-Owner: Daniel Kinzler <daniel.kinz...@wikimedia.de> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits