UndeletePage: Undelete associated talk page
* Adds option to undelete associated talk page in the context of a subject page * If subject page undeletion fails, talk page undeletion is aborted * If restoring the associated talk page fails, a status with a warning message is returned Bug: T304962 Change-Id: I7b30863060974d4079639f57178062d359956c2e
This commit is contained in:
parent
c11db77cb8
commit
4ce0b39043
9 changed files with 371 additions and 109 deletions
|
|
@ -2079,7 +2079,8 @@ return [
|
|||
$services->getPageUpdaterFactory(),
|
||||
$services->getMessageFormatterFactory()->getTextFormatter(
|
||||
$services->getContentLanguage()->getCode()
|
||||
)
|
||||
),
|
||||
$services->getArchivedRevisionLookup()
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ use MediaWiki\Content\IContentHandlerFactory;
|
|||
use MediaWiki\EditPage\SpamChecker;
|
||||
use MediaWiki\HookContainer\HookContainer;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Revision\ArchivedRevisionLookup;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MediaWiki\Storage\PageUpdaterFactory;
|
||||
use MediaWiki\User\ActorNormalization;
|
||||
|
|
@ -150,6 +151,9 @@ class PageCommandFactory implements
|
|||
/** @var ITextFormatter */
|
||||
private $contLangMsgTextFormatter;
|
||||
|
||||
/** @var ArchivedRevisionLookup */
|
||||
private $archivedRevisionLookup;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
LBFactory $lbFactory,
|
||||
|
|
@ -177,7 +181,8 @@ class PageCommandFactory implements
|
|||
BacklinkCacheFactory $backlinkCacheFactory,
|
||||
LoggerInterface $undeletePageLogger,
|
||||
PageUpdaterFactory $pageUpdaterFactory,
|
||||
ITextFormatter $contLangMsgTextFormatter
|
||||
ITextFormatter $contLangMsgTextFormatter,
|
||||
ArchivedRevisionLookup $archivedRevisionLookup
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->lbFactory = $lbFactory;
|
||||
|
|
@ -206,6 +211,7 @@ class PageCommandFactory implements
|
|||
$this->undeletePageLogger = $undeletePageLogger;
|
||||
$this->pageUpdaterFactory = $pageUpdaterFactory;
|
||||
$this->contLangMsgTextFormatter = $contLangMsgTextFormatter;
|
||||
$this->archivedRevisionLookup = $archivedRevisionLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -353,6 +359,8 @@ class PageCommandFactory implements
|
|||
$this->wikiPageFactory,
|
||||
$this->pageUpdaterFactory,
|
||||
$this->contentHandlerFactory,
|
||||
$this->archivedRevisionLookup,
|
||||
$this->namespaceInfo,
|
||||
$page,
|
||||
$authority
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,9 +32,11 @@ use MediaWiki\HookContainer\HookContainer;
|
|||
use MediaWiki\HookContainer\HookRunner;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Permissions\PermissionStatus;
|
||||
use MediaWiki\Revision\ArchivedRevisionLookup;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MediaWiki\Storage\PageUpdaterFactory;
|
||||
use NamespaceInfo;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReadOnlyError;
|
||||
use ReadOnlyMode;
|
||||
|
|
@ -75,6 +77,10 @@ class UndeletePage {
|
|||
private $pageUpdaterFactory;
|
||||
/** @var IContentHandlerFactory */
|
||||
private $contentHandlerFactory;
|
||||
/** @var ArchivedRevisionLookup */
|
||||
private $archivedRevisionLookup;
|
||||
/** @var NamespaceInfo */
|
||||
private $namespaceInfo;
|
||||
|
||||
/** @var ProperPageIdentity */
|
||||
private $page;
|
||||
|
|
@ -92,6 +98,8 @@ class UndeletePage {
|
|||
private $unsuppress = false;
|
||||
/** @var string[] */
|
||||
private $tags = [];
|
||||
/** @var WikiPage|null If not null, it means that we have to undelete it. */
|
||||
private $associatedTalk;
|
||||
|
||||
/**
|
||||
* @param HookContainer $hookContainer
|
||||
|
|
@ -104,6 +112,8 @@ class UndeletePage {
|
|||
* @param WikiPageFactory $wikiPageFactory
|
||||
* @param PageUpdaterFactory $pageUpdaterFactory
|
||||
* @param IContentHandlerFactory $contentHandlerFactory
|
||||
* @param ArchivedRevisionLookup $archivedRevisionLookup
|
||||
* @param NamespaceInfo $namespaceInfo
|
||||
* @param ProperPageIdentity $page
|
||||
* @param Authority $performer
|
||||
*/
|
||||
|
|
@ -118,6 +128,8 @@ class UndeletePage {
|
|||
WikiPageFactory $wikiPageFactory,
|
||||
PageUpdaterFactory $pageUpdaterFactory,
|
||||
IContentHandlerFactory $contentHandlerFactory,
|
||||
ArchivedRevisionLookup $archivedRevisionLookup,
|
||||
NamespaceInfo $namespaceInfo,
|
||||
ProperPageIdentity $page,
|
||||
Authority $performer
|
||||
) {
|
||||
|
|
@ -131,6 +143,8 @@ class UndeletePage {
|
|||
$this->wikiPageFactory = $wikiPageFactory;
|
||||
$this->pageUpdaterFactory = $pageUpdaterFactory;
|
||||
$this->contentHandlerFactory = $contentHandlerFactory;
|
||||
$this->archivedRevisionLookup = $archivedRevisionLookup;
|
||||
$this->namespaceInfo = $namespaceInfo;
|
||||
|
||||
$this->page = $page;
|
||||
$this->performer = $performer;
|
||||
|
|
@ -180,6 +194,48 @@ class UndeletePage {
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether it's probably possible to undelete the associated talk page. This checks the replica,
|
||||
* so it may not see the latest master change, and is useful e.g. for building the UI.
|
||||
*
|
||||
* @return StatusValue
|
||||
*/
|
||||
public function canProbablyUndeleteAssociatedTalk(): StatusValue {
|
||||
if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
|
||||
return StatusValue::newFatal( 'undelete-error-associated-alreadytalk' );
|
||||
}
|
||||
// @todo FIXME: NamespaceInfo should work with PageIdentity
|
||||
$thisWikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
|
||||
$talkPage = $this->wikiPageFactory->newFromLinkTarget(
|
||||
$this->namespaceInfo->getTalkPage( $thisWikiPage->getTitle() )
|
||||
);
|
||||
// NOTE: The talk may exist, but have some deleted revision. That's fine.
|
||||
if ( !$this->archivedRevisionLookup->hasArchivedRevisions( $talkPage ) ) {
|
||||
return StatusValue::newFatal( 'undelete-error-associated-notdeleted' );
|
||||
}
|
||||
return StatusValue::newGood();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wether to delete the associated talk page with the subject page
|
||||
*
|
||||
* @param bool $undelete
|
||||
* @return self For chaining
|
||||
*/
|
||||
public function setUndeleteAssociatedTalk( bool $undelete ): self {
|
||||
if ( !$undelete ) {
|
||||
$this->associatedTalk = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// @todo FIXME: NamespaceInfo should accept PageIdentity
|
||||
$thisWikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
|
||||
$this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
|
||||
$this->namespaceInfo->getTalkPage( $thisWikiPage->getTitle() )
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as undeleteUnsafe, but checks permissions.
|
||||
*
|
||||
|
|
@ -201,6 +257,9 @@ class UndeletePage {
|
|||
private function authorizeUndeletion(): PermissionStatus {
|
||||
$status = PermissionStatus::newEmpty();
|
||||
$this->performer->authorizeWrite( 'undelete', $this->page, $status );
|
||||
if ( $this->associatedTalk ) {
|
||||
$this->performer->authorizeWrite( 'undelete', $this->associatedTalk, $status );
|
||||
}
|
||||
if ( $this->tags ) {
|
||||
$status->merge( ChangeTags::canAddTagsAccompanyingChange( $this->tags, $this->performer ) );
|
||||
}
|
||||
|
|
@ -226,18 +285,8 @@ class UndeletePage {
|
|||
* Fatal Status on failure.
|
||||
*/
|
||||
public function undeleteUnsafe( string $comment ): StatusValue {
|
||||
$hookStatus = StatusValue::newGood();
|
||||
$hookRes = $this->hookRunner->onPageUndelete(
|
||||
$this->page,
|
||||
$this->performer,
|
||||
$comment,
|
||||
$this->unsuppress,
|
||||
$this->timestamps,
|
||||
$this->fileVersions,
|
||||
$hookStatus
|
||||
);
|
||||
if ( !$hookRes && !$hookStatus->isGood() ) {
|
||||
// Note: as per the PageUndeleteHook documentation, `return false` is ignored if $status is good.
|
||||
$hookStatus = $this->runPreUndeleteHook( $comment );
|
||||
if ( !$hookStatus->isGood() ) {
|
||||
return $hookStatus;
|
||||
}
|
||||
// If both the set of text revisions and file revisions are empty,
|
||||
|
|
@ -248,6 +297,7 @@ class UndeletePage {
|
|||
$restoreFiles = $restoreAll || $this->fileVersions !== [];
|
||||
|
||||
$resStatus = StatusValue::newGood();
|
||||
$filesRestored = 0;
|
||||
if ( $restoreFiles && $this->page->getNamespace() === NS_FILE ) {
|
||||
/** @var LocalFile $img */
|
||||
$img = $this->repoGroup->getLocalRepo()->newFile( $this->page );
|
||||
|
|
@ -258,35 +308,101 @@ class UndeletePage {
|
|||
}
|
||||
$filesRestored = $this->fileStatus->successCount;
|
||||
$resStatus->merge( $this->fileStatus );
|
||||
} else {
|
||||
$filesRestored = 0;
|
||||
}
|
||||
|
||||
$textRestored = 0;
|
||||
if ( $restoreText ) {
|
||||
$this->revisionStatus = $this->undeleteRevisions( $comment );
|
||||
$this->revisionStatus = $this->undeleteRevisions( $this->page, $this->timestamps, $comment );
|
||||
if ( !$this->revisionStatus->isOK() ) {
|
||||
return $this->revisionStatus;
|
||||
}
|
||||
|
||||
$textRestored = $this->revisionStatus->getValue();
|
||||
$resStatus->merge( $this->revisionStatus );
|
||||
} else {
|
||||
$textRestored = 0;
|
||||
}
|
||||
|
||||
$talkRestored = 0;
|
||||
if ( $this->associatedTalk ) {
|
||||
$talkStatus = $this->canProbablyUndeleteAssociatedTalk();
|
||||
// if undeletion of the page fails we don't want to undelete the talk page
|
||||
if ( $talkStatus->isGood() && $this->revisionStatus->isGood() ) {
|
||||
$talkStatus = $this->undeleteRevisions( $this->associatedTalk, [], $comment );
|
||||
if ( !$talkStatus->isOK() ) {
|
||||
return $talkStatus;
|
||||
}
|
||||
$talkRestored = $talkStatus->getValue();
|
||||
|
||||
} else {
|
||||
// Add errors as warnings since the talk page is secondary to the main action
|
||||
foreach ( $talkStatus->getErrors() as $error ) {
|
||||
$resStatus->warning( $error['message'], $error['params'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$resStatus->value = [
|
||||
self::REVISIONS_RESTORED => $textRestored,
|
||||
self::REVISIONS_RESTORED => $textRestored + $talkRestored,
|
||||
self::FILES_RESTORED => $filesRestored
|
||||
];
|
||||
|
||||
if ( !$textRestored && !$filesRestored ) {
|
||||
if ( !$textRestored && !$filesRestored && !$talkRestored ) {
|
||||
$this->logger->debug( "Undelete: nothing undeleted..." );
|
||||
return $resStatus;
|
||||
}
|
||||
|
||||
if ( $textRestored || $filesRestored ) {
|
||||
$this->addLogEntry( $this->page, $comment, $textRestored, $filesRestored );
|
||||
}
|
||||
if ( $talkRestored ) {
|
||||
$this->addLogEntry( $this->associatedTalk, $comment, $talkRestored, 0 );
|
||||
}
|
||||
|
||||
return $resStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comment
|
||||
* @return StatusValue
|
||||
*/
|
||||
private function runPreUndeleteHook( string $comment ): StatusValue {
|
||||
$checkPages = [ $this->page ];
|
||||
if ( $this->associatedTalk ) {
|
||||
$checkPages[] = $this->associatedTalk;
|
||||
}
|
||||
foreach ( $checkPages as $page ) {
|
||||
$hookStatus = StatusValue::newGood();
|
||||
$hookRes = $this->hookRunner->onPageUndelete(
|
||||
$page,
|
||||
$this->performer,
|
||||
$comment,
|
||||
$this->unsuppress,
|
||||
$this->timestamps,
|
||||
$this->fileVersions,
|
||||
$hookStatus
|
||||
);
|
||||
if ( !$hookRes && !$hookStatus->isGood() ) {
|
||||
// Note: as per the PageUndeleteHook documentation, `return false` is ignored if $status is good.
|
||||
return $hookStatus;
|
||||
}
|
||||
}
|
||||
return Status::newGood();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProperPageIdentity $page
|
||||
* @param string $comment
|
||||
* @param int $textRestored
|
||||
* @param int $filesRestored
|
||||
*/
|
||||
private function addLogEntry(
|
||||
ProperPageIdentity $page,
|
||||
string $comment,
|
||||
int $textRestored,
|
||||
int $filesRestored
|
||||
): void {
|
||||
$logEntry = new ManualLogEntry( 'delete', 'restore' );
|
||||
$logEntry->setPerformer( $this->performer->getUser() );
|
||||
$logEntry->setTarget( $this->page );
|
||||
$logEntry->setTarget( $page );
|
||||
$logEntry->setComment( $comment );
|
||||
$logEntry->addTags( $this->tags );
|
||||
$logEntry->setParameters( [
|
||||
|
|
@ -298,19 +414,19 @@ class UndeletePage {
|
|||
|
||||
$logid = $logEntry->insert();
|
||||
$logEntry->publish( $logid );
|
||||
|
||||
return $resStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the meaty bit -- It restores archived revisions of the given page
|
||||
* to the revision table.
|
||||
*
|
||||
* @param ProperPageIdentity $page
|
||||
* @param string[] $timestamps
|
||||
* @param string $comment
|
||||
* @throws ReadOnlyError
|
||||
* @return StatusValue Status object containing the number of revisions restored on success
|
||||
*/
|
||||
private function undeleteRevisions( string $comment ): StatusValue {
|
||||
private function undeleteRevisions( ProperPageIdentity $page, array $timestamps, string $comment ): StatusValue {
|
||||
if ( $this->readOnlyMode->isReadOnly() ) {
|
||||
throw new ReadOnlyError();
|
||||
}
|
||||
|
|
@ -319,11 +435,11 @@ class UndeletePage {
|
|||
$dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
|
||||
|
||||
$oldWhere = [
|
||||
'ar_namespace' => $this->page->getNamespace(),
|
||||
'ar_title' => $this->page->getDBkey(),
|
||||
'ar_namespace' => $page->getNamespace(),
|
||||
'ar_title' => $page->getDBkey(),
|
||||
];
|
||||
if ( $this->timestamps ) {
|
||||
$oldWhere['ar_timestamp'] = array_map( [ $dbw, 'timestamp' ], $this->timestamps );
|
||||
if ( $timestamps ) {
|
||||
$oldWhere['ar_timestamp'] = array_map( [ $dbw, 'timestamp' ], $timestamps );
|
||||
}
|
||||
|
||||
$revisionStore = $this->revisionStore;
|
||||
|
|
@ -392,7 +508,7 @@ class UndeletePage {
|
|||
// move back
|
||||
$result->seek( 0 );
|
||||
|
||||
$wikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
|
||||
$wikiPage = $this->wikiPageFactory->newFromTitle( $page );
|
||||
|
||||
$oldPageId = 0;
|
||||
/** @var RevisionRecord|null $revision */
|
||||
|
|
@ -416,7 +532,7 @@ class UndeletePage {
|
|||
$revision = $revisionStore->newRevisionFromArchiveRow(
|
||||
$latestRestorableRow,
|
||||
0,
|
||||
$this->page
|
||||
$page
|
||||
);
|
||||
|
||||
foreach ( $revision->getSlotRoles() as $role ) {
|
||||
|
|
@ -494,7 +610,7 @@ class UndeletePage {
|
|||
$revision = $revisionStore->newRevisionFromArchiveRow(
|
||||
$row,
|
||||
0,
|
||||
$this->page,
|
||||
$page,
|
||||
[
|
||||
'page_id' => $pageId,
|
||||
'deleted' => $this->unsuppress ? 0 : $row->ar_deleted
|
||||
|
|
@ -566,9 +682,9 @@ class UndeletePage {
|
|||
$this->hookRunner->onArticleUndelete(
|
||||
$wikiPage->getTitle(), $created, $comment, $oldPageId, $restoredPages );
|
||||
|
||||
if ( $this->page->getNamespace() === NS_FILE ) {
|
||||
if ( $page->getNamespace() === NS_FILE ) {
|
||||
$job = HTMLCacheUpdateJob::newForBacklinks(
|
||||
$this->page,
|
||||
$page,
|
||||
'imagelinks',
|
||||
[ 'causeAction' => 'file-restore' ]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class WikiPageFactory {
|
|||
*
|
||||
* @return WikiPage
|
||||
*/
|
||||
public function newFromLinkTarget( LinkTarget $title ) {
|
||||
public function newFromLinkTarget( LinkTarget $title ): WikiPage {
|
||||
return $this->newFromTitle( $this->titleFactory->newFromLinkTarget( $title ) );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2637,6 +2637,8 @@
|
|||
"undelete-cleanup-error": "Error deleting unused archive file \"$1\".",
|
||||
"undelete-missing-filearchive": "Unable to restore file archive ID $1 because it is not in the database.\nIt may have already been undeleted.",
|
||||
"undelete-error": "Error undeleting page",
|
||||
"undelete-error-associated-alreadytalk": "Cannot undelete associated talk page of a talk page.",
|
||||
"undelete-error-associated-notdeleted": "The associated talk page has no revisions that can be restored.",
|
||||
"undelete-show-file-confirm": "Are you sure you want to view the deleted revision of the file \"<nowiki>$1</nowiki>\" from $2 at $3?",
|
||||
"undelete-show-file-submit": "Yes",
|
||||
"undelete-revision-row2": "$1 ($2) $3 . . $4 $5 $6 $7 $8",
|
||||
|
|
|
|||
|
|
@ -2871,6 +2871,8 @@
|
|||
"undelete-cleanup-error": "Used as error message. Parameters:\n* $1 - file path",
|
||||
"undelete-missing-filearchive": "Used as error message. Parameters:\n* $1 - missing ID",
|
||||
"undelete-error": "Page title when a page could not be undeleted",
|
||||
"undelete-error-associated-alreadytalk": "Error message shown when attempting to undelete the associated talk page of a page in the talk namespace.",
|
||||
"undelete-error-associated-notdeleted": "Error message shown when attempting to undelete the associated talk page but it has no revisions to undelete.",
|
||||
"undelete-show-file-confirm": "A confirmation message shown on [[Special:Undelete]] when the request does not contain a valid token (e.g. when a user clicks a link received in mail).\n\nParameters:\n* $1 - the name of the file being undeleted\n* $2 - the date of the displayed revision\n* $3 - the time of the displayed revision\n{{Identical|Are you sure you want to view the deleted revision of the file...}}",
|
||||
"undelete-show-file-submit": "{{Identical|Yes}}",
|
||||
"undelete-revision-row2": "{{Optional}}\nA revision row in the undelete page. Parameters:\n* $1 is a checkBox to indicate whether to restore this specific revision\n* $2 is a link to the last revision of a page ({{msg-mw|last}})\n* $3 is a link to the page\n* $4 is a link to the revision's user\n* $5 is the revision's minor edit identifier\n* $6 is the revision size\n* $7 is the revision comment\n* $8 is the revision's tags",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Revision\MutableRevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Storage\SlotRecord;
|
||||
use MediaWiki\Page\UndeletePage;
|
||||
use MediaWiki\User\UserIdentityValue;
|
||||
use Wikimedia\IPUtils;
|
||||
|
||||
|
|
@ -12,9 +10,9 @@ use Wikimedia\IPUtils;
|
|||
*/
|
||||
class UndeletePageTest extends MediaWikiIntegrationTestCase {
|
||||
/**
|
||||
* @var WikiPage
|
||||
* @var array
|
||||
*/
|
||||
private $page;
|
||||
private $pages = [];
|
||||
|
||||
/**
|
||||
* A logged out user who edited the page before it was archived.
|
||||
|
|
@ -22,12 +20,6 @@ class UndeletePageTest extends MediaWikiIntegrationTestCase {
|
|||
*/
|
||||
private $ipEditor;
|
||||
|
||||
/**
|
||||
* Revision of the IP edit (the second edit)
|
||||
* @var RevisionRecord
|
||||
*/
|
||||
private $ipRev;
|
||||
|
||||
protected function addCoreDBData() {
|
||||
// Blanked out to keep auto-increment values stable.
|
||||
}
|
||||
|
|
@ -55,40 +47,31 @@ class UndeletePageTest extends MediaWikiIntegrationTestCase {
|
|||
]
|
||||
);
|
||||
|
||||
// First create our dummy page
|
||||
$page = Title::newFromText( 'UndeletePageTest_thePage' );
|
||||
$page = new WikiPage( $page );
|
||||
$content = ContentHandler::makeContent(
|
||||
'testing',
|
||||
$page->getTitle(),
|
||||
CONTENT_MODEL_WIKITEXT
|
||||
);
|
||||
|
||||
$user = $this->getTestUser()->getUser();
|
||||
$page->doUserEditContent( $content, $user, 'testing', EDIT_NEW );
|
||||
|
||||
// Insert IP revision
|
||||
$this->ipEditor = '2001:DB8:0:0:0:0:0:1';
|
||||
$this->setupPage( 'UndeletePageTest_thePage', NS_MAIN, ' ' );
|
||||
$this->setupPage( 'UndeletePageTest_thePage', NS_TALK, ' ' );
|
||||
}
|
||||
|
||||
$revisionStore = $this->getServiceContainer()->getRevisionStore();
|
||||
/**
|
||||
* @param string $titleText
|
||||
* @param int $ns
|
||||
* @param string $content
|
||||
*/
|
||||
private function setupPage( string $titleText, int $ns, string $content ): void {
|
||||
$title = Title::newFromText( $titleText, $ns );
|
||||
$page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
|
||||
$performer = static::getTestUser()->getUser();
|
||||
$content = ContentHandler::makeContent( $content, $page->getTitle(), CONTENT_MODEL_WIKITEXT );
|
||||
$updater = $page->newPageUpdater( UserIdentityValue::newAnonymous( $this->ipEditor ) )
|
||||
->setContent( 'main', $content );
|
||||
|
||||
$firstRev = $page->getRevisionRecord();
|
||||
$ipTimestamp = wfTimestamp(
|
||||
TS_MW,
|
||||
wfTimestamp( TS_UNIX, $firstRev->getTimestamp() ) + 1
|
||||
);
|
||||
$rev = new MutableRevisionRecord( $page );
|
||||
$rev->setUser( UserIdentityValue::newAnonymous( $this->ipEditor ) );
|
||||
$rev->setTimestamp( $ipTimestamp );
|
||||
$rev->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
|
||||
$rev->setComment( CommentStoreComment::newUnsavedComment( 'just a test' ) );
|
||||
$revisionRecord = $updater->saveRevision( CommentStoreComment::newUnsavedComment( "testing" ) );
|
||||
if ( !$updater->wasSuccessful() ) {
|
||||
$this->fail( $updater->getStatus()->getWikiText() );
|
||||
}
|
||||
|
||||
$dbw = wfGetDB( DB_PRIMARY );
|
||||
$this->ipRev = $revisionStore->insertRevisionOn( $rev, $dbw );
|
||||
|
||||
$this->deletePage( $page, '', $user );
|
||||
|
||||
$this->page = $page;
|
||||
$this->pages[] = [ 'page' => $page, 'revId' => $revisionRecord->getId() ];
|
||||
$this->deletePage( $page, '', $performer );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -102,47 +85,54 @@ class UndeletePageTest extends MediaWikiIntegrationTestCase {
|
|||
// First make sure old revisions are archived
|
||||
$dbr = wfGetDB( DB_REPLICA );
|
||||
$arQuery = $revisionStore->getArchiveQueryInfo();
|
||||
$row = $dbr->selectRow(
|
||||
$arQuery['tables'],
|
||||
$arQuery['fields'],
|
||||
[ 'ar_rev_id' => $this->ipRev->getId() ],
|
||||
__METHOD__,
|
||||
[],
|
||||
$arQuery['joins']
|
||||
);
|
||||
$this->assertEquals( $this->ipEditor, $row->ar_user_text );
|
||||
|
||||
// Should not be in revision
|
||||
$row = $dbr->selectRow( 'revision', '1', [ 'rev_id' => $this->ipRev->getId() ] );
|
||||
$this->assertFalse( $row );
|
||||
foreach ( [ 0, 1 ] as $key ) {
|
||||
$row = $dbr->selectRow(
|
||||
$arQuery['tables'],
|
||||
$arQuery['fields'],
|
||||
[ 'ar_rev_id' => $this->pages[$key]['revId'] ],
|
||||
__METHOD__,
|
||||
[],
|
||||
$arQuery['joins']
|
||||
);
|
||||
$this->assertEquals( $this->ipEditor, $row->ar_user_text );
|
||||
|
||||
// Should not be in ip_changes
|
||||
$row = $dbr->selectRow( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRev->getId() ] );
|
||||
$this->assertFalse( $row );
|
||||
// Should not be in revision
|
||||
$row = $dbr->selectRow( 'revision', '1', [ 'rev_id' => $this->pages[$key]['revId'] ] );
|
||||
$this->assertFalse( $row );
|
||||
|
||||
// Should not be in ip_changes
|
||||
$row = $dbr->selectRow( 'ip_changes', '1', [ 'ipc_rev_id' => $this->pages[$key]['revId'] ] );
|
||||
$this->assertFalse( $row );
|
||||
}
|
||||
|
||||
// Restore the page
|
||||
$undeletePage = $this->getServiceContainer()->getUndeletePageFactory()->newUndeletePage(
|
||||
$this->page,
|
||||
$this->pages[0]['page'],
|
||||
$this->getTestSysop()->getUser()
|
||||
);
|
||||
$undeletePage->undeleteUnsafe( '' );
|
||||
|
||||
// Should be back in revision
|
||||
$status = $undeletePage->setUndeleteAssociatedTalk( true )->undeleteUnsafe( '' );
|
||||
$this->assertEquals( 2, $status->value[UndeletePage::REVISIONS_RESTORED] );
|
||||
|
||||
$revQuery = $revisionStore->getQueryInfo();
|
||||
$row = $dbr->selectRow(
|
||||
$revQuery['tables'],
|
||||
$revQuery['fields'],
|
||||
[ 'rev_id' => $this->ipRev->getId() ],
|
||||
__METHOD__,
|
||||
[],
|
||||
$revQuery['joins']
|
||||
);
|
||||
$this->assertNotFalse( $row, 'row exists in revision table' );
|
||||
$this->assertEquals( $this->ipEditor, $row->rev_user_text );
|
||||
// check subject page and talk page are both back in the revision table
|
||||
foreach ( [ 0, 1 ] as $key ) {
|
||||
$row = $dbr->selectRow(
|
||||
$revQuery['tables'],
|
||||
$revQuery['fields'],
|
||||
[ 'rev_id' => $this->pages[$key]['revId'] ],
|
||||
__METHOD__,
|
||||
[],
|
||||
$revQuery['joins']
|
||||
);
|
||||
$this->assertNotFalse( $row, 'row exists in revision table' );
|
||||
$this->assertEquals( $this->ipEditor, $row->rev_user_text );
|
||||
|
||||
// Should be back in ip_changes
|
||||
$row = $dbr->selectRow( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRev->getId() ] );
|
||||
$this->assertNotFalse( $row, 'row exists in ip_changes table' );
|
||||
$this->assertEquals( IPUtils::toHex( $this->ipEditor ), $row->ipc_hex );
|
||||
// Should be back in ip_changes
|
||||
$row = $dbr->selectRow( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->pages[$key]['revId'] ] );
|
||||
$this->assertNotFalse( $row, 'row exists in ip_changes table' );
|
||||
$this->assertEquals( IPUtils::toHex( $this->ipEditor ), $row->ipc_hex );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ class DeletePageTest extends MediaWikiUnitTestCase {
|
|||
} );
|
||||
$wpFactory->method( 'newFromLinkTarget' )->willReturnCallback(
|
||||
function ( LinkTarget $t ) use ( $talkExists ) {
|
||||
$existingTalk = $this->createMock( PageIdentity::class );
|
||||
$existingTalk = $this->createMock( WikiPage::class );
|
||||
$existingTalk->expects( $this->atLeastOnce() )->method( 'exists' )->willReturn( $talkExists );
|
||||
return $existingTalk;
|
||||
}
|
||||
|
|
|
|||
143
tests/phpunit/unit/includes/page/UndeletePageTest.php
Normal file
143
tests/phpunit/unit/includes/page/UndeletePageTest.php
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Unit\Page;
|
||||
|
||||
use Generator;
|
||||
use JobQueueGroup;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
use MediaWiki\Linker\LinkTarget;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Page\PageIdentityValue;
|
||||
use MediaWiki\Page\ProperPageIdentity;
|
||||
use MediaWiki\Page\UndeletePage;
|
||||
use MediaWiki\Page\WikiPageFactory;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Revision\ArchivedRevisionLookup;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MediaWiki\Storage\PageUpdaterFactory;
|
||||
use MediaWikiUnitTestCase;
|
||||
use NamespaceInfo;
|
||||
use Psr\Log\NullLogger;
|
||||
use ReadOnlyMode;
|
||||
use RepoGroup;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
use WikiPage;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \MediaWiki\Page\UndeletePage
|
||||
*/
|
||||
class UndeletePageTest extends MediaWikiUnitTestCase {
|
||||
/**
|
||||
* @param ProperPageIdentity|null $page
|
||||
* @param WikiPageFactory|null $wpFactory
|
||||
* @param NamespaceInfo|null $namespaceInfo
|
||||
* @param ArchivedRevisionLookup|null $archivedRevisionLookup
|
||||
* @return UndeletePage
|
||||
*/
|
||||
private function getUndeletePage(
|
||||
ProperPageIdentity $page = null,
|
||||
WikiPageFactory $wpFactory = null,
|
||||
NamespaceInfo $namespaceInfo = null,
|
||||
ArchivedRevisionLookup $archivedRevisionLookup = null
|
||||
): UndeletePage {
|
||||
return new UndeletePage(
|
||||
$this->createHookContainer(),
|
||||
$this->createMock( JobQueueGroup::class ),
|
||||
$this->createMock( ILoadBalancer::class ),
|
||||
$this->createMock( ReadOnlyMode::class ),
|
||||
$this->createMock( RepoGroup::class ),
|
||||
new NullLogger(),
|
||||
$this->createMock( RevisionStore::class ),
|
||||
$wpFactory ?? $this->createMock( WikiPageFactory::class ),
|
||||
$this->createMock( PageUpdaterFactory::class ),
|
||||
$this->createMock( IContentHandlerFactory::class ),
|
||||
$archivedRevisionLookup ?? $this->createMock( ArchivedRevisionLookup::class ),
|
||||
$namespaceInfo ?? $this->createMock( NamespaceInfo::class ),
|
||||
$page ?? $this->createMock( ProperPageIdentity::class ),
|
||||
$this->createMock( Authority::class )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProperPageIdentity $page
|
||||
* @param WikiPageFactory $wpFactory
|
||||
* @param NamespaceInfo|null $nsInfo
|
||||
* @param ArchivedRevisionLookup $archivedRevisionLookup
|
||||
* @param string|null $expectedMsg
|
||||
* @covers ::canProbablyUndeleteAssociatedTalk
|
||||
* @dataProvider provideAssociatedTalk
|
||||
*/
|
||||
public function testCanProbablyUndeleteAssociatedTalk(
|
||||
ProperPageIdentity $page,
|
||||
WikiPageFactory $wpFactory,
|
||||
?NamespaceInfo $nsInfo,
|
||||
ArchivedRevisionLookup $archivedRevisionLookup,
|
||||
?string $expectedMsg
|
||||
): void {
|
||||
$res = $this->getUndeletePage( $page, $wpFactory, $nsInfo, $archivedRevisionLookup )
|
||||
->canProbablyUndeleteAssociatedTalk();
|
||||
if ( $expectedMsg === null ) {
|
||||
$this->assertTrue( $res->isGood(), $res->__toString() );
|
||||
} else {
|
||||
$this->assertFalse( $res->isOK() );
|
||||
$this->assertTrue( $res->hasMessage( $expectedMsg ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function provideAssociatedTalk(): Generator {
|
||||
$getWpFactory = function ( bool $talkExists ): WikiPageFactory {
|
||||
$wpFactory = $this->createMock( WikiPageFactory::class );
|
||||
$wpFactory->method( 'newFromTitle' )->willReturnCallback( static function ( $t ) {
|
||||
return new WikiPage( $t );
|
||||
} );
|
||||
$wpFactory->method( 'newFromLinkTarget' )->willReturnCallback(
|
||||
function ( LinkTarget $t ) use ( $talkExists ) {
|
||||
$existingTalk = $this->createMock( WikiPage::class );
|
||||
$existingTalk->method( 'exists' )->willReturn( $talkExists );
|
||||
return $existingTalk;
|
||||
}
|
||||
);
|
||||
return $wpFactory;
|
||||
};
|
||||
$getArchiveLookup = function ( bool $hasDeletedRevs ): ArchivedRevisionLookup {
|
||||
$ret = $this->createMock( ArchivedRevisionLookup::class );
|
||||
$ret->method( 'hasArchivedRevisions' )->willReturn( $hasDeletedRevs );
|
||||
return $ret;
|
||||
};
|
||||
$nsInfo = new NamespaceInfo( $this->createMock( ServiceOptions::class ), $this->createHookContainer() );
|
||||
|
||||
$talkPage = new PageIdentityValue( 42, NS_TALK, 'Test talk page', PageIdentity::LOCAL );
|
||||
yield 'Talk page' => [
|
||||
$talkPage,
|
||||
$getWpFactory( false ),
|
||||
$nsInfo,
|
||||
$getArchiveLookup( false ),
|
||||
'undelete-error-associated-alreadytalk'
|
||||
];
|
||||
|
||||
$nonTalkPage = new PageIdentityValue( 44, NS_MAIN, 'Test article', PageIdentity::LOCAL );
|
||||
|
||||
yield 'Article whose talk page exists and does not have deleted revisions' => [
|
||||
$nonTalkPage,
|
||||
$getWpFactory( true ),
|
||||
$nsInfo,
|
||||
$getArchiveLookup( false ),
|
||||
'undelete-error-associated-notdeleted'
|
||||
];
|
||||
|
||||
yield 'Article whose talk page does not exist and does not have deleted revisions' => [
|
||||
$nonTalkPage,
|
||||
$getWpFactory( false ),
|
||||
$nsInfo,
|
||||
$getArchiveLookup( false ),
|
||||
'undelete-error-associated-notdeleted'
|
||||
];
|
||||
|
||||
yield 'Article whose talk page exists and has deleted revisions' =>
|
||||
[ $nonTalkPage, $getWpFactory( true ), $nsInfo, $getArchiveLookup( true ), null ];
|
||||
|
||||
yield 'Article whose talk page does not exist and has deleted revisions' =>
|
||||
[ $nonTalkPage, $getWpFactory( false ), $nsInfo, $getArchiveLookup( true ), null ];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue