Factor out rollback logic from WikiPage
Change-Id: I95da91875fcf2f53143c315560e35ccd5ffbf4b3
This commit is contained in:
parent
817a3c203b
commit
46db19ecdf
19 changed files with 1156 additions and 660 deletions
|
|
@ -58,6 +58,7 @@ use MediaWiki\Page\MovePageFactory;
|
|||
use MediaWiki\Page\PageStore;
|
||||
use MediaWiki\Page\PageStoreFactory;
|
||||
use MediaWiki\Page\ParserOutputAccess;
|
||||
use MediaWiki\Page\RollbackPageFactory;
|
||||
use MediaWiki\Page\WikiPageFactory;
|
||||
use MediaWiki\Parser\ParserCacheFactory;
|
||||
use MediaWiki\Permissions\GroupPermissionsLookup;
|
||||
|
|
@ -1328,6 +1329,14 @@ class MediaWikiServices extends ServiceContainer {
|
|||
return $this->getService( 'RevisionStoreFactory' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.37
|
||||
* @return RollbackPageFactory
|
||||
*/
|
||||
public function getRollbackPageFactory() : RollbackPageFactory {
|
||||
return $this->getService( 'RollbackPageFactory' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.27
|
||||
* @return SearchEngine
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class MovePage {
|
|||
private $userFactory;
|
||||
|
||||
/**
|
||||
* @internal For use by MovePageTest
|
||||
* @internal For use by PageCommandFactory
|
||||
*/
|
||||
public const CONSTRUCTOR_OPTIONS = [
|
||||
'CategoryCollation',
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ use MediaWiki\Page\PageCommandFactory;
|
|||
use MediaWiki\Page\PageStore;
|
||||
use MediaWiki\Page\PageStoreFactory;
|
||||
use MediaWiki\Page\ParserOutputAccess;
|
||||
use MediaWiki\Page\RollbackPageFactory;
|
||||
use MediaWiki\Page\WikiPageFactory;
|
||||
use MediaWiki\Parser\ParserCacheFactory;
|
||||
use MediaWiki\Permissions\GroupPermissionsLookup;
|
||||
|
|
@ -1262,6 +1263,10 @@ return [
|
|||
return $store;
|
||||
},
|
||||
|
||||
'RollbackPageFactory' => static function ( MediaWikiServices $services ) : RollbackPageFactory {
|
||||
return $services->get( '_PageCommandFactory' );
|
||||
},
|
||||
|
||||
'SearchEngineConfig' => static function ( MediaWikiServices $services ) : SearchEngineConfig {
|
||||
// @todo This should not take a Config object, but it's not so easy to remove because it
|
||||
// exposes it in a getter, which is actually used.
|
||||
|
|
@ -1702,17 +1707,20 @@ return [
|
|||
|
||||
'_PageCommandFactory' => static function ( MediaWikiServices $services ) : PageCommandFactory {
|
||||
return new PageCommandFactory(
|
||||
new ServiceOptions( PageCommandFactory::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ),
|
||||
$services->getMainConfig(),
|
||||
$services->getDBLoadBalancer(),
|
||||
$services->getNamespaceInfo(),
|
||||
$services->getWatchedItemStore(),
|
||||
$services->getRepoGroup(),
|
||||
$services->getReadOnlyMode(),
|
||||
$services->getContentHandlerFactory(),
|
||||
$services->getRevisionStore(),
|
||||
$services->getSpamChecker(),
|
||||
$services->getTitleFormatter(),
|
||||
$services->getHookContainer(),
|
||||
$services->getWikiPageFactory(),
|
||||
$services->getUserFactory()
|
||||
$services->getUserFactory(),
|
||||
$services->getActorMigration()
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -114,25 +114,28 @@ class RollbackAction extends FormAction {
|
|||
] );
|
||||
}
|
||||
|
||||
$data = null;
|
||||
$errors = $this->getWikiPage()->doRollback(
|
||||
$from,
|
||||
$request->getText( 'summary' ),
|
||||
$request->getVal( 'token' ),
|
||||
$request->getBool( 'bot' ),
|
||||
$data,
|
||||
$this->getContext()->getAuthority()
|
||||
);
|
||||
// The revision has the user suppressed, so the rollback has empty 'from',
|
||||
// so the check above would succeed in that case.
|
||||
if ( !$revUser ) {
|
||||
$revUser = $rev->getUser( RevisionRecord::RAW );
|
||||
}
|
||||
|
||||
if ( in_array( [ 'actionthrottledtext' ], $errors ) ) {
|
||||
$rollbackResult = MediaWikiServices::getInstance()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $this->getWikiPage(), $this->getContext()->getAuthority(), $revUser )
|
||||
->setSummary( $request->getText( 'summary' ) )
|
||||
->markAsBot( $request->getVal( 'token' ) )
|
||||
->rollbackIfAllowed();
|
||||
$data = $rollbackResult->getValue();
|
||||
|
||||
if ( $rollbackResult->hasMessage( 'actionthrottledtext' ) ) {
|
||||
throw new ThrottledError;
|
||||
}
|
||||
|
||||
if ( $this->hasRollbackRelatedErrors( $errors ) ) {
|
||||
if ( $rollbackResult->hasMessage( 'alreadyrolled' ) || $rollbackResult->hasMessage( 'cantrollback' ) ) {
|
||||
$this->getOutput()->setPageTitle( $this->msg( 'rollbackfailed' ) );
|
||||
$errArray = $errors[0];
|
||||
$errMsg = array_shift( $errArray );
|
||||
$this->getOutput()->addWikiMsgArray( $errMsg, $errArray );
|
||||
$errArray = $rollbackResult->getErrors()[0];
|
||||
$this->getOutput()->addWikiMsgArray( $errArray['message'], $errArray['params'] );
|
||||
|
||||
if ( isset( $data['current-revision-record'] ) ) {
|
||||
/** @var RevisionRecord $current */
|
||||
|
|
@ -154,14 +157,14 @@ class RollbackAction extends FormAction {
|
|||
}
|
||||
|
||||
# NOTE: Permission errors already handled by Action::checkExecute.
|
||||
if ( $errors == [ [ 'readonlytext' ] ] ) {
|
||||
if ( $rollbackResult->hasMessage( 'readonlytext' ) ) {
|
||||
throw new ReadOnlyError;
|
||||
}
|
||||
|
||||
# XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object.
|
||||
# Right now, we only show the first error
|
||||
foreach ( $errors as $error ) {
|
||||
throw new ErrorPageError( 'rollbackfailed', $error[0], array_slice( $error, 1 ) );
|
||||
foreach ( $rollbackResult->getErrors() as $error ) {
|
||||
throw new ErrorPageError( 'rollbackfailed', $error['message'], $error['params'] );
|
||||
}
|
||||
|
||||
/** @var RevisionRecord $current */
|
||||
|
|
@ -251,11 +254,4 @@ class RollbackAction extends FormAction {
|
|||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function hasRollbackRelatedErrors( array $errors ) {
|
||||
return isset( $errors[0][0] ) &&
|
||||
( $errors[0][0] == 'alreadyrolled' ||
|
||||
$errors[0][0] == 'cantrollback'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,12 @@ class ApiMain extends ApiBase {
|
|||
'WatchedItemStore',
|
||||
]
|
||||
],
|
||||
'rollback' => ApiRollback::class,
|
||||
'rollback' => [
|
||||
'class' => ApiRollback::class,
|
||||
'services' => [
|
||||
'RollbackPageFactory',
|
||||
]
|
||||
],
|
||||
'delete' => ApiDelete::class,
|
||||
'undelete' => ApiUndelete::class,
|
||||
'protect' => ApiProtect::class,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
use MediaWiki\Page\RollbackPageFactory;
|
||||
use MediaWiki\ParamValidator\TypeDef\UserDef;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
||||
|
|
@ -30,11 +31,19 @@ class ApiRollback extends ApiBase {
|
|||
|
||||
use ApiWatchlistTrait;
|
||||
|
||||
public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) {
|
||||
parent::__construct( $mainModule, $moduleName, $modulePrefix );
|
||||
/** @var RollbackPageFactory */
|
||||
private $rollbackPageFactory;
|
||||
|
||||
public function __construct(
|
||||
ApiMain $mainModule,
|
||||
$moduleName,
|
||||
RollbackPageFactory $rollbackPageFactory
|
||||
) {
|
||||
parent::__construct( $mainModule, $moduleName );
|
||||
|
||||
$this->watchlistExpiryEnabled = $this->getConfig()->get( 'WatchlistExpiry' );
|
||||
$this->watchlistMaxDuration = $this->getConfig()->get( 'WatchlistExpiryMaxDuration' );
|
||||
$this->rollbackPageFactory = $rollbackPageFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,12 +63,9 @@ class ApiRollback extends ApiBase {
|
|||
$params = $this->extractRequestParams();
|
||||
|
||||
$titleObj = $this->getRbTitle( $params );
|
||||
$pageObj = WikiPage::factory( $titleObj );
|
||||
$summary = $params['summary'];
|
||||
$details = [];
|
||||
|
||||
// If change tagging was requested, check that the user is allowed to tag,
|
||||
// and the tags are valid
|
||||
// and the tags are valid. TODO: move inside rollback command?
|
||||
if ( $params['tags'] ) {
|
||||
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
|
||||
if ( !$tagStatus->isOK() ) {
|
||||
|
|
@ -76,18 +82,15 @@ class ApiRollback extends ApiBase {
|
|||
$trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
|
||||
} );
|
||||
|
||||
$retval = $pageObj->doRollback(
|
||||
$this->getRbUser( $params )->getName(),
|
||||
$summary,
|
||||
$params['token'],
|
||||
$params['markbot'],
|
||||
$details,
|
||||
$this->getAuthority(),
|
||||
$params['tags']
|
||||
);
|
||||
$rollbackResult = $this->rollbackPageFactory
|
||||
->newRollbackPage( $titleObj, $this->getAuthority(), $this->getRbUser( $params ) )
|
||||
->setSummary( $params['summary'] )
|
||||
->markAsBot( $params['markbot'] )
|
||||
->setChangeTags( $params['tags'] )
|
||||
->rollbackIfAllowed();
|
||||
|
||||
if ( $retval ) {
|
||||
$this->dieStatus( $this->errorArrayToStatus( $retval, $user ) );
|
||||
if ( !$rollbackResult->isGood() ) {
|
||||
$this->dieStatus( $rollbackResult );
|
||||
}
|
||||
|
||||
$watch = $params['watchlist'] ?? 'preferences';
|
||||
|
|
@ -96,6 +99,7 @@ class ApiRollback extends ApiBase {
|
|||
// Watch pages
|
||||
$this->setWatch( $watch, $titleObj, $user, 'watchrollback', $watchlistExpiry );
|
||||
|
||||
$details = $rollbackResult->getValue();
|
||||
$currentRevisionRecord = $details['current-revision-record'];
|
||||
$targetRevisionRecord = $details['target-revision-record'];
|
||||
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ class WebInstallerOptions extends WebInstallerPage {
|
|||
}
|
||||
}
|
||||
|
||||
// @phan-suppress-next-line SecurityCheck-XSS
|
||||
$text = wfMessage( 'config-extensions-requires' )
|
||||
->rawParams( $ext, $wgLang->commaList( $links ) )
|
||||
->escaped();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
namespace MediaWiki\Page;
|
||||
|
||||
use ActorMigration;
|
||||
use Config;
|
||||
use ContentModelChange;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
|
|
@ -30,11 +32,14 @@ use MediaWiki\HookContainer\HookContainer;
|
|||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MediaWiki\User\UserFactory;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
use MergeHistory;
|
||||
use MovePage;
|
||||
use NamespaceInfo;
|
||||
use ReadOnlyMode;
|
||||
use RepoGroup;
|
||||
use Title;
|
||||
use TitleFormatter;
|
||||
use WatchedItemStoreInterface;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
use WikiPage;
|
||||
|
|
@ -44,9 +49,15 @@ use WikiPage;
|
|||
*
|
||||
* @since 1.35
|
||||
*/
|
||||
class PageCommandFactory implements ContentModelChangeFactory, MergeHistoryFactory, MovePageFactory {
|
||||
/** @var ServiceOptions */
|
||||
private $options;
|
||||
class PageCommandFactory implements
|
||||
ContentModelChangeFactory,
|
||||
MergeHistoryFactory,
|
||||
MovePageFactory,
|
||||
RollbackPageFactory
|
||||
{
|
||||
|
||||
/** @var Config */
|
||||
private $config;
|
||||
|
||||
/** @var ILoadBalancer */
|
||||
private $loadBalancer;
|
||||
|
|
@ -60,6 +71,9 @@ class PageCommandFactory implements ContentModelChangeFactory, MergeHistoryFacto
|
|||
/** @var RepoGroup */
|
||||
private $repoGroup;
|
||||
|
||||
/** @var ReadOnlyMode */
|
||||
private $readOnlyMode;
|
||||
|
||||
/** @var IContentHandlerFactory */
|
||||
private $contentHandlerFactory;
|
||||
|
||||
|
|
@ -69,6 +83,9 @@ class PageCommandFactory implements ContentModelChangeFactory, MergeHistoryFacto
|
|||
/** @var SpamChecker */
|
||||
private $spamChecker;
|
||||
|
||||
/** @var TitleFormatter */
|
||||
private $titleFormatter;
|
||||
|
||||
/** @var HookContainer */
|
||||
private $hookContainer;
|
||||
|
||||
|
|
@ -78,40 +95,39 @@ class PageCommandFactory implements ContentModelChangeFactory, MergeHistoryFacto
|
|||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/**
|
||||
* @internal For use by ServiceWiring
|
||||
*/
|
||||
public const CONSTRUCTOR_OPTIONS = [
|
||||
'CategoryCollation',
|
||||
'MaximumMovedPages',
|
||||
];
|
||||
/** @var ActorMigration */
|
||||
private $actorMigration;
|
||||
|
||||
public function __construct(
|
||||
ServiceOptions $options,
|
||||
Config $config,
|
||||
ILoadBalancer $loadBalancer,
|
||||
NamespaceInfo $namespaceInfo,
|
||||
WatchedItemStoreInterface $watchedItemStore,
|
||||
RepoGroup $repoGroup,
|
||||
ReadOnlyMode $readOnlyMode,
|
||||
IContentHandlerFactory $contentHandlerFactory,
|
||||
RevisionStore $revisionStore,
|
||||
SpamChecker $spamChecker,
|
||||
TitleFormatter $titleFormatter,
|
||||
HookContainer $hookContainer,
|
||||
WikiPageFactory $wikiPageFactory,
|
||||
UserFactory $userFactory
|
||||
UserFactory $userFactory,
|
||||
ActorMigration $actorMigration
|
||||
) {
|
||||
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
||||
|
||||
$this->options = $options;
|
||||
$this->config = $config;
|
||||
$this->loadBalancer = $loadBalancer;
|
||||
$this->namespaceInfo = $namespaceInfo;
|
||||
$this->watchedItemStore = $watchedItemStore;
|
||||
$this->repoGroup = $repoGroup;
|
||||
$this->readOnlyMode = $readOnlyMode;
|
||||
$this->contentHandlerFactory = $contentHandlerFactory;
|
||||
$this->revisionStore = $revisionStore;
|
||||
$this->spamChecker = $spamChecker;
|
||||
$this->titleFormatter = $titleFormatter;
|
||||
$this->hookContainer = $hookContainer;
|
||||
$this->wikiPageFactory = $wikiPageFactory;
|
||||
$this->userFactory = $userFactory;
|
||||
$this->actorMigration = $actorMigration;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -175,7 +191,7 @@ class PageCommandFactory implements ContentModelChangeFactory, MergeHistoryFacto
|
|||
return new MovePage(
|
||||
$from,
|
||||
$to,
|
||||
$this->options,
|
||||
new ServiceOptions( MovePage::CONSTRUCTOR_OPTIONS, $this->config ),
|
||||
$this->loadBalancer,
|
||||
$this->namespaceInfo,
|
||||
$this->watchedItemStore,
|
||||
|
|
@ -188,4 +204,33 @@ class PageCommandFactory implements ContentModelChangeFactory, MergeHistoryFacto
|
|||
$this->userFactory
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new command instance for page rollback.
|
||||
*
|
||||
* @param PageIdentity $page
|
||||
* @param Authority $performer
|
||||
* @param UserIdentity $byUser
|
||||
* @return RollbackPage
|
||||
*/
|
||||
public function newRollbackPage(
|
||||
PageIdentity $page,
|
||||
Authority $performer,
|
||||
UserIdentity $byUser
|
||||
) : RollbackPage {
|
||||
return new RollbackPage(
|
||||
new ServiceOptions( RollbackPage::CONSTRUCTOR_OPTIONS, $this->config ),
|
||||
$this->loadBalancer,
|
||||
$this->userFactory,
|
||||
$this->readOnlyMode,
|
||||
$this->revisionStore,
|
||||
$this->titleFormatter,
|
||||
$this->hookContainer,
|
||||
$this->wikiPageFactory,
|
||||
$this->actorMigration,
|
||||
$page,
|
||||
$performer,
|
||||
$byUser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
518
includes/page/RollbackPage.php
Normal file
518
includes/page/RollbackPage.php
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
<?php
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Page;
|
||||
|
||||
use ActorMigration;
|
||||
use CommentStoreComment;
|
||||
use ManualLogEntry;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\HookContainer\HookContainer;
|
||||
use MediaWiki\HookContainer\HookRunner;
|
||||
use MediaWiki\Message\Converter;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Permissions\PermissionStatus;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MediaWiki\Revision\SlotRecord;
|
||||
use MediaWiki\Storage\EditResult;
|
||||
use MediaWiki\User\UserFactory;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
use Message;
|
||||
use RawMessage;
|
||||
use ReadOnlyMode;
|
||||
use RecentChange;
|
||||
use Revision;
|
||||
use StatusValue;
|
||||
use TitleFormatter;
|
||||
use TitleValue;
|
||||
use Wikimedia\Message\MessageValue;
|
||||
use Wikimedia\Rdbms\IDatabase;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
|
||||
/**
|
||||
* Logic for page rollbacks.
|
||||
* @since 1.37
|
||||
* @package MediaWiki\Page
|
||||
*/
|
||||
class RollbackPage {
|
||||
|
||||
/**
|
||||
* @internal for use in PageCommandFactory only
|
||||
* @var array
|
||||
*/
|
||||
public const CONSTRUCTOR_OPTIONS = [
|
||||
'UseRCPatrol',
|
||||
'DisableAnonTalk',
|
||||
];
|
||||
|
||||
/** @var ServiceOptions */
|
||||
private $options;
|
||||
|
||||
/** @var ILoadBalancer */
|
||||
private $loadBalancer;
|
||||
|
||||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/** @var ReadOnlyMode */
|
||||
private $readOnlyMode;
|
||||
|
||||
/** @var TitleFormatter */
|
||||
private $titleFormatter;
|
||||
|
||||
/** @var RevisionStore */
|
||||
private $revisionStore;
|
||||
|
||||
/** @var HookContainer */
|
||||
private $hookContainer;
|
||||
|
||||
/** @var HookRunner */
|
||||
private $hookRunner;
|
||||
|
||||
/** @var WikiPageFactory */
|
||||
private $wikiPageFactory;
|
||||
|
||||
/** @var ActorMigration */
|
||||
private $actorMigration;
|
||||
|
||||
/** @var PageIdentity */
|
||||
private $page;
|
||||
|
||||
/** @var Authority */
|
||||
private $performer;
|
||||
|
||||
/** @var UserIdentity who made the edits we are rolling back */
|
||||
private $byUser;
|
||||
|
||||
/** @var string */
|
||||
private $summary = '';
|
||||
|
||||
/** @var bool */
|
||||
private $bot = false;
|
||||
|
||||
/** @var string[] */
|
||||
private $tags = [];
|
||||
|
||||
/**
|
||||
* @param ServiceOptions $options
|
||||
* @param ILoadBalancer $loadBalancer
|
||||
* @param UserFactory $userFactory
|
||||
* @param ReadOnlyMode $readOnlyMode
|
||||
* @param RevisionStore $revisionStore
|
||||
* @param TitleFormatter $titleFormatter
|
||||
* @param HookContainer $hookContainer
|
||||
* @param WikiPageFactory $wikiPageFactory
|
||||
* @param ActorMigration $actorMigration
|
||||
* @param PageIdentity $page
|
||||
* @param Authority $performer
|
||||
* @param UserIdentity $byUser who made the edits we are rolling back
|
||||
*/
|
||||
public function __construct(
|
||||
ServiceOptions $options,
|
||||
ILoadBalancer $loadBalancer,
|
||||
UserFactory $userFactory,
|
||||
ReadOnlyMode $readOnlyMode,
|
||||
RevisionStore $revisionStore,
|
||||
TitleFormatter $titleFormatter,
|
||||
HookContainer $hookContainer,
|
||||
WikiPageFactory $wikiPageFactory,
|
||||
ActorMigration $actorMigration,
|
||||
PageIdentity $page,
|
||||
Authority $performer,
|
||||
UserIdentity $byUser
|
||||
) {
|
||||
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
||||
$this->options = $options;
|
||||
$this->loadBalancer = $loadBalancer;
|
||||
$this->userFactory = $userFactory;
|
||||
$this->readOnlyMode = $readOnlyMode;
|
||||
$this->revisionStore = $revisionStore;
|
||||
$this->titleFormatter = $titleFormatter;
|
||||
$this->hookContainer = $hookContainer;
|
||||
$this->hookRunner = new HookRunner( $hookContainer );
|
||||
$this->wikiPageFactory = $wikiPageFactory;
|
||||
$this->actorMigration = $actorMigration;
|
||||
|
||||
$this->page = $page;
|
||||
$this->performer = $performer;
|
||||
$this->byUser = $byUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom edit summary.
|
||||
*
|
||||
* @param string|null $summary
|
||||
* @return $this
|
||||
*/
|
||||
public function setSummary( ?string $summary ): self {
|
||||
$this->summary = $summary ?: '';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all reverted edits as bot.
|
||||
*
|
||||
* @param bool|null $bot
|
||||
* @return $this
|
||||
*/
|
||||
public function markAsBot( ?bool $bot ): self {
|
||||
if ( $bot && $this->performer->isAllowedAny( 'markbotedits', 'bot' ) ) {
|
||||
$this->bot = true;
|
||||
} elseif ( !$bot ) {
|
||||
$this->bot = false;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change tags to apply to the rollback.
|
||||
*
|
||||
* @note Callers are responsible for permission checks (with ChangeTags::canAddTagsAccompanyingChange)
|
||||
*
|
||||
* @param string[]|null $tags
|
||||
* @return $this
|
||||
*/
|
||||
public function setChangeTags( ?array $tags ): self {
|
||||
$this->tags = $tags ?: [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize the rollback.
|
||||
*
|
||||
* @return PermissionStatus
|
||||
*/
|
||||
public function authorizeRollback(): PermissionStatus {
|
||||
$permissionStatus = PermissionStatus::newEmpty();
|
||||
$this->performer->authorizeWrite( 'edit', $this->page, $permissionStatus );
|
||||
$this->performer->authorizeWrite( 'rollback', $this->page, $permissionStatus );
|
||||
|
||||
if ( $this->readOnlyMode->isReadOnly() ) {
|
||||
$permissionStatus->fatal( 'readonlytext' );
|
||||
}
|
||||
|
||||
$user = $this->userFactory->newFromAuthority( $this->performer );
|
||||
if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
|
||||
$permissionStatus->fatal( 'actionthrottledtext' );
|
||||
}
|
||||
return $permissionStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the most recent consecutive set of edits to a page
|
||||
* from the same user; fails if there are no eligible edits to
|
||||
* roll back to, e.g. user is the sole contributor. This function
|
||||
* performs permissions checks and executes ::rollback.
|
||||
*
|
||||
* @return StatusValue see ::rollback for return value documentation.
|
||||
* In case the rollback is not allowed, PermissionStatus is returned.
|
||||
*/
|
||||
public function rollbackIfAllowed(): StatusValue {
|
||||
$permissionStatus = $this->authorizeRollback();
|
||||
if ( !$permissionStatus->isGood() ) {
|
||||
return $permissionStatus;
|
||||
}
|
||||
return $this->rollback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend implementation of rollbackIfAllowed().
|
||||
*
|
||||
* @note This function does NOT check ANY permissions, it just commits the
|
||||
* rollback to the DB. Therefore, you should only call this function directly
|
||||
* if you want to use custom permissions checks. If you don't, use
|
||||
* ::rollbackIfAllowed() instead.
|
||||
*
|
||||
* @return StatusValue On success, wrapping the array with the following keys:
|
||||
* 'summary' - rollback edit summary
|
||||
* 'current-revision-record' - revision record that was current before rollback
|
||||
* 'target-revision-record' - revision record we are rolling back to
|
||||
* 'newid' => the id of the rollback revision
|
||||
* 'tags' => the tags applied to the rollback
|
||||
*/
|
||||
public function rollback() {
|
||||
// Begin revision creation cycle by creating a PageUpdater.
|
||||
// If the page is changed concurrently after grabParentRevision(), the rollback will fail.
|
||||
// TODO: move PageUpdater to PageStore or PageUpdaterFactory or something?
|
||||
$updater = $this->wikiPageFactory->newFromTitle( $this->page )->newPageUpdater( $this->performer );
|
||||
$currentRevision = $updater->grabParentRevision();
|
||||
|
||||
if ( !$currentRevision ) {
|
||||
// Something wrong... no page?
|
||||
return StatusValue::newFatal( 'notanarticle' );
|
||||
}
|
||||
|
||||
$currentEditor = $currentRevision->getUser( RevisionRecord::RAW );
|
||||
$currentEditorForPublic = $currentRevision->getUser( RevisionRecord::FOR_PUBLIC );
|
||||
// User name given should match up with the top revision.
|
||||
if ( !$this->byUser->equals( $currentEditor ) ) {
|
||||
$result = StatusValue::newGood( [
|
||||
'current-revision-record' => $currentRevision
|
||||
] );
|
||||
$result->fatal(
|
||||
'alreadyrolled',
|
||||
htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
|
||||
htmlspecialchars( $this->byUser->getName() ),
|
||||
htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
$dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
|
||||
|
||||
// TODO: move this query to RevisionSelectQueryBuilder when it's available
|
||||
// Get the last edit not by this person...
|
||||
// Note: these may not be public values
|
||||
$actorWhere = $this->actorMigration->getWhere( $dbw, 'rev_user', $currentEditor );
|
||||
$targetRevisionRow = $dbw->selectRow(
|
||||
[ 'revision' ] + $actorWhere['tables'],
|
||||
[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
|
||||
[
|
||||
'rev_page' => $currentRevision->getPageId(),
|
||||
'NOT(' . $actorWhere['conds'] . ')',
|
||||
],
|
||||
__METHOD__,
|
||||
[
|
||||
'USE INDEX' => [ 'revision' => 'page_timestamp' ],
|
||||
'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
|
||||
],
|
||||
$actorWhere['joins']
|
||||
);
|
||||
|
||||
if ( $targetRevisionRow === false ) {
|
||||
// No one else ever edited this page
|
||||
return StatusValue::newFatal( 'cantrollback' );
|
||||
} elseif ( $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_TEXT
|
||||
|| $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_USER
|
||||
) {
|
||||
// Only admins can see this text
|
||||
return StatusValue::newFatal( 'notvisiblerev' );
|
||||
}
|
||||
|
||||
// Generate the edit summary if necessary
|
||||
$targetRevision = $this->revisionStore
|
||||
->getRevisionById( $targetRevisionRow->rev_id, RevisionStore::READ_LATEST );
|
||||
|
||||
// Save
|
||||
$flags = EDIT_UPDATE | EDIT_INTERNAL;
|
||||
|
||||
if ( $this->performer->isAllowed( 'minoredit' ) ) {
|
||||
$flags |= EDIT_MINOR;
|
||||
}
|
||||
|
||||
if ( $this->bot ) {
|
||||
$flags |= EDIT_FORCE_BOT;
|
||||
}
|
||||
|
||||
// TODO: MCR: also log model changes in other slots, in case that becomes possible!
|
||||
$currentContent = $currentRevision->getContent( SlotRecord::MAIN );
|
||||
$targetContent = $targetRevision->getContent( SlotRecord::MAIN );
|
||||
$changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
|
||||
|
||||
// Build rollback revision:
|
||||
// Restore old content
|
||||
// TODO: MCR: test this once we can store multiple slots
|
||||
foreach ( $targetRevision->getSlots()->getSlots() as $slot ) {
|
||||
$updater->inheritSlot( $slot );
|
||||
}
|
||||
|
||||
// Remove extra slots
|
||||
// TODO: MCR: test this once we can store multiple slots
|
||||
foreach ( $currentRevision->getSlotRoles() as $role ) {
|
||||
if ( !$targetRevision->hasSlot( $role ) ) {
|
||||
$updater->removeSlot( $role );
|
||||
}
|
||||
}
|
||||
|
||||
$updater->setOriginalRevisionId( $targetRevision->getId() );
|
||||
$oldestRevertedRevision = $this->revisionStore->getNextRevision(
|
||||
$targetRevision,
|
||||
RevisionStore::READ_LATEST
|
||||
);
|
||||
if ( $oldestRevertedRevision !== null ) {
|
||||
$updater->markAsRevert(
|
||||
EditResult::REVERT_ROLLBACK,
|
||||
$oldestRevertedRevision->getId(),
|
||||
$currentRevision->getId()
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this logic should not be in the storage layer, it's here for compatibility
|
||||
// with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
|
||||
// place the 'bot' right is handled, which is currently in EditPage::attemptSave.
|
||||
if ( $this->options->get( 'UseRCPatrol' ) &&
|
||||
$this->performer->authorizeWrite( 'autopatrol', $this->page )
|
||||
) {
|
||||
$updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
|
||||
}
|
||||
|
||||
$summary = $this->getSummary( $currentRevision, $targetRevision );
|
||||
|
||||
// Actually store the rollback
|
||||
$rev = $updater->saveRevision(
|
||||
CommentStoreComment::newUnsavedComment( $summary ),
|
||||
$flags
|
||||
);
|
||||
|
||||
// This is done even on edit failure to have patrolling in that case (T64157).
|
||||
$this->updateRecentChange( $dbw, $currentRevision, $targetRevision );
|
||||
|
||||
if ( !$updater->wasSuccessful() ) {
|
||||
return $updater->getStatus();
|
||||
}
|
||||
|
||||
// Report if the edit was not created because it did not change the content.
|
||||
if ( $updater->isUnchanged() ) {
|
||||
$result = StatusValue::newGood( [
|
||||
'current-revision-record' => $currentRevision
|
||||
] );
|
||||
$result->fatal(
|
||||
'alreadyrolled',
|
||||
htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
|
||||
htmlspecialchars( $this->byUser->getName() ),
|
||||
htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( $changingContentModel ) {
|
||||
// If the content model changed during the rollback,
|
||||
// make sure it gets logged to Special:Log/contentmodel
|
||||
$log = new ManualLogEntry( 'contentmodel', 'change' );
|
||||
$log->setPerformer( $this->performer->getUser() );
|
||||
$log->setTarget( new TitleValue( $this->page->getNamespace(), $this->page->getDBkey() ) );
|
||||
$log->setComment( $summary );
|
||||
$log->setParameters( [
|
||||
'4::oldmodel' => $currentContent->getModel(),
|
||||
'5::newmodel' => $targetContent->getModel(),
|
||||
] );
|
||||
|
||||
$logId = $log->insert( $dbw );
|
||||
$log->publish( $logId );
|
||||
}
|
||||
|
||||
// Hook is hard deprecated since 1.35
|
||||
$user = $this->userFactory->newFromAuthority( $this->performer );
|
||||
$wikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
|
||||
if ( $this->hookContainer->isRegistered( 'ArticleRollbackComplete' ) ) {
|
||||
// Only create the Revision objects if needed
|
||||
$legacyCurrent = new Revision( $currentRevision );
|
||||
$legacyTarget = new Revision( $targetRevision );
|
||||
$this->hookRunner->onArticleRollbackComplete( $wikiPage, $user,
|
||||
$legacyTarget, $legacyCurrent );
|
||||
}
|
||||
|
||||
$this->hookRunner->onRollbackComplete( $wikiPage, $user, $targetRevision, $currentRevision );
|
||||
|
||||
return StatusValue::newGood( [
|
||||
'summary' => $summary,
|
||||
'current-revision-record' => $currentRevision,
|
||||
'target-revision-record' => $targetRevision,
|
||||
'newid' => $rev->getId(),
|
||||
'tags' => array_merge( $this->tags, $updater->getEditResult()->getRevertTags() )
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set patrolling and bot flag on the edits, which gets rolled back.
|
||||
*
|
||||
* @param IDatabase $dbw
|
||||
* @param RevisionRecord $current
|
||||
* @param RevisionRecord $target
|
||||
*/
|
||||
private function updateRecentChange(
|
||||
IDatabase $dbw,
|
||||
RevisionRecord $current,
|
||||
RevisionRecord $target
|
||||
) {
|
||||
$set = [];
|
||||
if ( $this->bot ) {
|
||||
// Mark all reverted edits as bot
|
||||
$set['rc_bot'] = 1;
|
||||
}
|
||||
|
||||
if ( $this->options->get( 'UseRCPatrol' ) ) {
|
||||
// Mark all reverted edits as patrolled
|
||||
$set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
|
||||
}
|
||||
|
||||
if ( $set ) {
|
||||
$actorWhere = $this->actorMigration->getWhere(
|
||||
$dbw,
|
||||
'rc_user',
|
||||
$current->getUser( RevisionRecord::RAW ),
|
||||
false
|
||||
);
|
||||
$dbw->update(
|
||||
'recentchanges',
|
||||
$set,
|
||||
[ /* WHERE */
|
||||
'rc_cur_id' => $current->getPageId(),
|
||||
'rc_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $target->getTimestamp() ) ),
|
||||
$actorWhere['conds'], // No tables/joins are needed for rc_user
|
||||
],
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and format summary for the rollback.
|
||||
*
|
||||
* @param RevisionRecord $current
|
||||
* @param RevisionRecord $target
|
||||
* @return string
|
||||
*/
|
||||
private function getSummary( RevisionRecord $current, RevisionRecord $target ): string {
|
||||
$currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
|
||||
if ( !$this->summary ) {
|
||||
if ( !$currentEditorForPublic ) { // no public user name
|
||||
$summary = MessageValue::new( 'revertpage-nouser' );
|
||||
} elseif ( $this->options->get( 'DisableAnonTalk' ) && !$currentEditorForPublic->isRegistered() ) {
|
||||
$summary = MessageValue::new( 'revertpage-anon' );
|
||||
} else {
|
||||
$summary = MessageValue::new( 'revertpage' );
|
||||
}
|
||||
} else {
|
||||
$summary = $this->summary;
|
||||
}
|
||||
|
||||
$targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
|
||||
// Allow the custom summary to use the same args as the default message
|
||||
$args = [
|
||||
$targetEditorForPublic ? $targetEditorForPublic->getName() : null,
|
||||
$currentEditorForPublic ? $currentEditorForPublic->getName() : null,
|
||||
$target->getId(),
|
||||
Message::dateTimeParam( $target->getTimestamp() ),
|
||||
$current->getId(),
|
||||
Message::dateTimeParam( $current->getTimestamp() ),
|
||||
];
|
||||
if ( $summary instanceof MessageValue ) {
|
||||
$summary = ( new Converter() )->convertMessageValue( $summary );
|
||||
$summary = $summary->params( $args )->inContentLanguage()->text();
|
||||
} else {
|
||||
$summary = ( new RawMessage( $summary, $args ) )->inContentLanguage()->plain();
|
||||
}
|
||||
|
||||
// Trim spaces on user supplied text
|
||||
return trim( $summary );
|
||||
}
|
||||
}
|
||||
47
includes/page/RollbackPageFactory.php
Normal file
47
includes/page/RollbackPageFactory.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
/**
|
||||
* Special handling for category pages.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Page;
|
||||
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
||||
/**
|
||||
* @since 1.37
|
||||
* @package MediaWiki\Page
|
||||
*/
|
||||
interface RollbackPageFactory {
|
||||
|
||||
/**
|
||||
* Create a new command instance for page rollback.
|
||||
*
|
||||
* @param PageIdentity $page
|
||||
* @param Authority $performer
|
||||
* @param UserIdentity $byUser
|
||||
* @return RollbackPage
|
||||
*/
|
||||
public function newRollbackPage(
|
||||
PageIdentity $page,
|
||||
Authority $performer,
|
||||
UserIdentity $byUser
|
||||
): RollbackPage;
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ use MediaWiki\Config\ServiceOptions;
|
|||
use MediaWiki\Content\ContentHandlerFactory;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
use MediaWiki\DAO\WikiAwareEntityTrait;
|
||||
use MediaWiki\Debug\DeprecatablePropertyArray;
|
||||
use MediaWiki\Edit\PreparedEdit;
|
||||
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
|
|
@ -35,7 +34,6 @@ use MediaWiki\Page\PageRecord;
|
|||
use MediaWiki\Page\PageStoreRecord;
|
||||
use MediaWiki\Page\ParserOutputAccess;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Permissions\PermissionStatus;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRenderer;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
|
|
@ -3350,368 +3348,6 @@ class WikiPage implements Page, IDBAccessObject, PageRecord {
|
|||
DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll back the most recent consecutive set of edits to a page
|
||||
* from the same user; fails if there are no eligible edits to
|
||||
* roll back to, e.g. user is the sole contributor. This function
|
||||
* performs permissions checks on $user, then calls commitRollback()
|
||||
* to do the dirty work
|
||||
*
|
||||
* @internal since 1.35
|
||||
*
|
||||
* @todo Separate the business/permission stuff out from backend code
|
||||
* @todo Remove $token parameter. Already verified by RollbackAction and ApiRollback.
|
||||
*
|
||||
* @param string $fromP Name of the user whose edits to rollback.
|
||||
* @param string $summary Custom summary. Set to default summary if empty.
|
||||
* @param string $token Rollback token.
|
||||
* @param bool $bot If true, mark all reverted edits as bot.
|
||||
*
|
||||
* @param array &$resultDetails Array contains result-specific array of additional values
|
||||
* 'alreadyrolled' : 'current' (rev)
|
||||
* success : 'summary' (str), 'current' (rev), 'target' (rev)
|
||||
*
|
||||
* @param Authority $performer doing the rollback
|
||||
* @param array|null $tags Change tags to apply to the rollback
|
||||
* Callers are responsible for permission checks
|
||||
* (with ChangeTags::canAddTagsAccompanyingChange)
|
||||
*
|
||||
* @return array[] Array of errors, each error formatted as
|
||||
* [ messagekey, param1, param2, ... ].
|
||||
* On success, the array is empty. This array can also be passed to
|
||||
* OutputPage::showPermissionsErrorPage().
|
||||
*/
|
||||
public function doRollback(
|
||||
$fromP, $summary, $token, $bot, &$resultDetails, Authority $performer, $tags = null
|
||||
) {
|
||||
$this->assertProperPage();
|
||||
|
||||
$resultDetails = null;
|
||||
|
||||
// Check permissions
|
||||
$permissionStatus = PermissionStatus::newEmpty();
|
||||
$performer->authorizeWrite( 'edit', $this->getTitle(), $permissionStatus );
|
||||
$performer->authorizeWrite( 'rollback', $this->getTitle(), $permissionStatus );
|
||||
|
||||
$user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
|
||||
if ( !$user->matchEditToken( $token, 'rollback' ) ) {
|
||||
$permissionStatus->fatal( 'sessionfailure' );
|
||||
}
|
||||
|
||||
if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
|
||||
$permissionStatus->fatal( 'actionthrottledtext' );
|
||||
}
|
||||
|
||||
// If there were errors, bail out now
|
||||
if ( !$permissionStatus->isGood() ) {
|
||||
return $permissionStatus->toLegacyErrorArray();
|
||||
}
|
||||
|
||||
return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $performer, $tags );
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend implementation of doRollback(), please refer there for parameter
|
||||
* and return value documentation
|
||||
*
|
||||
* @internal since 1.35
|
||||
*
|
||||
* NOTE: This function does NOT check ANY permissions, it just commits the
|
||||
* rollback to the DB. Therefore, you should only call this function direct-
|
||||
* ly if you want to use custom permissions checks. If you don't, use
|
||||
* doRollback() instead.
|
||||
*
|
||||
* @param string $fromP Name of the user whose edits to rollback.
|
||||
* @param string $summary Custom summary. Set to default summary if empty.
|
||||
* @param bool $bot If true, mark all reverted edits as bot.
|
||||
* @param array &$resultDetails Contains result-specific array of additional values
|
||||
* @param Authority $performer The user performing the rollback
|
||||
* @param array|null $tags Change tags to apply to the rollback
|
||||
* Callers are responsible for permission checks
|
||||
* (with ChangeTags::canAddTagsAccompanyingChange)
|
||||
*
|
||||
* @return array An array of error messages, as returned by Status::getErrorsArray()
|
||||
*/
|
||||
public function commitRollback( $fromP, $summary, $bot,
|
||||
&$resultDetails, Authority $performer, $tags = null
|
||||
) {
|
||||
global $wgUseRCPatrol, $wgDisableAnonTalk;
|
||||
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
|
||||
if ( wfReadOnly() ) {
|
||||
return [ [ 'readonlytext' ] ];
|
||||
}
|
||||
|
||||
// Begin revision creation cycle by creating a PageUpdater.
|
||||
// If the page is changed concurrently after grabParentRevision(), the rollback will fail.
|
||||
$user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
|
||||
$updater = $this->newPageUpdater( $user );
|
||||
$current = $updater->grabParentRevision();
|
||||
|
||||
if ( $current === null ) {
|
||||
// Something wrong... no page?
|
||||
return [ [ 'notanarticle' ] ];
|
||||
}
|
||||
|
||||
$currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
|
||||
$legacyCurrentCallback = static function () use ( $current ) {
|
||||
// Only created when needed
|
||||
return new Revision( $current );
|
||||
};
|
||||
$from = str_replace( '_', ' ', $fromP );
|
||||
|
||||
// User name given should match up with the top revision.
|
||||
// If the revision's user is not visible, then $from should be empty.
|
||||
if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
|
||||
$resultDetails = new DeprecatablePropertyArray(
|
||||
[
|
||||
'current' => $legacyCurrentCallback,
|
||||
'current-revision-record' => $current,
|
||||
],
|
||||
[ 'current' => '1.35' ],
|
||||
__METHOD__
|
||||
);
|
||||
return [ [ 'alreadyrolled',
|
||||
htmlspecialchars( $this->mTitle->getPrefixedText() ),
|
||||
htmlspecialchars( $fromP ),
|
||||
htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
|
||||
] ];
|
||||
}
|
||||
|
||||
// Get the last edit not by this person...
|
||||
// Note: these may not be public values
|
||||
$actorWhere = ActorMigration::newMigration()->getWhere(
|
||||
$dbw,
|
||||
'rev_user',
|
||||
$current->getUser( RevisionRecord::RAW )
|
||||
);
|
||||
|
||||
$s = $dbw->selectRow(
|
||||
[ 'revision' ] + $actorWhere['tables'],
|
||||
[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
|
||||
[
|
||||
'rev_page' => $current->getPageId(),
|
||||
'NOT(' . $actorWhere['conds'] . ')',
|
||||
],
|
||||
__METHOD__,
|
||||
[
|
||||
'USE INDEX' => [ 'revision' => 'page_timestamp' ],
|
||||
'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
|
||||
],
|
||||
$actorWhere['joins']
|
||||
);
|
||||
if ( $s === false ) {
|
||||
// No one else ever edited this page
|
||||
return [ [ 'cantrollback' ] ];
|
||||
} elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
|
||||
|| $s->rev_deleted & RevisionRecord::DELETED_USER
|
||||
) {
|
||||
// Only admins can see this text
|
||||
return [ [ 'notvisiblerev' ] ];
|
||||
}
|
||||
|
||||
// Generate the edit summary if necessary
|
||||
$target = $this->getRevisionStore()->getRevisionById(
|
||||
$s->rev_id,
|
||||
RevisionStore::READ_LATEST
|
||||
);
|
||||
if ( empty( $summary ) ) {
|
||||
if ( !$currentEditorForPublic ) { // no public user name
|
||||
$summary = wfMessage( 'revertpage-nouser' );
|
||||
} elseif ( $wgDisableAnonTalk && $current->getUser() === 0 ) {
|
||||
$summary = wfMessage( 'revertpage-anon' );
|
||||
} else {
|
||||
$summary = wfMessage( 'revertpage' );
|
||||
}
|
||||
}
|
||||
$targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
|
||||
|
||||
// Allow the custom summary to use the same args as the default message
|
||||
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
|
||||
$args = [
|
||||
$targetEditorForPublic ? $targetEditorForPublic->getName() : null,
|
||||
$currentEditorForPublic ? $currentEditorForPublic->getName() : null,
|
||||
$s->rev_id,
|
||||
$contLang->timeanddate( MWTimestamp::convert( TS_MW, $s->rev_timestamp ) ),
|
||||
$current->getId(),
|
||||
$contLang->timeanddate( $current->getTimestamp() )
|
||||
];
|
||||
if ( $summary instanceof Message ) {
|
||||
$summary = $summary->params( $args )->inContentLanguage()->text();
|
||||
} else {
|
||||
$summary = wfMsgReplaceArgs( $summary, $args );
|
||||
}
|
||||
|
||||
// Trim spaces on user supplied text
|
||||
$summary = trim( $summary );
|
||||
|
||||
// Save
|
||||
$flags = EDIT_UPDATE | EDIT_INTERNAL;
|
||||
|
||||
if ( $performer->isAllowed( 'minoredit' ) ) {
|
||||
$flags |= EDIT_MINOR;
|
||||
}
|
||||
|
||||
if ( $bot && ( $performer->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
|
||||
$flags |= EDIT_FORCE_BOT;
|
||||
}
|
||||
|
||||
// TODO: MCR: also log model changes in other slots, in case that becomes possible!
|
||||
$currentContent = $current->getContent( SlotRecord::MAIN );
|
||||
$targetContent = $target->getContent( SlotRecord::MAIN );
|
||||
$changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
|
||||
|
||||
// Build rollback revision:
|
||||
// Restore old content
|
||||
// TODO: MCR: test this once we can store multiple slots
|
||||
foreach ( $target->getSlots()->getSlots() as $slot ) {
|
||||
$updater->inheritSlot( $slot );
|
||||
}
|
||||
|
||||
// Remove extra slots
|
||||
// TODO: MCR: test this once we can store multiple slots
|
||||
foreach ( $current->getSlotRoles() as $role ) {
|
||||
if ( !$target->hasSlot( $role ) ) {
|
||||
$updater->removeSlot( $role );
|
||||
}
|
||||
}
|
||||
|
||||
$updater->setOriginalRevisionId( $target->getId() );
|
||||
$oldestRevertedRevision = $this->getRevisionStore()->getNextRevision(
|
||||
$target,
|
||||
RevisionStore::READ_LATEST
|
||||
);
|
||||
if ( $oldestRevertedRevision !== null ) {
|
||||
$updater->markAsRevert(
|
||||
EditResult::REVERT_ROLLBACK,
|
||||
$oldestRevertedRevision->getId(),
|
||||
$current->getId()
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this logic should not be in the storage layer, it's here for compatibility
|
||||
// with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
|
||||
// place the 'bot' right is handled, which is currently in EditPage::attemptSave.
|
||||
|
||||
if ( $wgUseRCPatrol && $performer->authorizeWrite( 'autopatrol', $this->getTitle() ) ) {
|
||||
$updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
|
||||
}
|
||||
|
||||
// Actually store the rollback
|
||||
$rev = $updater->saveRevision(
|
||||
CommentStoreComment::newUnsavedComment( $summary ),
|
||||
$flags
|
||||
);
|
||||
|
||||
// Set patrolling and bot flag on the edits, which gets rollbacked.
|
||||
// This is done even on edit failure to have patrolling in that case (T64157).
|
||||
$set = [];
|
||||
if ( $bot && $performer->isAllowed( 'markbotedits' ) ) {
|
||||
// Mark all reverted edits as bot
|
||||
$set['rc_bot'] = 1;
|
||||
}
|
||||
|
||||
if ( $wgUseRCPatrol ) {
|
||||
// Mark all reverted edits as patrolled
|
||||
$set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
|
||||
}
|
||||
|
||||
if ( count( $set ) ) {
|
||||
$actorWhere = ActorMigration::newMigration()->getWhere(
|
||||
$dbw,
|
||||
'rc_user',
|
||||
$current->getUser( RevisionRecord::RAW ),
|
||||
false
|
||||
);
|
||||
$dbw->update( 'recentchanges', $set,
|
||||
[ /* WHERE */
|
||||
'rc_cur_id' => $current->getPageId(),
|
||||
'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
|
||||
$actorWhere['conds'], // No tables/joins are needed for rc_user
|
||||
],
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
if ( !$updater->wasSuccessful() ) {
|
||||
return $updater->getStatus()->getErrorsArray();
|
||||
}
|
||||
|
||||
// Report if the edit was not created because it did not change the content.
|
||||
if ( $updater->isUnchanged() ) {
|
||||
$resultDetails = new DeprecatablePropertyArray(
|
||||
[
|
||||
'current' => $legacyCurrentCallback,
|
||||
'current-revision-record' => $current,
|
||||
],
|
||||
[ 'current' => '1.35' ],
|
||||
__METHOD__
|
||||
);
|
||||
return [ [ 'alreadyrolled',
|
||||
htmlspecialchars( $this->mTitle->getPrefixedText() ),
|
||||
htmlspecialchars( $fromP ),
|
||||
htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
|
||||
] ];
|
||||
}
|
||||
|
||||
if ( $changingContentModel ) {
|
||||
// If the content model changed during the rollback,
|
||||
// make sure it gets logged to Special:Log/contentmodel
|
||||
$log = new ManualLogEntry( 'contentmodel', 'change' );
|
||||
$log->setPerformer( $performer->getUser() );
|
||||
$log->setTarget( $this->mTitle );
|
||||
$log->setComment( $summary );
|
||||
$log->setParameters( [
|
||||
'4::oldmodel' => $currentContent->getModel(),
|
||||
'5::newmodel' => $targetContent->getModel(),
|
||||
] );
|
||||
|
||||
$logId = $log->insert( $dbw );
|
||||
$log->publish( $logId );
|
||||
}
|
||||
|
||||
$revId = $rev->getId();
|
||||
|
||||
// Hook is hard deprecated since 1.35
|
||||
if ( $this->getHookContainer()->isRegistered( 'ArticleRollbackComplete' ) ) {
|
||||
// Only create the Revision objects if needed
|
||||
$legacyCurrent = new Revision( $current );
|
||||
$legacyTarget = new Revision( $target );
|
||||
$this->getHookRunner()->onArticleRollbackComplete( $this, $user,
|
||||
$legacyTarget, $legacyCurrent );
|
||||
}
|
||||
|
||||
$this->getHookRunner()->onRollbackComplete( $this, $user, $target, $current );
|
||||
|
||||
$legacyTargetCallback = static function () use ( $target ) {
|
||||
// Only create the Revision object if needed
|
||||
return new Revision( $target );
|
||||
};
|
||||
|
||||
$tags = array_merge(
|
||||
$tags ?: [],
|
||||
$updater->getEditResult()->getRevertTags()
|
||||
);
|
||||
|
||||
$resultDetails = new DeprecatablePropertyArray(
|
||||
[
|
||||
'summary' => $summary,
|
||||
'current' => $legacyCurrentCallback,
|
||||
'current-revision-record' => $current,
|
||||
'target' => $legacyTargetCallback,
|
||||
'target-revision-record' => $target,
|
||||
'newid' => $revId,
|
||||
'tags' => $tags
|
||||
],
|
||||
[ 'current' => '1.35', 'target' => '1.35' ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
// TODO: make this return a Status object and wrap $resultDetails in that.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The onArticle*() functions are supposed to be a kind of hooks
|
||||
* which should be called whenever any of the specified actions
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ class RollbackEdits extends Maintenance {
|
|||
$bot = $this->hasOption( 'bot' );
|
||||
$summary = $this->getOption( 'summary', $this->mSelf . ' mass rollback' );
|
||||
$titles = [];
|
||||
$results = [];
|
||||
if ( $this->hasOption( 'titles' ) ) {
|
||||
foreach ( explode( '|', $this->getOption( 'titles' ) ) as $title ) {
|
||||
$t = Title::newFromText( $title );
|
||||
|
|
@ -81,10 +80,17 @@ class RollbackEdits extends Maintenance {
|
|||
$doer = User::newSystemUser( 'Maintenance script', [ 'steal' => true ] );
|
||||
|
||||
$wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
|
||||
$rollbackPageFactory = MediaWikiServices::getInstance()
|
||||
->getRollbackPageFactory();
|
||||
foreach ( $titles as $t ) {
|
||||
$page = $wikiPageFactory->newFromTitle( $t );
|
||||
$this->output( 'Processing ' . $t->getPrefixedText() . '... ' );
|
||||
if ( !$page->commitRollback( $user, $summary, $bot, $results, $doer ) ) {
|
||||
$rollbackResult = $rollbackPageFactory
|
||||
->newRollbackPage( $page, $doer, $user )
|
||||
->markAsBot( $bot )
|
||||
->setSummary( $summary )
|
||||
->rollback();
|
||||
if ( $rollbackResult->isGood() ) {
|
||||
$this->output( "Done!\n" );
|
||||
} else {
|
||||
$this->output( "Failed!\n" );
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use MediaWiki\Logger\LegacySpi;
|
|||
use MediaWiki\Logger\LogCapturingSpi;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use PHPUnit\Framework\TestResult;
|
||||
|
|
@ -2386,7 +2387,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
|
|||
* @param string|Content $content the new content of the page
|
||||
* @param string $summary Optional summary string for the revision
|
||||
* @param int $defaultNs Optional namespace id
|
||||
* @param User|null $user If null, static::getTestUser()->getUser() is used.
|
||||
* @param Authority|null $performer If null, static::getTestUser()->getUser() is used.
|
||||
* @return Status Object as returned by WikiPage::doEditContent()
|
||||
* @throws MWException If this test cases's needsDB() method doesn't return true.
|
||||
* Test cases can use "@group Database" to enable database test support,
|
||||
|
|
@ -2398,7 +2399,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
|
|||
$content,
|
||||
$summary = '',
|
||||
$defaultNs = NS_MAIN,
|
||||
User $user = null
|
||||
Authority $performer = null
|
||||
) {
|
||||
if ( !$this->needsDB() ) {
|
||||
throw new MWException( 'When testing with pages, the test cases\'s needsDB()' .
|
||||
|
|
@ -2415,8 +2416,8 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
|
|||
$page = WikiPage::factory( $title );
|
||||
}
|
||||
|
||||
if ( $user === null ) {
|
||||
$user = static::getTestUser()->getUser();
|
||||
if ( $performer === null ) {
|
||||
$performer = static::getTestUser()->getUser();
|
||||
}
|
||||
|
||||
if ( is_string( $content ) ) {
|
||||
|
|
@ -2428,7 +2429,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
|
|||
$summary,
|
||||
0,
|
||||
false,
|
||||
$user
|
||||
$performer
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Interwiki\InterwikiLookup;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Page\PageCommandFactory;
|
||||
use MediaWiki\Revision\SlotRecord;
|
||||
use Wikimedia\Rdbms\IDatabase;
|
||||
use Wikimedia\Rdbms\LoadBalancer;
|
||||
|
|
@ -80,7 +79,7 @@ class MovePageTest extends MediaWikiIntegrationTestCase {
|
|||
$old,
|
||||
$new,
|
||||
new ServiceOptions(
|
||||
PageCommandFactory::CONSTRUCTOR_OPTIONS,
|
||||
MovePage::CONSTRUCTOR_OPTIONS,
|
||||
$params['options'] ?? [],
|
||||
[
|
||||
'CategoryCollation' => 'uppercase',
|
||||
|
|
|
|||
|
|
@ -1378,229 +1378,6 @@ more stuff
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers WikiPage::doRollback
|
||||
* @covers WikiPage::commitRollback
|
||||
*/
|
||||
public function testDoRollback() {
|
||||
$this->hideDeprecated( 'Revision::countByPageId' );
|
||||
$this->hideDeprecated( 'Revision::getUserText' );
|
||||
$this->hideDeprecated( 'Revision::__construct' );
|
||||
$this->hideDeprecated( 'Revision::getRevisionRecord' );
|
||||
$this->hideDeprecated( "MediaWiki\Storage\PageUpdater::doCreate status get 'revision'" );
|
||||
$this->hideDeprecated( "MediaWiki\Storage\PageUpdater::doModify status get 'revision'" );
|
||||
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
// Use the confirmed group for user2 to make sure the user is different
|
||||
$user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
|
||||
|
||||
// make sure we can test autopatrolling
|
||||
$this->setMwGlobals( 'wgUseRCPatrol', true );
|
||||
|
||||
// TODO: MCR: test rollback of multiple slots!
|
||||
$page = $this->newPage( __METHOD__ );
|
||||
|
||||
// Make some edits
|
||||
$text = "one";
|
||||
$status1 = $page->doUserEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
|
||||
$admin, "section one", EDIT_NEW );
|
||||
|
||||
$text .= "\n\ntwo";
|
||||
$status2 = $page->doUserEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
|
||||
$user1, "adding section two" );
|
||||
|
||||
$text .= "\n\nthree";
|
||||
$status3 = $page->doUserEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
|
||||
$user2, "adding section three" );
|
||||
|
||||
/** @var Revision $rev1 */
|
||||
/** @var Revision $rev2 */
|
||||
/** @var Revision $rev3 */
|
||||
$rev1 = $status1->getValue()['revision'];
|
||||
$rev2 = $status2->getValue()['revision'];
|
||||
$rev3 = $status3->getValue()['revision'];
|
||||
|
||||
/**
|
||||
* We are having issues with doRollback spuriously failing. Apparently
|
||||
* the last revision somehow goes missing or not committed under some
|
||||
* circumstances. So, make sure the revisions have the correct usernames.
|
||||
*/
|
||||
$this->assertEquals( 3, Revision::countByPageId( wfGetDB( DB_REPLICA ), $page->getId() ) );
|
||||
$this->assertEquals( $admin->getName(), $rev1->getUserText() );
|
||||
$this->assertEquals( $user1->getName(), $rev2->getUserText() );
|
||||
$this->assertEquals( $user2->getName(), $rev3->getUserText() );
|
||||
|
||||
// Now, try the actual rollback
|
||||
$token = $admin->getEditToken( 'rollback' );
|
||||
$rollbackErrors = $page->doRollback(
|
||||
$user2->getName(),
|
||||
"testing rollback",
|
||||
$token,
|
||||
false,
|
||||
$resultDetails,
|
||||
$admin
|
||||
);
|
||||
|
||||
if ( $rollbackErrors ) {
|
||||
$this->fail(
|
||||
"Rollback failed:\n" .
|
||||
print_r( $rollbackErrors, true ) . ";\n" .
|
||||
print_r( $resultDetails, true )
|
||||
);
|
||||
}
|
||||
|
||||
$page = new WikiPage( $page->getTitle() );
|
||||
$this->assertEquals(
|
||||
$rev2->getRevisionRecord()->getSha1(),
|
||||
$page->getRevisionRecord()->getSha1(),
|
||||
"rollback did not revert to the correct revision" );
|
||||
$this->assertEquals( "one\n\ntwo", $page->getContent()->getText() );
|
||||
|
||||
$rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange(
|
||||
$page->getRevisionRecord()
|
||||
);
|
||||
|
||||
$this->assertNotNull( $rc, 'RecentChanges entry' );
|
||||
$this->assertEquals(
|
||||
RecentChange::PRC_AUTOPATROLLED,
|
||||
$rc->getAttribute( 'rc_patrolled' ),
|
||||
'rc_patrolled'
|
||||
);
|
||||
|
||||
// TODO: MCR: assert origin once we write slot data
|
||||
// $mainSlot = $page->getRevision()->getRevisionRecord()->getSlot( SlotRecord::MAIN );
|
||||
// $this->assertTrue( $mainSlot->isInherited(), 'isInherited' );
|
||||
// $this->assertSame( $rev2->getId(), $mainSlot->getOrigin(), 'getOrigin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers WikiPage::doRollback
|
||||
* @covers WikiPage::commitRollback
|
||||
*/
|
||||
public function testDoRollbackFailureSameContent() {
|
||||
$this->hideDeprecated( 'Revision::getSha1' );
|
||||
$this->hideDeprecated( 'Revision::__construct' );
|
||||
$this->hideDeprecated( 'WikiPage::getRevision' );
|
||||
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
|
||||
$text = "one";
|
||||
$page = $this->newPage( __METHOD__ );
|
||||
$page->doUserEditContent(
|
||||
ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
|
||||
$admin,
|
||||
"section one",
|
||||
EDIT_NEW
|
||||
);
|
||||
$rev1 = $page->getRevision();
|
||||
|
||||
$user1 = $this->getTestUser( [ 'sysop' ] )->getUser();
|
||||
$text .= "\n\ntwo";
|
||||
$page = new WikiPage( $page->getTitle() );
|
||||
$page->doUserEditContent(
|
||||
ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
|
||||
$user1,
|
||||
"adding section two"
|
||||
);
|
||||
|
||||
# now, do a the rollback from the same user was doing the edit before
|
||||
$resultDetails = [];
|
||||
$token = $user1->getEditToken( 'rollback' );
|
||||
$errors = $page->doRollback(
|
||||
$user1->getName(),
|
||||
"testing revert same user",
|
||||
$token,
|
||||
false,
|
||||
$resultDetails,
|
||||
$admin
|
||||
);
|
||||
|
||||
$this->assertEquals( [], $errors, "Rollback failed same user" );
|
||||
|
||||
# now, try the rollback
|
||||
$resultDetails = [];
|
||||
$token = $admin->getEditToken( 'rollback' );
|
||||
$errors = $page->doRollback(
|
||||
$user1->getName(),
|
||||
"testing revert",
|
||||
$token,
|
||||
false,
|
||||
$resultDetails,
|
||||
$admin
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
[
|
||||
'alreadyrolled',
|
||||
__METHOD__,
|
||||
$user1->getName(),
|
||||
$admin->getName(),
|
||||
],
|
||||
],
|
||||
$errors,
|
||||
"Rollback not failed"
|
||||
);
|
||||
|
||||
$page = new WikiPage( $page->getTitle() );
|
||||
$this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
|
||||
"rollback did not revert to the correct revision" );
|
||||
$this->assertEquals( "one", $page->getContent()->getText() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests tagging for edits that do rollback action
|
||||
* @covers WikiPage::doRollback
|
||||
*/
|
||||
public function testDoRollbackTagging() {
|
||||
if ( !in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
|
||||
$this->markTestSkipped( 'Rollback tag deactivated, skipped the test.' );
|
||||
}
|
||||
|
||||
$admin = new User();
|
||||
$admin->setName( 'Administrator' );
|
||||
$admin->addToDatabase();
|
||||
|
||||
$text = 'First line';
|
||||
$page = $this->newPage( 'WikiPageTest_testDoRollbackTagging' );
|
||||
$page->doUserEditContent(
|
||||
ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
|
||||
$admin,
|
||||
'Added first line',
|
||||
EDIT_NEW
|
||||
);
|
||||
|
||||
$secondUser = new User();
|
||||
$secondUser->setName( '92.65.217.32' );
|
||||
$text .= '\n\nSecond line';
|
||||
$page = new WikiPage( $page->getTitle() );
|
||||
$page->doUserEditContent(
|
||||
ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
|
||||
$secondUser,
|
||||
'Adding second line'
|
||||
);
|
||||
|
||||
// Now, try the rollback
|
||||
$admin->addGroup( 'sysop' ); // Make the test user a sysop
|
||||
MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
|
||||
$token = $admin->getEditToken( 'rollback' );
|
||||
$errors = $page->doRollback(
|
||||
$secondUser->getName(),
|
||||
'testing rollback',
|
||||
$token,
|
||||
false,
|
||||
$resultDetails,
|
||||
$admin
|
||||
);
|
||||
|
||||
// If doRollback completed without errors
|
||||
if ( $errors === [] ) {
|
||||
$tags = $resultDetails[ 'tags' ];
|
||||
$this->assertContains( 'mw-rollback', $tags );
|
||||
}
|
||||
}
|
||||
|
||||
public function provideGetAutoDeleteReason() {
|
||||
return [
|
||||
[
|
||||
|
|
|
|||
380
tests/phpunit/integration/includes/page/RollbackPageTest.php
Normal file
380
tests/phpunit/integration/includes/page/RollbackPageTest.php
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Page;
|
||||
|
||||
use ChangeTags;
|
||||
use DatabaseLogEntry;
|
||||
use JsonContent;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Page\PageIdentityValue;
|
||||
use MediaWiki\Page\RollbackPage;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Revision\SlotRecord;
|
||||
use MediaWiki\Storage\RevisionRecord;
|
||||
use MediaWiki\Tests\Unit\MockServiceDependenciesTrait;
|
||||
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
|
||||
use MediaWiki\User\UserFactory;
|
||||
use MediaWiki\User\UserIdentityValue;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
use ReadOnlyMode;
|
||||
use RecentChange;
|
||||
use Title;
|
||||
use User;
|
||||
use WikiPage;
|
||||
use WikitextContent;
|
||||
|
||||
/**
|
||||
* @group Database
|
||||
* @covers \MediaWiki\Page\RollbackPage
|
||||
* @coversDefaultClass \MediaWiki\Page\RollbackPage
|
||||
* @package MediaWiki\Tests\Page
|
||||
* @method RollbackPage newServiceInstance(string $serviceClass, array $parameterOverrides)
|
||||
*/
|
||||
class RollbackPageTest extends MediaWikiIntegrationTestCase {
|
||||
use MockAuthorityTrait;
|
||||
use MockServiceDependenciesTrait;
|
||||
|
||||
protected function setUp() : void {
|
||||
parent::setUp();
|
||||
$this->setMwGlobals( 'wgUseRCPatrol', true );
|
||||
$this->tablesUsed = array_merge( $this->tablesUsed, [
|
||||
'page',
|
||||
'recentchanges',
|
||||
'logging',
|
||||
] );
|
||||
}
|
||||
|
||||
public function provideAuthorize() {
|
||||
yield 'Allowed' => [
|
||||
'authority' => $this->mockRegisteredUltimateAuthority(),
|
||||
'expect' => true,
|
||||
];
|
||||
yield 'No edit' => [
|
||||
'authority' => $this->mockRegisteredAuthorityWithoutPermissions( [ 'edit' ] ),
|
||||
'expect' => true,
|
||||
];
|
||||
yield 'No rollback' => [
|
||||
'authority' => $this->mockRegisteredAuthorityWithoutPermissions( [ 'rollback' ] ),
|
||||
'expect' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::authorizeRollback
|
||||
* @dataProvider provideAuthorize
|
||||
*/
|
||||
public function testAuthorize( Authority $authority, bool $expect ) {
|
||||
$this->assertSame(
|
||||
$expect,
|
||||
$this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage(
|
||||
new PageIdentityValue( 10, NS_MAIN, 'Test', PageIdentity::LOCAL ),
|
||||
$authority,
|
||||
new UserIdentityValue( 0, '127.0.0.1' )
|
||||
)
|
||||
->authorizeRollback()
|
||||
->isGood()
|
||||
);
|
||||
}
|
||||
|
||||
public function testAuthorizeReadOnly() {
|
||||
$mockReadOnly = $this->createMock( ReadOnlyMode::class );
|
||||
$mockReadOnly->method( 'isReadOnly' )->willReturn( true );
|
||||
$rollback = $this->newServiceInstance( RollbackPage::class, [
|
||||
'readOnlyMode' => $mockReadOnly,
|
||||
'performer' => $this->mockRegisteredUltimateAuthority()
|
||||
] );
|
||||
$this->assertFalse( $rollback->authorizeRollback()->isGood() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::authorizeRollback
|
||||
*/
|
||||
public function testAuthorizePingLimiter() {
|
||||
$performer = $this->mockRegisteredUltimateAuthority();
|
||||
$userMock = $this->createMock( User::class );
|
||||
$userMock->method( 'pingLimiter' )
|
||||
->withConsecutive( [ 'rollback', 1 ], [ 'edit', 1 ] )
|
||||
->willReturnOnConsecutiveCalls( false, false );
|
||||
$userFactoryMock = $this->createMock( UserFactory::class );
|
||||
$userFactoryMock->method( 'newFromAuthority' )
|
||||
->with( $performer )
|
||||
->willReturn( $userMock );
|
||||
$rollbackPage = $this->newServiceInstance( RollbackPage::class, [
|
||||
'performer' => $performer,
|
||||
'userFactory' => $userFactoryMock
|
||||
] );
|
||||
$this->assertTrue( $rollbackPage->authorizeRollback()->isGood() );
|
||||
}
|
||||
|
||||
public function testRollbackNotAllowed() {
|
||||
$this->assertFalse( $this->newServiceInstance( RollbackPage::class, [
|
||||
'performer' => $this->mockRegisteredNullAuthority()
|
||||
] )->rollbackIfAllowed()->isGood() );
|
||||
}
|
||||
|
||||
public function testRollback() {
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
// Use the confirmed group for user2 to make sure the user is different
|
||||
$user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
|
||||
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
// Make some edits
|
||||
$text = "one";
|
||||
$status1 = $this->editPage( $page, $text, "section one", NS_MAIN, $admin );
|
||||
$this->assertTrue( $status1->isGood(), 'Sanity: edit 1 success' );
|
||||
|
||||
$text .= "\n\ntwo";
|
||||
$status2 = $this->editPage( $page, $text, "adding section two", NS_MAIN, $user1 );
|
||||
$this->assertTrue( $status2->isGood(), 'Sanity: edit 2 success' );
|
||||
|
||||
$text .= "\n\nthree";
|
||||
$status3 = $this->editPage( $page, $text, "adding section three", NS_MAIN, $user2 );
|
||||
$this->assertTrue( $status3->isGood(), 'Sanity: edit 3 success' );
|
||||
|
||||
/** @var RevisionRecord $rev1 */
|
||||
/** @var RevisionRecord $rev2 */
|
||||
/** @var RevisionRecord $rev3 */
|
||||
$rev1 = $status1->getValue()['revision-record'];
|
||||
$rev2 = $status2->getValue()['revision-record'];
|
||||
$rev3 = $status3->getValue()['revision-record'];
|
||||
|
||||
/**
|
||||
* We are having issues with doRollback spuriously failing. Apparently
|
||||
* the last revision somehow goes missing or not committed under some
|
||||
* circumstances. So, make sure the revisions have the correct usernames.
|
||||
*/
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$this->getServiceContainer()
|
||||
->getRevisionStore()
|
||||
->countRevisionsByPageId( $this->db, $page->getId() )
|
||||
);
|
||||
$this->assertEquals( $admin->getName(), $rev1->getUser()->getName() );
|
||||
$this->assertEquals( $user1->getName(), $rev2->getUser()->getName() );
|
||||
$this->assertEquals( $user2->getName(), $rev3->getUser()->getName() );
|
||||
|
||||
// Now, try the actual rollback
|
||||
$rollbackStatus = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user2 )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackStatus->isGood() );
|
||||
|
||||
$this->assertEquals(
|
||||
$rev2->getSha1(),
|
||||
$page->getRevisionRecord()->getSha1(),
|
||||
"rollback did not revert to the correct revision" );
|
||||
$this->assertEquals( "one\n\ntwo", $page->getContent()->getText() );
|
||||
|
||||
$rc = $this->getServiceContainer()->getRevisionStore()->getRecentChange(
|
||||
$page->getRevisionRecord()
|
||||
);
|
||||
|
||||
$this->assertNotNull( $rc, 'RecentChanges entry' );
|
||||
$this->assertEquals(
|
||||
RecentChange::PRC_AUTOPATROLLED,
|
||||
$rc->getAttribute( 'rc_patrolled' ),
|
||||
'rc_patrolled'
|
||||
);
|
||||
|
||||
$mainSlot = $page->getRevisionRecord()->getSlot( SlotRecord::MAIN );
|
||||
$this->assertTrue( $mainSlot->isInherited(), 'isInherited' );
|
||||
$this->assertSame( $rev2->getId(), $mainSlot->getOrigin(), 'getOrigin' );
|
||||
}
|
||||
|
||||
public function testRollbackFailSameContent() {
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
|
||||
$text = "one";
|
||||
$status1 = $this->editPage( $page, $text, "section one", NS_MAIN, $admin );
|
||||
$this->assertTrue( $status1->isGood(), 'Sanity: edit 1 success' );
|
||||
$rev1 = $page->getRevisionRecord();
|
||||
|
||||
$user1 = $this->getTestUser( [ 'sysop' ] )->getUser();
|
||||
$text .= "\n\ntwo";
|
||||
$status1 = $this->editPage( $page, $text, "adding section two", NS_MAIN, $user1 );
|
||||
$this->assertTrue( $status1->isGood(), 'Sanity: edit 2 success' );
|
||||
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackResult->isGood() );
|
||||
|
||||
# now, try the rollback again
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->rollback();
|
||||
$this->assertFalse( $rollbackResult->isGood() );
|
||||
$this->assertTrue( $rollbackResult->hasMessage( 'alreadyrolled' ) );
|
||||
|
||||
$this->assertEquals( $rev1->getSha1(), $page->getRevisionRecord()->getSha1(),
|
||||
"rollback did not revert to the correct revision" );
|
||||
$this->assertEquals( "one", $page->getContent()->getText() );
|
||||
}
|
||||
|
||||
public function testRollbackFailNotExisting() {
|
||||
$rollbackStatus = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage(
|
||||
new PageIdentityValue( 0, NS_MAIN, __METHOD__, PageIdentityValue::LOCAL ),
|
||||
$this->mockRegisteredUltimateAuthority(),
|
||||
new UserIdentityValue( 0, '127.0.0.1' )
|
||||
)
|
||||
->rollback();
|
||||
$this->assertFalse( $rollbackStatus->isGood() );
|
||||
$this->assertTrue( $rollbackStatus->hasMessage( 'notanarticle' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Authority $user1
|
||||
* @param Authority $user2
|
||||
* @param WikiPage $page
|
||||
* @return array with info about created page:
|
||||
* 'revision-one' => RevisionRecord
|
||||
* 'revision-two' => RevisionRecord
|
||||
*/
|
||||
private function prepareForRollback( Authority $user1, Authority $user2, WikiPage $page ): array {
|
||||
$result = [];
|
||||
$text = "one";
|
||||
$status = $this->editPage( $page, $text, "section one", NS_MAIN, $user1 );
|
||||
$this->assertTrue( $status->isGood(), 'Sanity: edit 1 success' );
|
||||
$result['revision-one'] = $status->getValue()['revision-record'];
|
||||
|
||||
$text .= "\n\ntwo";
|
||||
$status = $this->editPage( $page, $text, "adding section two", NS_MAIN, $user2 );
|
||||
$this->assertTrue( $status->isGood(), 'Sanity: edit 2 success' );
|
||||
$result['revision-two'] = $status->getValue()['revision-record'];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function testRollbackTagging() {
|
||||
if ( !in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
|
||||
$this->markTestSkipped( 'Rollback tag deactivated, skipped the test.' );
|
||||
}
|
||||
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
|
||||
$this->prepareForRollback( $admin, $user1, $page );
|
||||
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->setChangeTags( [ 'tag' ] )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackResult->isGood() );
|
||||
$this->assertContains( 'mw-rollback', $rollbackResult->getValue()['tags'] );
|
||||
$this->assertContains( 'tag', $rollbackResult->getValue()['tags'] );
|
||||
}
|
||||
|
||||
public function testRollbackBot() {
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
|
||||
$this->prepareForRollback( $admin, $user1, $page );
|
||||
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->markAsBot( true )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackResult->isGood() );
|
||||
$rc = $this->getServiceContainer()->getRevisionStore()->getRecentChange( $page->getRevisionRecord() );
|
||||
$this->assertNotNull( $rc );
|
||||
$this->assertSame( '1', $rc->getAttribute( 'rc_bot' ) );
|
||||
}
|
||||
|
||||
public function testRollbackBotNotAllowed() {
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
$admin = $this->mockUserAuthorityWithoutPermissions(
|
||||
$this->getTestSysop()->getUser(), [ 'markbotedits', 'bot' ] );
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
|
||||
$this->prepareForRollback( $admin, $user1, $page );
|
||||
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->markAsBot( true )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackResult->isGood() );
|
||||
$rc = $this->getServiceContainer()->getRevisionStore()->getRecentChange( $page->getRevisionRecord() );
|
||||
$this->assertNotNull( $rc );
|
||||
$this->assertSame( '0', $rc->getAttribute( 'rc_bot' ) );
|
||||
}
|
||||
|
||||
public function testRollbackCustomSummary() {
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
|
||||
$revisions = $this->prepareForRollback( $admin, $user1, $page );
|
||||
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->setSummary( 'TEST! $1 $2 $3 $4 $5 $6' )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackResult->isGood() );
|
||||
$targetTimestamp = $this->getServiceContainer()
|
||||
->getContentLanguage()
|
||||
->timeanddate( $revisions['revision-one']->getTimestamp() );
|
||||
$currentTimestamp = $this->getServiceContainer()
|
||||
->getContentLanguage()
|
||||
->timeanddate( $revisions['revision-two']->getTimestamp() );
|
||||
$expectedSummary = "TEST! {$admin->getName()} {$user1->getName()}" .
|
||||
" {$revisions['revision-one']->getId()}" .
|
||||
" {$targetTimestamp}" .
|
||||
" {$revisions['revision-two']->getId()}" .
|
||||
" {$currentTimestamp}";
|
||||
$this->assertSame( $expectedSummary, $page->getRevisionRecord()->getComment()->text );
|
||||
$rc = $this->getServiceContainer()->getRevisionStore()->getRecentChange( $page->getRevisionRecord() );
|
||||
$this->assertNotNull( $rc );
|
||||
$this->assertSame( $expectedSummary, $rc->getAttribute( 'rc_comment' ) );
|
||||
}
|
||||
|
||||
public function testRollbackChangesContentModel() {
|
||||
$page = new WikiPage( Title::newFromText( __METHOD__ ) );
|
||||
$admin = $this->getTestSysop()->getUser();
|
||||
$user1 = $this->getTestUser()->getUser();
|
||||
|
||||
$status1 = $this->editPage( $page, new JsonContent( '{}' ),
|
||||
"it's json", NS_MAIN, $admin );
|
||||
$this->assertTrue( $status1->isGood(), 'Sanity: edit 1 success' );
|
||||
|
||||
$status1 = $this->editPage( $page, new WikitextContent( 'bla' ),
|
||||
"no, it's wikitext", NS_MAIN, $user1 );
|
||||
$this->assertTrue( $status1->isGood(), 'Sanity: edit 2 success' );
|
||||
|
||||
$rollbackResult = $this->getServiceContainer()
|
||||
->getRollbackPageFactory()
|
||||
->newRollbackPage( $page, $admin, $user1 )
|
||||
->setSummary( 'TESTING' )
|
||||
->rollbackIfAllowed();
|
||||
$this->assertTrue( $rollbackResult->isGood() );
|
||||
$logQuery = DatabaseLogEntry::getSelectQueryData();
|
||||
$logRow = $this->db->selectRow(
|
||||
$logQuery['tables'],
|
||||
$logQuery['fields'],
|
||||
[
|
||||
'log_namespace' => NS_MAIN,
|
||||
'log_title' => __METHOD__,
|
||||
'log_type' => 'contentmodel'
|
||||
],
|
||||
__METHOD__,
|
||||
[],
|
||||
$logQuery['join_conds']
|
||||
);
|
||||
$this->assertNotNull( $logRow );
|
||||
$this->assertSame( $admin->getUser()->getName(), $logRow->user_name );
|
||||
$this->assertSame( 'TESTING', $logRow->log_comment_text );
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ trait FactoryArgTestTrait {
|
|||
* @return string
|
||||
*/
|
||||
protected function getFactoryMethodName() {
|
||||
return 'new' . $this->getInstanceClass();
|
||||
return 'new' . ( new ReflectionClass( $this->getInstanceClass() ) )->getShortName();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,7 +17,20 @@ class MovePageFactoryTest extends MediaWikiUnitTestCase {
|
|||
}
|
||||
|
||||
protected static function getExtraClassArgCount() {
|
||||
// $to and $from
|
||||
return 2;
|
||||
// $to + $from - $readOnlyMode - $titleFormatter - $actorMigration
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
|
||||
if ( $param->getName() === 'config' ) {
|
||||
return [ new HashConfig(
|
||||
array_fill_keys( MovePage::CONSTRUCTOR_OPTIONS, 'test' )
|
||||
) ];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getIgnoredParamNames() {
|
||||
return [ 'readOnlyMode', 'config', 'titleFormatter', 'actorMigration' ];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
tests/phpunit/unit/includes/page/RollbackPageFactoryTest.php
Normal file
51
tests/phpunit/unit/includes/page/RollbackPageFactoryTest.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Tests\Unit\Page;
|
||||
|
||||
use FactoryArgTestTrait;
|
||||
use HashConfig;
|
||||
use MediaWiki\Page\PageCommandFactory;
|
||||
use MediaWiki\Page\RollbackPage;
|
||||
use MediaWikiUnitTestCase;
|
||||
use ReflectionParameter;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Page\PageCommandFactory
|
||||
* @package MediaWiki\Tests\Unit\Page
|
||||
*/
|
||||
class RollbackPageFactoryTest extends MediaWikiUnitTestCase {
|
||||
use FactoryArgTestTrait;
|
||||
|
||||
protected static function getFactoryClass() {
|
||||
return PageCommandFactory::class;
|
||||
}
|
||||
|
||||
protected static function getInstanceClass() {
|
||||
return RollbackPage::class;
|
||||
}
|
||||
|
||||
protected static function getExtraClassArgCount() {
|
||||
// $page, $performer, $forUser - ignored params
|
||||
return -2;
|
||||
}
|
||||
|
||||
protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
|
||||
if ( $param->getName() === 'config' ) {
|
||||
return [ new HashConfig(
|
||||
array_fill_keys( RollbackPage::CONSTRUCTOR_OPTIONS, 'test' )
|
||||
) ];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getIgnoredParamNames() {
|
||||
return [
|
||||
'config',
|
||||
'namespaceInfo',
|
||||
'watchedItemStore',
|
||||
'repoGroup',
|
||||
'contentHandlerFactory',
|
||||
'spamChecker'
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue