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;
110 \MediaWiki\Session\SessionManager::getGlobalSession()->persist();
116 $out->setRobotPolicy(
'noindex,nofollow' );
120 if ( $this->readOnlyMode->isReadOnly() ) {
122 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
123 [
'readonlywarning', $this->readOnlyMode->getReason() ]
125 } elseif ( $this->context->getUser()->isAnon() ) {
128 if ( !$this->
getRequest()->getCheck(
'wpPreview' ) ) {
135 'returnto' => $this->getTitle()->getPrefixedDBkey()
139 'returnto' => $this->getTitle()->getPrefixedDBkey()
142 'mw-anon-edit-warning'
148 $out->msg(
'anonpreviewwarning' )->parse(),
149 'mw-anon-preview-warning'
159 $this->undoafter = $this->
getRequest()->getInt(
'undoafter' );
160 $this->undo = $this->
getRequest()->getInt(
'undo' );
162 if ( $this->undo == 0 || $this->undoafter == 0 ) {
163 throw new ErrorPageError(
'mcrundofailed',
'mcrundo-missingparam' );
171 $this->cur = $this->
getRequest()->getInt(
'cur', $this->curRev->getId() );
175 parent::checkCanExecute( $user );
181 $undoRev = $this->revisionLookup->getRevisionByTitle( $title, $this->undo );
182 $oldRev = $this->revisionLookup->getRevisionByTitle( $title, $this->undoafter );
184 if ( $undoRev ===
null || $oldRev ===
null ||
185 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
186 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
197 private function getNewRevision() {
198 $undoRev = $this->revisionLookup->getRevisionById( $this->undo );
199 $oldRev = $this->revisionLookup->getRevisionById( $this->undoafter );
204 if ( $undoRev ===
null || $oldRev ===
null ||
205 $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
206 $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
220 $rolesToMerge = array_unique( array_merge(
221 $oldRev->getSlotRoles(),
222 $undoRev->getSlotRoles(),
228 $rolesToMerge = array_intersect(
229 $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $undoRev->getSlots() )
231 if ( !$rolesToMerge ) {
232 throw new ErrorPageError(
'mcrundofailed',
'undo-nochange' );
237 $rolesToMerge = array_intersect(
238 $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent(
$curRev->
getSlots() )
240 if ( !$rolesToMerge ) {
241 throw new ErrorPageError(
'mcrundofailed',
'undo-nochange' );
246 $diffRoles = array_intersect(
247 $rolesToMerge, $undoRev->getSlots()->getRolesWithDifferentContent(
$curRev->
getSlots() )
249 foreach ( array_diff( $rolesToMerge, $diffRoles ) as $role ) {
250 if ( $oldRev->hasSlot( $role ) ) {
251 $newRev->inheritSlot( $oldRev->getSlot( $role, RevisionRecord::RAW ) );
253 $newRev->removeSlot( $role );
256 $rolesToMerge = $diffRoles;
267 foreach ( $rolesToMerge as $role ) {
268 if ( !$oldRev->hasSlot( $role ) || !$undoRev->hasSlot( $role ) || !
$curRev->
hasSlot( $role ) ) {
269 throw new ErrorPageError(
'mcrundofailed',
'undo-failure' );
274 foreach ( $rolesToMerge as $role ) {
275 $oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent();
276 $undoContent = $undoRev->getSlot( $role, RevisionRecord::RAW )->getContent();
277 $curContent =
$curRev->
getSlot( $role, RevisionRecord::RAW )->getContent();
278 $newContent = $undoContent->getContentHandler()
279 ->getUndoContent( $curContent, $undoContent, $oldContent, $isLatest );
280 if ( !$newContent ) {
281 throw new ErrorPageError(
'mcrundofailed',
'undo-failure' );
283 $newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) );
289 private function generateDiffOrPreview(): string {
290 $newRev = $this->getNewRevision();
291 if ( $newRev->hasSameContent( $this->curRev ) ) {
292 throw new ErrorPageError(
'mcrundofailed',
'undo-nochange' );
296 $diffEngine->setRevisions( $this->curRev, $newRev );
298 $oldtitle = $this->context->msg(
'currentrev' )->parse();
299 $newtitle = $this->context->msg(
'yourtext' )->parse();
301 if ( $this->
getRequest()->getCheck(
'wpPreview' ) ) {
302 $this->showPreview( $newRev );
305 $diffText = $diffEngine->getDiff( $oldtitle, $newtitle );
306 $diffEngine->showDiffStyle();
307 return '<div id="wikiDiff">' . $diffText .
'</div>';
311 private function showPreview( RevisionRecord $rev ) {
313 $out = $this->getOutput();
316 # provide a anchor link to the form
317 $continueEditing =
'<span class="mw-continue-editing">' .
318 '[[#mw-mcrundo-form|' .
319 $this->context->getLanguage()->getArrow() .
' ' .
320 $this->context->msg(
'continue-editing' )->text() .
']]</span>';
322 $note = $this->context->msg(
'previewnote' )->plain() .
' ' . $continueEditing;
324 $parserOptions = $this->getWikiPage()->makeParserOptions( $this->context );
325 $parserOptions->setRenderReason(
'page-preview' );
326 $parserOptions->setIsPreview(
true );
327 $parserOptions->setIsSectionPreview(
false );
329 $parserOutput = $this->revisionRenderer
330 ->getRenderedRevision( $rev, $parserOptions, $this->
getAuthority() )
331 ->getRevisionParserOutput();
333 $previewHTML = $parserOutput->runOutputPipeline( $parserOptions, [
334 'enableSectionEditLinks' =>
false,
335 'includeDebugInfo' =>
true,
336 ] )->getContentHolderText();
338 $out->addParserOutputMetadata( $parserOutput );
339 if ( count( $parserOutput->getWarnings() ) ) {
340 $note .=
"\n\n" . implode(
"\n\n", $parserOutput->getWarnings() );
342 }
catch ( MWContentSerializationException $ex ) {
343 $m = $this->context->msg(
344 'content-failed-to-parse',
347 $note .=
"\n\n" . $m->parse();
351 $previewhead = Html::rawElement(
352 'div', [
'class' =>
'previewnote' ],
354 'h2', [
'id' =>
'mw-previewheader' ],
355 $this->context->msg(
'preview' )->text()
358 $out->parseAsInterface( $note )
362 $out->addHTML( $previewhead . $previewHTML );
366 if ( !$this->getRequest()->getCheck(
'wpSave' ) ) {
371 $updater = $this->getWikiPage()->newPageUpdater( $this->context->getUser() );
372 $curRev = $updater->grabParentRevision();
377 if ( $this->cur !== $curRev->getId() ) {
378 return Status::newFatal(
'mcrundo-changed' );
382 $this->getAuthority()->authorizeWrite(
'edit', $this->
getTitle(), $status );
383 if ( !$status->isOK() ) {
387 $newRev = $this->getNewRevision();
388 if ( !$newRev->hasSameContent( $curRev ) ) {
389 $hookRunner = $this->getHookRunner();
390 foreach ( $newRev->getSlotRoles() as $slotRole ) {
391 $slot = $newRev->getSlot( $slotRole, RevisionRecord::RAW );
394 $hookResult = $hookRunner->onEditFilterMergedContent(
398 trim( $this->getRequest()->getVal(
'wpSummary' ) ??
'' ),
403 if ( !$hookResult ) {
404 if ( $status->isGood() ) {
405 $status->error(
'hookaborted' );
409 } elseif ( !$status->isOK() ) {
410 if ( !$status->getMessages() ) {
411 $status->error(
'hookaborted' );
420 foreach ( $newRev->getSlots()->getSlots() as $slot ) {
421 $updater->setSlot( $slot );
423 foreach ( $curRev->getSlotRoles() as $role ) {
424 if ( !$newRev->hasSlot( $role ) ) {
425 $updater->removeSlot( $role );
429 $updater->setCause( PageUpdateCauses::CAUSE_UNDO );
432 if ( $this->useRCPatrol && $this->getAuthority()
433 ->authorizeWrite(
'autopatrol', $this->
getTitle() )
435 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
438 $updater->saveRevision(
440 trim( $this->getRequest()->getVal(
'wpSummary' ) ??
'' ) ),
444 return $updater->getStatus();
447 return Status::newGood();
455 $request = $this->getRequest();
460 'default' =>
function () {
461 return $this->generateDiffOrPreview();
467 'name' =>
'wpSummary',
468 'cssclass' =>
'mw-summary',
469 'label-message' =>
'summary',
470 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
471 'value' => $request->getVal(
'wpSummary',
'' ),
473 'spellcheck' =>
'true',
475 'summarypreview' => [
477 'label-message' =>
'summary-preview',
482 if ( $request->getCheck(
'wpSummary' ) ) {
483 $ret[
'summarypreview'][
'default'] = Html::rawElement(
485 [
'class' =>
'mw-summary-preview' ],
486 $this->commentFormatter->formatBlock(
487 trim( $request->getVal(
'wpSummary' ) ),
493 unset( $ret[
'summarypreview'] );
504 $form->
setId(
'mw-mcrundo-form' );
507 $form->
setSubmitTextMsg( $labelAsPublish ?
'publishchanges' :
'savechanges' );
511 'name' =>
'wpPreview',
513 'label-message' =>
'showpreview',
514 'attribs' => Linker::tooltipAndAccesskeyAttribs(
'preview' ),
519 'label-message' =>
'showdiff',
520 'attribs' => Linker::tooltipAndAccesskeyAttribs(
'diff' ),
523 $this->addStatePropagationFields( $form );
533 $this->getOutput()->redirect( $this->
getTitle()->getFullURL() );
537 return '<div style="clear:both"></div>';
542class_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.