Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1426975
SpecialReplaceText.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
32 KB
Referenced Files
None
Subscribers
None
SpecialReplaceText.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.
* https://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace
MediaWiki\Extension\ReplaceText
;
use
ErrorPageError
;
use
JobQueueGroup
;
use
MediaWiki\HookContainer\HookContainer
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Language\Language
;
use
MediaWiki\Linker\LinkRenderer
;
use
MediaWiki\Page\MovePageFactory
;
use
MediaWiki\Page\WikiPageFactory
;
use
MediaWiki\Permissions\PermissionManager
;
use
MediaWiki\Revision\SlotRecord
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Storage\NameTableStore
;
use
MediaWiki\Title\NamespaceInfo
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\Options\UserOptionsLookup
;
use
MediaWiki\User\UserFactory
;
use
MediaWiki\Watchlist\WatchlistManager
;
use
OOUI
;
use
PermissionsError
;
use
SearchEngineConfig
;
use
Wikimedia\Rdbms\IConnectionProvider
;
use
Wikimedia\Rdbms\ReadOnlyMode
;
class
SpecialReplaceText
extends
SpecialPage
{
/** @var string */
private
$target
;
/** @var string */
private
$targetString
;
/** @var string */
private
$replacement
;
/** @var bool */
private
$use_regex
;
/** @var string */
private
$category
;
/** @var string */
private
$prefix
;
/** @var string|int */
private
$pageLimit
;
/** @var bool */
private
$edit_pages
;
/** @var bool */
private
$move_pages
;
/** @var int[] */
private
$selected_namespaces
;
/** @var bool */
private
$botEdit
;
/** @var HookHelper */
private
$hookHelper
;
/** @var IConnectionProvider */
private
$dbProvider
;
/** @var Language */
private
$contentLanguage
;
/** @var JobQueueGroup */
private
$jobQueueGroup
;
/** @var LinkRenderer */
private
$linkRenderer
;
/** @var MovePageFactory */
private
$movePageFactory
;
/** @var NamespaceInfo */
private
$namespaceInfo
;
/** @var PermissionManager */
private
$permissionManager
;
/** @var ReadOnlyMode */
private
$readOnlyMode
;
/** @var SearchEngineConfig */
private
$searchEngineConfig
;
/** @var NameTableStore */
private
$slotRoleStore
;
/** @var UserFactory */
private
$userFactory
;
/** @var UserOptionsLookup */
private
$userOptionsLookup
;
private
WatchlistManager
$watchlistManager
;
private
WikiPageFactory
$wikiPageFactory
;
/** @var Search */
private
$search
;
/**
* @param HookContainer $hookContainer
* @param IConnectionProvider $dbProvider
* @param Language $contentLanguage
* @param JobQueueGroup $jobQueueGroup
* @param LinkRenderer $linkRenderer
* @param MovePageFactory $movePageFactory
* @param NamespaceInfo $namespaceInfo
* @param PermissionManager $permissionManager
* @param ReadOnlyMode $readOnlyMode
* @param SearchEngineConfig $searchEngineConfig
* @param NameTableStore $slotRoleStore
* @param UserFactory $userFactory
* @param UserOptionsLookup $userOptionsLookup
* @param WatchlistManager $watchlistManager
* @param WikiPageFactory $wikiPageFactory
*/
public
function
__construct
(
HookContainer
$hookContainer
,
IConnectionProvider
$dbProvider
,
Language
$contentLanguage
,
JobQueueGroup
$jobQueueGroup
,
LinkRenderer
$linkRenderer
,
MovePageFactory
$movePageFactory
,
NamespaceInfo
$namespaceInfo
,
PermissionManager
$permissionManager
,
ReadOnlyMode
$readOnlyMode
,
SearchEngineConfig
$searchEngineConfig
,
NameTableStore
$slotRoleStore
,
UserFactory
$userFactory
,
UserOptionsLookup
$userOptionsLookup
,
WatchlistManager
$watchlistManager
,
WikiPageFactory
$wikiPageFactory
)
{
parent
::
__construct
(
'ReplaceText'
,
'replacetext'
);
$this
->
hookHelper
=
new
HookHelper
(
$hookContainer
);
$this
->
dbProvider
=
$dbProvider
;
$this
->
contentLanguage
=
$contentLanguage
;
$this
->
jobQueueGroup
=
$jobQueueGroup
;
$this
->
linkRenderer
=
$linkRenderer
;
$this
->
movePageFactory
=
$movePageFactory
;
$this
->
namespaceInfo
=
$namespaceInfo
;
$this
->
permissionManager
=
$permissionManager
;
$this
->
readOnlyMode
=
$readOnlyMode
;
$this
->
searchEngineConfig
=
$searchEngineConfig
;
$this
->
slotRoleStore
=
$slotRoleStore
;
$this
->
userFactory
=
$userFactory
;
$this
->
userOptionsLookup
=
$userOptionsLookup
;
$this
->
watchlistManager
=
$watchlistManager
;
$this
->
wikiPageFactory
=
$wikiPageFactory
;
$this
->
search
=
new
Search
(
$this
->
getConfig
(),
$dbProvider
);
}
/**
* @inheritDoc
*/
public
function
doesWrites
()
{
return
true
;
}
/**
* @param null|string $query
*/
function
execute
(
$query
)
{
if
(
!
$this
->
getUser
()->
isAllowed
(
'replacetext'
)
)
{
throw
new
PermissionsError
(
'replacetext'
);
}
// Replace Text can't be run with certain settings, due to the
// changes they make to the DB storage setup.
if
(
$this
->
getConfig
()->
get
(
'CompressRevisions'
)
)
{
throw
new
ErrorPageError
(
'replacetext_cfg_error'
,
'replacetext_no_compress'
);
}
if
(
$this
->
getConfig
()->
get
(
'ExternalStores'
)
)
{
throw
new
ErrorPageError
(
'replacetext_cfg_error'
,
'replacetext_no_external_stores'
);
}
$out
=
$this
->
getOutput
();
if
(
$this
->
readOnlyMode
->
isReadOnly
()
)
{
$permissionErrors
=
[
[
'readonlytext'
,
[
$this
->
readOnlyMode
->
getReason
()
]
]
];
$out
->
setPageTitleMsg
(
$this
->
msg
(
'badaccess'
)
);
$out
->
addWikiTextAsInterface
(
$out
->
formatPermissionsErrorMessage
(
$permissionErrors
,
'replacetext'
)
);
return
;
}
$out
->
enableOOUI
();
$this
->
setHeaders
();
$this
->
doSpecialReplaceText
();
}
/**
* @return array namespaces selected for search
*/
function
getSelectedNamespaces
()
{
$all_namespaces
=
$this
->
searchEngineConfig
->
searchableNamespaces
();
$selected_namespaces
=
[];
foreach
(
$all_namespaces
as
$ns
=>
$name
)
{
if
(
$this
->
getRequest
()->
getCheck
(
'ns'
.
$ns
)
)
{
$selected_namespaces
[]
=
$ns
;
}
}
return
$selected_namespaces
;
}
/**
* Do the actual display and logic of Special:ReplaceText.
*/
function
doSpecialReplaceText
()
{
$out
=
$this
->
getOutput
();
$request
=
$this
->
getRequest
();
$this
->
target
=
$request
->
getText
(
'target'
);
$this
->
targetString
=
str_replace
(
"
\n
"
,
"
\u
{21B5}"
,
$this
->
target
);
$this
->
replacement
=
$request
->
getText
(
'replacement'
);
$this
->
use_regex
=
$request
->
getBool
(
'use_regex'
);
$this
->
category
=
$request
->
getText
(
'category'
);
$this
->
prefix
=
$request
->
getText
(
'prefix'
);
$this
->
pageLimit
=
$request
->
getText
(
'pageLimit'
);
$this
->
edit_pages
=
$request
->
getBool
(
'edit_pages'
);
$this
->
move_pages
=
$request
->
getBool
(
'move_pages'
);
$this
->
botEdit
=
$request
->
getBool
(
'botEdit'
);
$this
->
selected_namespaces
=
$this
->
getSelectedNamespaces
();
if
(
$request
->
getCheck
(
'continue'
)
&&
$this
->
target
===
''
)
{
$this
->
showForm
(
'replacetext_givetarget'
);
return
;
}
if
(
$request
->
getCheck
(
'continue'
)
&&
$this
->
pageLimit
===
''
)
{
$this
->
pageLimit
=
$this
->
getConfig
()->
get
(
'ReplaceTextResultsLimit'
);
}
else
{
$this
->
pageLimit
=
(
int
)
$this
->
pageLimit
;
}
if
(
$request
->
getCheck
(
'replace'
)
)
{
// check for CSRF
if
(
!
$this
->
checkToken
()
)
{
$out
->
addWikiMsg
(
'sessionfailure'
);
return
;
}
$jobs
=
$this
->
createJobsForTextReplacements
();
$this
->
jobQueueGroup
->
push
(
$jobs
);
$count
=
$this
->
getLanguage
()->
formatNum
(
count
(
$jobs
)
);
$out
->
addWikiMsg
(
'replacetext_success'
,
"<code><nowiki>{$this->targetString}</nowiki></code>"
,
"<code><nowiki>{$this->replacement}</nowiki></code>"
,
$count
);
// Link back
$out
->
addHTML
(
$this
->
linkRenderer
->
makeLink
(
$this
->
getPageTitle
(),
$this
->
msg
(
'replacetext_return'
)->
text
()
)
);
return
;
}
if
(
$request
->
getCheck
(
'target'
)
)
{
// check for CSRF
if
(
!
$this
->
checkToken
()
)
{
$out
->
addWikiMsg
(
'sessionfailure'
);
return
;
}
// first, check that at least one namespace has been
// picked, and that either editing or moving pages
// has been selected
if
(
count
(
$this
->
selected_namespaces
)
==
0
)
{
$this
->
showForm
(
'replacetext_nonamespace'
);
return
;
}
if
(
!
$this
->
edit_pages
&&
!
$this
->
move_pages
)
{
$this
->
showForm
(
'replacetext_editormove'
);
return
;
}
// If user is replacing text within pages...
$titles_for_edit
=
$titles_for_move
=
$unmoveable_titles
=
$uneditable_titles
=
[];
if
(
$this
->
edit_pages
)
{
[
$titles_for_edit
,
$uneditable_titles
]
=
$this
->
getTitlesForEditingWithContext
();
}
if
(
$this
->
move_pages
)
{
[
$titles_for_move
,
$unmoveable_titles
]
=
$this
->
getTitlesForMoveAndUnmoveableTitles
();
}
// If no results were found, check to see if a bad
// category name was entered.
if
(
count
(
$titles_for_edit
)
==
0
&&
count
(
$titles_for_move
)
==
0
)
{
$category_title_exists
=
true
;
if
(
$this
->
category
)
{
$category_title
=
Title
::
makeTitleSafe
(
NS_CATEGORY
,
$this
->
category
);
if
(
!
$category_title
->
exists
()
)
{
$category_title_exists
=
false
;
$link
=
$this
->
linkRenderer
->
makeLink
(
$category_title
,
ucfirst
(
$this
->
category
)
);
$out
->
addHTML
(
$this
->
msg
(
'replacetext_nosuchcategory'
)->
rawParams
(
$link
)->
escaped
()
);
}
}
if
(
$this
->
edit_pages
&&
$category_title_exists
)
{
$out
->
addWikiMsg
(
'replacetext_noreplacement'
,
"<code><nowiki>{$this->targetString}</nowiki></code>"
);
}
if
(
$this
->
move_pages
&&
$category_title_exists
)
{
$out
->
addWikiMsg
(
'replacetext_nomove'
,
"<code><nowiki>{$this->targetString}</nowiki></code>"
);
}
// link back to starting form
$out
->
addHTML
(
'<p>'
.
$this
->
linkRenderer
->
makeLink
(
$this
->
getPageTitle
(),
$this
->
msg
(
'replacetext_return'
)->
text
()
)
.
'</p>'
);
}
else
{
$warning_msg
=
$this
->
getAnyWarningMessageBeforeReplace
(
$titles_for_edit
,
$titles_for_move
);
if
(
$warning_msg
!==
null
)
{
$warningLabel
=
new
OOUI\LabelWidget
(
[
'label'
=>
new
OOUI\HtmlSnippet
(
$warning_msg
)
]
);
$warning
=
new
OOUI\MessageWidget
(
[
'type'
=>
'warning'
,
'label'
=>
$warningLabel
]
);
$out
->
addHTML
(
$warning
);
}
$this
->
pageListForm
(
$titles_for_edit
,
$titles_for_move
,
$uneditable_titles
,
$unmoveable_titles
);
}
return
;
}
// If we're still here, show the starting form.
$this
->
showForm
();
}
/**
* Returns the set of MediaWiki jobs that will do all the actual replacements.
*
* @return array jobs
*/
function
createJobsForTextReplacements
()
{
$replacement_params
=
[
'user_id'
=>
$this
->
getReplaceTextUser
()->
getId
(),
'target_str'
=>
$this
->
target
,
'replacement_str'
=>
$this
->
replacement
,
'use_regex'
=>
$this
->
use_regex
,
'create_redirect'
=>
false
,
'watch_page'
=>
false
,
'botEdit'
=>
$this
->
botEdit
];
$replacement_params
[
'edit_summary'
]
=
$this
->
msg
(
'replacetext_editsummary'
,
$this
->
targetString
,
$this
->
replacement
)->
inContentLanguage
()->
plain
();
$request
=
$this
->
getRequest
();
foreach
(
$request
->
getValues
()
as
$key
=>
$value
)
{
if
(
$key
==
'create-redirect'
&&
$value
==
'1'
)
{
$replacement_params
[
'create_redirect'
]
=
true
;
}
elseif
(
$key
==
'watch-pages'
&&
$value
==
'1'
)
{
$replacement_params
[
'watch_page'
]
=
true
;
}
}
$jobs
=
[];
$pages_to_edit
=
[];
// These are OOUI checkboxes - we don't determine whether they
// were checked by their value (which will be null), but rather
// by whether they were submitted at all.
foreach
(
$request
->
getValues
()
as
$key
=>
$value
)
{
if
(
$key
===
'replace'
||
$key
===
'use_regex'
)
{
continue
;
}
if
(
strpos
(
$key
,
'move-'
)
!==
false
)
{
$title
=
Title
::
newFromID
(
(
int
)
substr
(
$key
,
5
)
);
$replacement_params
[
'move_page'
]
=
true
;
if
(
$title
!==
null
)
{
$jobs
[]
=
new
Job
(
$title
,
$replacement_params
,
$this
->
movePageFactory
,
$this
->
permissionManager
,
$this
->
userFactory
,
$this
->
watchlistManager
,
$this
->
wikiPageFactory
);
}
unset
(
$replacement_params
[
'move_page'
]
);
}
elseif
(
strpos
(
$key
,
'|'
)
!==
false
)
{
// Bundle multiple edits to the same page for a different slot into one job
[
$page_id
,
$role
]
=
explode
(
'|'
,
$key
,
2
);
$pages_to_edit
[
$page_id
][]
=
$role
;
}
}
// Create jobs for the bundled page edits
foreach
(
$pages_to_edit
as
$page_id
=>
$roles
)
{
$title
=
Title
::
newFromID
(
(
int
)
$page_id
);
$replacement_params
[
'roles'
]
=
$roles
;
if
(
$title
!==
null
)
{
$jobs
[]
=
new
Job
(
$title
,
$replacement_params
,
$this
->
movePageFactory
,
$this
->
permissionManager
,
$this
->
userFactory
,
$this
->
watchlistManager
,
$this
->
wikiPageFactory
);
}
unset
(
$replacement_params
[
'roles'
]
);
}
return
$jobs
;
}
/**
* Returns the set of Titles whose contents would be modified by this
* replacement, along with the "search context" string for each one.
*
* @return array The set of Titles and their search context strings
*/
function
getTitlesForEditingWithContext
()
{
$titles_for_edit
=
[];
$res
=
$this
->
search
->
doSearchQuery
(
$this
->
target
,
$this
->
selected_namespaces
,
$this
->
category
,
$this
->
prefix
,
$this
->
pageLimit
,
$this
->
use_regex
);
$titles_to_process
=
$this
->
hookHelper
->
filterPageTitlesForEdit
(
$res
);
$titles_to_skip
=
[];
foreach
(
$res
as
$row
)
{
$title
=
Title
::
makeTitleSafe
(
$row
->
page_namespace
,
$row
->
page_title
);
if
(
$title
==
null
)
{
continue
;
}
if
(
!
isset
(
$titles_to_process
[
$title
->
getPrefixedText
()
]
)
)
{
// Title has been filtered out by the hook: ReplaceTextFilterPageTitlesForEdit
$titles_to_skip
[]
=
$title
;
continue
;
}
// @phan-suppress-next-line SecurityCheck-ReDoS target could be a regex from user
$context
=
$this
->
extractContext
(
$row
->
old_text
,
$this
->
target
,
$this
->
use_regex
);
$role
=
$this
->
extractRole
(
(
int
)
$row
->
slot_role_id
);
$titles_for_edit
[]
=
[
$title
,
$context
,
$role
];
}
return
[
$titles_for_edit
,
$titles_to_skip
];
}
/**
* Returns two lists: the set of titles that would be moved/renamed by
* the current text replacement, and the set of titles that would
* ordinarily be moved but are not moveable, due to permissions or any
* other reason.
*
* @return array
*/
function
getTitlesForMoveAndUnmoveableTitles
()
{
$titles_for_move
=
[];
$unmoveable_titles
=
[];
$res
=
$this
->
search
->
getMatchingTitles
(
$this
->
target
,
$this
->
selected_namespaces
,
$this
->
category
,
$this
->
prefix
,
$this
->
pageLimit
,
$this
->
use_regex
);
$titles_to_process
=
$this
->
hookHelper
->
filterPageTitlesForRename
(
$res
);
foreach
(
$res
as
$row
)
{
$title
=
Title
::
makeTitleSafe
(
$row
->
page_namespace
,
$row
->
page_title
);
if
(
!
$title
)
{
continue
;
}
if
(
!
isset
(
$titles_to_process
[
$title
->
getPrefixedText
()
]
)
)
{
$unmoveable_titles
[]
=
$title
;
continue
;
}
$new_title
=
Search
::
getReplacedTitle
(
$title
,
$this
->
target
,
$this
->
replacement
,
$this
->
use_regex
);
if
(
!
$new_title
)
{
// New title is not valid because it contains invalid characters.
$unmoveable_titles
[]
=
$title
;
continue
;
}
$mvPage
=
$this
->
movePageFactory
->
newMovePage
(
$title
,
$new_title
);
$moveStatus
=
$mvPage
->
isValidMove
();
$permissionStatus
=
$mvPage
->
checkPermissions
(
$this
->
getUser
(),
null
);
if
(
$permissionStatus
->
isOK
()
&&
$moveStatus
->
isOK
()
)
{
$titles_for_move
[]
=
$title
;
}
else
{
$unmoveable_titles
[]
=
$title
;
}
}
return
[
$titles_for_move
,
$unmoveable_titles
];
}
/**
* Get the warning message if the replacement string is either blank
* or found elsewhere on the wiki (since undoing the replacement
* would be difficult in either case).
*
* @param array $titles_for_edit
* @param array $titles_for_move
* @return string|null Warning message, if any
*/
function
getAnyWarningMessageBeforeReplace
(
$titles_for_edit
,
$titles_for_move
)
{
if
(
$this
->
replacement
===
''
)
{
return
$this
->
msg
(
'replacetext_blankwarning'
)->
parse
();
}
elseif
(
$this
->
use_regex
)
{
// If it's a regex, don't bother checking for existing
// pages - if the replacement string includes wildcards,
// it's a meaningless check.
return
null
;
}
elseif
(
count
(
$titles_for_edit
)
>
0
)
{
$res
=
$this
->
search
->
doSearchQuery
(
$this
->
replacement
,
$this
->
selected_namespaces
,
$this
->
category
,
$this
->
prefix
,
$this
->
pageLimit
,
$this
->
use_regex
);
$titles
=
$this
->
hookHelper
->
filterPageTitlesForEdit
(
$res
);
$count
=
count
(
$titles
);
if
(
$count
>
0
)
{
return
$this
->
msg
(
'replacetext_warning'
)->
numParams
(
$count
)
->
params
(
"<code><nowiki>{$this->replacement}</nowiki></code>"
)->
parse
();
}
}
elseif
(
count
(
$titles_for_move
)
>
0
)
{
$res
=
$this
->
search
->
getMatchingTitles
(
$this
->
replacement
,
$this
->
selected_namespaces
,
$this
->
category
,
$this
->
prefix
,
$this
->
pageLimit
,
$this
->
use_regex
);
$titles
=
$this
->
hookHelper
->
filterPageTitlesForRename
(
$res
);
$count
=
count
(
$titles
);
if
(
$count
>
0
)
{
return
$this
->
msg
(
'replacetext_warning'
)->
numParams
(
$count
)
->
params
(
$this
->
replacement
)->
parse
();
}
}
return
null
;
}
/**
* @param string|null $warning_msg Message to be shown at top of form
*/
function
showForm
(
$warning_msg
=
null
)
{
$out
=
$this
->
getOutput
();
$out
->
addHTML
(
Html
::
openElement
(
'form'
,
[
'id'
=>
'powersearch'
,
'action'
=>
$this
->
getPageTitle
()->
getLocalURL
(),
'method'
=>
'post'
]
)
.
"
\n
"
.
Html
::
hidden
(
'title'
,
$this
->
getPageTitle
()->
getPrefixedText
()
)
.
Html
::
hidden
(
'continue'
,
1
)
.
Html
::
hidden
(
'token'
,
$this
->
getToken
()
)
);
if
(
$warning_msg
===
null
)
{
$out
->
addWikiMsg
(
'replacetext_docu'
);
}
else
{
$out
->
wrapWikiMsg
(
"<div class=
\"
errorbox
\"
>
\n
$1
\n
</div><br clear=
\"
both
\"
/>"
,
$warning_msg
);
}
$out
->
addHTML
(
'<table><tr><td style="vertical-align: top;">'
);
$out
->
addWikiMsg
(
'replacetext_originaltext'
);
$out
->
addHTML
(
'</td><td>'
);
// 'width: auto' style is needed to override MediaWiki's
// normal 'width: 100%', which causes the textarea to get
// zero width in IE
$out
->
addHTML
(
Html
::
textarea
(
'target'
,
$this
->
target
,
[
'cols'
=>
100
,
'rows'
=>
5
,
'style'
=>
'width: auto;'
]
)
);
$out
->
addHTML
(
'</td></tr><tr><td style="vertical-align: top;">'
);
$out
->
addWikiMsg
(
'replacetext_replacementtext'
);
$out
->
addHTML
(
'</td><td>'
);
$out
->
addHTML
(
Html
::
textarea
(
'replacement'
,
$this
->
replacement
,
[
'cols'
=>
100
,
'rows'
=>
5
,
'style'
=>
'width: auto;'
]
)
);
$out
->
addHTML
(
'</td></tr></table>'
);
// SQLite unfortunately lack a REGEXP
// function or operator by default, so disable regex(p)
// searches that DB type.
$dbr
=
$this
->
dbProvider
->
getReplicaDatabase
();
if
(
$dbr
->
getType
()
!==
'sqlite'
)
{
$out
->
addHTML
(
Html
::
rawElement
(
'p'
,
[],
Html
::
rawElement
(
'label'
,
[],
Html
::
input
(
'use_regex'
,
'1'
,
'checkbox'
)
.
' '
.
$this
->
msg
(
'replacetext_useregex'
)->
escaped
(),
)
)
.
"
\n
"
.
Html
::
element
(
'p'
,
[
'style'
=>
'font-style: italic'
],
$this
->
msg
(
'replacetext_regexdocu'
)->
text
()
)
);
}
// The interface is heavily based on the one in Special:Search.
$namespaces
=
$this
->
searchEngineConfig
->
searchableNamespaces
();
$tables
=
$this
->
namespaceTables
(
$namespaces
);
$out
->
addHTML
(
"<div class=
\"
mw-search-formheader
\"
></div>
\n
"
.
"<fieldset class=
\"
ext-replacetext-searchoptions
\"
>
\n
"
.
Html
::
element
(
'h4'
,
[],
$this
->
msg
(
'powersearch-ns'
)->
text
()
)
);
// The ability to select/unselect groups of namespaces in the
// search interface exists only in some skins, like Vector -
// check for the presence of the 'powersearch-togglelabel'
// message to see if we can use this functionality here.
if
(
$this
->
msg
(
'powersearch-togglelabel'
)->
isDisabled
()
)
{
// do nothing
}
else
{
$out
->
addHTML
(
Html
::
rawElement
(
'div'
,
[
'class'
=>
'ext-replacetext-search-togglebox'
],
Html
::
element
(
'label'
,
[],
$this
->
msg
(
'powersearch-togglelabel'
)->
text
()
)
.
Html
::
element
(
'input'
,
[
'id'
=>
'mw-search-toggleall'
,
'type'
=>
'button'
,
'value'
=>
$this
->
msg
(
'powersearch-toggleall'
)->
text
(),
]
)
.
Html
::
element
(
'input'
,
[
'id'
=>
'mw-search-togglenone'
,
'type'
=>
'button'
,
'value'
=>
$this
->
msg
(
'powersearch-togglenone'
)->
text
()
]
)
)
);
}
$out
->
addHTML
(
Html
::
element
(
'div'
,
[
'class'
=>
'ext-replacetext-divider'
]
)
.
"$tables
\n
</fieldset>"
);
$category_search_label
=
$this
->
msg
(
'replacetext_categorysearch'
)->
escaped
();
$prefix_search_label
=
$this
->
msg
(
'replacetext_prefixsearch'
)->
escaped
();
$page_limit_label
=
$this
->
msg
(
'replacetext_pagelimit'
)->
escaped
();
$this
->
pageLimit
=
$this
->
pageLimit
===
0
?
$this
->
getConfig
()->
get
(
'ReplaceTextResultsLimit'
)
:
$this
->
pageLimit
;
$out
->
addHTML
(
"<fieldset class=
\"
ext-replacetext-searchoptions
\"
>
\n
"
.
Html
::
element
(
'h4'
,
[],
$this
->
msg
(
'replacetext_optionalfilters'
)->
text
()
)
.
Html
::
element
(
'div'
,
[
'class'
=>
'ext-replacetext-divider'
]
)
.
"<p>$category_search_label
\n
"
.
Html
::
element
(
'input'
,
[
'name'
=>
'category'
,
'size'
=>
20
,
'value'
=>
$this
->
category
]
)
.
'</p>'
.
"<p>$prefix_search_label
\n
"
.
Html
::
element
(
'input'
,
[
'name'
=>
'prefix'
,
'size'
=>
20
,
'value'
=>
$this
->
prefix
]
)
.
'</p>'
.
"<p>$page_limit_label
\n
"
.
Html
::
element
(
'input'
,
[
'name'
=>
'pageLimit'
,
'size'
=>
20
,
'value'
=>
(
string
)
$this
->
pageLimit
,
'type'
=>
'number'
,
'min'
=>
0
]
)
.
"</p></fieldset>
\n
"
.
"<p>
\n
"
.
Html
::
rawElement
(
'label'
,
[],
Html
::
input
(
'edit_pages'
,
'1'
,
'checkbox'
,
[
'checked'
=>
true
]
)
.
' '
.
$this
->
msg
(
'replacetext_editpages'
)->
escaped
()
)
.
'<br />'
.
Html
::
rawElement
(
'label'
,
[],
Html
::
input
(
'move_pages'
,
'1'
,
'checkbox'
)
.
' '
.
$this
->
msg
(
'replacetext_movepages'
)->
escaped
()
)
);
// If the user is a bot, don't even show the "Mark changes as bot edits" checkbox -
// presumably a bot user should never be allowed to make non-bot edits.
if
(
!
$this
->
permissionManager
->
userHasRight
(
$this
->
getReplaceTextUser
(),
'bot'
)
)
{
$out
->
addHTML
(
'<br />'
.
Html
::
rawElement
(
'label'
,
[],
Html
::
input
(
'botEdit'
,
'1'
,
'checkbox'
)
.
' '
.
$this
->
msg
(
'replacetext_botedit'
)->
escaped
()
)
);
}
$continueButton
=
new
OOUI\ButtonInputWidget
(
[
'type'
=>
'submit'
,
'label'
=>
$this
->
msg
(
'replacetext_continue'
)->
text
(),
'flags'
=>
[
'primary'
,
'progressive'
]
]
);
$out
->
addHTML
(
"</p>
\n
"
.
$continueButton
.
Html
::
closeElement
(
'form'
)
);
$out
->
addModuleStyles
(
'ext.ReplaceTextStyles'
);
$out
->
addModules
(
'ext.ReplaceText'
);
}
/**
* This function is not currently used, but it may get used in the
* future if the "1st screen" interface changes to use OOUI.
*
* @param string $label
* @param string $name
* @param bool $selected
* @return string HTML
*/
function
checkLabel
(
$label
,
$name
,
$selected
=
false
)
{
$checkbox
=
new
OOUI\CheckboxInputWidget
(
[
'name'
=>
$name
,
'value'
=>
1
,
'selected'
=>
$selected
]
);
$layout
=
new
OOUI\FieldLayout
(
$checkbox
,
[
'align'
=>
'inline'
,
'label'
=>
$label
]
);
return
$layout
;
}
/**
* Copied almost exactly from MediaWiki's SpecialSearch class, i.e.
* the search page
* @param string[] $namespaces
* @param int $rowsPerTable
* @return string HTML
*/
function
namespaceTables
(
$namespaces
,
$rowsPerTable
=
3
)
{
// Group namespaces into rows according to subject.
// Try not to make too many assumptions about namespace numbering.
$rows
=
[];
$tables
=
''
;
foreach
(
$namespaces
as
$ns
=>
$name
)
{
$subj
=
$this
->
namespaceInfo
->
getSubject
(
$ns
);
if
(
!
array_key_exists
(
$subj
,
$rows
)
)
{
$rows
[
$subj
]
=
''
;
}
$name
=
str_replace
(
'_'
,
' '
,
$name
);
if
(
$name
==
''
)
{
$name
=
$this
->
msg
(
'blanknamespace'
)->
text
();
}
$id
=
"mw-search-ns{$ns}"
;
$rows
[
$subj
]
.=
Html
::
openElement
(
'td'
)
.
Html
::
input
(
"ns{$ns}"
,
'1'
,
'checkbox'
,
[
'id'
=>
$id
,
'checked'
=>
(
$ns
==
0
)
]
)
.
' '
.
Html
::
label
(
$name
,
$id
)
.
Html
::
closeElement
(
'td'
)
.
"
\n
"
;
}
$rows
=
array_values
(
$rows
);
$numRows
=
count
(
$rows
);
// Lay out namespaces in multiple floating two-column tables so they'll
// be arranged nicely while still accommodating different screen widths
// Build the final HTML table...
for
(
$i
=
0
;
$i
<
$numRows
;
$i
+=
$rowsPerTable
)
{
$tables
.=
Html
::
openElement
(
'table'
);
for
(
$j
=
$i
;
$j
<
$i
+
$rowsPerTable
&&
$j
<
$numRows
;
$j
++
)
{
$tables
.=
"<tr>
\n
"
.
$rows
[
$j
]
.
"</tr>"
;
}
$tables
.=
Html
::
closeElement
(
'table'
)
.
"
\n
"
;
}
return
$tables
;
}
/**
* @param array $titles_for_edit
* @param array $titles_for_move
* @param array $uneditable_titles
* @param array $unmoveable_titles
*/
function
pageListForm
(
$titles_for_edit
,
$titles_for_move
,
$uneditable_titles
,
$unmoveable_titles
)
{
$out
=
$this
->
getOutput
();
$formOpts
=
[
'id'
=>
'choose_pages'
,
'method'
=>
'post'
,
'action'
=>
$this
->
getPageTitle
()->
getLocalURL
()
];
$out
->
addHTML
(
Html
::
openElement
(
'form'
,
$formOpts
)
.
"
\n
"
.
Html
::
hidden
(
'title'
,
$this
->
getPageTitle
()->
getPrefixedText
()
)
.
Html
::
hidden
(
'target'
,
$this
->
target
)
.
Html
::
hidden
(
'replacement'
,
$this
->
replacement
)
.
Html
::
hidden
(
'use_regex'
,
$this
->
use_regex
)
.
Html
::
hidden
(
'move_pages'
,
$this
->
move_pages
)
.
Html
::
hidden
(
'edit_pages'
,
$this
->
edit_pages
)
.
Html
::
hidden
(
'botEdit'
,
$this
->
botEdit
)
.
Html
::
hidden
(
'replace'
,
1
)
.
Html
::
hidden
(
'token'
,
$this
->
getToken
()
)
);
foreach
(
$this
->
selected_namespaces
as
$ns
)
{
$out
->
addHTML
(
Html
::
hidden
(
'ns'
.
$ns
,
1
)
);
}
$out
->
addModules
(
'ext.ReplaceText'
);
$out
->
addModuleStyles
(
'ext.ReplaceTextStyles'
);
// Only show "invert selections" link if there are more than
// five pages.
if
(
count
(
$titles_for_edit
)
+
count
(
$titles_for_move
)
>
5
)
{
$invertButton
=
new
OOUI\ButtonWidget
(
[
'label'
=>
$this
->
msg
(
'replacetext_invertselections'
)->
text
(),
'classes'
=>
[
'ext-replacetext-invert'
]
]
);
$out
->
addHTML
(
$invertButton
);
}
if
(
count
(
$titles_for_edit
)
>
0
)
{
$out
->
addWikiMsg
(
'replacetext_choosepagesforedit'
,
"<code><nowiki>{$this->targetString}</nowiki></code>"
,
"<code><nowiki>{$this->replacement}</nowiki></code>"
,
$this
->
getLanguage
()->
formatNum
(
count
(
$titles_for_edit
)
)
);
foreach
(
$titles_for_edit
as
$title_and_context
)
{
/**
* @var $title Title
*/
[
$title
,
$context
,
$role
]
=
$title_and_context
;
$checkbox
=
new
OOUI\CheckboxInputWidget
(
[
'name'
=>
$title
->
getArticleID
()
.
'|'
.
$role
,
'selected'
=>
true
]
);
if
(
$role
===
SlotRecord
::
MAIN
)
{
$labelText
=
$this
->
linkRenderer
->
makeLink
(
$title
)
.
"<br /><small>$context</small>"
;
}
else
{
$labelText
=
$this
->
linkRenderer
->
makeLink
(
$title
)
.
" ($role) <br /><small>$context</small>"
;
}
$checkboxLabel
=
new
OOUI\LabelWidget
(
[
'label'
=>
new
OOUI\HtmlSnippet
(
$labelText
)
]
);
$layout
=
new
OOUI\FieldLayout
(
$checkbox
,
[
'align'
=>
'inline'
,
'label'
=>
$checkboxLabel
]
);
$out
->
addHTML
(
$layout
);
}
$out
->
addHTML
(
'<br />'
);
}
if
(
count
(
$titles_for_move
)
>
0
)
{
$out
->
addWikiMsg
(
'replacetext_choosepagesformove'
,
$this
->
targetString
,
$this
->
replacement
,
$this
->
getLanguage
()->
formatNum
(
count
(
$titles_for_move
)
)
);
foreach
(
$titles_for_move
as
$title
)
{
$out
->
addHTML
(
Html
::
check
(
'move-'
.
$title
->
getArticleID
(),
true
)
.
"
\u
{00A0}"
.
$this
->
linkRenderer
->
makeLink
(
$title
)
.
"<br />
\n
"
);
}
$out
->
addHTML
(
'<br />'
);
$out
->
addWikiMsg
(
'replacetext_formovedpages'
);
$out
->
addHTML
(
Html
::
rawElement
(
'label'
,
[],
Html
::
input
(
'create-redirect'
,
'1'
,
'checkbox'
,
[
'checked'
=>
true
]
)
.
' '
.
$this
->
msg
(
'replacetext_savemovedpages'
)->
escaped
()
)
.
"<br />
\n
"
.
Html
::
rawElement
(
'label'
,
[],
Html
::
input
(
'watch-pages'
,
'1'
,
'checkbox'
)
.
' '
.
$this
->
msg
(
'replacetext_watchmovedpages'
)->
escaped
()
)
.
'<br />'
);
$out
->
addHTML
(
'<br />'
);
}
$submitButton
=
new
OOUI\ButtonInputWidget
(
[
'type'
=>
'submit'
,
'flags'
=>
[
'primary'
,
'progressive'
],
'label'
=>
$this
->
msg
(
'replacetext_replace'
)->
text
()
]
);
$out
->
addHTML
(
$submitButton
);
$out
->
addHTML
(
'</form>'
);
if
(
count
(
$uneditable_titles
)
)
{
$out
->
addWikiMsg
(
'replacetext_cannotedit'
,
$this
->
getLanguage
()->
formatNum
(
count
(
$uneditable_titles
)
)
);
$out
->
addHTML
(
$this
->
displayTitles
(
$uneditable_titles
)
);
}
if
(
count
(
$unmoveable_titles
)
)
{
$out
->
addWikiMsg
(
'replacetext_cannotmove'
,
$this
->
getLanguage
()->
formatNum
(
count
(
$unmoveable_titles
)
)
);
$out
->
addHTML
(
$this
->
displayTitles
(
$unmoveable_titles
)
);
}
}
/**
* Extract context and highlights search text
*
* @todo The bolding needs to be fixed for regular expressions.
* @param string $text
* @param string $target
* @param bool $use_regex
* @return string
*/
function
extractContext
(
$text
,
$target
,
$use_regex
=
false
)
{
$cw
=
$this
->
userOptionsLookup
->
getOption
(
$this
->
getUser
(),
'contextchars'
,
40
,
true
);
// Get all indexes
if
(
$use_regex
)
{
$targetq
=
str_replace
(
"/"
,
"
\\
/"
,
$target
);
preg_match_all
(
"/$targetq/Uu"
,
$text
,
$matches
,
PREG_OFFSET_CAPTURE
);
}
else
{
$targetq
=
preg_quote
(
$target
,
'/'
);
preg_match_all
(
"/$targetq/"
,
$text
,
$matches
,
PREG_OFFSET_CAPTURE
);
}
$strLengths
=
[];
$poss
=
[];
$match
=
$matches
[
0
]
??
[];
foreach
(
$match
as
$_
)
{
$strLengths
[]
=
strlen
(
$_
[
0
]
);
$poss
[]
=
$_
[
1
];
}
$cuts
=
[];
for
(
$i
=
0
;
$i
<
count
(
$poss
);
$i
++
)
{
$index
=
$poss
[
$i
];
$len
=
$strLengths
[
$i
];
// Merge to the next if possible
while
(
isset
(
$poss
[
$i
+
1
]
)
)
{
if
(
$poss
[
$i
+
1
]
<
$index
+
$len
+
$cw
*
2
)
{
$len
+=
$poss
[
$i
+
1
]
-
$poss
[
$i
];
$i
++;
}
else
{
// Can't merge, exit the inner loop
break
;
}
}
$cuts
[]
=
[
$index
,
$len
];
}
if
(
$use_regex
)
{
$targetStr
=
"/$target/Uu"
;
}
else
{
$targetq
=
preg_quote
(
$this
->
convertWhiteSpaceToHTML
(
$target
),
'/'
);
$targetStr
=
"/$targetq/i"
;
}
$context
=
''
;
foreach
(
$cuts
as
$_
)
{
[
$index
,
$len
,
]
=
$_
;
$contextBefore
=
substr
(
$text
,
0
,
$index
);
$contextAfter
=
substr
(
$text
,
$index
+
$len
);
$contextBefore
=
$this
->
getLanguage
()->
truncateForDatabase
(
$contextBefore
,
-
$cw
,
'...'
,
false
);
$contextAfter
=
$this
->
getLanguage
()->
truncateForDatabase
(
$contextAfter
,
$cw
,
'...'
,
false
);
$context
.=
$this
->
convertWhiteSpaceToHTML
(
$contextBefore
);
$snippet
=
$this
->
convertWhiteSpaceToHTML
(
substr
(
$text
,
$index
,
$len
)
);
$context
.=
preg_replace
(
$targetStr
,
'<span class="ext-replacetext-searchmatch">
\0
</span>'
,
$snippet
);
$context
.=
$this
->
convertWhiteSpaceToHTML
(
$contextAfter
);
}
// Display newlines as "line break" characters.
$context
=
str_replace
(
"
\n
"
,
"
\u
{21B5}"
,
$context
);
return
$context
;
}
/**
* Extracts the role name
*
* @param int $role_id
* @return string
*/
private
function
extractRole
(
$role_id
)
{
return
$this
->
slotRoleStore
->
getName
(
$role_id
);
}
private
function
convertWhiteSpaceToHTML
(
$message
)
{
$msg
=
htmlspecialchars
(
$message
);
$msg
=
preg_replace
(
'/^ /m'
,
"
\u
{00A0} "
,
$msg
);
$msg
=
preg_replace
(
'/ $/m'
,
"
\u
{00A0}"
,
$msg
);
$msg
=
str_replace
(
' '
,
"
\u
{00A0} "
,
$msg
);
# $msg = str_replace( "\n", '<br />', $msg );
return
$msg
;
}
private
function
getReplaceTextUser
()
{
$replaceTextUser
=
$this
->
getConfig
()->
get
(
'ReplaceTextUser'
);
if
(
$replaceTextUser
!==
null
)
{
return
$this
->
userFactory
->
newFromName
(
$replaceTextUser
);
}
return
$this
->
getUser
();
}
/**
* @inheritDoc
*/
protected
function
getGroupName
()
{
return
'wiki'
;
}
private
function
displayTitles
(
array
$titlesToDisplay
):
string
{
$text
=
"<ul>
\n
"
;
foreach
(
$titlesToDisplay
as
$title
)
{
$text
.=
"<li>"
.
$this
->
linkRenderer
->
makeLink
(
$title
)
.
"</li>
\n
"
;
}
$text
.=
"</ul>
\n
"
;
return
$text
;
}
private
function
getToken
():
string
{
return
$this
->
getContext
()->
getCsrfTokenSet
()->
getToken
();
}
private
function
checkToken
():
bool
{
return
$this
->
getContext
()->
getCsrfTokenSet
()->
matchTokenField
(
'token'
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 14:00 (1 d, 18 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
5d/0f/6f7bdaf4b26644b1d4bcf2d7c175
Default Alt Text
SpecialReplaceText.php (32 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment