Daniel Kinzler has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/366879 )
Change subject: Simplify edit conflict detection in EditEntity.
......................................................................
Simplify edit conflict detection in EditEntity.
This change does two things:
* Always check for "late" conflicts: when a new revision was added while
the edit was in progress, always fail.
* If no base revision is given, always use the latest revision as base
revision. That removes the "no base revision" mode.
Change-Id: I514a4acfa4e6ead356a5a7f0480a172f2086e012
---
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, 76 insertions(+), 69 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase
refs/changes/79/366879/1
diff --git a/repo/includes/Api/EntitySavingHelper.php
b/repo/includes/Api/EntitySavingHelper.php
index 845e2b5..831bfb3 100644
--- a/repo/includes/Api/EntitySavingHelper.php
+++ b/repo/includes/Api/EntitySavingHelper.php
@@ -333,7 +333,7 @@
}
if ( !$this->baseRevisionId ) {
- $this->baseRevisionId = isset( $params['baserevid'] ) ?
(int)$params['baserevid'] : null;
+ $this->baseRevisionId = isset( $params['baserevid'] ) ?
(int)$params['baserevid'] : 0;
}
$editEntityHandler = $this->editEntityFactory->newEditEntity(
diff --git a/repo/includes/EditEntity.php b/repo/includes/EditEntity.php
index b51c76e..3b9b66a 100644
--- a/repo/includes/EditEntity.php
+++ b/repo/includes/EditEntity.php
@@ -8,6 +8,7 @@
use MWException;
use ReadOnlyError;
use RequestContext;
+use RuntimeException;
use Status;
use Title;
use User;
@@ -173,12 +174,12 @@
* 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.
- * Defaults to false, disabling conflict checks.
- * `true` can be used to set the base revision to the latest
revision:
- * This will detect "late" edit conflicts, i.e. someone
squeezing in an edit
- * just before the actual database transaction for saving beings.
- * The empty string and 0 are both treated as `false`, disabling
conflict checks.
+ * @param int $baseRevId the base revision ID for conflict checking.
+ * Use 0 to indicate that the current revision should be used as
the base revision,
+ * effectively disabling conflict detections. true and false
will be accepted for
+ * backwards compatibility, but both will be treated like 0.
Note that the behavior
+ * of this class changed so that "late" conflicts that arise
between edit conflict
+ * detection and database update are always detected, and result
in the update to fail.
* @param IContextSource|null $context the context to use while
processing
* the edit; defaults to RequestContext::getMain().
*
@@ -194,7 +195,7 @@
EntityId $entityId = null,
User $user,
EditFilterHookRunner $editFilterHookRunner,
- $baseRevId = false,
+ $baseRevId = 0,
IContextSource $context = null
) {
$this->entityId = $entityId;
@@ -203,8 +204,8 @@
$baseRevId = (int)$baseRevId;
}
- if ( $baseRevId === 0 ) {
- $baseRevId = false;
+ if ( is_bool( $baseRevId ) ) {
+ $baseRevId = 0;
}
$this->user = $user;
@@ -313,13 +314,14 @@
}
/**
- * If no base revision was supplied to the constructor, this will
return false.
- * In the trivial non-conflicting case, this will be the same as
$this->getLatestRevisionId().
+ * Return the ID of the base revision for the edit. If no base revision
ID was supplied to
+ * the constructor, this returns the ID of the latest revision. If the
entity does not exist
+ * yet, this returns 0.
*
- * @return int|bool
+ * @return int
*/
private function getBaseRevisionId() {
- if ( $this->baseRevId === null || $this->baseRevId === true ) {
+ if ( $this->baseRevId === 0 ) {
$this->baseRevId = $this->getLatestRevisionId();
}
@@ -327,9 +329,9 @@
}
/**
- * Returns the edits base revision.
- * If no base revision was supplied to the constructor, this will
return null.
- * In the trivial non-conflicting case, this will be the same as
$this->getLatestRevision().
+ * Return the the base revision for the edit. If no base revision ID
was supplied to
+ * the constructor, this returns the latest revision. If the entity
does not exist
+ * yet, this returns null.
*
* @return EntityRevision|null
* @throws MWException
@@ -338,9 +340,7 @@
if ( $this->baseRev === null ) {
$baseRevId = $this->getBaseRevisionId();
- if ( $baseRevId === false ) {
- return null;
- } elseif ( $baseRevId === $this->getLatestRevisionId()
) {
+ if ( $baseRevId === $this->getLatestRevisionId() ) {
$this->baseRev = $this->getLatestRevision();
} else {
$id = $this->getEntityId();
@@ -367,9 +367,13 @@
* - revision: Revision the new revision object
* - errorFlags: bit field indicating errors, see the XXX_ERROR
constants.
*
- * @return Status|null
+ * @return Status
*/
public function getStatus() {
+ if ( $this->status === null ) {
+ throw new RuntimeException( 'The status is undefined
until attemptSave() has been called' );
+ }
+
return $this->status;
}
@@ -395,14 +399,19 @@
}
/**
- * Determines whether an edit conflict exists, that is, whether another
user has edited the same item
- * after the base revision was created.
+ * Determines whether an edit conflict exists, that is, whether another
user has edited the
+ * same item after the base revision was created. In other words, this
method checks whether
+ * the base revision (as provided to the constructor) is still current.
If no base revision
+ * was provided to the constructor, this will always return false.
+ *
+ * If the base revision is different from the current revision, this
will return true even if
+ * the edit conflict is resolvable. Indeed, it is used to determine
whether conflict resolution
+ * should be attempted.
*
* @return bool
*/
public function hasEditConflict() {
- return $this->doesCheckForEditConflicts()
- && !$this->isNew()
+ return !$this->isNew()
&& $this->getBaseRevisionId() !==
$this->getLatestRevisionId();
}
@@ -548,7 +557,12 @@
}
/**
- * Attempts to save the new entity content, chile first checking for
permissions, edit conflicts, etc.
+ * Attempts to save the given Entity object.
+ *
+ * This method performs entity level permission checks, checks the edit
toke, enforces rate
+ * limits, resolves edit conflicts, and updates user watchlists if
appropriate.
+ *
+ * Success or failure are reported via the STatus object returned by
this method.
*
* @param EntityDocument $newEntity
* @param string $summary The edit summary.
@@ -558,10 +572,14 @@
* Null will fail the token text, as
will the empty string.
* @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
+ *
* @see WikiPage::doEditContent
+ * @see EntityStore::saveEntity
*/
public function attemptSave( EntityDocument $newEntity, $summary,
$flags, $token, $watch = null ) {
if ( wfReadOnly() ) {
@@ -595,10 +613,20 @@
}
//NOTE: Make sure the latest revision is loaded and cached.
- // Would happen on demand anyway, but we want a
well-defined point at which "latest" is frozen
- // to a specific revision, just before the first check for
edit conflicts.
+ // Would happen on demand anyway, but we want a
well-defined point at which "latest" is
+ // frozen to a specific revision, just before the first
check for edit conflicts.
+ // We can use the ID of the latest revision to protect
against race conditions:
+ // if getLatestRevision() was called earlier by
application logic, saving will fail
+ // if any new revisions were created between then and now.
+ // Note that this protection against "late" conflicts is
unrelated to the detection
+ // of edit conflicts during user interaction, which use
the base revision supplied
+ // to the constructor.
$this->getLatestRevision();
- $this->getLatestRevisionId();
+ $raceProtectionRevId = $this->getLatestRevisionId();
+
+ if ( $raceProtectionRevId === 0 || ( $flags & EDIT_NEW ) > 0 ) {
+ $raceProtectionRevId = false;
+ }
$this->applyPreSaveChecks( $newEntity ); // modifies
$this->status
@@ -628,7 +656,7 @@
$summary,
$this->user,
$flags | EDIT_AUTOSUMMARY,
- $this->doesCheckForEditConflicts() ?
$this->getLatestRevisionId() : false
+ $raceProtectionRevId
);
$this->entityId = $newEntity->getId();
@@ -672,15 +700,6 @@
$this->getBaseRevision();
return $this->status;
- }
-
- /**
- * Whether this EditEntity will check for edit conflicts
- *
- * @return bool
- */
- public function doesCheckForEditConflicts() {
- return $this->getBaseRevisionId() !== false;
}
/**
diff --git a/repo/includes/EditEntityFactory.php
b/repo/includes/EditEntityFactory.php
index b815e8d..c56fbaf 100644
--- a/repo/includes/EditEntityFactory.php
+++ b/repo/includes/EditEntityFactory.php
@@ -93,12 +93,12 @@
/**
* @param User $user the user performing the edit
* @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:
- * This will detect "late" edit conflicts, i.e. someone
squeezing in an edit
- * just before the actual database transaction for saving beings.
- * The empty string and 0 are both treated as `false`, disabling
conflict checks.
+ * @param int $baseRevId the base revision ID for conflict checking.
+ * Use 0 to indicate that the current revision should be used as
the base revision,
+ * effectively disabling conflict detections. true and false
will be accepted for
+ * backwards compatibility, but both will be treated like 0.
Note that the behavior
+ * of EditEntity was changed so that "late" conflicts that arise
between edit conflict
+ * detection and database update are always detected, and result
in the update to fail.
*
* @return EditEntity
*/
diff --git a/repo/includes/Specials/SpecialWikibaseRepoPage.php
b/repo/includes/Specials/SpecialWikibaseRepoPage.php
index 6383a19..6ad3a13 100644
--- a/repo/includes/Specials/SpecialWikibaseRepoPage.php
+++ b/repo/includes/Specials/SpecialWikibaseRepoPage.php
@@ -130,7 +130,7 @@
* @param Summary $summary
* @param string $token
* @param int $flags The edit flags (see WikiPage::doEditContent)
- * @param bool|int $baseRev the base revision, for conflict detection
+ * @param int $baseRev the base revision, for conflict detection
*
* @return Status
*/
@@ -139,7 +139,7 @@
Summary $summary,
$token,
$flags = EDIT_UPDATE,
- $baseRev = false
+ $baseRev = 0
) {
$editEntity = $this->editEntityFactory->newEditEntity(
$this->getUser(),
diff --git a/repo/includes/UpdateRepo/UpdateRepoJob.php
b/repo/includes/UpdateRepo/UpdateRepoJob.php
index 80f9bcf..8cee916 100644
--- a/repo/includes/UpdateRepo/UpdateRepoJob.php
+++ b/repo/includes/UpdateRepo/UpdateRepoJob.php
@@ -153,7 +153,7 @@
$summaryString = $this->summaryFormatter->formatSummary(
$summary );
- $editEntity = $this->editEntityFactory->newEditEntity( $user,
$item->getId(), true );
+ $editEntity = $this->editEntityFactory->newEditEntity( $user,
$item->getId(), 0 );
$status = $editEntity->attemptSave(
$item,
$summaryString,
diff --git a/repo/tests/phpunit/includes/EditEntityTest.php
b/repo/tests/phpunit/includes/EditEntityTest.php
index af46969..84a71b8 100644
--- a/repo/tests/phpunit/includes/EditEntityTest.php
+++ b/repo/tests/phpunit/includes/EditEntityTest.php
@@ -358,17 +358,7 @@
];
}
- public function provideAttemptSaveWithLateConflict() {
- return [
- [ true, true ],
- [ false, false ],
- ];
- }
-
- /**
- * @dataProvider provideAttemptSaveWithLateConflict
- */
- public function testAttemptSaveWithLateConflict( $baseRevId,
$expectedConflict ) {
+ public function testAttemptSaveWithLateConflict() {
$repo = $this->getMockRepository();
$user = $this->getUser( 'EditEntityTestUser' );
@@ -384,10 +374,8 @@
$entity->setLabel( 'en', 'Trust' );
$titleLookup = $this->getEntityTitleLookup();
- $editEntity = $this->makeEditEntity( $repo, $entity->getId(),
$titleLookup, $user, $baseRevId );
+ $editEntity = $this->makeEditEntity( $repo, $entity->getId(),
$titleLookup, $user, true );
$editEntity->getLatestRevision(); // make sure EditEntity has
page and revision
-
- $this->assertEquals( $baseRevId !== false,
$editEntity->doesCheckForEditConflicts(), 'doesCheckForEditConflicts()' );
// create independent Entity instance for the same entity, and
modify and save it
$entity2 = new Item( new ItemId( 'Q42' ) );
@@ -408,16 +396,16 @@
$statusMessage = "Status ($id): " .
$status->getWikiText();
}
- $this->assertNotEquals( $expectedConflict, $status->isOK(),
- "Saving should have failed late if and only if a base
rev was provided.\n$statusMessage" );
+ $this->assertFalse( $status->isOK(),
+ "Saving should have failed late\n$statusMessage" );
- $this->assertEquals( $expectedConflict, $editEntity->hasError(),
- "Saving should have failed late if and only if a base
rev was provided.\n$statusMessage" );
+ $this->assertTrue( $editEntity->hasError(),
+ "Saving should have failed late\n$statusMessage" );
- $this->assertEquals( $expectedConflict, $status->hasMessage(
'edit-conflict' ),
- "Saving should have failed late if and only if a base
rev was provided.\n$statusMessage" );
+ $this->assertTrue( $status->hasMessage( 'edit-conflict' ),
+ "Saving should have failed late\n$statusMessage" );
- $this->assertEquals( $expectedConflict,
$editEntity->showErrorPage(),
+ $this->assertTrue( $editEntity->showErrorPage(),
"If and only if there was an error, an error page
should be shown.\n$statusMessage" );
}
--
To view, visit https://gerrit.wikimedia.org/r/366879
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I514a4acfa4e6ead356a5a7f0480a172f2086e012
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits