Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
36.63% |
100 / 273 |
|
11.76% |
2 / 17 |
CRAP | |
0.00% |
0 / 1 |
McrUndoAction | |
36.63% |
100 / 273 |
|
11.76% |
2 / 17 |
1106.34 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRestriction | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
show | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
20 | |||
initFromParameters | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
checkCanExecute | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
5.03 | |||
getNewRevision | |
82.98% |
39 / 47 |
|
0.00% |
0 / 1 |
17.26 | |||
generateDiffOrPreview | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
showPreview | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
12 | |||
onSubmit | |
76.00% |
38 / 50 |
|
0.00% |
0 / 1 |
19.54 | |||
usesOOUI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFormFields | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
6 | |||
alterForm | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
addStatePropagationFields | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
onSuccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
preText | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Temporary action for MCR undos |
4 | * @file |
5 | * @ingroup Actions |
6 | */ |
7 | |
8 | use MediaWiki\CommentFormatter\CommentFormatter; |
9 | use MediaWiki\CommentStore\CommentStore; |
10 | use MediaWiki\CommentStore\CommentStoreComment; |
11 | use MediaWiki\Config\Config; |
12 | use MediaWiki\Context\IContextSource; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\HTMLForm\HTMLForm; |
15 | use MediaWiki\Linker\Linker; |
16 | use MediaWiki\MainConfigNames; |
17 | use MediaWiki\Permissions\PermissionStatus; |
18 | use MediaWiki\Revision\MutableRevisionRecord; |
19 | use MediaWiki\Revision\RevisionLookup; |
20 | use MediaWiki\Revision\RevisionRecord; |
21 | use MediaWiki\Revision\RevisionRenderer; |
22 | use MediaWiki\Revision\SlotRecord; |
23 | use MediaWiki\SpecialPage\SpecialPage; |
24 | use MediaWiki\Status\Status; |
25 | use MediaWiki\Storage\EditResult; |
26 | use MediaWiki\User\User; |
27 | use Wikimedia\Rdbms\ReadOnlyMode; |
28 | |
29 | /** |
30 | * Temporary action for MCR undos |
31 | * |
32 | * This is intended to go away when real MCR support is added to EditPage and |
33 | * the standard undo-with-edit behavior can be implemented there instead. |
34 | * |
35 | * If this were going to be kept, we'd probably want to figure out a good way |
36 | * to reuse the same code for generating the headers, summary box, and buttons |
37 | * on EditPage and here, and to better share the diffing and preview logic |
38 | * between the two. But doing that now would require much of the rewriting of |
39 | * EditPage that we're trying to put off by doing this instead. |
40 | * |
41 | * @ingroup Actions |
42 | * @since 1.32 |
43 | */ |
44 | class McrUndoAction extends FormAction { |
45 | |
46 | protected int $undo = 0; |
47 | protected int $undoafter = 0; |
48 | protected int $cur = 0; |
49 | |
50 | /** @var RevisionRecord|null */ |
51 | protected $curRev = null; |
52 | |
53 | private ReadOnlyMode $readOnlyMode; |
54 | private RevisionLookup $revisionLookup; |
55 | private RevisionRenderer $revisionRenderer; |
56 | private CommentFormatter $commentFormatter; |
57 | private bool $useRCPatrol; |
58 | |
59 | /** |
60 | * @param Article $article |
61 | * @param IContextSource $context |
62 | * @param ReadOnlyMode $readOnlyMode |
63 | * @param RevisionLookup $revisionLookup |
64 | * @param RevisionRenderer $revisionRenderer |
65 | * @param CommentFormatter $commentFormatter |
66 | * @param Config $config |
67 | */ |
68 | public function __construct( |
69 | Article $article, |
70 | IContextSource $context, |
71 | ReadOnlyMode $readOnlyMode, |
72 | RevisionLookup $revisionLookup, |
73 | RevisionRenderer $revisionRenderer, |
74 | CommentFormatter $commentFormatter, |
75 | Config $config |
76 | ) { |
77 | parent::__construct( $article, $context ); |
78 | $this->readOnlyMode = $readOnlyMode; |
79 | $this->revisionLookup = $revisionLookup; |
80 | $this->revisionRenderer = $revisionRenderer; |
81 | $this->commentFormatter = $commentFormatter; |
82 | $this->useRCPatrol = $config->get( MainConfigNames::UseRCPatrol ); |
83 | } |
84 | |
85 | public function getName() { |
86 | return 'mcrundo'; |
87 | } |
88 | |
89 | public function getDescription() { |
90 | return ''; |
91 | } |
92 | |
93 | public function getRestriction() { |
94 | // Require 'edit' permission to even see this action (T297322) |
95 | return 'edit'; |
96 | } |
97 | |
98 | public function show() { |
99 | // Send a cookie so anons get talk message notifications |
100 | // (copied from SubmitAction) |
101 | MediaWiki\Session\SessionManager::getGlobalSession()->persist(); |
102 | |
103 | // Some stuff copied from EditAction |
104 | $this->useTransactionalTimeLimit(); |
105 | |
106 | $out = $this->getOutput(); |
107 | $out->setRobotPolicy( 'noindex,nofollow' ); |
108 | |
109 | // IP warning headers copied from EditPage |
110 | // (should more be copied?) |
111 | if ( $this->readOnlyMode->isReadOnly() ) { |
112 | $out->wrapWikiMsg( |
113 | "<div id=\"mw-read-only-warning\">\n$1\n</div>", |
114 | [ 'readonlywarning', $this->readOnlyMode->getReason() ] |
115 | ); |
116 | } elseif ( $this->context->getUser()->isAnon() ) { |
117 | // Note: EditPage has a special message for temp user creation intent here. |
118 | // But McrUndoAction doesn't support user creation. |
119 | if ( !$this->getRequest()->getCheck( 'wpPreview' ) ) { |
120 | $out->addHTML( |
121 | Html::warningBox( |
122 | $out->msg( |
123 | 'anoneditwarning', |
124 | // Log-in link |
125 | SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ |
126 | 'returnto' => $this->getTitle()->getPrefixedDBkey() |
127 | ] ), |
128 | // Sign-up link |
129 | SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ |
130 | 'returnto' => $this->getTitle()->getPrefixedDBkey() |
131 | ] ) |
132 | )->parse(), |
133 | 'mw-anon-edit-warning' |
134 | ) |
135 | ); |
136 | } else { |
137 | $out->addHTML( |
138 | Html::warningBox( |
139 | $out->msg( 'anonpreviewwarning' )->parse(), |
140 | 'mw-anon-preview-warning' |
141 | ) |
142 | ); |
143 | } |
144 | } |
145 | |
146 | parent::show(); |
147 | } |
148 | |
149 | protected function initFromParameters() { |
150 | $this->undoafter = $this->getRequest()->getInt( 'undoafter' ); |
151 | $this->undo = $this->getRequest()->getInt( 'undo' ); |
152 | |
153 | if ( $this->undo == 0 || $this->undoafter == 0 ) { |
154 | throw new ErrorPageError( 'mcrundofailed', 'mcrundo-missingparam' ); |
155 | } |
156 | |
157 | $curRev = $this->getWikiPage()->getRevisionRecord(); |
158 | if ( !$curRev ) { |
159 | throw new ErrorPageError( 'mcrundofailed', 'nopagetext' ); |
160 | } |
161 | $this->curRev = $curRev; |
162 | $this->cur = $this->getRequest()->getInt( 'cur', $this->curRev->getId() ); |
163 | } |
164 | |
165 | protected function checkCanExecute( User $user ) { |
166 | parent::checkCanExecute( $user ); |
167 | |
168 | $this->initFromParameters(); |
169 | |
170 | // We use getRevisionByTitle to verify the revisions belong to this page (T297322) |
171 | $title = $this->getTitle(); |
172 | $undoRev = $this->revisionLookup->getRevisionByTitle( $title, $this->undo ); |
173 | $oldRev = $this->revisionLookup->getRevisionByTitle( $title, $this->undoafter ); |
174 | |
175 | if ( $undoRev === null || $oldRev === null || |
176 | $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) || |
177 | $oldRev->isDeleted( RevisionRecord::DELETED_TEXT ) |
178 | ) { |
179 | throw new ErrorPageError( 'mcrundofailed', 'undo-norev' ); |
180 | } |
181 | |
182 | return true; |
183 | } |
184 | |
185 | /** |
186 | * @return MutableRevisionRecord |
187 | */ |
188 | private function getNewRevision() { |
189 | $undoRev = $this->revisionLookup->getRevisionById( $this->undo ); |
190 | $oldRev = $this->revisionLookup->getRevisionById( $this->undoafter ); |
191 | $curRev = $this->curRev; |
192 | |
193 | $isLatest = $curRev->getId() === $undoRev->getId(); |
194 | |
195 | if ( $undoRev === null || $oldRev === null || |
196 | $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) || |
197 | $oldRev->isDeleted( RevisionRecord::DELETED_TEXT ) |
198 | ) { |
199 | throw new ErrorPageError( 'mcrundofailed', 'undo-norev' ); |
200 | } |
201 | |
202 | if ( $isLatest ) { |
203 | // Short cut! Undoing the current revision means we just restore the old. |
204 | return MutableRevisionRecord::newFromParentRevision( $oldRev ); |
205 | } |
206 | |
207 | $newRev = MutableRevisionRecord::newFromParentRevision( $curRev ); |
208 | |
209 | // Figure out the roles that need merging by first collecting all roles |
210 | // and then removing the ones that don't. |
211 | $rolesToMerge = array_unique( array_merge( |
212 | $oldRev->getSlotRoles(), |
213 | $undoRev->getSlotRoles(), |
214 | $curRev->getSlotRoles() |
215 | ) ); |
216 | |
217 | // Any roles with the same content in $oldRev and $undoRev can be |
218 | // inherited because undo won't change them. |
219 | $rolesToMerge = array_intersect( |
220 | $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $undoRev->getSlots() ) |
221 | ); |
222 | if ( !$rolesToMerge ) { |
223 | throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' ); |
224 | } |
225 | |
226 | // Any roles with the same content in $oldRev and $curRev were already reverted |
227 | // and so can be inherited. |
228 | $rolesToMerge = array_intersect( |
229 | $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() ) |
230 | ); |
231 | if ( !$rolesToMerge ) { |
232 | throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' ); |
233 | } |
234 | |
235 | // Any roles with the same content in $undoRev and $curRev weren't |
236 | // changed since and so can be reverted to $oldRev. |
237 | $diffRoles = array_intersect( |
238 | $rolesToMerge, $undoRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() ) |
239 | ); |
240 | foreach ( array_diff( $rolesToMerge, $diffRoles ) as $role ) { |
241 | if ( $oldRev->hasSlot( $role ) ) { |
242 | $newRev->inheritSlot( $oldRev->getSlot( $role, RevisionRecord::RAW ) ); |
243 | } else { |
244 | $newRev->removeSlot( $role ); |
245 | } |
246 | } |
247 | $rolesToMerge = $diffRoles; |
248 | |
249 | // Any slot additions or removals not handled by the above checks can't be undone. |
250 | // There will be only one of the three revisions missing the slot: |
251 | // - !old means it was added in the undone revisions and modified after. |
252 | // Should it be removed entirely for the undo, or should the modified version be kept? |
253 | // - !undo means it was removed in the undone revisions and then readded with different content. |
254 | // Which content is should be kept, the old or the new? |
255 | // - !cur means it was changed in the undone revisions and then deleted after. |
256 | // Did someone delete vandalized content instead of undoing (meaning we should ideally restore |
257 | // it), or should it stay gone? |
258 | foreach ( $rolesToMerge as $role ) { |
259 | if ( !$oldRev->hasSlot( $role ) || !$undoRev->hasSlot( $role ) || !$curRev->hasSlot( $role ) ) { |
260 | throw new ErrorPageError( 'mcrundofailed', 'undo-failure' ); |
261 | } |
262 | } |
263 | |
264 | // Try to merge anything that's left. |
265 | foreach ( $rolesToMerge as $role ) { |
266 | $oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent(); |
267 | $undoContent = $undoRev->getSlot( $role, RevisionRecord::RAW )->getContent(); |
268 | $curContent = $curRev->getSlot( $role, RevisionRecord::RAW )->getContent(); |
269 | $newContent = $undoContent->getContentHandler() |
270 | ->getUndoContent( $curContent, $undoContent, $oldContent, $isLatest ); |
271 | if ( !$newContent ) { |
272 | throw new ErrorPageError( 'mcrundofailed', 'undo-failure' ); |
273 | } |
274 | $newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) ); |
275 | } |
276 | |
277 | return $newRev; |
278 | } |
279 | |
280 | private function generateDiffOrPreview() { |
281 | $newRev = $this->getNewRevision(); |
282 | if ( $newRev->hasSameContent( $this->curRev ) ) { |
283 | throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' ); |
284 | } |
285 | |
286 | $diffEngine = new DifferenceEngine( $this->context ); |
287 | $diffEngine->setRevisions( $this->curRev, $newRev ); |
288 | |
289 | $oldtitle = $this->context->msg( 'currentrev' )->parse(); |
290 | $newtitle = $this->context->msg( 'yourtext' )->parse(); |
291 | |
292 | if ( $this->getRequest()->getCheck( 'wpPreview' ) ) { |
293 | $this->showPreview( $newRev ); |
294 | return ''; |
295 | } else { |
296 | $diffText = $diffEngine->getDiff( $oldtitle, $newtitle ); |
297 | $diffEngine->showDiffStyle(); |
298 | return '<div id="wikiDiff">' . $diffText . '</div>'; |
299 | } |
300 | } |
301 | |
302 | private function showPreview( RevisionRecord $rev ) { |
303 | // Mostly copied from EditPage::getPreviewText() |
304 | $out = $this->getOutput(); |
305 | |
306 | try { |
307 | # provide a anchor link to the form |
308 | $continueEditing = '<span class="mw-continue-editing">' . |
309 | '[[#mw-mcrundo-form|' . |
310 | $this->context->getLanguage()->getArrow() . ' ' . |
311 | $this->context->msg( 'continue-editing' )->text() . ']]</span>'; |
312 | |
313 | $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing; |
314 | |
315 | $parserOptions = $this->getWikiPage()->makeParserOptions( $this->context ); |
316 | $parserOptions->setRenderReason( 'page-preview' ); |
317 | $parserOptions->setIsPreview( true ); |
318 | $parserOptions->setIsSectionPreview( false ); |
319 | |
320 | $parserOutput = $this->revisionRenderer |
321 | ->getRenderedRevision( $rev, $parserOptions, $this->getAuthority() ) |
322 | ->getRevisionParserOutput(); |
323 | // TODO T371004 move runOutputPipeline out of $parserOutput |
324 | $previewHTML = $parserOutput->runOutputPipeline( $parserOptions, [ |
325 | 'enableSectionEditLinks' => false, |
326 | 'includeDebugInfo' => true, |
327 | ] )->getContentHolderText(); |
328 | |
329 | $out->addParserOutputMetadata( $parserOutput ); |
330 | if ( count( $parserOutput->getWarnings() ) ) { |
331 | $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() ); |
332 | } |
333 | } catch ( MWContentSerializationException $ex ) { |
334 | $m = $this->context->msg( |
335 | 'content-failed-to-parse', |
336 | $ex->getMessage() |
337 | ); |
338 | $note .= "\n\n" . $m->parse(); |
339 | $previewHTML = ''; |
340 | } |
341 | |
342 | $previewhead = Html::rawElement( |
343 | 'div', [ 'class' => 'previewnote' ], |
344 | Html::element( |
345 | 'h2', [ 'id' => 'mw-previewheader' ], |
346 | $this->context->msg( 'preview' )->text() |
347 | ) . |
348 | Html::warningBox( |
349 | $out->parseAsInterface( $note ) |
350 | ) |
351 | ); |
352 | |
353 | $out->addHTML( $previewhead . $previewHTML ); |
354 | } |
355 | |
356 | public function onSubmit( $data ) { |
357 | if ( !$this->getRequest()->getCheck( 'wpSave' ) ) { |
358 | // Diff or preview |
359 | return false; |
360 | } |
361 | |
362 | $updater = $this->getWikiPage()->newPageUpdater( $this->context->getUser() ); |
363 | $curRev = $updater->grabParentRevision(); |
364 | if ( !$curRev ) { |
365 | throw new ErrorPageError( 'mcrundofailed', 'nopagetext' ); |
366 | } |
367 | |
368 | if ( $this->cur !== $curRev->getId() ) { |
369 | return Status::newFatal( 'mcrundo-changed' ); |
370 | } |
371 | |
372 | $status = new PermissionStatus(); |
373 | $this->getAuthority()->authorizeWrite( 'edit', $this->getTitle(), $status ); |
374 | if ( !$status->isOK() ) { |
375 | throw new PermissionsError( 'edit', $status ); |
376 | } |
377 | |
378 | $newRev = $this->getNewRevision(); |
379 | if ( !$newRev->hasSameContent( $curRev ) ) { |
380 | $hookRunner = $this->getHookRunner(); |
381 | foreach ( $newRev->getSlotRoles() as $slotRole ) { |
382 | $slot = $newRev->getSlot( $slotRole, RevisionRecord::RAW ); |
383 | |
384 | $status = new Status(); |
385 | $hookResult = $hookRunner->onEditFilterMergedContent( |
386 | $this->getContext(), |
387 | $slot->getContent(), |
388 | $status, |
389 | trim( $this->getRequest()->getVal( 'wpSummary' ) ?? '' ), |
390 | $this->getUser(), |
391 | false |
392 | ); |
393 | |
394 | if ( !$hookResult ) { |
395 | if ( $status->isGood() ) { |
396 | $status->error( 'hookaborted' ); |
397 | } |
398 | |
399 | return $status; |
400 | } elseif ( !$status->isOK() ) { |
401 | if ( !$status->getMessages() ) { |
402 | $status->error( 'hookaborted' ); |
403 | } |
404 | return $status; |
405 | } |
406 | } |
407 | |
408 | // Copy new slots into the PageUpdater, and remove any removed slots. |
409 | // TODO: This interface is awful, there should be a way to just pass $newRev. |
410 | // TODO: MCR: test this once we can store multiple slots |
411 | foreach ( $newRev->getSlots()->getSlots() as $slot ) { |
412 | $updater->setSlot( $slot ); |
413 | } |
414 | foreach ( $curRev->getSlotRoles() as $role ) { |
415 | if ( !$newRev->hasSlot( $role ) ) { |
416 | $updater->removeSlot( $role ); |
417 | } |
418 | } |
419 | |
420 | $updater->markAsRevert( EditResult::REVERT_UNDO, $this->undo, $this->undoafter ); |
421 | |
422 | if ( $this->useRCPatrol && $this->getAuthority() |
423 | ->authorizeWrite( 'autopatrol', $this->getTitle() ) |
424 | ) { |
425 | $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED ); |
426 | } |
427 | |
428 | $updater->saveRevision( |
429 | CommentStoreComment::newUnsavedComment( |
430 | trim( $this->getRequest()->getVal( 'wpSummary' ) ?? '' ) ), |
431 | EDIT_AUTOSUMMARY | EDIT_UPDATE |
432 | ); |
433 | |
434 | return $updater->getStatus(); |
435 | } |
436 | |
437 | return Status::newGood(); |
438 | } |
439 | |
440 | protected function usesOOUI() { |
441 | return true; |
442 | } |
443 | |
444 | protected function getFormFields() { |
445 | $request = $this->getRequest(); |
446 | $ret = [ |
447 | 'diff' => [ |
448 | 'type' => 'info', |
449 | 'raw' => true, |
450 | 'default' => function () { |
451 | return $this->generateDiffOrPreview(); |
452 | } |
453 | ], |
454 | 'summary' => [ |
455 | 'type' => 'text', |
456 | 'id' => 'wpSummary', |
457 | 'name' => 'wpSummary', |
458 | 'cssclass' => 'mw-summary', |
459 | 'label-message' => 'summary', |
460 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
461 | 'value' => $request->getVal( 'wpSummary', '' ), |
462 | 'size' => 60, |
463 | 'spellcheck' => 'true', |
464 | ], |
465 | 'summarypreview' => [ |
466 | 'type' => 'info', |
467 | 'label-message' => 'summary-preview', |
468 | 'raw' => true, |
469 | ], |
470 | ]; |
471 | |
472 | if ( $request->getCheck( 'wpSummary' ) ) { |
473 | $ret['summarypreview']['default'] = Html::rawElement( |
474 | 'div', |
475 | [ 'class' => 'mw-summary-preview' ], |
476 | $this->commentFormatter->formatBlock( |
477 | trim( $request->getVal( 'wpSummary' ) ), |
478 | $this->getTitle(), |
479 | false |
480 | ) |
481 | ); |
482 | } else { |
483 | unset( $ret['summarypreview'] ); |
484 | } |
485 | |
486 | return $ret; |
487 | } |
488 | |
489 | protected function alterForm( HTMLForm $form ) { |
490 | $form->setWrapperLegendMsg( 'confirm-mcrundo-title' ); |
491 | |
492 | $labelAsPublish = $this->context->getConfig()->get( MainConfigNames::EditSubmitButtonLabelPublish ); |
493 | |
494 | $form->setId( 'mw-mcrundo-form' ); |
495 | $form->setSubmitName( 'wpSave' ); |
496 | $form->setSubmitTooltip( $labelAsPublish ? 'publish' : 'save' ); |
497 | $form->setSubmitTextMsg( $labelAsPublish ? 'publishchanges' : 'savechanges' ); |
498 | $form->showCancel( true ); |
499 | $form->setCancelTarget( $this->getTitle() ); |
500 | $form->addButton( [ |
501 | 'name' => 'wpPreview', |
502 | 'value' => '1', |
503 | 'label-message' => 'showpreview', |
504 | 'attribs' => Linker::tooltipAndAccesskeyAttribs( 'preview' ), |
505 | ] ); |
506 | $form->addButton( [ |
507 | 'name' => 'wpDiff', |
508 | 'value' => '1', |
509 | 'label-message' => 'showdiff', |
510 | 'attribs' => Linker::tooltipAndAccesskeyAttribs( 'diff' ), |
511 | ] ); |
512 | |
513 | $this->addStatePropagationFields( $form ); |
514 | } |
515 | |
516 | protected function addStatePropagationFields( HTMLForm $form ) { |
517 | $form->addHiddenField( 'undo', $this->undo ); |
518 | $form->addHiddenField( 'undoafter', $this->undoafter ); |
519 | $form->addHiddenField( 'cur', $this->curRev->getId() ); |
520 | } |
521 | |
522 | public function onSuccess() { |
523 | $this->getOutput()->redirect( $this->getTitle()->getFullURL() ); |
524 | } |
525 | |
526 | protected function preText() { |
527 | return '<div style="clear:both"></div>'; |
528 | } |
529 | } |