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