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