Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4130415
McrUndoAction.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
16 KB
Referenced Files
None
Subscribers
None
McrUndoAction.php
View Options
<?php
/**
* Temporary action for MCR undos
* @file
* @ingroup Actions
*/
use
MediaWiki\CommentFormatter\CommentFormatter
;
use
MediaWiki\CommentStore\CommentStore
;
use
MediaWiki\CommentStore\CommentStoreComment
;
use
MediaWiki\Config\Config
;
use
MediaWiki\Context\IContextSource
;
use
MediaWiki\Html\Html
;
use
MediaWiki\HTMLForm\HTMLForm
;
use
MediaWiki\Linker\Linker
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\Permissions\PermissionStatus
;
use
MediaWiki\Revision\MutableRevisionRecord
;
use
MediaWiki\Revision\RevisionLookup
;
use
MediaWiki\Revision\RevisionRecord
;
use
MediaWiki\Revision\RevisionRenderer
;
use
MediaWiki\Revision\SlotRecord
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Status\Status
;
use
MediaWiki\Storage\EditResult
;
use
MediaWiki\User\User
;
use
Wikimedia\Rdbms\ReadOnlyMode
;
/**
* Temporary action for MCR undos
*
* This is intended to go away when real MCR support is added to EditPage and
* the standard undo-with-edit behavior can be implemented there instead.
*
* If this were going to be kept, we'd probably want to figure out a good way
* to reuse the same code for generating the headers, summary box, and buttons
* on EditPage and here, and to better share the diffing and preview logic
* between the two. But doing that now would require much of the rewriting of
* EditPage that we're trying to put off by doing this instead.
*
* @ingroup Actions
* @since 1.32
*/
class
McrUndoAction
extends
FormAction
{
protected
int
$undo
=
0
;
protected
int
$undoafter
=
0
;
protected
int
$cur
=
0
;
/** @var RevisionRecord|null */
protected
$curRev
=
null
;
private
ReadOnlyMode
$readOnlyMode
;
private
RevisionLookup
$revisionLookup
;
private
RevisionRenderer
$revisionRenderer
;
private
CommentFormatter
$commentFormatter
;
private
bool
$useRCPatrol
;
/**
* @param Article $article
* @param IContextSource $context
* @param ReadOnlyMode $readOnlyMode
* @param RevisionLookup $revisionLookup
* @param RevisionRenderer $revisionRenderer
* @param CommentFormatter $commentFormatter
* @param Config $config
*/
public
function
__construct
(
Article
$article
,
IContextSource
$context
,
ReadOnlyMode
$readOnlyMode
,
RevisionLookup
$revisionLookup
,
RevisionRenderer
$revisionRenderer
,
CommentFormatter
$commentFormatter
,
Config
$config
)
{
parent
::
__construct
(
$article
,
$context
);
$this
->
readOnlyMode
=
$readOnlyMode
;
$this
->
revisionLookup
=
$revisionLookup
;
$this
->
revisionRenderer
=
$revisionRenderer
;
$this
->
commentFormatter
=
$commentFormatter
;
$this
->
useRCPatrol
=
$config
->
get
(
MainConfigNames
::
UseRCPatrol
);
}
public
function
getName
()
{
return
'mcrundo'
;
}
public
function
getDescription
()
{
return
''
;
}
public
function
getRestriction
()
{
// Require 'edit' permission to even see this action (T297322)
return
'edit'
;
}
public
function
show
()
{
// Send a cookie so anons get talk message notifications
// (copied from SubmitAction)
MediaWiki\Session\SessionManager
::
getGlobalSession
()->
persist
();
// Some stuff copied from EditAction
$this
->
useTransactionalTimeLimit
();
$out
=
$this
->
getOutput
();
$out
->
setRobotPolicy
(
'noindex,nofollow'
);
// IP warning headers copied from EditPage
// (should more be copied?)
if
(
$this
->
readOnlyMode
->
isReadOnly
()
)
{
$out
->
wrapWikiMsg
(
"<div id=
\"
mw-read-only-warning
\"
>
\n
$1
\n
</div>"
,
[
'readonlywarning'
,
$this
->
readOnlyMode
->
getReason
()
]
);
}
elseif
(
$this
->
context
->
getUser
()->
isAnon
()
)
{
// Note: EditPage has a special message for temp user creation intent here.
// But McrUndoAction doesn't support user creation.
if
(
!
$this
->
getRequest
()->
getCheck
(
'wpPreview'
)
)
{
$out
->
addHTML
(
Html
::
warningBox
(
$out
->
msg
(
'anoneditwarning'
,
// Log-in link
SpecialPage
::
getTitleFor
(
'Userlogin'
)->
getFullURL
(
[
'returnto'
=>
$this
->
getTitle
()->
getPrefixedDBkey
()
]
),
// Sign-up link
SpecialPage
::
getTitleFor
(
'CreateAccount'
)->
getFullURL
(
[
'returnto'
=>
$this
->
getTitle
()->
getPrefixedDBkey
()
]
)
)->
parse
(),
'mw-anon-edit-warning'
)
);
}
else
{
$out
->
addHTML
(
Html
::
warningBox
(
$out
->
msg
(
'anonpreviewwarning'
)->
parse
(),
'mw-anon-preview-warning'
)
);
}
}
parent
::
show
();
}
protected
function
initFromParameters
()
{
$this
->
undoafter
=
$this
->
getRequest
()->
getInt
(
'undoafter'
);
$this
->
undo
=
$this
->
getRequest
()->
getInt
(
'undo'
);
if
(
$this
->
undo
==
0
||
$this
->
undoafter
==
0
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'mcrundo-missingparam'
);
}
$curRev
=
$this
->
getWikiPage
()->
getRevisionRecord
();
if
(
!
$curRev
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'nopagetext'
);
}
$this
->
curRev
=
$curRev
;
$this
->
cur
=
$this
->
getRequest
()->
getInt
(
'cur'
,
$this
->
curRev
->
getId
()
);
}
protected
function
checkCanExecute
(
User
$user
)
{
parent
::
checkCanExecute
(
$user
);
$this
->
initFromParameters
();
// We use getRevisionByTitle to verify the revisions belong to this page (T297322)
$title
=
$this
->
getTitle
();
$undoRev
=
$this
->
revisionLookup
->
getRevisionByTitle
(
$title
,
$this
->
undo
);
$oldRev
=
$this
->
revisionLookup
->
getRevisionByTitle
(
$title
,
$this
->
undoafter
);
if
(
$undoRev
===
null
||
$oldRev
===
null
||
$undoRev
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
||
$oldRev
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-norev'
);
}
return
true
;
}
/**
* @return MutableRevisionRecord
*/
private
function
getNewRevision
()
{
$undoRev
=
$this
->
revisionLookup
->
getRevisionById
(
$this
->
undo
);
$oldRev
=
$this
->
revisionLookup
->
getRevisionById
(
$this
->
undoafter
);
$curRev
=
$this
->
curRev
;
$isLatest
=
$curRev
->
getId
()
===
$undoRev
->
getId
();
if
(
$undoRev
===
null
||
$oldRev
===
null
||
$undoRev
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
||
$oldRev
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-norev'
);
}
if
(
$isLatest
)
{
// Short cut! Undoing the current revision means we just restore the old.
return
MutableRevisionRecord
::
newFromParentRevision
(
$oldRev
);
}
$newRev
=
MutableRevisionRecord
::
newFromParentRevision
(
$curRev
);
// Figure out the roles that need merging by first collecting all roles
// and then removing the ones that don't.
$rolesToMerge
=
array_unique
(
array_merge
(
$oldRev
->
getSlotRoles
(),
$undoRev
->
getSlotRoles
(),
$curRev
->
getSlotRoles
()
)
);
// Any roles with the same content in $oldRev and $undoRev can be
// inherited because undo won't change them.
$rolesToMerge
=
array_intersect
(
$rolesToMerge
,
$oldRev
->
getSlots
()->
getRolesWithDifferentContent
(
$undoRev
->
getSlots
()
)
);
if
(
!
$rolesToMerge
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-nochange'
);
}
// Any roles with the same content in $oldRev and $curRev were already reverted
// and so can be inherited.
$rolesToMerge
=
array_intersect
(
$rolesToMerge
,
$oldRev
->
getSlots
()->
getRolesWithDifferentContent
(
$curRev
->
getSlots
()
)
);
if
(
!
$rolesToMerge
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-nochange'
);
}
// Any roles with the same content in $undoRev and $curRev weren't
// changed since and so can be reverted to $oldRev.
$diffRoles
=
array_intersect
(
$rolesToMerge
,
$undoRev
->
getSlots
()->
getRolesWithDifferentContent
(
$curRev
->
getSlots
()
)
);
foreach
(
array_diff
(
$rolesToMerge
,
$diffRoles
)
as
$role
)
{
if
(
$oldRev
->
hasSlot
(
$role
)
)
{
$newRev
->
inheritSlot
(
$oldRev
->
getSlot
(
$role
,
RevisionRecord
::
RAW
)
);
}
else
{
$newRev
->
removeSlot
(
$role
);
}
}
$rolesToMerge
=
$diffRoles
;
// Any slot additions or removals not handled by the above checks can't be undone.
// There will be only one of the three revisions missing the slot:
// - !old means it was added in the undone revisions and modified after.
// Should it be removed entirely for the undo, or should the modified version be kept?
// - !undo means it was removed in the undone revisions and then readded with different content.
// Which content is should be kept, the old or the new?
// - !cur means it was changed in the undone revisions and then deleted after.
// Did someone delete vandalized content instead of undoing (meaning we should ideally restore
// it), or should it stay gone?
foreach
(
$rolesToMerge
as
$role
)
{
if
(
!
$oldRev
->
hasSlot
(
$role
)
||
!
$undoRev
->
hasSlot
(
$role
)
||
!
$curRev
->
hasSlot
(
$role
)
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-failure'
);
}
}
// Try to merge anything that's left.
foreach
(
$rolesToMerge
as
$role
)
{
$oldContent
=
$oldRev
->
getSlot
(
$role
,
RevisionRecord
::
RAW
)->
getContent
();
$undoContent
=
$undoRev
->
getSlot
(
$role
,
RevisionRecord
::
RAW
)->
getContent
();
$curContent
=
$curRev
->
getSlot
(
$role
,
RevisionRecord
::
RAW
)->
getContent
();
$newContent
=
$undoContent
->
getContentHandler
()
->
getUndoContent
(
$curContent
,
$undoContent
,
$oldContent
,
$isLatest
);
if
(
!
$newContent
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-failure'
);
}
$newRev
->
setSlot
(
SlotRecord
::
newUnsaved
(
$role
,
$newContent
)
);
}
return
$newRev
;
}
private
function
generateDiffOrPreview
()
{
$newRev
=
$this
->
getNewRevision
();
if
(
$newRev
->
hasSameContent
(
$this
->
curRev
)
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'undo-nochange'
);
}
$diffEngine
=
new
DifferenceEngine
(
$this
->
context
);
$diffEngine
->
setRevisions
(
$this
->
curRev
,
$newRev
);
$oldtitle
=
$this
->
context
->
msg
(
'currentrev'
)->
parse
();
$newtitle
=
$this
->
context
->
msg
(
'yourtext'
)->
parse
();
if
(
$this
->
getRequest
()->
getCheck
(
'wpPreview'
)
)
{
$this
->
showPreview
(
$newRev
);
return
''
;
}
else
{
$diffText
=
$diffEngine
->
getDiff
(
$oldtitle
,
$newtitle
);
$diffEngine
->
showDiffStyle
();
return
'<div id="wikiDiff">'
.
$diffText
.
'</div>'
;
}
}
private
function
showPreview
(
RevisionRecord
$rev
)
{
// Mostly copied from EditPage::getPreviewText()
$out
=
$this
->
getOutput
();
try
{
# provide a anchor link to the form
$continueEditing
=
'<span class="mw-continue-editing">'
.
'[[#mw-mcrundo-form|'
.
$this
->
context
->
getLanguage
()->
getArrow
()
.
' '
.
$this
->
context
->
msg
(
'continue-editing'
)->
text
()
.
']]</span>'
;
$note
=
$this
->
context
->
msg
(
'previewnote'
)->
plain
()
.
' '
.
$continueEditing
;
$parserOptions
=
$this
->
getWikiPage
()->
makeParserOptions
(
$this
->
context
);
$parserOptions
->
setRenderReason
(
'page-preview'
);
$parserOptions
->
setIsPreview
(
true
);
$parserOptions
->
setIsSectionPreview
(
false
);
$parserOutput
=
$this
->
revisionRenderer
->
getRenderedRevision
(
$rev
,
$parserOptions
,
$this
->
getAuthority
()
)
->
getRevisionParserOutput
();
// TODO T371004 move runOutputPipeline out of $parserOutput
$previewHTML
=
$parserOutput
->
runOutputPipeline
(
$parserOptions
,
[
'enableSectionEditLinks'
=>
false
,
'includeDebugInfo'
=>
true
,
]
)->
getContentHolderText
();
$out
->
addParserOutputMetadata
(
$parserOutput
);
if
(
count
(
$parserOutput
->
getWarnings
()
)
)
{
$note
.=
"
\n\n
"
.
implode
(
"
\n\n
"
,
$parserOutput
->
getWarnings
()
);
}
}
catch
(
MWContentSerializationException
$ex
)
{
$m
=
$this
->
context
->
msg
(
'content-failed-to-parse'
,
$ex
->
getMessage
()
);
$note
.=
"
\n\n
"
.
$m
->
parse
();
$previewHTML
=
''
;
}
$previewhead
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'previewnote'
],
Html
::
element
(
'h2'
,
[
'id'
=>
'mw-previewheader'
],
$this
->
context
->
msg
(
'preview'
)->
text
()
)
.
Html
::
warningBox
(
$out
->
parseAsInterface
(
$note
)
)
);
$out
->
addHTML
(
$previewhead
.
$previewHTML
);
}
public
function
onSubmit
(
$data
)
{
if
(
!
$this
->
getRequest
()->
getCheck
(
'wpSave'
)
)
{
// Diff or preview
return
false
;
}
$updater
=
$this
->
getWikiPage
()->
newPageUpdater
(
$this
->
context
->
getUser
()
);
$curRev
=
$updater
->
grabParentRevision
();
if
(
!
$curRev
)
{
throw
new
ErrorPageError
(
'mcrundofailed'
,
'nopagetext'
);
}
if
(
$this
->
cur
!==
$curRev
->
getId
()
)
{
return
Status
::
newFatal
(
'mcrundo-changed'
);
}
$status
=
new
PermissionStatus
();
$this
->
getAuthority
()->
authorizeWrite
(
'edit'
,
$this
->
getTitle
(),
$status
);
if
(
!
$status
->
isOK
()
)
{
throw
new
PermissionsError
(
'edit'
,
$status
);
}
$newRev
=
$this
->
getNewRevision
();
if
(
!
$newRev
->
hasSameContent
(
$curRev
)
)
{
$hookRunner
=
$this
->
getHookRunner
();
foreach
(
$newRev
->
getSlotRoles
()
as
$slotRole
)
{
$slot
=
$newRev
->
getSlot
(
$slotRole
,
RevisionRecord
::
RAW
);
$status
=
new
Status
();
$hookResult
=
$hookRunner
->
onEditFilterMergedContent
(
$this
->
getContext
(),
$slot
->
getContent
(),
$status
,
trim
(
$this
->
getRequest
()->
getVal
(
'wpSummary'
)
??
''
),
$this
->
getUser
(),
false
);
if
(
!
$hookResult
)
{
if
(
$status
->
isGood
()
)
{
$status
->
error
(
'hookaborted'
);
}
return
$status
;
}
elseif
(
!
$status
->
isOK
()
)
{
if
(
!
$status
->
getMessages
()
)
{
$status
->
error
(
'hookaborted'
);
}
return
$status
;
}
}
// Copy new slots into the PageUpdater, and remove any removed slots.
// TODO: This interface is awful, there should be a way to just pass $newRev.
// TODO: MCR: test this once we can store multiple slots
foreach
(
$newRev
->
getSlots
()->
getSlots
()
as
$slot
)
{
$updater
->
setSlot
(
$slot
);
}
foreach
(
$curRev
->
getSlotRoles
()
as
$role
)
{
if
(
!
$newRev
->
hasSlot
(
$role
)
)
{
$updater
->
removeSlot
(
$role
);
}
}
$updater
->
markAsRevert
(
EditResult
::
REVERT_UNDO
,
$this
->
undo
,
$this
->
undoafter
);
if
(
$this
->
useRCPatrol
&&
$this
->
getAuthority
()
->
authorizeWrite
(
'autopatrol'
,
$this
->
getTitle
()
)
)
{
$updater
->
setRcPatrolStatus
(
RecentChange
::
PRC_AUTOPATROLLED
);
}
$updater
->
saveRevision
(
CommentStoreComment
::
newUnsavedComment
(
trim
(
$this
->
getRequest
()->
getVal
(
'wpSummary'
)
??
''
)
),
EDIT_AUTOSUMMARY
|
EDIT_UPDATE
);
return
$updater
->
getStatus
();
}
return
Status
::
newGood
();
}
protected
function
usesOOUI
()
{
return
true
;
}
protected
function
getFormFields
()
{
$request
=
$this
->
getRequest
();
$ret
=
[
'diff'
=>
[
'type'
=>
'info'
,
'raw'
=>
true
,
'default'
=>
function
()
{
return
$this
->
generateDiffOrPreview
();
}
],
'summary'
=>
[
'type'
=>
'text'
,
'id'
=>
'wpSummary'
,
'name'
=>
'wpSummary'
,
'cssclass'
=>
'mw-summary'
,
'label-message'
=>
'summary'
,
'maxlength'
=>
CommentStore
::
COMMENT_CHARACTER_LIMIT
,
'value'
=>
$request
->
getVal
(
'wpSummary'
,
''
),
'size'
=>
60
,
'spellcheck'
=>
'true'
,
],
'summarypreview'
=>
[
'type'
=>
'info'
,
'label-message'
=>
'summary-preview'
,
'raw'
=>
true
,
],
];
if
(
$request
->
getCheck
(
'wpSummary'
)
)
{
$ret
[
'summarypreview'
][
'default'
]
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'mw-summary-preview'
],
$this
->
commentFormatter
->
formatBlock
(
trim
(
$request
->
getVal
(
'wpSummary'
)
),
$this
->
getTitle
(),
false
)
);
}
else
{
unset
(
$ret
[
'summarypreview'
]
);
}
return
$ret
;
}
protected
function
alterForm
(
HTMLForm
$form
)
{
$form
->
setWrapperLegendMsg
(
'confirm-mcrundo-title'
);
$labelAsPublish
=
$this
->
context
->
getConfig
()->
get
(
MainConfigNames
::
EditSubmitButtonLabelPublish
);
$form
->
setId
(
'mw-mcrundo-form'
);
$form
->
setSubmitName
(
'wpSave'
);
$form
->
setSubmitTooltip
(
$labelAsPublish
?
'publish'
:
'save'
);
$form
->
setSubmitTextMsg
(
$labelAsPublish
?
'publishchanges'
:
'savechanges'
);
$form
->
showCancel
(
true
);
$form
->
setCancelTarget
(
$this
->
getTitle
()
);
$form
->
addButton
(
[
'name'
=>
'wpPreview'
,
'value'
=>
'1'
,
'label-message'
=>
'showpreview'
,
'attribs'
=>
Linker
::
tooltipAndAccesskeyAttribs
(
'preview'
),
]
);
$form
->
addButton
(
[
'name'
=>
'wpDiff'
,
'value'
=>
'1'
,
'label-message'
=>
'showdiff'
,
'attribs'
=>
Linker
::
tooltipAndAccesskeyAttribs
(
'diff'
),
]
);
$this
->
addStatePropagationFields
(
$form
);
}
protected
function
addStatePropagationFields
(
HTMLForm
$form
)
{
$form
->
addHiddenField
(
'undo'
,
$this
->
undo
);
$form
->
addHiddenField
(
'undoafter'
,
$this
->
undoafter
);
$form
->
addHiddenField
(
'cur'
,
$this
->
curRev
->
getId
()
);
}
public
function
onSuccess
()
{
$this
->
getOutput
()->
redirect
(
$this
->
getTitle
()->
getFullURL
()
);
}
protected
function
preText
()
{
return
'<div style="clear:both"></div>'
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 10:23 (2 w, 6 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d6/59/69377e6f87cad3d450e05601c796
Default Alt Text
McrUndoAction.php (16 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment