Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.71% |
83 / 139 |
|
14.29% |
2 / 14 |
CRAP | |
0.00% |
0 / 1 |
RollbackAction | |
59.71% |
83 / 139 |
|
14.29% |
2 / 14 |
144.63 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRestriction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
usesOOUI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSuccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSubmit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
alterForm | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
show | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
handleRollbackRequest | |
74.44% |
67 / 90 |
|
0.00% |
0 / 1 |
26.68 | |||
enableTransactionalTimelimit | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
showRollbackConfirmationForm | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getFormFields | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Edit rollback user interface |
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 |
16 | * along with this program; if not, write to the Free Software |
17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
18 | * |
19 | * @file |
20 | * @ingroup Actions |
21 | */ |
22 | |
23 | use MediaWiki\CommentFormatter\CommentFormatter; |
24 | use MediaWiki\Config\ConfigException; |
25 | use MediaWiki\Content\IContentHandlerFactory; |
26 | use MediaWiki\Context\IContextSource; |
27 | use MediaWiki\Deferred\DeferredUpdates; |
28 | use MediaWiki\Linker\Linker; |
29 | use MediaWiki\MainConfigNames; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Message\Message; |
32 | use MediaWiki\Page\RollbackPageFactory; |
33 | use MediaWiki\Revision\RevisionRecord; |
34 | use MediaWiki\Revision\SlotRecord; |
35 | use MediaWiki\User\Options\UserOptionsLookup; |
36 | use MediaWiki\Watchlist\WatchlistManager; |
37 | |
38 | /** |
39 | * User interface for the rollback action |
40 | * |
41 | * @ingroup Actions |
42 | */ |
43 | class RollbackAction extends FormAction { |
44 | |
45 | private IContentHandlerFactory $contentHandlerFactory; |
46 | private RollbackPageFactory $rollbackPageFactory; |
47 | private UserOptionsLookup $userOptionsLookup; |
48 | private WatchlistManager $watchlistManager; |
49 | private CommentFormatter $commentFormatter; |
50 | |
51 | /** |
52 | * @param Article $article |
53 | * @param IContextSource $context |
54 | * @param IContentHandlerFactory $contentHandlerFactory |
55 | * @param RollbackPageFactory $rollbackPageFactory |
56 | * @param UserOptionsLookup $userOptionsLookup |
57 | * @param WatchlistManager $watchlistManager |
58 | * @param CommentFormatter $commentFormatter |
59 | */ |
60 | public function __construct( |
61 | Article $article, |
62 | IContextSource $context, |
63 | IContentHandlerFactory $contentHandlerFactory, |
64 | RollbackPageFactory $rollbackPageFactory, |
65 | UserOptionsLookup $userOptionsLookup, |
66 | WatchlistManager $watchlistManager, |
67 | CommentFormatter $commentFormatter |
68 | ) { |
69 | parent::__construct( $article, $context ); |
70 | $this->contentHandlerFactory = $contentHandlerFactory; |
71 | $this->rollbackPageFactory = $rollbackPageFactory; |
72 | $this->userOptionsLookup = $userOptionsLookup; |
73 | $this->watchlistManager = $watchlistManager; |
74 | $this->commentFormatter = $commentFormatter; |
75 | } |
76 | |
77 | public function getName() { |
78 | return 'rollback'; |
79 | } |
80 | |
81 | public function getRestriction() { |
82 | return 'rollback'; |
83 | } |
84 | |
85 | protected function usesOOUI() { |
86 | return true; |
87 | } |
88 | |
89 | protected function getDescription() { |
90 | return ''; |
91 | } |
92 | |
93 | public function doesWrites() { |
94 | return true; |
95 | } |
96 | |
97 | public function onSuccess() { |
98 | return false; |
99 | } |
100 | |
101 | public function onSubmit( $data ) { |
102 | return false; |
103 | } |
104 | |
105 | protected function alterForm( HTMLForm $form ) { |
106 | $form->setWrapperLegendMsg( 'confirm-rollback-top' ); |
107 | $form->setSubmitTextMsg( 'confirm-rollback-button' ); |
108 | $form->setTokenSalt( 'rollback' ); |
109 | |
110 | $from = $this->getRequest()->getVal( 'from' ); |
111 | if ( $from === null ) { |
112 | throw new BadRequestError( 'rollbackfailed', 'rollback-missingparam' ); |
113 | } |
114 | foreach ( [ 'from', 'bot', 'hidediff', 'summary', 'token' ] as $param ) { |
115 | $val = $this->getRequest()->getVal( $param ); |
116 | if ( $val !== null ) { |
117 | $form->addHiddenField( $param, $val ); |
118 | } |
119 | } |
120 | } |
121 | |
122 | /** |
123 | * @throws ErrorPageError |
124 | * @throws ReadOnlyError |
125 | * @throws ThrottledError |
126 | */ |
127 | public function show() { |
128 | $this->setHeaders(); |
129 | // This will throw exceptions if there's a problem |
130 | $this->checkCanExecute( $this->getUser() ); |
131 | |
132 | if ( !$this->userOptionsLookup->getOption( $this->getUser(), 'showrollbackconfirmation' ) || |
133 | $this->getRequest()->wasPosted() |
134 | ) { |
135 | $this->handleRollbackRequest(); |
136 | } else { |
137 | $this->showRollbackConfirmationForm(); |
138 | } |
139 | } |
140 | |
141 | public function handleRollbackRequest() { |
142 | $this->enableTransactionalTimelimit(); |
143 | $this->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' ); |
144 | |
145 | $request = $this->getRequest(); |
146 | $user = $this->getUser(); |
147 | $from = $request->getVal( 'from' ); |
148 | $rev = $this->getWikiPage()->getRevisionRecord(); |
149 | if ( $from === null ) { |
150 | throw new ErrorPageError( 'rollbackfailed', 'rollback-missingparam' ); |
151 | } |
152 | if ( !$rev ) { |
153 | throw new ErrorPageError( 'rollbackfailed', 'rollback-missingrevision' ); |
154 | } |
155 | |
156 | $revUser = $rev->getUser(); |
157 | $userText = $revUser ? $revUser->getName() : ''; |
158 | if ( $from !== $userText ) { |
159 | throw new ErrorPageError( 'rollbackfailed', 'alreadyrolled', [ |
160 | $this->getTitle()->getPrefixedText(), |
161 | wfEscapeWikiText( $from ), |
162 | $userText |
163 | ] ); |
164 | } |
165 | |
166 | if ( !$user->matchEditToken( $request->getVal( 'token' ), 'rollback' ) ) { |
167 | throw new ErrorPageError( 'sessionfailure-title', 'sessionfailure' ); |
168 | } |
169 | |
170 | // The revision has the user suppressed, so the rollback has empty 'from', |
171 | // so the check above would succeed in that case. |
172 | // T307278 - Also check if the user has rights to view suppressed usernames |
173 | if ( !$revUser ) { |
174 | if ( $this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
175 | $revUser = $rev->getUser( RevisionRecord::RAW ); |
176 | } else { |
177 | $userFactory = MediaWikiServices::getInstance()->getUserFactory(); |
178 | $revUser = $userFactory->newFromName( $this->context->msg( 'rev-deleted-user' )->plain() ); |
179 | } |
180 | } |
181 | |
182 | $rollbackResult = $this->rollbackPageFactory |
183 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable use of raw avoids null here |
184 | ->newRollbackPage( $this->getWikiPage(), $this->getAuthority(), $revUser ) |
185 | ->setSummary( $request->getText( 'summary' ) ) |
186 | ->markAsBot( $request->getBool( 'bot' ) ) |
187 | ->rollbackIfAllowed(); |
188 | $data = $rollbackResult->getValue(); |
189 | |
190 | if ( $rollbackResult->hasMessage( 'actionthrottledtext' ) ) { |
191 | throw new ThrottledError; |
192 | } |
193 | |
194 | if ( $rollbackResult->hasMessage( 'alreadyrolled' ) || $rollbackResult->hasMessage( 'cantrollback' ) ) { |
195 | $this->getOutput()->setPageTitleMsg( $this->msg( 'rollbackfailed' ) ); |
196 | $errArray = $rollbackResult->getErrors()[0]; |
197 | $this->getOutput()->addWikiMsgArray( $errArray['message'], $errArray['params'] ); |
198 | |
199 | if ( isset( $data['current-revision-record'] ) ) { |
200 | /** @var RevisionRecord $current */ |
201 | $current = $data['current-revision-record']; |
202 | |
203 | if ( $current->getComment() != null ) { |
204 | $this->getOutput()->addWikiMsg( |
205 | 'editcomment', |
206 | Message::rawParam( |
207 | $this->commentFormatter |
208 | ->format( $current->getComment()->text ) |
209 | ) |
210 | ); |
211 | } |
212 | } |
213 | |
214 | return; |
215 | } |
216 | |
217 | # NOTE: Permission errors already handled by Action::checkExecute. |
218 | if ( $rollbackResult->hasMessage( 'readonlytext' ) ) { |
219 | throw new ReadOnlyError; |
220 | } |
221 | |
222 | # XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object. |
223 | # Right now, we only show the first error |
224 | foreach ( $rollbackResult->getErrors() as $error ) { |
225 | throw new ErrorPageError( 'rollbackfailed', $error['message'], $error['params'] ); |
226 | } |
227 | |
228 | /** @var RevisionRecord $current */ |
229 | $current = $data['current-revision-record']; |
230 | $target = $data['target-revision-record']; |
231 | $newId = $data['newid']; |
232 | $this->getOutput()->setPageTitleMsg( $this->msg( 'actioncomplete' ) ); |
233 | $this->getOutput()->setRobotPolicy( 'noindex,nofollow' ); |
234 | |
235 | $old = Linker::revUserTools( $current ); |
236 | $new = Linker::revUserTools( $target ); |
237 | |
238 | $currentUser = $current->getUser( RevisionRecord::FOR_THIS_USER, $user ); |
239 | $targetUser = $target->getUser( RevisionRecord::FOR_THIS_USER, $user ); |
240 | $this->getOutput()->addHTML( |
241 | $this->msg( 'rollback-success' ) |
242 | ->rawParams( $old, $new ) |
243 | ->params( $currentUser ? $currentUser->getName() : '' ) |
244 | ->params( $targetUser ? $targetUser->getName() : '' ) |
245 | ->parseAsBlock() |
246 | ); |
247 | // Load the mediawiki.misc-authed-curate module, so that we can fire the JavaScript |
248 | // postEdit hook on a successful rollback. |
249 | $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' ); |
250 | // Export a success flag to the frontend, so that the mediawiki.misc-authed-curate |
251 | // ResourceLoader module can use this as an indicator to fire the postEdit hook. |
252 | $this->getOutput()->addJsConfigVars( [ |
253 | 'wgRollbackSuccess' => true, |
254 | // Don't show an edit confirmation with mw.notify(), the rollback success page |
255 | // is already a visual confirmation. |
256 | 'wgPostEditConfirmationDisabled' => true, |
257 | ] ); |
258 | |
259 | if ( $this->userOptionsLookup->getBoolOption( $user, 'watchrollback' ) ) { |
260 | $this->watchlistManager->addWatchIgnoringRights( $user, $this->getTitle() ); |
261 | } |
262 | |
263 | $this->getOutput()->returnToMain( false, $this->getTitle() ); |
264 | |
265 | if ( !$request->getBool( 'hidediff', false ) && |
266 | !$this->userOptionsLookup->getBoolOption( $this->getUser(), 'norollbackdiff' ) |
267 | ) { |
268 | $contentModel = $current->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ) |
269 | ->getModel(); |
270 | $contentHandler = $this->contentHandlerFactory->getContentHandler( $contentModel ); |
271 | $de = $contentHandler->createDifferenceEngine( |
272 | $this->getContext(), |
273 | $current->getId(), |
274 | $newId, |
275 | 0, |
276 | true |
277 | ); |
278 | $de->showDiff( '', '' ); |
279 | } |
280 | } |
281 | |
282 | /** |
283 | * Enables transactional time limit for POST and GET requests to RollbackAction |
284 | * @throws ConfigException |
285 | */ |
286 | private function enableTransactionalTimelimit() { |
287 | // If Rollbacks are made POST-only, use $this->useTransactionalTimeLimit() |
288 | wfTransactionalTimeLimit(); |
289 | if ( !$this->getRequest()->wasPosted() ) { |
290 | /** |
291 | * We apply the higher POST limits on GET requests |
292 | * to prevent logstash.wikimedia.org from being spammed |
293 | */ |
294 | $fname = __METHOD__; |
295 | $trxLimits = $this->context->getConfig()->get( MainConfigNames::TrxProfilerLimits ); |
296 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
297 | $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname ); |
298 | DeferredUpdates::addCallableUpdate( static function () use ( $trxProfiler, $trxLimits, $fname |
299 | ) { |
300 | $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname ); |
301 | } ); |
302 | } |
303 | } |
304 | |
305 | private function showRollbackConfirmationForm() { |
306 | $form = $this->getForm(); |
307 | if ( $form->show() ) { |
308 | $this->onSuccess(); |
309 | } |
310 | } |
311 | |
312 | protected function getFormFields() { |
313 | return [ |
314 | 'intro' => [ |
315 | 'type' => 'info', |
316 | 'raw' => true, |
317 | 'default' => $this->msg( 'confirm-rollback-bottom' )->parse() |
318 | ] |
319 | ]; |
320 | } |
321 | } |