Merge "Add MessagesBi.php"
[lhc/web/wiklou.git] / includes / actions / McrUndoAction.php
1 <?php
2 /**
3 * Temporary action for MCR undos
4 * @file
5 * @ingroup Actions
6 */
7
8 use MediaWiki\MediaWikiServices;
9 use MediaWiki\Storage\MutableRevisionRecord;
10 use MediaWiki\Storage\RevisionRecord;
11 use MediaWiki\Storage\SlotRecord;
12
13 /**
14 * Temporary action for MCR undos
15 *
16 * This is intended to go away when real MCR support is added to EditPage and
17 * the standard undo-with-edit behavior can be implemented there instead.
18 *
19 * If this were going to be kept, we'd probably want to figure out a good way
20 * to reuse the same code for generating the headers, summary box, and buttons
21 * on EditPage and here, and to better share the diffing and preview logic
22 * between the two. But doing that now would require much of the rewriting of
23 * EditPage that we're trying to put off by doing this instead.
24 *
25 * @ingroup Actions
26 * @since 1.32
27 * @deprecated since 1.32
28 */
29 class McrUndoAction extends FormAction {
30
31 private $undo = 0, $undoafter = 0, $cur = 0;
32
33 /** @param RevisionRecord|null */
34 private $curRev = null;
35
36 public function getName() {
37 return 'mcrundo';
38 }
39
40 public function getDescription() {
41 return '';
42 }
43
44 public function show() {
45 // Send a cookie so anons get talk message notifications
46 // (copied from SubmitAction)
47 MediaWiki\Session\SessionManager::getGlobalSession()->persist();
48
49 // Some stuff copied from EditAction
50 $this->useTransactionalTimeLimit();
51
52 $out = $this->getOutput();
53 $out->setRobotPolicy( 'noindex,nofollow' );
54 if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
55 $out->addModuleStyles( [
56 'mediawiki.ui.input',
57 'mediawiki.ui.checkbox',
58 ] );
59 }
60
61 // IP warning headers copied from EditPage
62 // (should more be copied?)
63 if ( wfReadOnly() ) {
64 $out->wrapWikiMsg(
65 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
66 [ 'readonlywarning', wfReadOnlyReason() ]
67 );
68 } elseif ( $this->context->getUser()->isAnon() ) {
69 if ( !$this->getRequest()->getCheck( 'wpPreview' ) ) {
70 $out->wrapWikiMsg(
71 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
72 [ 'anoneditwarning',
73 // Log-in link
74 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
75 'returnto' => $this->getTitle()->getPrefixedDBkey()
76 ] ),
77 // Sign-up link
78 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
79 'returnto' => $this->getTitle()->getPrefixedDBkey()
80 ] )
81 ]
82 );
83 } else {
84 $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
85 'anonpreviewwarning'
86 );
87 }
88 }
89
90 parent::show();
91 }
92
93 protected function checkCanExecute( User $user ) {
94 parent::checkCanExecute( $user );
95
96 $this->undoafter = $this->getRequest()->getInt( 'undoafter' );
97 $this->undo = $this->getRequest()->getInt( 'undo' );
98
99 if ( $this->undo == 0 || $this->undoafter == 0 ) {
100 throw new ErrorPageError( 'mcrundofailed', 'mcrundo-missingparam' );
101 }
102
103 $curRev = $this->page->getRevision();
104 if ( !$curRev ) {
105 throw new ErrorPageError( 'mcrundofailed', 'nopagetext' );
106 }
107 $this->curRev = $curRev->getRevisionRecord();
108 $this->cur = $this->getRequest()->getInt( 'cur', $this->curRev->getId() );
109
110 $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
111
112 $undoRev = $revisionLookup->getRevisionById( $this->undo );
113 $oldRev = $revisionLookup->getRevisionById( $this->undoafter );
114
115 if ( $undoRev === null || $oldRev === null ||
116 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
117 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
118 ) {
119 throw new ErrorPageError( 'mcrundofailed', 'undo-norev' );
120 }
121
122 return true;
123 }
124
125 /**
126 * @return MutableRevisionRecord
127 */
128 private function getNewRevision() {
129 $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
130
131 $undoRev = $revisionLookup->getRevisionById( $this->undo );
132 $oldRev = $revisionLookup->getRevisionById( $this->undoafter );
133 $curRev = $this->curRev;
134
135 $isLatest = $curRev->getId() === $undoRev->getId();
136
137 if ( $undoRev === null || $oldRev === null ||
138 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
139 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
140 ) {
141 throw new ErrorPageError( 'mcrundofailed', 'undo-norev' );
142 }
143
144 if ( $isLatest ) {
145 // Short cut! Undoing the current revision means we just restore the old.
146 return MutableRevisionRecord::newFromParentRevision( $oldRev );
147 }
148
149 $newRev = MutableRevisionRecord::newFromParentRevision( $curRev );
150
151 // Figure out the roles that need merging by first collecting all roles
152 // and then removing the ones that don't.
153 $rolesToMerge = array_unique( array_merge(
154 $oldRev->getSlotRoles(),
155 $undoRev->getSlotRoles(),
156 $curRev->getSlotRoles()
157 ) );
158
159 // Any roles with the same content in $oldRev and $undoRev can be
160 // inherited because undo won't change them.
161 $rolesToMerge = array_intersect(
162 $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $undoRev->getSlots() )
163 );
164 if ( !$rolesToMerge ) {
165 throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
166 }
167
168 // Any roles with the same content in $oldRev and $curRev were already reverted
169 // and so can be inherited.
170 $rolesToMerge = array_intersect(
171 $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() )
172 );
173 if ( !$rolesToMerge ) {
174 throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
175 }
176
177 // Any roles with the same content in $undoRev and $curRev weren't
178 // changed since and so can be reverted to $oldRev.
179 $diffRoles = array_intersect(
180 $rolesToMerge, $undoRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() )
181 );
182 foreach ( array_diff( $rolesToMerge, $diffRoles ) as $role ) {
183 if ( $oldRev->hasSlot( $role ) ) {
184 $newRev->inheritSlot( $oldRev->getSlot( $role, RevisionRecord::RAW ) );
185 } else {
186 $newRev->removeSlot( $role );
187 }
188 }
189 $rolesToMerge = $diffRoles;
190
191 // Any slot additions or removals not handled by the above checks can't be undone.
192 // There will be only one of the three revisions missing the slot:
193 // - !old means it was added in the undone revisions and modified after.
194 // Should it be removed entirely for the undo, or should the modified version be kept?
195 // - !undo means it was removed in the undone revisions and then readded with different content.
196 // Which content is should be kept, the old or the new?
197 // - !cur means it was changed in the undone revisions and then deleted after.
198 // Did someone delete vandalized content instead of undoing (meaning we should ideally restore
199 // it), or should it stay gone?
200 foreach ( $rolesToMerge as $role ) {
201 if ( !$oldRev->hasSlot( $role ) || !$undoRev->hasSlot( $role ) || !$curRev->hasSlot( $role ) ) {
202 throw new ErrorPageError( 'mcrundofailed', 'undo-failure' );
203 }
204 }
205
206 // Try to merge anything that's left.
207 foreach ( $rolesToMerge as $role ) {
208 $oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent();
209 $undoContent = $undoRev->getSlot( $role, RevisionRecord::RAW )->getContent();
210 $curContent = $curRev->getSlot( $role, RevisionRecord::RAW )->getContent();
211 $newContent = $undoContent->getContentHandler()
212 ->getUndoContent( $curContent, $undoContent, $oldContent, $isLatest );
213 if ( !$newContent ) {
214 throw new ErrorPageError( 'mcrundofailed', 'undo-failure' );
215 }
216 $newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) );
217 }
218
219 return $newRev;
220 }
221
222 private function generateDiff() {
223 $newRev = $this->getNewRevision();
224 if ( $newRev->hasSameContent( $this->curRev ) ) {
225 throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
226 }
227
228 $diffEngine = new DifferenceEngine( $this->context );
229 $diffEngine->setRevisions( $this->curRev, $newRev );
230
231 $oldtitle = $this->context->msg( 'currentrev' )->parse();
232 $newtitle = $this->context->msg( 'yourtext' )->parse();
233
234 if ( $this->getRequest()->getCheck( 'wpPreview' ) ) {
235 $diffEngine->renderNewRevision();
236 return '';
237 } else {
238 $diffText = $diffEngine->getDiff( $oldtitle, $newtitle );
239 $diffEngine->showDiffStyle();
240 return '<div id="wikiDiff">' . $diffText . '</div>';
241 }
242 }
243
244 public function onSubmit( $data ) {
245 global $wgUseRCPatrol;
246
247 if ( !$this->getRequest()->getCheck( 'wpSave' ) ) {
248 // Diff or preview
249 return false;
250 }
251
252 $updater = $this->page->getPage()->newPageUpdater( $this->context->getUser() );
253 $curRev = $updater->grabParentRevision();
254 if ( !$curRev ) {
255 throw new ErrorPageError( 'mcrundofailed', 'nopagetext' );
256 }
257
258 if ( $this->cur !== $curRev->getId() ) {
259 return Status::newFatal( 'mcrundo-changed' );
260 }
261
262 $newRev = $this->getNewRevision();
263 if ( !$newRev->hasSameContent( $curRev ) ) {
264 // Copy new slots into the PageUpdater, and remove any removed slots.
265 // TODO: This interface is awful, there should be a way to just pass $newRev.
266 // TODO: MCR: test this once we can store multiple slots
267 foreach ( $newRev->getSlots()->getSlots() as $slot ) {
268 $updater->setSlot( $slot );
269 }
270 foreach ( $curRev->getSlotRoles() as $role ) {
271 if ( !$newRev->hasSlot( $role ) ) {
272 $updater->removeSlot( $role );
273 }
274 }
275
276 $updater->setOriginalRevisionId( false );
277 $updater->setUndidRevisionId( $this->undo );
278
279 // TODO: Ugh.
280 if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $this->getUser() ) ) {
281 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
282 }
283
284 $updater->saveRevision(
285 CommentStoreComment::newUnsavedComment( trim( $this->getRequest()->getVal( 'wpSummary' ) ) ),
286 EDIT_AUTOSUMMARY | EDIT_UPDATE
287 );
288
289 return $updater->getStatus();
290 }
291
292 return Status::newGood();
293 }
294
295 protected function usesOOUI() {
296 return true;
297 }
298
299 protected function getFormFields() {
300 $request = $this->getRequest();
301 $config = $this->context->getConfig();
302 $oldCommentSchema = $config->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
303 $ret = [
304 'diff' => [
305 'type' => 'info',
306 'vertical-label' => true,
307 'raw' => true,
308 'default' => function () {
309 return $this->generateDiff();
310 }
311 ],
312 'summary' => [
313 'type' => 'text',
314 'id' => 'wpSummary',
315 'name' => 'wpSummary',
316 'cssclass' => 'mw-summary',
317 'label-message' => 'summary',
318 'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
319 'value' => $request->getVal( 'wpSummary', '' ),
320 'size' => 60,
321 'spellcheck' => 'true',
322 ],
323 'summarypreview' => [
324 'type' => 'info',
325 'label-message' => 'summary-preview',
326 'raw' => true,
327 ],
328 ];
329
330 if ( $request->getCheck( 'wpSummary' ) ) {
331 $ret['summarypreview']['default'] = Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ],
332 Linker::commentBlock( trim( $request->getVal( 'wpSummary' ) ), $this->getTitle(), false )
333 );
334 } else {
335 unset( $ret['summarypreview'] );
336 }
337
338 return $ret;
339 }
340
341 protected function alterForm( HTMLForm $form ) {
342 $form->setWrapperLegendMsg( 'confirm-mcrundo-title' );
343
344 $labelAsPublish = $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
345
346 $form->setSubmitName( 'wpSave' );
347 $form->setSubmitTooltip( $labelAsPublish ? 'publish' : 'save' );
348 $form->setSubmitTextMsg( $labelAsPublish ? 'publishchanges' : 'savechanges' );
349 $form->showCancel( true );
350 $form->setCancelTarget( $this->getTitle() );
351 $form->addButton( [
352 'name' => 'wpPreview',
353 'value' => '1',
354 'label-message' => 'showpreview',
355 'attribs' => Linker::tooltipAndAccesskeyAttribs( 'preview' ),
356 ] );
357 $form->addButton( [
358 'name' => 'wpDiff',
359 'value' => '1',
360 'label-message' => 'showdiff',
361 'attribs' => Linker::tooltipAndAccesskeyAttribs( 'diff' ),
362 ] );
363
364 $form->addHiddenField( 'undo', $this->undo );
365 $form->addHiddenField( 'undoafter', $this->undoafter );
366 $form->addHiddenField( 'cur', $this->curRev->getId() );
367 }
368
369 public function onSuccess() {
370 $this->getOutput()->redirect( $this->getTitle()->getFullURL() );
371 }
372
373 protected function preText() {
374 return '<div style="clear:both"></div>';
375 }
376 }