X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22auteur_infos%22%2C%22id_auteur=%24connect_id_auteur%22%29%20.%20%22?a=blobdiff_plain;f=includes%2Fspecials%2FSpecialUndelete.php;h=efac6153246bd46b9ad52456392518d8017fa53f;hb=dcdb8e463e3b2be121c61c91df13ea36d270a602;hp=5d230c069760800c1d57d198a7c3c24c6fd4dd3f;hpb=887ba639a2b8d8f8a40babf93524eb05b1387c71;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php index 5d230c0697..efac615324 100644 --- a/includes/specials/SpecialUndelete.php +++ b/includes/specials/SpecialUndelete.php @@ -20,6 +20,7 @@ * @file * @ingroup SpecialPage */ +use MediaWiki\MediaWikiServices; /** * Used to show archived pages and eventually restore them. @@ -46,7 +47,7 @@ class PageArchive { $this->title = $title; if ( $config === null ) { wfDebug( __METHOD__ . ' did not have a Config object passed to it' ); - $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + $config = MediaWikiServices::getInstance()->getMainConfig(); } $this->config = $config; } @@ -63,7 +64,7 @@ class PageArchive { * @return ResultWrapper */ public static function listAllPages() { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); return self::listPages( $dbr, '' ); } @@ -77,7 +78,7 @@ class PageArchive { * @return ResultWrapper */ public static function listPagesByPrefix( $prefix ) { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $title = Title::newFromText( $prefix ); if ( $title ) { @@ -127,7 +128,7 @@ class PageArchive { * @return ResultWrapper */ function listRevisions() { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $tables = [ 'archive' ]; @@ -179,7 +180,7 @@ class PageArchive { return null; } - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); return $dbr->select( 'filearchive', ArchivedFile::selectFields(), @@ -197,7 +198,7 @@ class PageArchive { * @return Revision|null */ function getRevision( $timestamp ) { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $fields = [ 'ar_rev_id', @@ -244,7 +245,7 @@ class PageArchive { * @return Revision|null Null when there is no previous revision */ function getPreviousRevision( $timestamp ) { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); // Check the previous deleted revision... $row = $dbr->selectRow( 'archive', @@ -300,7 +301,7 @@ class PageArchive { } // New-style: keyed to the text storage backend. - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $text = $dbr->selectRow( 'text', [ 'old_text', 'old_flags' ], [ 'old_id' => $row->ar_text_id ], @@ -318,7 +319,7 @@ class PageArchive { * @return string|null */ function getLastRevisionText() { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $row = $dbr->selectRow( 'archive', [ 'ar_text', 'ar_flags', 'ar_text_id' ], [ 'ar_namespace' => $this->title->getNamespace(), @@ -339,7 +340,7 @@ class PageArchive { * @return bool */ function isDeleted() { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); $n = $dbr->selectField( 'archive', 'COUNT(ar_title)', [ 'ar_namespace' => $this->title->getNamespace(), 'ar_title' => $this->title->getDBkey() ], @@ -354,6 +355,9 @@ class PageArchive { * Once restored, the items will be removed from the archive tables. * The deletion log will be updated with an undeletion notice. * + * This also sets Status objects, $this->fileStatus and $this->revisionStatus + * (depending what operations are attempted). + * * @param array $timestamps Pass an empty array to restore all revisions, * otherwise list the ones to undelete. * @param string $comment @@ -439,9 +443,8 @@ class PageArchive { } /** - * This is the meaty bit -- restores archived revisions of the given page - * to the cur/old tables. If the page currently exists, all revisions will - * be stuffed into old, otherwise the most recent will go into cur. + * This is the meaty bit -- It restores archived revisions of the given page + * to the revision table. * * @param array $timestamps Pass an empty array to restore all revisions, * otherwise list the ones to undelete. @@ -455,8 +458,10 @@ class PageArchive { throw new ReadOnlyError(); } - $restoreAll = empty( $timestamps ); $dbw = wfGetDB( DB_MASTER ); + $dbw->startAtomic( __METHOD__ ); + + $restoreAll = empty( $timestamps ); # Does this page already exist? We'll have to update it... $article = WikiPage::factory( $this->title ); @@ -477,11 +482,9 @@ class PageArchive { # Page already exists. Import the history, and if necessary # we'll update the latest revision field in the record. - $previousRevId = $page->page_latest; - # Get the time span of this page $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp', - [ 'rev_id' => $previousRevId ], + [ 'rev_id' => $page->page_latest ], __METHOD__ ); if ( $previousTimestamp === false ) { @@ -489,13 +492,13 @@ class PageArchive { $status = Status::newGood( 0 ); $status->warning( 'undeleterevision-missing' ); + $dbw->endAtomic( __METHOD__ ); return $status; } } else { # Have to create a new article... $makepage = true; - $previousRevId = 0; $previousTimestamp = 0; } @@ -508,7 +511,9 @@ class PageArchive { } $fields = [ + 'ar_id', 'ar_rev_id', + 'rev_id', 'ar_text', 'ar_comment', 'ar_user', @@ -531,11 +536,14 @@ class PageArchive { /** * Select each archived revision... */ - $result = $dbw->select( 'archive', + $result = $dbw->select( + [ 'archive', 'revision' ], $fields, $oldWhere, __METHOD__, - /* options */ [ 'ORDER BY' => 'ar_timestamp' ] + /* options */ + [ 'ORDER BY' => 'ar_timestamp' ], + [ 'revision' => [ 'LEFT JOIN', 'ar_rev_id=rev_id' ] ] ); $rev_count = $result->numRows(); @@ -544,116 +552,182 @@ class PageArchive { $status = Status::newGood( 0 ); $status->warning( "undelete-no-results" ); + $dbw->endAtomic( __METHOD__ ); return $status; } - $result->seek( $rev_count - 1 ); // move to last - $row = $result->fetchObject(); // get newest archived rev - $oldPageId = (int)$row->ar_page_id; // pass this to ArticleUndelete hook - $result->seek( 0 ); // move back + // We use ar_id because there can be duplicate ar_rev_id even for the same + // page. In this case, we may be able to restore the first one. + $restoreFailedArIds = []; - // grab the content to check consistency with global state before restoring the page. - $revision = Revision::newFromArchiveRow( $row, - [ - 'title' => $article->getTitle(), // used to derive default content model - ] - ); - $user = User::newFromName( $revision->getUserText( Revision::RAW ), false ); - $content = $revision->getContent( Revision::RAW ); + // Map rev_id to the ar_id that is allowed to use it. When checking later, + // if it doesn't match, the current ar_id can not be restored. - // NOTE: article ID may not be known yet. prepareSave() should not modify the database. - $status = $content->prepareSave( $article, 0, -1, $user ); + // Value can be an ar_id or -1 (-1 means no ar_id can use it, since the + // rev_id is taken before we even start the restore). + $allowedRevIdToArIdMap = []; - if ( !$status->isOK() ) { - return $status; - } + $latestRestorableRow = null; - if ( $makepage ) { - // Check the state of the newest to-be version... - if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) { - return Status::newFatal( "undeleterevdel" ); + foreach ( $result as $row ) { + if ( $row->ar_rev_id ) { + // rev_id is taken even before we start restoring. + if ( $row->ar_rev_id === $row->rev_id ) { + $restoreFailedArIds[] = $row->ar_id; + $allowedRevIdToArIdMap[$row->ar_rev_id] = -1; + } else { + // rev_id is not taken yet in the DB, but it might be taken + // by a prior revision in the same restore operation. If + // not, we need to reserve it. + if ( isset( $allowedRevIdToArIdMap[$row->ar_rev_id] ) ) { + $restoreFailedArIds[] = $row->ar_id; + } else { + $allowedRevIdToArIdMap[$row->ar_rev_id] = $row->ar_id; + $latestRestorableRow = $row; + } + } + } else { + // If ar_rev_id is null, there can't be a collision, and a + // rev_id will be chosen automatically. + $latestRestorableRow = $row; } - // Safe to insert now... - $newid = $article->insertOn( $dbw, $row->ar_page_id ); - if ( $newid === false ) { - // The old ID is reserved; let's pick another - $newid = $article->insertOn( $dbw ); + } + + $result->seek( 0 ); // move back + + $oldPageId = 0; + if ( $latestRestorableRow !== null ) { + $oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook + + // grab the content to check consistency with global state before restoring the page. + $revision = Revision::newFromArchiveRow( $latestRestorableRow, + [ + 'title' => $article->getTitle(), // used to derive default content model + ] + ); + $user = User::newFromName( $revision->getUserText( Revision::RAW ), false ); + $content = $revision->getContent( Revision::RAW ); + + // NOTE: article ID may not be known yet. prepareSave() should not modify the database. + $status = $content->prepareSave( $article, 0, -1, $user ); + if ( !$status->isOK() ) { + $dbw->endAtomic( __METHOD__ ); + + return $status; } - $pageId = $newid; + } + + $newid = false; // newly created page ID + $restored = 0; // number of revisions restored + /** @var Revision $revision */ + $revision = null; + + // If there are no restorable revisions, we can skip most of the steps. + if ( $latestRestorableRow === null ) { + $failedRevisionCount = $rev_count; } else { - // Check if a deleted revision will become the current revision... - if ( $row->ar_timestamp > $previousTimestamp ) { + if ( $makepage ) { // Check the state of the newest to-be version... - if ( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) { + if ( !$unsuppress + && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT ) + ) { + $dbw->endAtomic( __METHOD__ ); + return Status::newFatal( "undeleterevdel" ); } + // Safe to insert now... + $newid = $article->insertOn( $dbw, $latestRestorableRow->ar_page_id ); + if ( $newid === false ) { + // The old ID is reserved; let's pick another + $newid = $article->insertOn( $dbw ); + } + $pageId = $newid; + } else { + // Check if a deleted revision will become the current revision... + if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) { + // Check the state of the newest to-be version... + if ( !$unsuppress + && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT ) + ) { + $dbw->endAtomic( __METHOD__ ); + + return Status::newFatal( "undeleterevdel" ); + } + } + + $newid = false; + $pageId = $article->getId(); } - $newid = false; - $pageId = $article->getId(); - } + foreach ( $result as $row ) { + // Check for key dupes due to needed archive integrity. + if ( $row->ar_rev_id && $allowedRevIdToArIdMap[$row->ar_rev_id] !== $row->ar_id ) { + continue; + } + // Insert one revision at a time...maintaining deletion status + // unless we are specifically removing all restrictions... + $revision = Revision::newFromArchiveRow( $row, + [ + 'page' => $pageId, + 'title' => $this->title, + 'deleted' => $unsuppress ? 0 : $row->ar_deleted + ] ); - $revision = null; - $restored = 0; + $revision->insertOn( $dbw ); + $restored++; - foreach ( $result as $row ) { - // Check for key dupes due to needed archive integrity. - if ( $row->ar_rev_id ) { - $exists = $dbw->selectField( 'revision', '1', - [ 'rev_id' => $row->ar_rev_id ], __METHOD__ ); - if ( $exists ) { - continue; // don't throw DB errors - } + Hooks::run( 'ArticleRevisionUndeleted', + [ &$this->title, $revision, $row->ar_page_id ] ); } - // Insert one revision at a time...maintaining deletion status - // unless we are specifically removing all restrictions... - $revision = Revision::newFromArchiveRow( $row, - [ - 'page' => $pageId, - 'title' => $this->title, - 'deleted' => $unsuppress ? 0 : $row->ar_deleted - ] ); - - $revision->insertOn( $dbw ); - $restored++; - Hooks::run( 'ArticleRevisionUndeleted', [ &$this->title, $revision, $row->ar_page_id ] ); - } - # Now that it's safely stored, take it out of the archive - $dbw->delete( 'archive', - $oldWhere, - __METHOD__ ); + // Now that it's safely stored, take it out of the archive + // Don't delete rows that we failed to restore + $toDeleteConds = $oldWhere; + $failedRevisionCount = count( $restoreFailedArIds ); + if ( $failedRevisionCount > 0 ) { + $toDeleteConds[] = 'ar_id NOT IN ( ' . $dbw->makeList( $restoreFailedArIds ) . ' )'; + } - // Was anything restored at all? - if ( $restored == 0 ) { - return Status::newGood( 0 ); + $dbw->delete( 'archive', + $toDeleteConds, + __METHOD__ ); } - $created = (bool)$newid; + $status = Status::newGood( $restored ); - // Attach the latest revision to the page... - $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId ); - if ( $created || $wasnew ) { - // Update site stats, link tables, etc - $article->doEditUpdates( - $revision, - User::newFromName( $revision->getUserText( Revision::RAW ), false ), - [ - 'created' => $created, - 'oldcountable' => $oldcountable, - 'restored' => true - ] - ); + if ( $failedRevisionCount > 0 ) { + $status->warning( + wfMessage( 'undeleterevision-duplicate-revid', $failedRevisionCount ) ); } - Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] ); + // Was anything restored at all? + if ( $restored ) { + $created = (bool)$newid; + // Attach the latest revision to the page... + $wasnew = $article->updateIfNewerOn( $dbw, $revision ); + if ( $created || $wasnew ) { + // Update site stats, link tables, etc + $article->doEditUpdates( + $revision, + User::newFromName( $revision->getUserText( Revision::RAW ), false ), + [ + 'created' => $created, + 'oldcountable' => $oldcountable, + 'restored' => true + ] + ); + } - if ( $this->title->getNamespace() == NS_FILE ) { - DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) ); + Hooks::run( 'ArticleUndelete', [ &$this->title, $created, $comment, $oldPageId ] ); + if ( $this->title->getNamespace() == NS_FILE ) { + DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->title, 'imagelinks' ) ); + } } - return Status::newGood( $restored ); + $dbw->endAtomic( __METHOD__ ); + + return $status; } /** @@ -1174,7 +1248,7 @@ class SpecialUndelete extends SpecialPage { $minor = $rev->isMinor() ? ChangesList::flag( 'minor' ) : ''; - $tags = wfGetDB( DB_SLAVE )->selectField( + $tags = wfGetDB( DB_REPLICA )->selectField( 'tag_summary', 'ts_tags', [ 'ts_rev_id' => $rev->getId() ],