From 9aa66316a7da528d66de75dffe12c6ee2f0ff3d8 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Tue, 24 Nov 2015 15:22:16 -0800 Subject: [PATCH] Switch EditForm to using editRevId in place of edittimestamp This avoids sub-second timestamp collision race conditions Bug: T58849 Change-Id: Iee12e251a3b7ef1332274df520333afc2ff83889 --- includes/EditPage.php | 59 +++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/includes/EditPage.php b/includes/EditPage.php index f2403fe222..8b52d90e3d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -335,6 +335,9 @@ class EditPage { /** @var string */ public $edittime = ''; + /** @var integer */ + private $editRevId = null; + /** @var string */ public $section = ''; @@ -839,6 +842,7 @@ class EditPage { $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle ); $this->edittime = $request->getVal( 'wpEdittime' ); + $this->editRevId = $request->getIntOrNull( 'editRevId' ); $this->starttime = $request->getVal( 'wpStarttime' ); $undidRev = $request->getInt( 'wpUndidRevision' ); @@ -935,6 +939,7 @@ class EditPage { $this->summary = ''; $this->sectiontitle = ''; $this->edittime = ''; + $this->editRevId = null; $this->starttime = wfTimestampNow(); $this->edit = false; $this->preview = false; @@ -1020,6 +1025,7 @@ class EditPage { function initialiseForm() { global $wgUser; $this->edittime = $this->page->getTimestamp(); + $this->editRevId = $this->page->getLatest(); $content = $this->getContentObject( false ); # TODO: track content object?! if ( $content === false ) { @@ -1862,10 +1868,14 @@ class EditPage { $this->page->clear(); # Force reload of dates, etc. $timestamp = $this->page->getTimestamp(); + $latest = $this->page->getLatest(); wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" ); - if ( $timestamp != $this->edittime ) { + // Check editRevId if set, which handles same-second timestamp collisions + if ( $timestamp != $this->edittime + || ( $this->editRevId !== null && $this->editRevId != $latest ) + ) { $this->isConflict = true; if ( $this->section == 'new' ) { if ( $this->page->getUserText() == $wgUser->getName() && @@ -1905,14 +1915,24 @@ class EditPage { if ( $this->isConflict ) { wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'" - . " (article time '{$timestamp}')\n" ); - - $content = $this->page->replaceSectionContent( - $this->section, - $textbox_content, - $sectionTitle, - $this->edittime - ); + . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" ); + // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case + // ...or disable section editing for non-current revisions (not exposed anyway). + if ( $this->editRevId !== null ) { + $content = $this->page->replaceSectionAtRev( + $this->section, + $textbox_content, + $sectionTitle, + $this->editRevId + ); + } else { + $content = $this->page->replaceSectionContent( + $this->section, + $textbox_content, + $sectionTitle, + $this->edittime + ); + } } else { wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" ); $content = $this->page->replaceSectionContent( @@ -2172,8 +2192,9 @@ class EditPage { function getBaseRevision() { if ( !$this->mBaseRevision ) { $db = wfGetDB( DB_MASTER ); - $this->mBaseRevision = Revision::loadFromTimestamp( - $db, $this->mTitle, $this->edittime ); + $this->mBaseRevision = $this->editRevId + ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST ) + : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime ); } return $this->mBaseRevision; } @@ -2745,7 +2766,7 @@ class EditPage { if ( $this->isConflict ) { $wgOut->wrapWikiMsg( "
\n$1\n
", 'explainconflict' ); - $this->edittime = $this->page->getTimestamp(); + $this->editRevId = $this->page->getLatest(); } else { if ( $this->section != '' && !$this->isSectionEditSupported() ) { // We use $this->section to much before this and getVal('wgSection') directly in other places @@ -3052,6 +3073,7 @@ class EditPage { + HTML @@ -3260,10 +3282,15 @@ HTML } $textboxContent = $this->toEditContent( $this->textbox1 ); - - $newContent = $this->page->replaceSectionContent( - $this->section, $textboxContent, - $this->summary, $this->edittime ); + if ( $this->editRevId !== null ) { + $newContent = $this->page->replaceSectionAtRev( + $this->section, $textboxContent, $this->summary, $this->editRevId + ); + } else { + $newContent = $this->page->replaceSectionContent( + $this->section, $textboxContent, $this->summary, $this->edittime + ); + } if ( $newContent ) { ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] ); -- 2.20.1