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