66 private bool $useRCPatrol;
86 parent::__construct( $article,
$context );
87 $this->readOnlyMode = $readOnlyMode;
88 $this->revisionLookup = $revisionLookup;
89 $this->revisionRenderer = $revisionRenderer;
90 $this->commentFormatter = $commentFormatter;
120 $out->setRobotPolicy(
'noindex,nofollow' );
124 if ( $this->readOnlyMode->isReadOnly() ) {
126 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
127 [
'readonlywarning', $this->readOnlyMode->getReason() ]
129 } elseif ( $this->context->getUser()->isAnon() ) {
132 if ( !$this->
getRequest()->getCheck(
'wpPreview' ) ) {
139 'returnto' => $this->getTitle()->getPrefixedDBkey()
143 'returnto' => $this->getTitle()->getPrefixedDBkey()
146 'mw-anon-edit-warning'
152 $out->msg(
'anonpreviewwarning' )->parse(),
153 'mw-anon-preview-warning'
163 $this->undoafter = $this->
getRequest()->getInt(
'undoafter' );
164 $this->undo = $this->
getRequest()->getInt(
'undo' );
166 if ( $this->undo == 0 || $this->undoafter == 0 ) {
167 throw new ErrorPageError(
'mcrundofailed',
'mcrundo-missingparam' );
175 $this->cur = $this->
getRequest()->getInt(
'cur', $this->curRev->getId() );
180 parent::checkCanExecute( $user );
186 $undoRev = $this->revisionLookup->getRevisionByTitle( $title, $this->undo );
187 $oldRev = $this->revisionLookup->getRevisionByTitle( $title, $this->undoafter );
189 if ( $undoRev ===
null || $oldRev ===
null ||
190 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
191 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
202 private function getNewRevision() {
203 $undoRev = $this->revisionLookup->getRevisionById( $this->undo );
204 $oldRev = $this->revisionLookup->getRevisionById( $this->undoafter );
209 if ( $undoRev ===
null || $oldRev ===
null ||
210 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
211 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
225 $rolesToMerge = array_unique( array_merge(
226 $oldRev->getSlotRoles(),
227 $undoRev->getSlotRoles(),
233 $rolesToMerge = array_intersect(
234 $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $undoRev->getSlots() )
236 if ( !$rolesToMerge ) {
237 throw new ErrorPageError(
'mcrundofailed',
'undo-nochange' );
242 $rolesToMerge = array_intersect(
243 $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent(
$curRev->
getSlots() )
245 if ( !$rolesToMerge ) {
246 throw new ErrorPageError(
'mcrundofailed',
'undo-nochange' );
251 $diffRoles = array_intersect(
252 $rolesToMerge, $undoRev->getSlots()->getRolesWithDifferentContent(
$curRev->
getSlots() )
254 foreach ( array_diff( $rolesToMerge, $diffRoles ) as $role ) {
255 if ( $oldRev->hasSlot( $role ) ) {
256 $newRev->inheritSlot( $oldRev->getSlot( $role, RevisionRecord::RAW ) );
258 $newRev->removeSlot( $role );
261 $rolesToMerge = $diffRoles;
272 foreach ( $rolesToMerge as $role ) {
273 if ( !$oldRev->hasSlot( $role ) || !$undoRev->hasSlot( $role ) || !
$curRev->
hasSlot( $role ) ) {
274 throw new ErrorPageError(
'mcrundofailed',
'undo-failure' );
279 foreach ( $rolesToMerge as $role ) {
280 $oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent();
281 $undoContent = $undoRev->getSlot( $role, RevisionRecord::RAW )->getContent();
282 $curContent =
$curRev->
getSlot( $role, RevisionRecord::RAW )->getContent();
283 $newContent = $undoContent->getContentHandler()
284 ->getUndoContent( $curContent, $undoContent, $oldContent, $isLatest );
285 if ( !$newContent ) {
286 throw new ErrorPageError(
'mcrundofailed',
'undo-failure' );
288 $newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) );
294 private function generateDiffOrPreview(): string {
295 $newRev = $this->getNewRevision();
296 if ( $newRev->hasSameContent( $this->curRev ) ) {
297 throw new ErrorPageError(
'mcrundofailed',
'undo-nochange' );
301 $diffEngine->setRevisions( $this->curRev, $newRev );
303 $oldtitle = $this->context->msg(
'currentrev' )->parse();
304 $newtitle = $this->context->msg(
'yourtext' )->parse();
306 if ( $this->
getRequest()->getCheck(
'wpPreview' ) ) {
307 $this->showPreview( $newRev );
310 $diffText = $diffEngine->getDiff( $oldtitle, $newtitle );
311 $diffEngine->showDiffStyle();
312 return '<div id="wikiDiff">' . $diffText .
'</div>';
316 private function showPreview( RevisionRecord $rev ) {
318 $out = $this->getOutput();
321 # provide a anchor link to the form
322 $continueEditing =
'<span class="mw-continue-editing">' .
323 '[[#mw-mcrundo-form|' .
324 $this->context->getLanguage()->getArrow() .
' ' .
325 $this->context->msg(
'continue-editing' )->text() .
']]</span>';
327 $note = $this->context->msg(
'previewnote' )->plain() .
' ' . $continueEditing;
329 $parserOptions = $this->getWikiPage()->makeParserOptions( $this->context );
330 $parserOptions->setRenderReason(
'page-preview' );
331 $parserOptions->setIsPreview(
true );
332 $parserOptions->setIsSectionPreview(
false );
334 $parserOutput = $this->revisionRenderer
335 ->getRenderedRevision( $rev, $parserOptions, $this->
getAuthority() )
336 ->getRevisionParserOutput();
338 $previewHTML = $parserOutput->runOutputPipeline( $parserOptions, [
339 'enableSectionEditLinks' =>
false,
340 'includeDebugInfo' =>
true,
341 ] )->getContentHolderText();
343 $out->addParserOutputMetadata( $parserOutput );
344 foreach ( $parserOutput->getWarningMsgs() as $mv ) {
345 $note .=
"\n\n" . $this->context->msg( $mv )->text();
347 }
catch ( MWContentSerializationException $ex ) {
348 $m = $this->context->msg(
349 'content-failed-to-parse',
352 $note .=
"\n\n" . $m->parse();
356 $previewhead = Html::rawElement(
357 'div', [
'class' =>
'previewnote' ],
359 'h2', [
'id' =>
'mw-previewheader' ],
360 $this->context->msg(
'preview' )->text()
363 $out->parseAsInterface( $note )
367 $out->addHTML( $previewhead . $previewHTML );
372 if ( !$this->getRequest()->getCheck(
'wpSave' ) ) {
377 $updater = $this->getWikiPage()->newPageUpdater( $this->context->getUser() );
378 $curRev = $updater->grabParentRevision();
383 if ( $this->cur !== $curRev->getId() ) {
384 return Status::newFatal(
'mcrundo-changed' );
388 $this->getAuthority()->authorizeWrite(
'edit', $this->getTitle(), $status );
389 if ( !$status->isOK() ) {
393 $newRev = $this->getNewRevision();
394 if ( !$newRev->hasSameContent( $curRev ) ) {
395 $hookRunner = $this->getHookRunner();
396 foreach ( $newRev->getSlotRoles() as $slotRole ) {
397 $slot = $newRev->getSlot( $slotRole, RevisionRecord::RAW );
400 $hookResult = $hookRunner->onEditFilterMergedContent(
404 trim( $this->getRequest()->getVal(
'wpSummary' ) ??
'' ),
409 if ( !$hookResult ) {
410 if ( $status->isGood() ) {
411 $status->error(
'hookaborted' );
415 } elseif ( !$status->isOK() ) {
416 if ( !$status->getMessages() ) {
417 $status->error(
'hookaborted' );
426 foreach ( $newRev->getSlots()->getSlots() as $slot ) {
427 $updater->setSlot( $slot );
429 foreach ( $curRev->getSlotRoles() as $role ) {
430 if ( !$newRev->hasSlot( $role ) ) {
431 $updater->removeSlot( $role );
435 $updater->setCause( PageUpdateCauses::CAUSE_UNDO );
438 if ( $this->useRCPatrol && $this->getAuthority()
439 ->authorizeWrite(
'autopatrol', $this->getTitle() )
441 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
444 $updater->saveRevision(
446 trim( $this->getRequest()->getVal(
'wpSummary' ) ??
'' ) ),
450 return $updater->getStatus();
453 return Status::newGood();
463 $request = $this->getRequest();
468 'default' =>
function () {
469 return $this->generateDiffOrPreview();
475 'name' =>
'wpSummary',
476 'cssclass' =>
'mw-summary',
477 'label-message' =>
'summary',
478 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
479 'value' => $request->getVal(
'wpSummary',
'' ),
481 'spellcheck' =>
'true',
483 'summarypreview' => [
485 'label-message' =>
'summary-preview',
490 if ( $request->getCheck(
'wpSummary' ) ) {
491 $ret[
'summarypreview'][
'default'] = Html::rawElement(
493 [
'class' =>
'mw-summary-preview' ],
494 $this->commentFormatter->formatBlock(
495 trim( $request->getVal(
'wpSummary' ) ),
501 unset( $ret[
'summarypreview'] );
512 $form->
setId(
'mw-mcrundo-form' );
515 $form->
setSubmitTextMsg( $labelAsPublish ?
'publishchanges' :
'savechanges' );
519 'name' =>
'wpPreview',
521 'label-message' =>
'showpreview',
522 'attribs' => Linker::tooltipAndAccesskeyAttribs(
'preview' ),
527 'label-message' =>
'showdiff',
528 'attribs' => Linker::tooltipAndAccesskeyAttribs(
'diff' ),
531 $this->addStatePropagationFields( $form );
542 $this->getOutput()->redirect( $this->getTitle()->getFullURL() );
547 return '<div style="clear:both"></div>';
552class_alias( McrUndoAction::class,
'McrUndoAction' );
const EDIT_UPDATE
Article is assumed to be pre-existing, fail if it doesn't exist.
const EDIT_AUTOSUMMARY
Fill in blank summaries with generated text where possible.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
An error page which can definitely be safely rendered using the OutputPage.
Exception representing a failure to serialize or unserialize a content object.
A class containing constants representing the names of configuration variables.
const UseRCPatrol
Name constant for the UseRCPatrol setting, for use with Config::get()
const EditSubmitButtonLabelPublish
Name constant for the EditSubmitButtonLabelPublish setting, for use with Config::get()
Legacy class representing an editable page and handling UI for some page actions.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Interface for objects which can provide a MediaWiki context on request.
Constants for representing well known causes for page updates.