Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4125459
SpecialUndelete.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
50 KB
Referenced Files
None
Subscribers
None
SpecialUndelete.php
View Options
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace
MediaWiki\Specials
;
use
ArchivedFile
;
use
ChangesList
;
use
ChangeTags
;
use
ErrorPageError
;
use
File
;
use
LocalRepo
;
use
LogEventsList
;
use
LogPage
;
use
MediaWiki\Cache\LinkBatch
;
use
MediaWiki\Cache\LinkBatchFactory
;
use
MediaWiki\CommentFormatter\CommentFormatter
;
use
MediaWiki\CommentStore\CommentStore
;
use
MediaWiki\Content\IContentHandlerFactory
;
use
MediaWiki\Content\TextContent
;
use
MediaWiki\Context\DerivativeContext
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Linker\Linker
;
use
MediaWiki\Linker\LinkTarget
;
use
MediaWiki\MainConfigNames
;
use
MediaWiki\Message\Message
;
use
MediaWiki\Page\UndeletePage
;
use
MediaWiki\Page\UndeletePageFactory
;
use
MediaWiki\Page\WikiPageFactory
;
use
MediaWiki\Permissions\PermissionManager
;
use
MediaWiki\Revision\ArchivedRevisionLookup
;
use
MediaWiki\Revision\RevisionAccessException
;
use
MediaWiki\Revision\RevisionArchiveRecord
;
use
MediaWiki\Revision\RevisionRecord
;
use
MediaWiki\Revision\RevisionRenderer
;
use
MediaWiki\Revision\RevisionStore
;
use
MediaWiki\Revision\SlotRecord
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Status\Status
;
use
MediaWiki\Storage\NameTableAccessException
;
use
MediaWiki\Storage\NameTableStore
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\Options\UserOptionsLookup
;
use
MediaWiki\User\User
;
use
MediaWiki\Watchlist\WatchlistManager
;
use
MediaWiki\Xml\Xml
;
use
OOUI\ActionFieldLayout
;
use
OOUI\ButtonInputWidget
;
use
OOUI\CheckboxInputWidget
;
use
OOUI\DropdownInputWidget
;
use
OOUI\FieldLayout
;
use
OOUI\FieldsetLayout
;
use
OOUI\FormLayout
;
use
OOUI\HorizontalLayout
;
use
OOUI\HtmlSnippet
;
use
OOUI\Layout
;
use
OOUI\PanelLayout
;
use
OOUI\TextInputWidget
;
use
OOUI\Widget
;
use
PageArchive
;
use
PermissionsError
;
use
RepoGroup
;
use
SearchEngineFactory
;
use
UserBlockedError
;
use
Wikimedia\Rdbms\IConnectionProvider
;
use
Wikimedia\Rdbms\IDBAccessObject
;
use
Wikimedia\Rdbms\IResultWrapper
;
/**
* Special page allowing users with the appropriate permissions to view
* and restore deleted content.
*
* @ingroup SpecialPage
*/
class
SpecialUndelete
extends
SpecialPage
{
/**
* Limit of revisions (Page history) to display.
* (If there are more items to display - "Load more" button will appear).
*/
private
const
REVISION_HISTORY_LIMIT
=
500
;
/** @var string|null */
private
$mAction
;
/** @var string */
private
$mTarget
;
/** @var string */
private
$mTimestamp
;
/** @var bool */
private
$mRestore
;
/** @var bool */
private
$mRevdel
;
/** @var bool */
private
$mInvert
;
/** @var string */
private
$mFilename
;
/** @var string[] */
private
$mTargetTimestamp
=
[];
/** @var bool */
private
$mAllowed
;
/** @var bool */
private
$mCanView
;
/** @var string */
private
$mComment
=
''
;
/** @var string */
private
$mToken
;
/** @var bool|null */
private
$mPreview
;
/** @var bool|null */
private
$mDiff
;
/** @var bool|null */
private
$mDiffOnly
;
/** @var bool|null */
private
$mUnsuppress
;
/** @var int[] */
private
$mFileVersions
=
[];
/** @var bool|null */
private
$mUndeleteTalk
;
/** @var string|null Timestamp at which to start a "load more" request (open interval) */
private
$mHistoryOffset
;
/** @var Title|null */
private
$mTargetObj
;
/**
* @var string Search prefix
*/
private
$mSearchPrefix
;
private
PermissionManager
$permissionManager
;
private
RevisionStore
$revisionStore
;
private
RevisionRenderer
$revisionRenderer
;
private
IContentHandlerFactory
$contentHandlerFactory
;
private
NameTableStore
$changeTagDefStore
;
private
LinkBatchFactory
$linkBatchFactory
;
private
LocalRepo
$localRepo
;
private
IConnectionProvider
$dbProvider
;
private
UserOptionsLookup
$userOptionsLookup
;
private
WikiPageFactory
$wikiPageFactory
;
private
SearchEngineFactory
$searchEngineFactory
;
private
UndeletePageFactory
$undeletePageFactory
;
private
ArchivedRevisionLookup
$archivedRevisionLookup
;
private
CommentFormatter
$commentFormatter
;
private
WatchlistManager
$watchlistManager
;
/**
* @param PermissionManager $permissionManager
* @param RevisionStore $revisionStore
* @param RevisionRenderer $revisionRenderer
* @param IContentHandlerFactory $contentHandlerFactory
* @param NameTableStore $changeTagDefStore
* @param LinkBatchFactory $linkBatchFactory
* @param RepoGroup $repoGroup
* @param IConnectionProvider $dbProvider
* @param UserOptionsLookup $userOptionsLookup
* @param WikiPageFactory $wikiPageFactory
* @param SearchEngineFactory $searchEngineFactory
* @param UndeletePageFactory $undeletePageFactory
* @param ArchivedRevisionLookup $archivedRevisionLookup
* @param CommentFormatter $commentFormatter
* @param WatchlistManager $watchlistManager
*/
public
function
__construct
(
PermissionManager
$permissionManager
,
RevisionStore
$revisionStore
,
RevisionRenderer
$revisionRenderer
,
IContentHandlerFactory
$contentHandlerFactory
,
NameTableStore
$changeTagDefStore
,
LinkBatchFactory
$linkBatchFactory
,
RepoGroup
$repoGroup
,
IConnectionProvider
$dbProvider
,
UserOptionsLookup
$userOptionsLookup
,
WikiPageFactory
$wikiPageFactory
,
SearchEngineFactory
$searchEngineFactory
,
UndeletePageFactory
$undeletePageFactory
,
ArchivedRevisionLookup
$archivedRevisionLookup
,
CommentFormatter
$commentFormatter
,
WatchlistManager
$watchlistManager
)
{
parent
::
__construct
(
'Undelete'
,
'deletedhistory'
);
$this
->
permissionManager
=
$permissionManager
;
$this
->
revisionStore
=
$revisionStore
;
$this
->
revisionRenderer
=
$revisionRenderer
;
$this
->
contentHandlerFactory
=
$contentHandlerFactory
;
$this
->
changeTagDefStore
=
$changeTagDefStore
;
$this
->
linkBatchFactory
=
$linkBatchFactory
;
$this
->
localRepo
=
$repoGroup
->
getLocalRepo
();
$this
->
dbProvider
=
$dbProvider
;
$this
->
userOptionsLookup
=
$userOptionsLookup
;
$this
->
wikiPageFactory
=
$wikiPageFactory
;
$this
->
searchEngineFactory
=
$searchEngineFactory
;
$this
->
undeletePageFactory
=
$undeletePageFactory
;
$this
->
archivedRevisionLookup
=
$archivedRevisionLookup
;
$this
->
commentFormatter
=
$commentFormatter
;
$this
->
watchlistManager
=
$watchlistManager
;
}
public
function
doesWrites
()
{
return
true
;
}
private
function
loadRequest
(
$par
)
{
$request
=
$this
->
getRequest
();
$user
=
$this
->
getUser
();
$this
->
mAction
=
$request
->
getRawVal
(
'action'
);
if
(
$par
!==
null
&&
$par
!==
''
)
{
$this
->
mTarget
=
$par
;
}
else
{
$this
->
mTarget
=
$request
->
getVal
(
'target'
);
}
$this
->
mTargetObj
=
null
;
if
(
$this
->
mTarget
!==
null
&&
$this
->
mTarget
!==
''
)
{
$this
->
mTargetObj
=
Title
::
newFromText
(
$this
->
mTarget
);
}
$this
->
mSearchPrefix
=
$request
->
getText
(
'prefix'
);
$time
=
$request
->
getVal
(
'timestamp'
);
$this
->
mTimestamp
=
$time
?
wfTimestamp
(
TS_MW
,
$time
)
:
''
;
$this
->
mFilename
=
$request
->
getVal
(
'file'
);
$posted
=
$request
->
wasPosted
()
&&
$user
->
matchEditToken
(
$request
->
getVal
(
'wpEditToken'
)
);
$this
->
mRestore
=
$request
->
getCheck
(
'restore'
)
&&
$posted
;
$this
->
mRevdel
=
$request
->
getCheck
(
'revdel'
)
&&
$posted
;
$this
->
mInvert
=
$request
->
getCheck
(
'invert'
)
&&
$posted
;
$this
->
mPreview
=
$request
->
getCheck
(
'preview'
)
&&
$posted
;
$this
->
mDiff
=
$request
->
getCheck
(
'diff'
);
$this
->
mDiffOnly
=
$request
->
getBool
(
'diffonly'
,
$this
->
userOptionsLookup
->
getOption
(
$this
->
getUser
(),
'diffonly'
)
);
$commentList
=
$request
->
getText
(
'wpCommentList'
,
'other'
);
$comment
=
$request
->
getText
(
'wpComment'
);
if
(
$commentList
===
'other'
)
{
$this
->
mComment
=
$comment
;
}
elseif
(
$comment
!==
''
)
{
$this
->
mComment
=
$commentList
.
$this
->
msg
(
'colon-separator'
)->
inContentLanguage
()->
text
()
.
$comment
;
}
else
{
$this
->
mComment
=
$commentList
;
}
$this
->
mUnsuppress
=
$request
->
getVal
(
'wpUnsuppress'
)
&&
$this
->
permissionManager
->
userHasRight
(
$user
,
'suppressrevision'
);
$this
->
mToken
=
$request
->
getVal
(
'token'
);
$this
->
mUndeleteTalk
=
$request
->
getCheck
(
'undeletetalk'
);
$this
->
mHistoryOffset
=
$request
->
getVal
(
'historyoffset'
);
if
(
$this
->
isAllowed
(
'undelete'
)
)
{
$this
->
mAllowed
=
true
;
// user can restore
$this
->
mCanView
=
true
;
// user can view content
}
elseif
(
$this
->
isAllowed
(
'deletedtext'
)
)
{
$this
->
mAllowed
=
false
;
// user cannot restore
$this
->
mCanView
=
true
;
// user can view content
$this
->
mRestore
=
false
;
}
else
{
// user can only view the list of revisions
$this
->
mAllowed
=
false
;
$this
->
mCanView
=
false
;
$this
->
mTimestamp
=
''
;
$this
->
mRestore
=
false
;
}
if
(
$this
->
mRestore
||
$this
->
mInvert
)
{
$timestamps
=
[];
$this
->
mFileVersions
=
[];
foreach
(
$request
->
getValues
()
as
$key
=>
$val
)
{
$matches
=
[];
if
(
preg_match
(
'/^ts(
\d
{14})$/'
,
$key
,
$matches
)
)
{
$timestamps
[]
=
$matches
[
1
];
}
if
(
preg_match
(
'/^fileid(
\d
+)$/'
,
$key
,
$matches
)
)
{
$this
->
mFileVersions
[]
=
intval
(
$matches
[
1
]
);
}
}
rsort
(
$timestamps
);
$this
->
mTargetTimestamp
=
$timestamps
;
}
}
/**
* Checks whether a user is allowed the permission for the
* specific title if one is set.
*
* @param string $permission
* @param User|null $user
* @return bool
*/
protected
function
isAllowed
(
$permission
,
?
User
$user
=
null
)
{
$user
??=
$this
->
getUser
();
$block
=
$user
->
getBlock
();
if
(
$this
->
mTargetObj
!==
null
)
{
return
$this
->
permissionManager
->
userCan
(
$permission
,
$user
,
$this
->
mTargetObj
);
}
else
{
$hasRight
=
$this
->
permissionManager
->
userHasRight
(
$user
,
$permission
);
$sitewideBlock
=
$block
&&
$block
->
isSitewide
();
return
$permission
===
'undelete'
?
(
$hasRight
&&
!
$sitewideBlock
)
:
$hasRight
;
}
}
public
function
userCanExecute
(
User
$user
)
{
return
$this
->
isAllowed
(
$this
->
mRestriction
,
$user
);
}
/**
* @inheritDoc
*/
public
function
checkPermissions
()
{
$user
=
$this
->
getUser
();
// First check if user has the right to use this page. If not,
// show a permissions error whether they are blocked or not.
if
(
!
parent
::
userCanExecute
(
$user
)
)
{
$this
->
displayRestrictionError
();
}
// If a user has the right to use this page, but is blocked from
// the target, show a block error.
if
(
$this
->
mTargetObj
&&
$this
->
permissionManager
->
isBlockedFrom
(
$user
,
$this
->
mTargetObj
)
)
{
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
throw
new
UserBlockedError
(
$user
->
getBlock
()
);
}
// Finally, do the comprehensive permission check via isAllowed.
if
(
!
$this
->
userCanExecute
(
$user
)
)
{
$this
->
displayRestrictionError
();
}
}
public
function
execute
(
$par
)
{
$this
->
useTransactionalTimeLimit
();
$user
=
$this
->
getUser
();
$this
->
setHeaders
();
$this
->
outputHeader
();
$this
->
addHelpLink
(
'Help:Deletion_and_undeletion'
);
$this
->
loadRequest
(
$par
);
$this
->
checkPermissions
();
// Needs to be after mTargetObj is set
$out
=
$this
->
getOutput
();
if
(
$this
->
mTargetObj
===
null
)
{
$out
->
addWikiMsg
(
'undelete-header'
);
# Not all users can just browse every deleted page from the list
if
(
$this
->
permissionManager
->
userHasRight
(
$user
,
'browsearchive'
)
)
{
$this
->
showSearchForm
();
}
return
;
}
$this
->
addHelpLink
(
'Help:Undelete'
);
if
(
$this
->
mAllowed
)
{
$out
->
setPageTitleMsg
(
$this
->
msg
(
'undeletepage'
)
);
}
else
{
$out
->
setPageTitleMsg
(
$this
->
msg
(
'viewdeletedpage'
)
);
}
$this
->
getSkin
()->
setRelevantTitle
(
$this
->
mTargetObj
);
if
(
$this
->
mTimestamp
!==
''
)
{
$this
->
showRevision
(
$this
->
mTimestamp
);
}
elseif
(
$this
->
mFilename
!==
null
&&
$this
->
mTargetObj
->
inNamespace
(
NS_FILE
)
)
{
$file
=
new
ArchivedFile
(
$this
->
mTargetObj
,
0
,
$this
->
mFilename
);
// Check if user is allowed to see this file
if
(
!
$file
->
exists
()
)
{
$out
->
addWikiMsg
(
'filedelete-nofile'
,
$this
->
mFilename
);
}
elseif
(
!
$file
->
userCan
(
File
::
DELETED_FILE
,
$user
)
)
{
if
(
$file
->
isDeleted
(
File
::
DELETED_RESTRICTED
)
)
{
throw
new
PermissionsError
(
'suppressrevision'
);
}
else
{
throw
new
PermissionsError
(
'deletedtext'
);
}
}
elseif
(
!
$user
->
matchEditToken
(
$this
->
mToken
,
$this
->
mFilename
)
)
{
$this
->
showFileConfirmationForm
(
$this
->
mFilename
);
}
else
{
$this
->
showFile
(
$this
->
mFilename
);
}
}
elseif
(
$this
->
mAction
===
'submit'
)
{
if
(
$this
->
mRestore
)
{
$this
->
undelete
();
}
elseif
(
$this
->
mRevdel
)
{
$this
->
redirectToRevDel
();
}
}
elseif
(
$this
->
mAction
===
'render'
)
{
$this
->
showMoreHistory
();
}
else
{
$this
->
showHistory
();
}
}
/**
* Convert submitted form data to format expected by RevisionDelete and
* redirect the request
*/
private
function
redirectToRevDel
()
{
$revisions
=
[];
foreach
(
$this
->
getRequest
()->
getValues
()
as
$key
=>
$val
)
{
$matches
=
[];
if
(
preg_match
(
"/^ts(
\d
{14})$/"
,
$key
,
$matches
)
)
{
$revisionRecord
=
$this
->
archivedRevisionLookup
->
getRevisionRecordByTimestamp
(
$this
->
mTargetObj
,
$matches
[
1
]
);
if
(
$revisionRecord
)
{
// Can return null
$revisions
[
$revisionRecord
->
getId
()
]
=
1
;
}
}
}
$query
=
[
'type'
=>
'revision'
,
'ids'
=>
$revisions
,
'target'
=>
$this
->
mTargetObj
->
getPrefixedText
()
];
$url
=
SpecialPage
::
getTitleFor
(
'Revisiondelete'
)->
getFullURL
(
$query
);
$this
->
getOutput
()->
redirect
(
$url
);
}
private
function
showSearchForm
()
{
$out
=
$this
->
getOutput
();
$out
->
setPageTitleMsg
(
$this
->
msg
(
'undelete-search-title'
)
);
$fuzzySearch
=
$this
->
getRequest
()->
getVal
(
'fuzzy'
,
'1'
);
$out
->
enableOOUI
();
$fields
=
[];
$fields
[]
=
new
ActionFieldLayout
(
new
TextInputWidget
(
[
'name'
=>
'prefix'
,
'inputId'
=>
'prefix'
,
'infusable'
=>
true
,
'value'
=>
$this
->
mSearchPrefix
,
'autofocus'
=>
true
,
]
),
new
ButtonInputWidget
(
[
'label'
=>
$this
->
msg
(
'undelete-search-submit'
)->
text
(),
'flags'
=>
[
'primary'
,
'progressive'
],
'inputId'
=>
'searchUndelete'
,
'type'
=>
'submit'
,
]
),
[
'label'
=>
new
HtmlSnippet
(
$this
->
msg
(
$fuzzySearch
?
'undelete-search-full'
:
'undelete-search-prefix'
)->
parse
()
),
'align'
=>
'left'
,
]
);
$fieldset
=
new
FieldsetLayout
(
[
'label'
=>
$this
->
msg
(
'undelete-search-box'
)->
text
(),
'items'
=>
$fields
,
]
);
$form
=
new
FormLayout
(
[
'method'
=>
'get'
,
'action'
=>
wfScript
(),
]
);
$form
->
appendContent
(
$fieldset
,
new
HtmlSnippet
(
Html
::
hidden
(
'title'
,
$this
->
getPageTitle
()->
getPrefixedDBkey
()
)
.
Html
::
hidden
(
'fuzzy'
,
$fuzzySearch
)
)
);
$out
->
addHTML
(
new
PanelLayout
(
[
'expanded'
=>
false
,
'padded'
=>
true
,
'framed'
=>
true
,
'content'
=>
$form
,
]
)
);
# List undeletable articles
if
(
$this
->
mSearchPrefix
)
{
// For now, we enable search engine match only when specifically asked to
// by using fuzzy=1 parameter.
if
(
$fuzzySearch
)
{
$result
=
PageArchive
::
listPagesBySearch
(
$this
->
mSearchPrefix
);
}
else
{
$result
=
PageArchive
::
listPagesByPrefix
(
$this
->
mSearchPrefix
);
}
$this
->
showList
(
$result
);
}
}
/**
* Generic list of deleted pages
*
* @param IResultWrapper $result
* @return bool
*/
private
function
showList
(
$result
)
{
$out
=
$this
->
getOutput
();
if
(
$result
->
numRows
()
==
0
)
{
$out
->
addWikiMsg
(
'undelete-no-results'
);
return
false
;
}
$out
->
addWikiMsg
(
'undeletepagetext'
,
$this
->
getLanguage
()->
formatNum
(
$result
->
numRows
()
)
);
$linkRenderer
=
$this
->
getLinkRenderer
();
$undelete
=
$this
->
getPageTitle
();
$out
->
addHTML
(
"<ul id='undeleteResultsList'>
\n
"
);
foreach
(
$result
as
$row
)
{
$title
=
Title
::
makeTitleSafe
(
$row
->
ar_namespace
,
$row
->
ar_title
);
if
(
$title
!==
null
)
{
$item
=
$linkRenderer
->
makeKnownLink
(
$undelete
,
$title
->
getPrefixedText
(),
[],
[
'target'
=>
$title
->
getPrefixedText
()
]
);
}
else
{
// The title is no longer valid, show as text
$item
=
Html
::
element
(
'span'
,
[
'class'
=>
'mw-invalidtitle'
],
Linker
::
getInvalidTitleDescription
(
$this
->
getContext
(),
$row
->
ar_namespace
,
$row
->
ar_title
)
);
}
$revs
=
$this
->
msg
(
'undeleterevisions'
)->
numParams
(
$row
->
count
)->
parse
();
$out
->
addHTML
(
Html
::
rawElement
(
'li'
,
[
'class'
=>
'undeleteResult'
],
$item
.
$this
->
msg
(
'word-separator'
)->
escaped
()
.
$this
->
msg
(
'parentheses'
)->
rawParams
(
$revs
)->
escaped
()
)
);
}
$result
->
free
();
$out
->
addHTML
(
"</ul>
\n
"
);
return
true
;
}
private
function
showRevision
(
$timestamp
)
{
if
(
!
preg_match
(
'/[0-9]{14}/'
,
$timestamp
)
)
{
return
;
}
$out
=
$this
->
getOutput
();
$out
->
addModuleStyles
(
'mediawiki.interface.helpers.styles'
);
// When viewing a specific revision, add a subtitle link back to the overall
// history, see T284114
$listLink
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$this
->
getPageTitle
(),
$this
->
msg
(
'undelete-back-to-list'
)->
text
(),
[],
[
'target'
=>
$this
->
mTargetObj
->
getPrefixedText
()
]
);
// same < arrow as with subpages
$subtitle
=
"< $listLink"
;
$out
->
setSubtitle
(
$subtitle
);
$archive
=
new
PageArchive
(
$this
->
mTargetObj
);
// FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
if
(
!
$this
->
getHookRunner
()->
onUndeleteForm__showRevision
(
$archive
,
$this
->
mTargetObj
)
)
{
return
;
}
$revRecord
=
$this
->
archivedRevisionLookup
->
getRevisionRecordByTimestamp
(
$this
->
mTargetObj
,
$timestamp
);
$user
=
$this
->
getUser
();
if
(
!
$revRecord
)
{
$out
->
addWikiMsg
(
'undeleterevision-missing'
);
return
;
}
if
(
$revRecord
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
)
{
// Used in wikilinks, should not contain whitespaces
$titleText
=
$this
->
mTargetObj
->
getPrefixedDBkey
();
if
(
!
$revRecord
->
userCan
(
RevisionRecord
::
DELETED_TEXT
,
$this
->
getAuthority
()
)
)
{
$msg
=
$revRecord
->
isDeleted
(
RevisionRecord
::
DELETED_RESTRICTED
)
?
[
'rev-suppressed-text-permission'
,
$titleText
]
:
[
'rev-deleted-text-permission'
,
$titleText
];
$out
->
addHTML
(
Html
::
warningBox
(
$this
->
msg
(
$msg
[
0
],
$msg
[
1
]
)->
parse
(),
'plainlinks'
)
);
return
;
}
$msg
=
$revRecord
->
isDeleted
(
RevisionRecord
::
DELETED_RESTRICTED
)
?
[
'rev-suppressed-text-view'
,
$titleText
]
:
[
'rev-deleted-text-view'
,
$titleText
];
$out
->
addHTML
(
Html
::
warningBox
(
$this
->
msg
(
$msg
[
0
],
$msg
[
1
]
)->
parse
(),
'plainlinks'
)
);
// and we are allowed to see...
}
if
(
$this
->
mDiff
)
{
$previousRevRecord
=
$this
->
archivedRevisionLookup
->
getPreviousRevisionRecord
(
$this
->
mTargetObj
,
$timestamp
);
if
(
$previousRevRecord
)
{
$this
->
showDiff
(
$previousRevRecord
,
$revRecord
);
if
(
$this
->
mDiffOnly
)
{
return
;
}
$out
->
addHTML
(
'<hr />'
);
}
else
{
$out
->
addWikiMsg
(
'undelete-nodiff'
);
}
}
$link
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$this
->
getPageTitle
(
$this
->
mTargetObj
->
getPrefixedDBkey
()
),
$this
->
mTargetObj
->
getPrefixedText
()
);
$lang
=
$this
->
getLanguage
();
// date and time are separate parameters to facilitate localisation.
// $time is kept for backward compat reasons.
$time
=
$lang
->
userTimeAndDate
(
$timestamp
,
$user
);
$d
=
$lang
->
userDate
(
$timestamp
,
$user
);
$t
=
$lang
->
userTime
(
$timestamp
,
$user
);
$userLink
=
Linker
::
revUserTools
(
$revRecord
);
try
{
$content
=
$revRecord
->
getContent
(
SlotRecord
::
MAIN
,
RevisionRecord
::
FOR_THIS_USER
,
$user
);
}
catch
(
RevisionAccessException
$e
)
{
$content
=
null
;
}
// TODO: MCR: this will have to become something like $hasTextSlots and $hasNonTextSlots
$isText
=
(
$content
instanceof
TextContent
);
$undeleteRevisionContent
=
''
;
// Revision delete links
if
(
!
$this
->
mDiff
)
{
$revdel
=
Linker
::
getRevDeleteLink
(
$user
,
$revRecord
,
$this
->
mTargetObj
);
if
(
$revdel
)
{
$undeleteRevisionContent
=
$revdel
.
' '
;
}
}
$undeleteRevisionContent
.=
$out
->
msg
(
'undelete-revision'
,
Message
::
rawParam
(
$link
),
$time
,
Message
::
rawParam
(
$userLink
),
$d
,
$t
)->
parseAsBlock
();
if
(
$this
->
mPreview
||
$isText
)
{
$out
->
addHTML
(
Html
::
warningBox
(
$undeleteRevisionContent
,
'mw-undelete-revision'
)
);
}
else
{
$out
->
addHTML
(
Html
::
rawElement
(
'div'
,
[
'class'
=>
'mw-undelete-revision'
,
],
$undeleteRevisionContent
)
);
}
if
(
$this
->
mPreview
||
!
$isText
)
{
// NOTE: non-text content has no source view, so always use rendered preview
$popts
=
$out
->
parserOptions
();
try
{
$rendered
=
$this
->
revisionRenderer
->
getRenderedRevision
(
$revRecord
,
$popts
,
$user
,
[
'audience'
=>
RevisionRecord
::
FOR_THIS_USER
,
'causeAction'
=>
'undelete-preview'
]
);
// Fail hard if the audience check fails, since we already checked
// at the beginning of this method.
$pout
=
$rendered
->
getRevisionParserOutput
();
$out
->
addParserOutput
(
$pout
,
[
'enableSectionEditLinks'
=>
false
,
]
);
}
catch
(
RevisionAccessException
$e
)
{
}
}
$out
->
enableOOUI
();
$buttonFields
=
[];
if
(
$isText
)
{
'@phan-var TextContent $content'
;
// TODO: MCR: make this work for multiple slots
// source view for textual content
$sourceView
=
Xml
::
element
(
'textarea'
,
[
'readonly'
=>
'readonly'
,
'cols'
=>
80
,
'rows'
=>
25
],
$content
->
getText
()
.
"
\n
"
);
$buttonFields
[]
=
new
ButtonInputWidget
(
[
'type'
=>
'submit'
,
'name'
=>
'preview'
,
'label'
=>
$this
->
msg
(
'showpreview'
)->
text
()
]
);
}
else
{
$sourceView
=
''
;
}
$buttonFields
[]
=
new
ButtonInputWidget
(
[
'name'
=>
'diff'
,
'type'
=>
'submit'
,
'label'
=>
$this
->
msg
(
'showdiff'
)->
text
()
]
);
$out
->
addHTML
(
$sourceView
.
Xml
::
openElement
(
'div'
,
[
'style'
=>
'clear: both'
]
)
.
Xml
::
openElement
(
'form'
,
[
'method'
=>
'post'
,
'action'
=>
$this
->
getPageTitle
()->
getLocalURL
(
[
'action'
=>
'submit'
]
)
]
)
.
Xml
::
element
(
'input'
,
[
'type'
=>
'hidden'
,
'name'
=>
'target'
,
'value'
=>
$this
->
mTargetObj
->
getPrefixedDBkey
()
]
)
.
Xml
::
element
(
'input'
,
[
'type'
=>
'hidden'
,
'name'
=>
'timestamp'
,
'value'
=>
$timestamp
]
)
.
Xml
::
element
(
'input'
,
[
'type'
=>
'hidden'
,
'name'
=>
'wpEditToken'
,
'value'
=>
$user
->
getEditToken
()
]
)
.
new
FieldLayout
(
new
Widget
(
[
'content'
=>
new
HorizontalLayout
(
[
'items'
=>
$buttonFields
]
)
]
)
)
.
Xml
::
closeElement
(
'form'
)
.
Xml
::
closeElement
(
'div'
)
);
}
/**
* Build a diff display between this and the previous either deleted
* or non-deleted edit.
*
* @param RevisionRecord $previousRevRecord
* @param RevisionRecord $currentRevRecord
*/
private
function
showDiff
(
RevisionRecord
$previousRevRecord
,
RevisionRecord
$currentRevRecord
)
{
$currentTitle
=
Title
::
newFromLinkTarget
(
$currentRevRecord
->
getPageAsLinkTarget
()
);
$diffContext
=
new
DerivativeContext
(
$this
->
getContext
()
);
$diffContext
->
setTitle
(
$currentTitle
);
$diffContext
->
setWikiPage
(
$this
->
wikiPageFactory
->
newFromTitle
(
$currentTitle
)
);
$contentModel
=
$currentRevRecord
->
getSlot
(
SlotRecord
::
MAIN
,
RevisionRecord
::
RAW
)->
getModel
();
$diffEngine
=
$this
->
contentHandlerFactory
->
getContentHandler
(
$contentModel
)
->
createDifferenceEngine
(
$diffContext
);
$diffEngine
->
setRevisions
(
$previousRevRecord
,
$currentRevRecord
);
$diffEngine
->
showDiffStyle
();
$formattedDiff
=
$diffEngine
->
getDiff
(
$this
->
diffHeader
(
$previousRevRecord
,
'o'
),
$this
->
diffHeader
(
$currentRevRecord
,
'n'
)
);
$this
->
getOutput
()->
addHTML
(
"<div>$formattedDiff</div>
\n
"
);
}
/**
* @param RevisionRecord $revRecord
* @param string $prefix
* @return string
*/
private
function
diffHeader
(
RevisionRecord
$revRecord
,
$prefix
)
{
if
(
$revRecord
instanceof
RevisionArchiveRecord
)
{
// Revision in the archive table, only viewable via this special page
$targetPage
=
$this
->
getPageTitle
();
$targetQuery
=
[
'target'
=>
$this
->
mTargetObj
->
getPrefixedText
(),
'timestamp'
=>
wfTimestamp
(
TS_MW
,
$revRecord
->
getTimestamp
()
)
];
}
else
{
// Revision in the revision table, viewable by oldid
$targetPage
=
$revRecord
->
getPageAsLinkTarget
();
$targetQuery
=
[
'oldid'
=>
$revRecord
->
getId
()
];
}
// Add show/hide deletion links if available
$user
=
$this
->
getUser
();
$lang
=
$this
->
getLanguage
();
$rdel
=
Linker
::
getRevDeleteLink
(
$user
,
$revRecord
,
$this
->
mTargetObj
);
if
(
$rdel
)
{
$rdel
=
" $rdel"
;
}
$minor
=
$revRecord
->
isMinor
()
?
ChangesList
::
flag
(
'minor'
)
:
''
;
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
$tagIds
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'ct_tag_id'
)
->
from
(
'change_tag'
)
->
where
(
[
'ct_rev_id'
=>
$revRecord
->
getId
()
]
)
->
caller
(
__METHOD__
)->
fetchFieldValues
();
$tags
=
[];
foreach
(
$tagIds
as
$tagId
)
{
try
{
$tags
[]
=
$this
->
changeTagDefStore
->
getName
(
(
int
)
$tagId
);
}
catch
(
NameTableAccessException
$exception
)
{
continue
;
}
}
$tags
=
implode
(
','
,
$tags
);
$tagSummary
=
ChangeTags
::
formatSummaryRow
(
$tags
,
'deleteddiff'
,
$this
->
getContext
()
);
$asof
=
$this
->
getLinkRenderer
()->
makeLink
(
$targetPage
,
$this
->
msg
(
'revisionasof'
,
$lang
->
userTimeAndDate
(
$revRecord
->
getTimestamp
(),
$user
),
$lang
->
userDate
(
$revRecord
->
getTimestamp
(),
$user
),
$lang
->
userTime
(
$revRecord
->
getTimestamp
(),
$user
)
)->
text
(),
[],
$targetQuery
);
if
(
$revRecord
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
)
{
$asof
=
Html
::
rawElement
(
'span'
,
[
'class'
=>
Linker
::
getRevisionDeletedClass
(
$revRecord
)
],
$asof
);
}
// FIXME This is reimplementing DifferenceEngine#getRevisionHeader
// and partially #showDiffPage, but worse
return
'<div id="mw-diff-'
.
$prefix
.
'title1"><strong>'
.
$asof
.
'</strong></div>'
.
'<div id="mw-diff-'
.
$prefix
.
'title2">'
.
Linker
::
revUserTools
(
$revRecord
)
.
'<br />'
.
'</div>'
.
'<div id="mw-diff-'
.
$prefix
.
'title3">'
.
$minor
.
$this
->
commentFormatter
->
formatRevision
(
$revRecord
,
$user
)
.
$rdel
.
'<br />'
.
'</div>'
.
'<div id="mw-diff-'
.
$prefix
.
'title5">'
.
$tagSummary
[
0
]
.
'<br />'
.
'</div>'
;
}
/**
* Show a form confirming whether a tokenless user really wants to see a file
* @param string $key
*/
private
function
showFileConfirmationForm
(
$key
)
{
$out
=
$this
->
getOutput
();
$lang
=
$this
->
getLanguage
();
$user
=
$this
->
getUser
();
$file
=
new
ArchivedFile
(
$this
->
mTargetObj
,
0
,
$this
->
mFilename
);
$out
->
addWikiMsg
(
'undelete-show-file-confirm'
,
$this
->
mTargetObj
->
getText
(),
$lang
->
userDate
(
$file
->
getTimestamp
(),
$user
),
$lang
->
userTime
(
$file
->
getTimestamp
(),
$user
)
);
$out
->
addHTML
(
Html
::
rawElement
(
'form'
,
[
'method'
=>
'POST'
,
'action'
=>
$this
->
getPageTitle
()->
getLocalURL
(
[
'target'
=>
$this
->
mTarget
,
'file'
=>
$key
,
'token'
=>
$user
->
getEditToken
(
$key
),
]
),
],
Xml
::
submitButton
(
$this
->
msg
(
'undelete-show-file-submit'
)->
text
()
)
)
);
}
/**
* Show a deleted file version requested by the visitor.
* @param string $key
*/
private
function
showFile
(
$key
)
{
$this
->
getOutput
()->
disable
();
# We mustn't allow the output to be CDN cached, otherwise
# if an admin previews a deleted image, and it's cached, then
# a user without appropriate permissions can toddle off and
# nab the image, and CDN will serve it
$response
=
$this
->
getRequest
()->
response
();
$response
->
header
(
'Expires: '
.
gmdate
(
'D, d M Y H:i:s'
,
0
)
.
' GMT'
);
$response
->
header
(
'Cache-Control: no-cache, no-store, max-age=0, must-revalidate'
);
$path
=
$this
->
localRepo
->
getZonePath
(
'deleted'
)
.
'/'
.
$this
->
localRepo
->
getDeletedHashPath
(
$key
)
.
$key
;
$this
->
localRepo
->
streamFileWithStatus
(
$path
);
}
/**
* @param LinkBatch $batch
* @param IResultWrapper $revisions
*/
private
function
addRevisionsToBatch
(
LinkBatch
$batch
,
IResultWrapper
$revisions
)
{
foreach
(
$revisions
as
$row
)
{
$batch
->
add
(
NS_USER
,
$row
->
ar_user_text
);
$batch
->
add
(
NS_USER_TALK
,
$row
->
ar_user_text
);
}
}
/**
* @param LinkBatch $batch
* @param IResultWrapper $files
*/
private
function
addFilesToBatch
(
LinkBatch
$batch
,
IResultWrapper
$files
)
{
foreach
(
$files
as
$row
)
{
$batch
->
add
(
NS_USER
,
$row
->
fa_user_text
);
$batch
->
add
(
NS_USER_TALK
,
$row
->
fa_user_text
);
}
}
/**
* Handle XHR "show more history" requests (T249977)
*/
protected
function
showMoreHistory
()
{
$out
=
$this
->
getOutput
();
$out
->
setArticleBodyOnly
(
true
);
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
if
(
$this
->
mHistoryOffset
)
{
$extraConds
=
[
$dbr
->
expr
(
'ar_timestamp'
,
'<'
,
$dbr
->
timestamp
(
$this
->
mHistoryOffset
)
)
];
}
else
{
$extraConds
=
[];
}
$revisions
=
$this
->
archivedRevisionLookup
->
listRevisions
(
$this
->
mTargetObj
,
$extraConds
,
self
::
REVISION_HISTORY_LIMIT
+
1
);
$batch
=
$this
->
linkBatchFactory
->
newLinkBatch
();
$this
->
addRevisionsToBatch
(
$batch
,
$revisions
);
$batch
->
execute
();
$out
->
addHTML
(
$this
->
formatRevisionHistory
(
$revisions
)
);
if
(
$revisions
->
numRows
()
>
self
::
REVISION_HISTORY_LIMIT
)
{
// Indicate to JS that the "show more" button should remain active
$out
->
setStatusCode
(
206
);
}
}
/**
* Generate the <ul> element representing a list of deleted revisions
*
* @param IResultWrapper $revisions
* @return string
*/
protected
function
formatRevisionHistory
(
IResultWrapper
$revisions
)
{
$history
=
Html
::
openElement
(
'ul'
,
[
'class'
=>
'mw-undelete-revlist'
]
);
// Exclude the last data row if there is more data than history limit amount
$numRevisions
=
$revisions
->
numRows
();
$displayCount
=
min
(
$numRevisions
,
self
::
REVISION_HISTORY_LIMIT
);
$firstRev
=
$this
->
revisionStore
->
getFirstRevision
(
$this
->
mTargetObj
);
$earliestLiveTime
=
$firstRev
?
$firstRev
->
getTimestamp
()
:
null
;
$revisions
->
rewind
();
for
(
$i
=
0
;
$i
<
$displayCount
;
$i
++
)
{
$row
=
$revisions
->
fetchObject
();
// The $remaining parameter controls diff links and so must
// include the undisplayed row beyond the display limit.
$history
.=
$this
->
formatRevisionRow
(
$row
,
$earliestLiveTime
,
$numRevisions
-
$i
);
}
$history
.=
Html
::
closeElement
(
'ul'
);
return
$history
;
}
protected
function
showHistory
()
{
$this
->
checkReadOnly
();
$out
=
$this
->
getOutput
();
if
(
$this
->
mAllowed
)
{
$out
->
addModules
(
'mediawiki.misc-authed-ooui'
);
$out
->
addModuleStyles
(
'mediawiki.special'
);
}
$out
->
addModuleStyles
(
'mediawiki.interface.helpers.styles'
);
$out
->
wrapWikiMsg
(
"<div class='mw-undelete-pagetitle'>
\n
$1
\n
</div>
\n
"
,
[
'undeletepagetitle'
,
wfEscapeWikiText
(
$this
->
mTargetObj
->
getPrefixedText
()
)
]
);
$archive
=
new
PageArchive
(
$this
->
mTargetObj
);
// FIXME: This hook must be deprecated, passing PageArchive by ref is awful.
$this
->
getHookRunner
()->
onUndeleteForm__showHistory
(
$archive
,
$this
->
mTargetObj
);
$out
->
addHTML
(
Html
::
openElement
(
'div'
,
[
'class'
=>
'mw-undelete-history'
]
)
);
if
(
$this
->
mAllowed
)
{
$out
->
addWikiMsg
(
'undeletehistory'
);
$out
->
addWikiMsg
(
'undeleterevdel'
);
}
else
{
$out
->
addWikiMsg
(
'undeletehistorynoadmin'
);
}
$out
->
addHTML
(
Html
::
closeElement
(
'div'
)
);
# List all stored revisions
$revisions
=
$this
->
archivedRevisionLookup
->
listRevisions
(
$this
->
mTargetObj
,
[],
self
::
REVISION_HISTORY_LIMIT
+
1
);
$files
=
$archive
->
listFiles
();
$numRevisions
=
$revisions
->
numRows
();
$showLoadMore
=
$numRevisions
>
self
::
REVISION_HISTORY_LIMIT
;
$haveRevisions
=
$numRevisions
>
0
;
$haveFiles
=
$files
&&
$files
->
numRows
()
>
0
;
# Batch existence check on user and talk pages
if
(
$haveRevisions
||
$haveFiles
)
{
$batch
=
$this
->
linkBatchFactory
->
newLinkBatch
();
$this
->
addRevisionsToBatch
(
$batch
,
$revisions
);
if
(
$haveFiles
)
{
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable -- $files is non-null
$this
->
addFilesToBatch
(
$batch
,
$files
);
}
$batch
->
execute
();
}
if
(
$this
->
mAllowed
)
{
$out
->
enableOOUI
();
$action
=
$this
->
getPageTitle
()->
getLocalURL
(
[
'action'
=>
'submit'
]
);
# Start the form here
$form
=
new
FormLayout
(
[
'method'
=>
'post'
,
'action'
=>
$action
,
'id'
=>
'undelete'
,
]
);
}
# Show relevant lines from the deletion log:
$deleteLogPage
=
new
LogPage
(
'delete'
);
$out
->
addHTML
(
Xml
::
element
(
'h2'
,
null
,
$deleteLogPage
->
getName
()->
text
()
)
.
"
\n
"
);
LogEventsList
::
showLogExtract
(
$out
,
'delete'
,
$this
->
mTargetObj
);
# Show relevant lines from the suppression log:
$suppressLogPage
=
new
LogPage
(
'suppress'
);
if
(
$this
->
permissionManager
->
userHasRight
(
$this
->
getUser
(),
'suppressionlog'
)
)
{
$out
->
addHTML
(
Xml
::
element
(
'h2'
,
null
,
$suppressLogPage
->
getName
()->
text
()
)
.
"
\n
"
);
LogEventsList
::
showLogExtract
(
$out
,
'suppress'
,
$this
->
mTargetObj
);
}
if
(
$this
->
mAllowed
&&
(
$haveRevisions
||
$haveFiles
)
)
{
$unsuppressAllowed
=
$this
->
permissionManager
->
userHasRight
(
$this
->
getUser
(),
'suppressrevision'
);
$fields
=
[];
$fields
[]
=
new
Layout
(
[
'content'
=>
new
HtmlSnippet
(
$this
->
msg
(
'undeleteextrahelp'
)->
parseAsBlock
()
)
]
);
$dropdownComment
=
$this
->
msg
(
'undelete-comment-dropdown'
)
->
page
(
$this
->
mTargetObj
)->
inContentLanguage
()->
text
();
// Add additional specific reasons for unsuppress
if
(
$unsuppressAllowed
)
{
$dropdownComment
.=
"
\n
"
.
$this
->
msg
(
'undelete-comment-dropdown-unsuppress'
)
->
page
(
$this
->
mTargetObj
)->
inContentLanguage
()->
text
();
}
$options
=
Xml
::
listDropdownOptions
(
$dropdownComment
,
[
'other'
=>
$this
->
msg
(
'undeletecommentotherlist'
)->
text
()
]
);
$options
=
Xml
::
listDropdownOptionsOoui
(
$options
);
$fields
[]
=
new
FieldLayout
(
new
DropdownInputWidget
(
[
'name'
=>
'wpCommentList'
,
'inputId'
=>
'wpCommentList'
,
'infusable'
=>
true
,
'value'
=>
$this
->
getRequest
()->
getText
(
'wpCommentList'
,
'other'
),
'options'
=>
$options
,
]
),
[
'label'
=>
$this
->
msg
(
'undeletecomment'
)->
text
(),
'align'
=>
'top'
,
]
);
$fields
[]
=
new
FieldLayout
(
new
TextInputWidget
(
[
'name'
=>
'wpComment'
,
'inputId'
=>
'wpComment'
,
'infusable'
=>
true
,
'value'
=>
$this
->
getRequest
()->
getText
(
'wpComment'
),
'autofocus'
=>
true
,
// HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
// (e.g. emojis) count for two each. This limit is overridden in JS to instead count
// Unicode codepoints.
'maxLength'
=>
CommentStore
::
COMMENT_CHARACTER_LIMIT
,
]
),
[
'label'
=>
$this
->
msg
(
'undeleteothercomment'
)->
text
(),
'align'
=>
'top'
,
]
);
if
(
$this
->
getUser
()->
isRegistered
()
)
{
$checkWatch
=
$this
->
watchlistManager
->
isWatched
(
$this
->
getUser
(),
$this
->
mTargetObj
)
||
$this
->
getRequest
()->
getText
(
'wpWatch'
);
$fields
[]
=
new
FieldLayout
(
new
CheckboxInputWidget
(
[
'name'
=>
'wpWatch'
,
'inputId'
=>
'mw-undelete-watch'
,
'value'
=>
'1'
,
'selected'
=>
$checkWatch
,
]
),
[
'label'
=>
$this
->
msg
(
'watchthis'
)->
text
(),
'align'
=>
'inline'
,
]
);
}
if
(
$unsuppressAllowed
)
{
$fields
[]
=
new
FieldLayout
(
new
CheckboxInputWidget
(
[
'name'
=>
'wpUnsuppress'
,
'inputId'
=>
'mw-undelete-unsuppress'
,
'value'
=>
'1'
,
]
),
[
'label'
=>
$this
->
msg
(
'revdelete-unsuppress'
)->
text
(),
'align'
=>
'inline'
,
]
);
}
$undelPage
=
$this
->
undeletePageFactory
->
newUndeletePage
(
$this
->
wikiPageFactory
->
newFromTitle
(
$this
->
mTargetObj
),
$this
->
getContext
()->
getAuthority
()
);
if
(
$undelPage
->
canProbablyUndeleteAssociatedTalk
()->
isGood
()
)
{
$fields
[]
=
new
FieldLayout
(
new
CheckboxInputWidget
(
[
'name'
=>
'undeletetalk'
,
'inputId'
=>
'mw-undelete-undeletetalk'
,
'selected'
=>
false
,
]
),
[
'label'
=>
$this
->
msg
(
'undelete-undeletetalk'
)->
text
(),
'align'
=>
'inline'
,
]
);
}
$fields
[]
=
new
FieldLayout
(
new
Widget
(
[
'content'
=>
new
HorizontalLayout
(
[
'items'
=>
[
new
ButtonInputWidget
(
[
'name'
=>
'restore'
,
'inputId'
=>
'mw-undelete-submit'
,
'value'
=>
'1'
,
'label'
=>
$this
->
msg
(
'undeletebtn'
)->
text
(),
'flags'
=>
[
'primary'
,
'progressive'
],
'type'
=>
'submit'
,
]
),
new
ButtonInputWidget
(
[
'name'
=>
'invert'
,
'inputId'
=>
'mw-undelete-invert'
,
'value'
=>
'1'
,
'label'
=>
$this
->
msg
(
'undeleteinvert'
)->
text
()
]
),
]
]
)
]
)
);
$fieldset
=
new
FieldsetLayout
(
[
'label'
=>
$this
->
msg
(
'undelete-fieldset-title'
)->
text
(),
'id'
=>
'mw-undelete-table'
,
'items'
=>
$fields
,
]
);
$link
=
''
;
if
(
$this
->
getAuthority
()->
isAllowed
(
'editinterface'
)
)
{
if
(
$unsuppressAllowed
)
{
$link
.=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$this
->
msg
(
'undelete-comment-dropdown-unsuppress'
)->
inContentLanguage
()->
getTitle
(),
$this
->
msg
(
'undelete-edit-commentlist-unsuppress'
)->
text
(),
[],
[
'action'
=>
'edit'
]
);
$link
.=
$this
->
msg
(
'pipe-separator'
)->
escaped
();
}
$link
.=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$this
->
msg
(
'undelete-comment-dropdown'
)->
inContentLanguage
()->
getTitle
(),
$this
->
msg
(
'undelete-edit-commentlist'
)->
text
(),
[],
[
'action'
=>
'edit'
]
);
$link
=
Html
::
rawElement
(
'p'
,
[
'class'
=>
'mw-undelete-editcomments'
],
$link
);
}
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
$form
->
appendContent
(
new
PanelLayout
(
[
'expanded'
=>
false
,
'padded'
=>
true
,
'framed'
=>
true
,
'content'
=>
$fieldset
,
]
),
new
HtmlSnippet
(
$link
.
Html
::
hidden
(
'target'
,
$this
->
mTarget
)
.
Html
::
hidden
(
'wpEditToken'
,
$this
->
getUser
()->
getEditToken
()
)
)
);
}
$history
=
''
;
$history
.=
Xml
::
element
(
'h2'
,
null
,
$this
->
msg
(
'history'
)->
text
()
)
.
"
\n
"
;
if
(
$haveRevisions
)
{
# Show the page's stored (deleted) history
if
(
$this
->
permissionManager
->
userHasRight
(
$this
->
getUser
(),
'deleterevision'
)
)
{
$history
.=
Html
::
element
(
'button'
,
[
'name'
=>
'revdel'
,
'type'
=>
'submit'
,
'class'
=>
'deleterevision-log-submit mw-log-deleterevision-button'
],
$this
->
msg
(
'showhideselectedversions'
)->
text
()
)
.
"
\n
"
;
}
$history
.=
$this
->
formatRevisionHistory
(
$revisions
);
if
(
$showLoadMore
)
{
$history
.=
Html
::
openElement
(
'div'
)
.
Html
::
element
(
'span'
,
[
'id'
=>
'mw-load-more-revisions'
],
$this
->
msg
(
'undelete-load-more-revisions'
)->
text
()
)
.
Html
::
closeElement
(
'div'
)
.
"
\n
"
;
}
}
else
{
$out
->
addWikiMsg
(
'nohistory'
);
}
if
(
$haveFiles
)
{
$history
.=
Xml
::
element
(
'h2'
,
null
,
$this
->
msg
(
'filehist'
)->
text
()
)
.
"
\n
"
;
$history
.=
Html
::
openElement
(
'ul'
,
[
'class'
=>
'mw-undelete-revlist'
]
);
foreach
(
$files
as
$row
)
{
$history
.=
$this
->
formatFileRow
(
$row
);
}
$files
->
free
();
$history
.=
Html
::
closeElement
(
'ul'
);
}
if
(
$this
->
mAllowed
)
{
# Slip in the hidden controls here
$misc
=
Html
::
hidden
(
'target'
,
$this
->
mTarget
);
$misc
.=
Html
::
hidden
(
'wpEditToken'
,
$this
->
getUser
()->
getEditToken
()
);
$history
.=
$misc
;
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
$form
->
appendContent
(
new
HtmlSnippet
(
$history
)
);
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable form is set, when used here
$out
->
addHTML
(
(
string
)
$form
);
}
else
{
$out
->
addHTML
(
$history
);
}
return
true
;
}
protected
function
formatRevisionRow
(
$row
,
$earliestLiveTime
,
$remaining
)
{
$revRecord
=
$this
->
revisionStore
->
newRevisionFromArchiveRow
(
$row
,
IDBAccessObject
::
READ_NORMAL
,
$this
->
mTargetObj
);
$revTextSize
=
''
;
$ts
=
wfTimestamp
(
TS_MW
,
$row
->
ar_timestamp
);
// Build checkboxen...
if
(
$this
->
mAllowed
)
{
if
(
$this
->
mInvert
)
{
if
(
in_array
(
$ts
,
$this
->
mTargetTimestamp
)
)
{
$checkBox
=
Xml
::
check
(
"ts$ts"
);
}
else
{
$checkBox
=
Xml
::
check
(
"ts$ts"
,
true
);
}
}
else
{
$checkBox
=
Xml
::
check
(
"ts$ts"
);
}
}
else
{
$checkBox
=
''
;
}
// Build page & diff links...
$user
=
$this
->
getUser
();
if
(
$this
->
mCanView
)
{
$titleObj
=
$this
->
getPageTitle
();
# Last link
if
(
!
$revRecord
->
userCan
(
RevisionRecord
::
DELETED_TEXT
,
$this
->
getAuthority
()
)
)
{
$pageLink
=
htmlspecialchars
(
$this
->
getLanguage
()->
userTimeAndDate
(
$ts
,
$user
)
);
$last
=
$this
->
msg
(
'diff'
)->
escaped
();
}
elseif
(
$remaining
>
0
||
(
$earliestLiveTime
&&
$ts
>
$earliestLiveTime
)
)
{
$pageLink
=
$this
->
getPageLink
(
$revRecord
,
$titleObj
,
$ts
);
$last
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$titleObj
,
$this
->
msg
(
'diff'
)->
text
(),
[],
[
'target'
=>
$this
->
mTargetObj
->
getPrefixedText
(),
'timestamp'
=>
$ts
,
'diff'
=>
'prev'
]
);
}
else
{
$pageLink
=
$this
->
getPageLink
(
$revRecord
,
$titleObj
,
$ts
);
$last
=
$this
->
msg
(
'diff'
)->
escaped
();
}
}
else
{
$pageLink
=
htmlspecialchars
(
$this
->
getLanguage
()->
userTimeAndDate
(
$ts
,
$user
)
);
$last
=
$this
->
msg
(
'diff'
)->
escaped
();
}
// User links
$userLink
=
Linker
::
revUserTools
(
$revRecord
);
// Minor edit
$minor
=
$revRecord
->
isMinor
()
?
ChangesList
::
flag
(
'minor'
)
:
''
;
// Revision text size
$size
=
$row
->
ar_len
;
if
(
$size
!==
null
)
{
$revTextSize
=
Linker
::
formatRevisionSize
(
$size
);
}
// Edit summary
$comment
=
$this
->
commentFormatter
->
formatRevision
(
$revRecord
,
$user
);
// Tags
$attribs
=
[];
[
$tagSummary
,
$classes
]
=
ChangeTags
::
formatSummaryRow
(
$row
->
ts_tags
,
'deletedhistory'
,
$this
->
getContext
()
);
if
(
$classes
)
{
$attribs
[
'class'
]
=
implode
(
' '
,
$classes
);
}
$revisionRow
=
$this
->
msg
(
'undelete-revision-row2'
)
->
rawParams
(
$checkBox
,
$last
,
$pageLink
,
$userLink
,
$minor
,
$revTextSize
,
$comment
,
$tagSummary
)
->
escaped
();
return
Xml
::
tags
(
'li'
,
$attribs
,
$revisionRow
)
.
"
\n
"
;
}
private
function
formatFileRow
(
$row
)
{
$file
=
ArchivedFile
::
newFromRow
(
$row
);
$ts
=
wfTimestamp
(
TS_MW
,
$row
->
fa_timestamp
);
$user
=
$this
->
getUser
();
$checkBox
=
''
;
if
(
$this
->
mCanView
&&
$row
->
fa_storage_key
)
{
if
(
$this
->
mAllowed
)
{
$checkBox
=
Xml
::
check
(
'fileid'
.
$row
->
fa_id
);
}
$key
=
urlencode
(
$row
->
fa_storage_key
);
$pageLink
=
$this
->
getFileLink
(
$file
,
$this
->
getPageTitle
(),
$ts
,
$key
);
}
else
{
$pageLink
=
htmlspecialchars
(
$this
->
getLanguage
()->
userTimeAndDate
(
$ts
,
$user
)
);
}
$userLink
=
$this
->
getFileUser
(
$file
);
$data
=
$this
->
msg
(
'widthheight'
)->
numParams
(
$row
->
fa_width
,
$row
->
fa_height
)->
text
();
$bytes
=
$this
->
msg
(
'parentheses'
)
->
plaintextParams
(
$this
->
msg
(
'nbytes'
)->
numParams
(
$row
->
fa_size
)->
text
()
)
->
plain
();
$data
=
htmlspecialchars
(
$data
.
' '
.
$bytes
);
$comment
=
$this
->
getFileComment
(
$file
);
// Add show/hide deletion links if available
$canHide
=
$this
->
isAllowed
(
'deleterevision'
);
if
(
$canHide
||
(
$file
->
getVisibility
()
&&
$this
->
isAllowed
(
'deletedhistory'
)
)
)
{
if
(
!
$file
->
userCan
(
File
::
DELETED_RESTRICTED
,
$user
)
)
{
// Revision was hidden from sysops
$revdlink
=
Linker
::
revDeleteLinkDisabled
(
$canHide
);
}
else
{
$query
=
[
'type'
=>
'filearchive'
,
'target'
=>
$this
->
mTargetObj
->
getPrefixedDBkey
(),
'ids'
=>
$row
->
fa_id
];
$revdlink
=
Linker
::
revDeleteLink
(
$query
,
$file
->
isDeleted
(
File
::
DELETED_RESTRICTED
),
$canHide
);
}
}
else
{
$revdlink
=
''
;
}
return
"<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>
\n
"
;
}
/**
* Fetch revision text link if it's available to all users
*
* @param RevisionRecord $revRecord
* @param LinkTarget $target
* @param string $ts Timestamp
* @return string
*/
private
function
getPageLink
(
RevisionRecord
$revRecord
,
LinkTarget
$target
,
$ts
)
{
$user
=
$this
->
getUser
();
$time
=
$this
->
getLanguage
()->
userTimeAndDate
(
$ts
,
$user
);
if
(
!
$revRecord
->
userCan
(
RevisionRecord
::
DELETED_TEXT
,
$this
->
getAuthority
()
)
)
{
// TODO The condition cannot be true when the function is called
return
Html
::
element
(
'span'
,
[
'class'
=>
'history-deleted'
],
$time
);
}
$link
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$target
,
$time
,
[],
[
'target'
=>
$this
->
mTargetObj
->
getPrefixedText
(),
'timestamp'
=>
$ts
]
);
if
(
$revRecord
->
isDeleted
(
RevisionRecord
::
DELETED_TEXT
)
)
{
$class
=
Linker
::
getRevisionDeletedClass
(
$revRecord
);
$link
=
'<span class="'
.
$class
.
'">'
.
$link
.
'</span>'
;
}
return
$link
;
}
/**
* Fetch image view link if it's available to all users
*
* @param File|ArchivedFile $file
* @param LinkTarget $target
* @param string $ts A timestamp
* @param string $key A storage key
*
* @return string HTML fragment
*/
private
function
getFileLink
(
$file
,
LinkTarget
$target
,
$ts
,
$key
)
{
$user
=
$this
->
getUser
();
$time
=
$this
->
getLanguage
()->
userTimeAndDate
(
$ts
,
$user
);
if
(
!
$file
->
userCan
(
File
::
DELETED_FILE
,
$user
)
)
{
return
Html
::
element
(
'span'
,
[
'class'
=>
'history-deleted'
],
$time
);
}
if
(
$file
->
exists
()
)
{
$link
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$target
,
$time
,
[],
[
'target'
=>
$this
->
mTargetObj
->
getPrefixedText
(),
'file'
=>
$key
,
'token'
=>
$user
->
getEditToken
(
$key
)
]
);
}
else
{
$link
=
htmlspecialchars
(
$time
);
}
if
(
$file
->
isDeleted
(
File
::
DELETED_FILE
)
)
{
$link
=
'<span class="history-deleted">'
.
$link
.
'</span>'
;
}
return
$link
;
}
/**
* Fetch file's user id if it's available to this user
*
* @param File|ArchivedFile $file
* @return string HTML fragment
*/
private
function
getFileUser
(
$file
)
{
$uploader
=
$file
->
getUploader
(
File
::
FOR_THIS_USER
,
$this
->
getAuthority
()
);
if
(
!
$uploader
)
{
return
Html
::
rawElement
(
'span'
,
[
'class'
=>
'history-deleted'
],
$this
->
msg
(
'rev-deleted-user'
)->
escaped
()
);
}
$link
=
Linker
::
userLink
(
$uploader
->
getId
(),
$uploader
->
getName
()
)
.
Linker
::
userToolLinks
(
$uploader
->
getId
(),
$uploader
->
getName
()
);
if
(
$file
->
isDeleted
(
File
::
DELETED_USER
)
)
{
$link
=
Html
::
rawElement
(
'span'
,
[
'class'
=>
'history-deleted'
],
$link
);
}
return
$link
;
}
/**
* Fetch file upload comment if it's available to this user
*
* @param File|ArchivedFile $file
* @return string HTML fragment
*/
private
function
getFileComment
(
$file
)
{
if
(
!
$file
->
userCan
(
File
::
DELETED_COMMENT
,
$this
->
getAuthority
()
)
)
{
return
Html
::
rawElement
(
'span'
,
[
'class'
=>
'history-deleted'
],
Html
::
rawElement
(
'span'
,
[
'class'
=>
'comment'
],
$this
->
msg
(
'rev-deleted-comment'
)->
escaped
()
)
);
}
$comment
=
$file
->
getDescription
(
File
::
FOR_THIS_USER
,
$this
->
getAuthority
()
);
$link
=
$this
->
commentFormatter
->
formatBlock
(
$comment
);
if
(
$file
->
isDeleted
(
File
::
DELETED_COMMENT
)
)
{
$link
=
Html
::
rawElement
(
'span'
,
[
'class'
=>
'history-deleted'
],
$link
);
}
return
$link
;
}
private
function
undelete
()
{
if
(
$this
->
getConfig
()->
get
(
MainConfigNames
::
UploadMaintenance
)
&&
$this
->
mTargetObj
->
getNamespace
()
===
NS_FILE
)
{
throw
new
ErrorPageError
(
'undelete-error'
,
'filedelete-maintenance'
);
}
$this
->
checkReadOnly
();
$out
=
$this
->
getOutput
();
$undeletePage
=
$this
->
undeletePageFactory
->
newUndeletePage
(
$this
->
wikiPageFactory
->
newFromTitle
(
$this
->
mTargetObj
),
$this
->
getAuthority
()
);
if
(
$this
->
mUndeleteTalk
&&
$undeletePage
->
canProbablyUndeleteAssociatedTalk
()->
isGood
()
)
{
$undeletePage
->
setUndeleteAssociatedTalk
(
true
);
}
$status
=
$undeletePage
->
setUndeleteOnlyTimestamps
(
$this
->
mTargetTimestamp
)
->
setUndeleteOnlyFileVersions
(
$this
->
mFileVersions
)
->
setUnsuppress
(
$this
->
mUnsuppress
)
// TODO This is currently duplicating some permission checks, but we do need it (T305680)
->
undeleteIfAllowed
(
$this
->
mComment
);
if
(
!
$status
->
isGood
()
)
{
$out
->
setPageTitleMsg
(
$this
->
msg
(
'undelete-error'
)
);
$out
->
wrapWikiTextAsInterface
(
'error'
,
Status
::
wrap
(
$status
)->
getWikiText
(
'cannotundelete'
,
'cannotundelete'
,
$this
->
getLanguage
()
)
);
return
;
}
$restoredRevs
=
$status
->
getValue
()[
UndeletePage
::
REVISIONS_RESTORED
];
$restoredFiles
=
$status
->
getValue
()[
UndeletePage
::
FILES_RESTORED
];
if
(
$restoredRevs
===
0
&&
$restoredFiles
===
0
)
{
// TODO Should use a different message here
$out
->
setPageTitleMsg
(
$this
->
msg
(
'undelete-error'
)
);
}
else
{
if
(
$status
->
getValue
()[
UndeletePage
::
FILES_RESTORED
]
!==
0
)
{
$this
->
getHookRunner
()->
onFileUndeleteComplete
(
$this
->
mTargetObj
,
$this
->
mFileVersions
,
$this
->
getUser
(),
$this
->
mComment
);
}
$link
=
$this
->
getLinkRenderer
()->
makeKnownLink
(
$this
->
mTargetObj
);
$out
->
addWikiMsg
(
'undeletedpage'
,
Message
::
rawParam
(
$link
)
);
$this
->
watchlistManager
->
setWatch
(
$this
->
getRequest
()->
getCheck
(
'wpWatch'
),
$this
->
getAuthority
(),
$this
->
mTargetObj
);
}
}
/**
* Return an array of subpages beginning with $search that this special page will accept.
*
* @param string $search Prefix to search for
* @param int $limit Maximum number of results to return (usually 10)
* @param int $offset Number of results to skip (usually 0)
* @return string[] Matching subpages
*/
public
function
prefixSearchSubpages
(
$search
,
$limit
,
$offset
)
{
return
$this
->
prefixSearchString
(
$search
,
$limit
,
$offset
,
$this
->
searchEngineFactory
);
}
protected
function
getGroupName
()
{
return
'pagetools'
;
}
}
/**
* Retain the old class name for backwards compatibility.
* @deprecated since 1.41
*/
class_alias
(
SpecialUndelete
::
class
,
'SpecialUndelete'
);
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 07:28 (2 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
29/e8/08cd97c0fbc94edca2ad4dd9d96d
Default Alt Text
SpecialUndelete.php (50 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment