Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.23% covered (warning)
77.23%
78 / 101
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiRollback
78.00% covered (warning)
78.00%
78 / 100
50.00% covered (danger)
50.00%
5 / 10
25.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 execute
89.47% covered (warning)
89.47%
34 / 38
0.00% covered (danger)
0.00%
0 / 1
4.02
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRbUser
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getRbTitle
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
16.00
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\ChangeTags\ChangeTags;
12use MediaWiki\Deferred\DeferredUpdates;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Page\RollbackPageFactory;
15use MediaWiki\ParamValidator\TypeDef\UserDef;
16use MediaWiki\Profiler\Profiler;
17use MediaWiki\Title\Title;
18use MediaWiki\User\Options\UserOptionsLookup;
19use MediaWiki\User\UserIdentity;
20use MediaWiki\Watchlist\WatchedItemStoreInterface;
21use MediaWiki\Watchlist\WatchlistManager;
22use Wikimedia\ParamValidator\ParamValidator;
23
24/**
25 * @ingroup API
26 */
27class ApiRollback extends ApiBase {
28
29    use ApiWatchlistTrait;
30
31    public function __construct(
32        ApiMain $mainModule,
33        string $moduleName,
34        private readonly RollbackPageFactory $rollbackPageFactory,
35        WatchlistManager $watchlistManager,
36        WatchedItemStoreInterface $watchedItemStore,
37        UserOptionsLookup $userOptionsLookup,
38    ) {
39        parent::__construct( $mainModule, $moduleName );
40        // Variables needed in ApiWatchlistTrait trait
41        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
42        $this->watchlistMaxDuration =
43            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
44        $this->watchlistManager = $watchlistManager;
45        $this->watchedItemStore = $watchedItemStore;
46        $this->userOptionsLookup = $userOptionsLookup;
47    }
48
49    /**
50     * @var Title
51     */
52    private $mTitleObj = null;
53
54    /**
55     * @var UserIdentity
56     */
57    private $mUser = null;
58
59    public function execute() {
60        $this->useTransactionalTimeLimit();
61
62        $user = $this->getUser();
63        $params = $this->extractRequestParams();
64
65        $titleObj = $this->getRbTitle( $params );
66
67        // If change tagging was requested, check that the user is allowed to tag,
68        // and the tags are valid. TODO: move inside rollback command?
69        if ( $params['tags'] ) {
70            $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
71            if ( !$tagStatus->isOK() ) {
72                $this->dieStatus( $tagStatus );
73            }
74        }
75
76        // @TODO: remove this hack once rollback uses POST (T88044)
77        $fname = __METHOD__;
78        $trxLimits = $this->getConfig()->get( MainConfigNames::TrxProfilerLimits );
79        $trxProfiler = Profiler::instance()->getTransactionProfiler();
80        $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname );
81        DeferredUpdates::addCallableUpdate( static function () use ( $trxProfiler, $trxLimits, $fname ) {
82            $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
83        } );
84
85        $rollbackResult = $this->rollbackPageFactory
86            ->newRollbackPage( $titleObj, $this->getAuthority(), $this->getRbUser( $params ) )
87            ->setSummary( $params['summary'] )
88            ->markAsBot( $params['markbot'] )
89            ->setChangeTags( $params['tags'] )
90            ->rollbackIfAllowed();
91
92        if ( !$rollbackResult->isGood() ) {
93            $this->dieStatus( $rollbackResult );
94        }
95
96        $watch = $params['watchlist'] ?? 'preferences';
97        $watchlistExpiry = $this->getExpiryFromParams( $params, $titleObj, $user, 'watchrollback-expiry' );
98
99        // Watch pages
100        $this->setWatch( $watch, $titleObj, $user, 'watchrollback', $watchlistExpiry );
101
102        $details = $rollbackResult->getValue();
103        $currentRevisionRecord = $details['current-revision-record'];
104        $targetRevisionRecord = $details['target-revision-record'];
105
106        $info = [
107            'title' => $titleObj->getPrefixedText(),
108            'pageid' => $currentRevisionRecord->getPageId(),
109            'summary' => $details['summary'],
110            'revid' => (int)$details['newid'],
111            // The revision being reverted (previously the latest revision of the page)
112            'old_revid' => $currentRevisionRecord->getID(),
113            // The revision being restored (the last revision before revision(s) by the reverted user)
114            'last_revid' => $targetRevisionRecord->getID()
115        ];
116
117        $this->getResult()->addValue( null, $this->getModuleName(), $info );
118    }
119
120    /** @inheritDoc */
121    public function mustBePosted() {
122        return true;
123    }
124
125    /** @inheritDoc */
126    public function isWriteMode() {
127        return true;
128    }
129
130    /** @inheritDoc */
131    public function getAllowedParams() {
132        $params = [
133            'title' => null,
134            'pageid' => [
135                ParamValidator::PARAM_TYPE => 'integer'
136            ],
137            'tags' => [
138                ParamValidator::PARAM_TYPE => 'tags',
139                ParamValidator::PARAM_ISMULTI => true,
140            ],
141            'user' => [
142                ParamValidator::PARAM_TYPE => 'user',
143                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
144                UserDef::PARAM_RETURN_OBJECT => true,
145                ParamValidator::PARAM_REQUIRED => true
146            ],
147            'summary' => '',
148            'markbot' => false,
149        ];
150
151        // Params appear in the docs in the order they are defined,
152        // which is why this is here (we want it above the token param).
153        $params += $this->getWatchlistParams();
154
155        return $params + [
156            'token' => [
157                // Standard definition automatically inserted
158                ApiBase::PARAM_HELP_MSG_APPEND => [ 'api-help-param-token-webui' ],
159            ],
160        ];
161    }
162
163    /** @inheritDoc */
164    public function needsToken() {
165        return 'rollback';
166    }
167
168    private function getRbUser( array $params ): UserIdentity {
169        if ( $this->mUser !== null ) {
170            return $this->mUser;
171        }
172
173        $this->mUser = $params['user'];
174
175        return $this->mUser;
176    }
177
178    /**
179     * @param array $params
180     *
181     * @return Title
182     */
183    private function getRbTitle( array $params ) {
184        if ( $this->mTitleObj !== null ) {
185            return $this->mTitleObj;
186        }
187
188        $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
189
190        if ( isset( $params['title'] ) ) {
191            $this->mTitleObj = Title::newFromText( $params['title'] );
192            if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) {
193                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['title'] ) ] );
194            }
195        } elseif ( isset( $params['pageid'] ) ) {
196            $this->mTitleObj = Title::newFromID( $params['pageid'] );
197            if ( !$this->mTitleObj ) {
198                $this->dieWithError( [ 'apierror-nosuchpageid', $params['pageid'] ] );
199            }
200        }
201
202        if ( !$this->mTitleObj->exists() ) {
203            $this->dieWithError( 'apierror-missingtitle' );
204        }
205
206        return $this->mTitleObj;
207    }
208
209    /** @inheritDoc */
210    protected function getExamplesMessages() {
211        $title = Title::newMainPage()->getPrefixedText();
212        $mp = rawurlencode( $title );
213
214        return [
215            "action=rollback&title={$mp}&user=Example&token=123ABC" =>
216                'apihelp-rollback-example-simple',
217            "action=rollback&title={$mp}&user=192.0.2.5&" .
218                'token=123ABC&summary=Reverting%20vandalism&markbot=1' =>
219                'apihelp-rollback-example-summary',
220        ];
221    }
222
223    /** @inheritDoc */
224    public function getHelpUrls() {
225        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Rollback';
226    }
227}
228
229/** @deprecated class alias since 1.43 */
230class_alias( ApiRollback::class, 'ApiRollback' );