Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1427520
ManageGroupsSpecialPage.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
36 KB
Referenced Files
None
Subscribers
None
ManageGroupsSpecialPage.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
MediaWiki\Extension\Translate\Synchronization
;
use
Cdb\Reader
;
use
ContentHandler
;
use
DifferenceEngine
;
use
Exception
;
use
FileBasedMessageGroup
;
use
JobQueueGroup
;
use
Language
;
use
MediaWiki\Cache\LinkBatchFactory
;
use
MediaWiki\Deferred\DeferredUpdates
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupSubscription
;
use
MediaWiki\Extension\Translate\MessageLoading\MessageHandle
;
use
MediaWiki\Extension\Translate\MessageLoading\MessageIndex
;
use
MediaWiki\Extension\Translate\MessageSync\MessageSourceChange
;
use
MediaWiki\Extension\Translate\Utilities\Utilities
;
use
MediaWiki\Html\Html
;
use
MediaWiki\Logger\LoggerFactory
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\Output\OutputPage
;
use
MediaWiki\Request\WebRequest
;
use
MediaWiki\Revision\RevisionLookup
;
use
MediaWiki\Revision\SlotRecord
;
use
MediaWiki\SpecialPage\DisabledSpecialPage
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Title\NamespaceInfo
;
use
MediaWiki\Title\Title
;
use
MessageGroup
;
use
OOUI\ButtonInputWidget
;
use
PermissionsError
;
use
RuntimeException
;
use
Skin
;
use
TextContent
;
use
UserBlockedError
;
/**
* Class for special page Special:ManageMessageGroups. On this special page
* file based message groups can be managed (FileBasedMessageGroup). This page
* allows updating of the file cache, import and fuzzy for source language
* messages, as well as import/update of messages in other languages.
*
* @author Niklas Laxström
* @author Siebrand Mazeland
* @ingroup SpecialPage TranslateSpecialPage
* @license GPL-2.0-or-later
*/
class
ManageGroupsSpecialPage
extends
SpecialPage
{
private
const
GROUP_SYNC_INFO_WRAPPER_CLASS
=
'smg-group-sync-cache-info'
;
private
const
RIGHT
=
'translate-manage'
;
protected
DifferenceEngine
$diff
;
/** Path to the change cdb file. */
protected
string
$cdb
;
/** Has the necessary right specified by the RIGHT constant */
protected
bool
$hasRight
=
false
;
private
Language
$contLang
;
private
NamespaceInfo
$nsInfo
;
private
RevisionLookup
$revLookup
;
private
GroupSynchronizationCache
$synchronizationCache
;
private
DisplayGroupSynchronizationInfo
$displayGroupSyncInfo
;
private
JobQueueGroup
$jobQueueGroup
;
private
MessageIndex
$messageIndex
;
private
LinkBatchFactory
$linkBatchFactory
;
private
MessageGroupSubscription
$messageGroupSubscription
;
public
function
__construct
(
Language
$contLang
,
NamespaceInfo
$nsInfo
,
RevisionLookup
$revLookup
,
GroupSynchronizationCache
$synchronizationCache
,
JobQueueGroup
$jobQueueGroup
,
MessageIndex
$messageIndex
,
LinkBatchFactory
$linkBatchFactory
,
MessageGroupSubscription
$messageGroupSubscription
)
{
// Anyone is allowed to see, but actions are restricted
parent
::
__construct
(
'ManageMessageGroups'
);
$this
->
contLang
=
$contLang
;
$this
->
nsInfo
=
$nsInfo
;
$this
->
revLookup
=
$revLookup
;
$this
->
synchronizationCache
=
$synchronizationCache
;
$this
->
displayGroupSyncInfo
=
new
DisplayGroupSynchronizationInfo
(
$this
,
$this
->
getLinkRenderer
()
);
$this
->
jobQueueGroup
=
$jobQueueGroup
;
$this
->
messageIndex
=
$messageIndex
;
$this
->
linkBatchFactory
=
$linkBatchFactory
;
$this
->
messageGroupSubscription
=
$messageGroupSubscription
;
}
public
function
doesWrites
()
{
return
true
;
}
protected
function
getGroupName
()
{
return
'translation'
;
}
public
function
getDescription
()
{
return
$this
->
msg
(
'managemessagegroups'
);
}
public
function
execute
(
$par
)
{
$this
->
setHeaders
();
$out
=
$this
->
getOutput
();
$out
->
addModuleStyles
(
'ext.translate.specialpages.styles'
);
$out
->
addModules
(
'ext.translate.special.managegroups'
);
$out
->
addHelpLink
(
'Help:Extension:Translate/Group_management'
);
$name
=
$par
?:
MessageChangeStorage
::
DEFAULT_NAME
;
$this
->
cdb
=
MessageChangeStorage
::
getCdbPath
(
$name
);
if
(
!
MessageChangeStorage
::
isValidCdbName
(
$name
)
||
!
file_exists
(
$this
->
cdb
)
)
{
if
(
$this
->
getConfig
()->
get
(
'TranslateGroupSynchronizationCache'
)
)
{
$out
->
addHTML
(
$this
->
displayGroupSyncInfo
->
getGroupsInSyncHtml
(
$this
->
synchronizationCache
->
getGroupsInSync
(),
self
::
GROUP_SYNC_INFO_WRAPPER_CLASS
)
);
$out
->
addHTML
(
$this
->
displayGroupSyncInfo
->
getHtmlForGroupsWithError
(
$this
->
synchronizationCache
,
self
::
GROUP_SYNC_INFO_WRAPPER_CLASS
,
$this
->
getLanguage
()
)
);
}
// @todo Tell them when changes was last checked/process
// or how to initiate recheck.
$out
->
addWikiMsg
(
'translate-smg-nochanges'
);
return
;
}
$user
=
$this
->
getUser
();
$this
->
hasRight
=
$user
->
isAllowed
(
self
::
RIGHT
);
$req
=
$this
->
getRequest
();
if
(
!
$req
->
wasPosted
()
)
{
$this
->
showChanges
(
$this
->
getLimit
()
);
return
;
}
$block
=
$user
->
getBlock
();
if
(
$block
&&
$block
->
isSitewide
()
)
{
throw
new
UserBlockedError
(
$block
,
$user
,
$this
->
getLanguage
(),
$req
->
getIP
()
);
}
$csrfTokenSet
=
$this
->
getContext
()->
getCsrfTokenSet
();
if
(
!
$this
->
hasRight
||
!
$csrfTokenSet
->
matchTokenField
(
'token'
)
)
{
throw
new
PermissionsError
(
self
::
RIGHT
);
}
$this
->
processSubmit
();
}
/** How many changes can be shown per page. */
protected
function
getLimit
():
int
{
$limits
=
[
1000
,
// Default max
ini_get
(
'max_input_vars'
),
ini_get
(
'suhosin.post.max_vars'
),
ini_get
(
'suhosin.request.max_vars'
)
];
// Ignore things not set
$limits
=
array_filter
(
$limits
);
return
(
int
)
min
(
$limits
);
}
protected
function
getLegend
():
string
{
$text
=
$this
->
diff
->
addHeader
(
''
,
$this
->
msg
(
'translate-smg-left'
)->
escaped
(),
$this
->
msg
(
'translate-smg-right'
)->
escaped
()
);
return
Html
::
rawElement
(
'div'
,
[
'class'
=>
'mw-translate-smg-header'
],
$text
);
}
protected
function
showChanges
(
int
$limit
):
void
{
$diff
=
new
DifferenceEngine
(
$this
->
getContext
()
);
$diff
->
showDiffStyle
();
$diff
->
setReducedLineNumbers
();
$this
->
diff
=
$diff
;
$out
=
$this
->
getOutput
();
$out
->
addHTML
(
Html
::
openElement
(
'form'
,
[
'method'
=>
'post'
]
)
.
Html
::
hidden
(
'title'
,
$this
->
getPageTitle
()->
getPrefixedText
(),
[
'id'
=>
'smgPageTitle'
]
)
.
Html
::
hidden
(
'token'
,
$this
->
getContext
()->
getCsrfTokenSet
()->
getToken
()
)
.
Html
::
hidden
(
'changesetModifiedTime'
,
MessageChangeStorage
::
getLastModifiedTime
(
$this
->
cdb
)
)
.
$this
->
getLegend
()
);
// The above count as three
$limit
-=
3
;
$groupSyncCacheEnabled
=
$this
->
getConfig
()->
get
(
'TranslateGroupSynchronizationCache'
);
if
(
$groupSyncCacheEnabled
)
{
$out
->
addHTML
(
$this
->
displayGroupSyncInfo
->
getGroupsInSyncHtml
(
$this
->
synchronizationCache
->
getGroupsInSync
(),
self
::
GROUP_SYNC_INFO_WRAPPER_CLASS
)
);
$out
->
addHTML
(
$this
->
displayGroupSyncInfo
->
getHtmlForGroupsWithError
(
$this
->
synchronizationCache
,
self
::
GROUP_SYNC_INFO_WRAPPER_CLASS
,
$this
->
getLanguage
()
)
);
}
$reader
=
Reader
::
open
(
$this
->
cdb
);
$groups
=
$this
->
getGroupsFromCdb
(
$reader
);
foreach
(
$groups
as
$id
=>
$group
)
{
$sourceChanges
=
MessageSourceChange
::
loadModifications
(
Utilities
::
deserialize
(
$reader
->
get
(
$id
)
)
);
$out
->
addHTML
(
Html
::
element
(
'h2'
,
[],
$group
->
getLabel
()
)
);
if
(
$groupSyncCacheEnabled
&&
$this
->
synchronizationCache
->
groupHasErrors
(
$id
)
)
{
$out
->
addHTML
(
Html
::
warningBox
(
$this
->
msg
(
'translate-smg-group-sync-error-warn'
)->
escaped
(),
'center'
)
);
}
// Reduce page existence queries to one per group
$lb
=
$this
->
linkBatchFactory
->
newLinkBatch
();
$ns
=
$group
->
getNamespace
();
$isCap
=
$this
->
nsInfo
->
isCapitalized
(
$ns
);
$languages
=
$sourceChanges
->
getLanguages
();
foreach
(
$languages
as
$language
)
{
$languageChanges
=
$sourceChanges
->
getModificationsForLanguage
(
$language
);
foreach
(
$languageChanges
as
$changes
)
{
foreach
(
$changes
as
$params
)
{
// Constructing title objects is way slower
$key
=
$params
[
'key'
];
if
(
$isCap
)
{
$key
=
$this
->
contLang
->
ucfirst
(
$key
);
}
$lb
->
add
(
$ns
,
"$key/$language"
);
}
}
}
$lb
->
execute
();
foreach
(
$languages
as
$language
)
{
// Handle and generate UI for additions, deletions, change
$changes
=
[];
$changes
[
MessageSourceChange
::
ADDITION
]
=
$sourceChanges
->
getAdditions
(
$language
);
$changes
[
MessageSourceChange
::
DELETION
]
=
$sourceChanges
->
getDeletions
(
$language
);
$changes
[
MessageSourceChange
::
CHANGE
]
=
$sourceChanges
->
getChanges
(
$language
);
foreach
(
$changes
as
$type
=>
$messages
)
{
foreach
(
$messages
as
$params
)
{
$change
=
$this
->
formatChange
(
$group
,
$sourceChanges
,
$language
,
$type
,
$params
,
$limit
);
$out
->
addHTML
(
$change
);
if
(
$limit
<=
0
)
{
// We need to restrict the changes per page per form submission
// limitations as well as performance.
$out
->
wrapWikiMsg
(
"<div class=warning>
\n
$1
\n
</div>"
,
'translate-smg-more'
);
break
4
;
}
}
}
// Handle and generate UI for renames
$this
->
showRenames
(
$group
,
$sourceChanges
,
$out
,
$language
,
$limit
);
}
}
$out
->
enableOOUI
();
$button
=
new
ButtonInputWidget
(
[
'type'
=>
'submit'
,
'label'
=>
$this
->
msg
(
'translate-smg-submit'
)->
plain
(),
'disabled'
=>
!
$this
->
hasRight
?
'disabled'
:
null
,
'classes'
=>
[
'mw-translate-smg-submit'
],
'title'
=>
!
$this
->
hasRight
?
$this
->
msg
(
'translate-smg-notallowed'
)->
plain
()
:
null
,
'flags'
=>
[
'primary'
,
'progressive'
],
]
);
$out
->
addHTML
(
$button
);
$out
->
addHTML
(
Html
::
closeElement
(
'form'
)
);
}
protected
function
formatChange
(
MessageGroup
$group
,
MessageSourceChange
$changes
,
string
$language
,
string
$type
,
array
$params
,
int
&
$limit
):
string
{
$key
=
$params
[
'key'
];
$title
=
Title
::
makeTitleSafe
(
$group
->
getNamespace
(),
"$key/$language"
);
$id
=
self
::
changeId
(
$group
->
getId
(),
$language
,
$type
,
$key
);
$noticeHtml
=
''
;
$isReusedKey
=
false
;
if
(
$title
&&
$type
===
'addition'
&&
$title
->
exists
()
)
{
// The message has for some reason dropped out from cache
// or, perhaps it is being reused. In any case treat it
// as a change for display, so the admin can see if
// action is needed and let the message be processed.
// Otherwise, it will end up in the postponed category
// forever and will prevent rebuilding the cache, which
// leads to many other annoying problems.
$type
=
'change'
;
$noticeHtml
.=
Html
::
warningBox
(
$this
->
msg
(
'translate-manage-key-reused'
)->
parse
()
);
$isReusedKey
=
true
;
}
elseif
(
$title
&&
(
$type
===
'deletion'
||
$type
===
'change'
)
&&
!
$title
->
exists
()
)
{
// This happens if a message key has been renamed
// The change can be ignored.
return
''
;
}
$text
=
''
;
$titleLink
=
$this
->
getLinkRenderer
()->
makeLink
(
$title
);
if
(
$type
===
'deletion'
)
{
$revTitle
=
$this
->
revLookup
->
getRevisionByTitle
(
$title
);
if
(
!
$revTitle
)
{
wfWarn
(
"[ManageGroupSpecialPage] No revision associated with {$title->getPrefixedText()}"
);
}
$content
=
$revTitle
?
$revTitle
->
getContent
(
SlotRecord
::
MAIN
)
:
null
;
$wiki
=
(
$content
instanceof
TextContent
)
?
$content
->
getText
()
:
''
;
if
(
$wiki
===
''
)
{
$noticeHtml
.=
Html
::
warningBox
(
$this
->
msg
(
'translate-manage-empty-content'
)->
parse
()
);
}
$oldContent
=
ContentHandler
::
makeContent
(
(
string
)
$wiki
,
$title
);
$newContent
=
ContentHandler
::
makeContent
(
''
,
$title
);
$this
->
diff
->
setContent
(
$oldContent
,
$newContent
);
$text
=
$this
->
diff
->
getDiff
(
$titleLink
,
''
,
$noticeHtml
);
}
elseif
(
$type
===
'addition'
)
{
$menu
=
''
;
$sourceLanguage
=
$group
->
getSourceLanguage
();
if
(
$sourceLanguage
===
$language
)
{
if
(
$this
->
hasRight
)
{
$menu
=
Html
::
rawElement
(
'button'
,
[
'class'
=>
'smg-rename-actions'
,
'type'
=>
'button'
,
'data-group-id'
=>
$group
->
getId
(),
'data-lang'
=>
$language
,
'data-msgkey'
=>
$key
,
'data-msgtitle'
=>
$title
->
getFullText
()
]
);
}
}
elseif
(
!
self
::
isMessageDefinitionPresent
(
$group
,
$changes
,
$key
)
)
{
$noticeHtml
.=
Html
::
warningBox
(
$this
->
msg
(
'translate-manage-source-message-not-found'
)->
parse
(),
'mw-translate-smg-notice-important'
);
// Automatically ignore messages that don't have a definitions
$menu
=
Html
::
hidden
(
"msg/$id"
,
'ignore'
,
[
'id'
=>
"i/$id"
]
);
$limit
--;
}
if
(
$params
[
'content'
]
===
''
)
{
$noticeHtml
.=
Html
::
warningBox
(
$this
->
msg
(
'translate-manage-empty-content'
)->
parse
()
);
}
$oldContent
=
ContentHandler
::
makeContent
(
''
,
$title
);
$newContent
=
ContentHandler
::
makeContent
(
(
string
)
$params
[
'content'
],
$title
);
$this
->
diff
->
setContent
(
$oldContent
,
$newContent
);
$text
=
$this
->
diff
->
getDiff
(
''
,
$titleLink
.
$menu
,
$noticeHtml
);
}
elseif
(
$type
===
'change'
)
{
$wiki
=
Utilities
::
getContentForTitle
(
$title
,
true
);
$actions
=
''
;
$sourceLanguage
=
$group
->
getSourceLanguage
();
// Option to fuzzy is only available for source languages, and should be used
// if content has changed.
$shouldFuzzy
=
$sourceLanguage
===
$language
&&
$wiki
!==
$params
[
'content'
];
if
(
$sourceLanguage
===
$language
)
{
$label
=
$this
->
msg
(
'translate-manage-action-fuzzy'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"fuzzy"
,
$shouldFuzzy
);
}
if
(
$sourceLanguage
!==
$language
&&
$isReusedKey
&&
!
self
::
isMessageDefinitionPresent
(
$group
,
$changes
,
$key
)
)
{
$noticeHtml
.=
Html
::
warningBox
(
$this
->
msg
(
'translate-manage-source-message-not-found'
)->
parse
(),
'mw-translate-smg-notice-important'
);
// Automatically ignore messages that don't have a definitions
$actions
.=
Html
::
hidden
(
"msg/$id"
,
'ignore'
,
[
'id'
=>
"i/$id"
]
);
$limit
--;
}
else
{
$label
=
$this
->
msg
(
'translate-manage-action-import'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"import"
,
!
$shouldFuzzy
);
$label
=
$this
->
msg
(
'translate-manage-action-ignore'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"ignore"
);
$limit
--;
}
$oldContent
=
ContentHandler
::
makeContent
(
(
string
)
$wiki
,
$title
);
$newContent
=
ContentHandler
::
makeContent
(
(
string
)
$params
[
'content'
],
$title
);
$this
->
diff
->
setContent
(
$oldContent
,
$newContent
);
$text
.=
$this
->
diff
->
getDiff
(
$titleLink
,
$actions
,
$noticeHtml
);
}
$hidden
=
Html
::
hidden
(
$id
,
1
);
$limit
--;
$text
.=
$hidden
;
$classes
=
"mw-translate-smg-change smg-change-$type"
;
if
(
$limit
<
0
)
{
// Don't add if one of the fields might get dropped of at submission
return
''
;
}
return
Html
::
rawElement
(
'div'
,
[
'class'
=>
$classes
],
$text
);
}
protected
function
processSubmit
():
void
{
$req
=
$this
->
getRequest
();
$out
=
$this
->
getOutput
();
$errorGroups
=
[];
$modificationJobs
=
$renameJobData
=
[];
$lastModifiedTime
=
intval
(
$req
->
getVal
(
'changesetModifiedTime'
)
);
if
(
!
MessageChangeStorage
::
isModifiedSince
(
$this
->
cdb
,
$lastModifiedTime
)
)
{
$out
->
addWikiMsg
(
'translate-smg-changeset-modified'
);
return
;
}
$reader
=
Reader
::
open
(
$this
->
cdb
);
$groups
=
$this
->
getGroupsFromCdb
(
$reader
);
$groupSyncCacheEnabled
=
$this
->
getConfig
()->
get
(
'TranslateGroupSynchronizationCache'
);
$postponed
=
[];
foreach
(
$groups
as
$groupId
=>
$group
)
{
try
{
if
(
!
$group
instanceof
FileBasedMessageGroup
)
{
throw
new
RuntimeException
(
"Expected $groupId to be FileBasedMessageGroup, got "
.
get_class
(
$group
)
.
" instead."
);
}
$changes
=
Utilities
::
deserialize
(
$reader
->
get
(
$groupId
)
);
if
(
$groupSyncCacheEnabled
&&
$this
->
synchronizationCache
->
groupHasErrors
(
$groupId
)
)
{
$postponed
[
$groupId
]
=
$changes
;
continue
;
}
$sourceChanges
=
MessageSourceChange
::
loadModifications
(
$changes
);
$groupModificationJobs
=
[];
$groupRenameJobData
=
[];
$languages
=
$sourceChanges
->
getLanguages
();
foreach
(
$languages
as
$language
)
{
// Handle changes, additions, deletions
$this
->
handleModificationsSubmit
(
$group
,
$sourceChanges
,
$req
,
$language
,
$postponed
,
$groupModificationJobs
);
// Handle renames, this might also add modification jobs based on user selection.
$this
->
handleRenameSubmit
(
$group
,
$sourceChanges
,
$req
,
$language
,
$postponed
,
$groupRenameJobData
,
$groupModificationJobs
);
if
(
!
isset
(
$postponed
[
$groupId
][
$language
]
)
)
{
$group
->
getMessageGroupCache
(
$language
)->
create
();
}
}
if
(
$groupSyncCacheEnabled
&&
!
isset
(
$postponed
[
$groupId
]
)
)
{
$this
->
synchronizationCache
->
markGroupAsReviewed
(
$groupId
);
}
$modificationJobs
[
$groupId
]
=
$groupModificationJobs
;
$renameJobData
[
$groupId
]
=
$groupRenameJobData
;
}
catch
(
Exception
$e
)
{
error_log
(
"ManageGroupsSpecialPage: Error in processSubmit. Group: $groupId
\n
"
.
"Exception: $e"
);
$errorGroups
[]
=
$group
->
getLabel
();
}
}
$this
->
messageGroupSubscription
->
queueNotificationJob
();
$renameJobs
=
$this
->
createRenameJobs
(
$renameJobData
);
$this
->
startSync
(
$modificationJobs
,
$renameJobs
);
$reader
->
close
();
rename
(
$this
->
cdb
,
$this
->
cdb
.
'-'
.
wfTimestamp
()
);
if
(
$errorGroups
)
{
$errorMsg
=
$this
->
getProcessingErrorMessage
(
$errorGroups
,
count
(
$groups
)
);
$out
->
addHTML
(
Html
::
warningBox
(
$errorMsg
,
'mw-translate-smg-submitted'
)
);
}
if
(
count
(
$postponed
)
)
{
$postponedSourceChanges
=
[];
foreach
(
$postponed
as
$groupId
=>
$changes
)
{
$postponedSourceChanges
[
$groupId
]
=
MessageSourceChange
::
loadModifications
(
$changes
);
}
MessageChangeStorage
::
writeChanges
(
$postponedSourceChanges
,
$this
->
cdb
);
$this
->
showChanges
(
$this
->
getLimit
()
);
}
elseif
(
$errorGroups
===
[]
)
{
$out
->
addWikiMsg
(
'translate-smg-submitted'
);
}
}
protected
static
function
changeId
(
string
$groupId
,
string
$language
,
string
$type
,
string
$key
):
string
{
return
'smg/'
.
substr
(
sha1
(
"$groupId/$language/$type/$key"
),
0
,
7
);
}
/**
* Adds the task-based tabs on Special:Translate and few other special pages.
* Hook: SkinTemplateNavigation::Universal
*/
public
static
function
tabify
(
Skin
$skin
,
array
&
$tabs
):
void
{
$title
=
$skin
->
getTitle
();
if
(
!
$title
->
isSpecialPage
()
)
{
return
;
}
$specialPageFactory
=
MediaWikiServices
::
getInstance
()->
getSpecialPageFactory
();
[
$alias
,
]
=
$specialPageFactory
->
resolveAlias
(
$title
->
getText
()
);
$pagesInGroup
=
[
'ManageMessageGroups'
=>
'namespaces'
,
'AggregateGroups'
=>
'namespaces'
,
'SupportedLanguages'
=>
'views'
,
'TranslationStats'
=>
'views'
,
];
if
(
!
isset
(
$pagesInGroup
[
$alias
]
)
)
{
return
;
}
$tabs
[
'namespaces'
]
=
[];
foreach
(
$pagesInGroup
as
$spName
=>
$section
)
{
$spClass
=
$specialPageFactory
->
getPage
(
$spName
);
if
(
$spClass
===
null
||
$spClass
instanceof
DisabledSpecialPage
)
{
continue
;
// Page explicitly disabled
}
$spTitle
=
$spClass
->
getPageTitle
();
$tabs
[
$section
][
strtolower
(
$spName
)]
=
[
'text'
=>
$spClass
->
getDescription
(),
'href'
=>
$spTitle
->
getLocalURL
(),
'class'
=>
$alias
===
$spName
?
'selected'
:
''
,
];
}
}
/**
* Check if the message definition is present as an incoming addition
* OR exists already on the wiki
*/
private
static
function
isMessageDefinitionPresent
(
MessageGroup
$group
,
MessageSourceChange
$changes
,
string
$msgKey
):
bool
{
$sourceLanguage
=
$group
->
getSourceLanguage
();
if
(
$changes
->
findMessage
(
$sourceLanguage
,
$msgKey
,
[
MessageSourceChange
::
ADDITION
]
)
)
{
return
true
;
}
$namespace
=
$group
->
getNamespace
();
$sourceHandle
=
new
MessageHandle
(
Title
::
makeTitle
(
$namespace
,
$msgKey
)
);
return
$sourceHandle
->
isValid
();
}
private
function
showRenames
(
MessageGroup
$group
,
MessageSourceChange
$sourceChanges
,
OutputPage
$out
,
string
$language
,
int
&
$limit
):
void
{
$changes
=
$sourceChanges
->
getRenames
(
$language
);
foreach
(
$changes
as
$key
=>
$params
)
{
// Since we're removing items from the array within the loop add
// a check here to ensure that the current key is still set.
if
(
!
isset
(
$changes
[
$key
]
)
)
{
continue
;
}
if
(
$group
->
getSourceLanguage
()
!==
$language
&&
$sourceChanges
->
isEqual
(
$language
,
$key
)
)
{
// This is a translation rename, that does not have any changes.
// We can group this along with the source rename.
continue
;
}
// Determine added key, and corresponding removed key.
$firstMsg
=
$params
;
$secondKey
=
$sourceChanges
->
getMatchedKey
(
$language
,
$key
)
??
''
;
$secondMsg
=
$sourceChanges
->
getMatchedMessage
(
$language
,
$key
);
if
(
$secondMsg
===
null
)
{
throw
new
RuntimeException
(
"Could not find matched message for $key"
);
}
if
(
$sourceChanges
->
isPreviousState
(
$language
,
$key
,
[
MessageSourceChange
::
ADDITION
,
MessageSourceChange
::
CHANGE
]
)
)
{
$addedMsg
=
$firstMsg
;
$deletedMsg
=
$secondMsg
;
}
else
{
$addedMsg
=
$secondMsg
;
$deletedMsg
=
$firstMsg
;
}
$change
=
$this
->
formatRename
(
$group
,
$addedMsg
,
$deletedMsg
,
$language
,
$sourceChanges
->
isEqual
(
$language
,
$key
),
$limit
);
$out
->
addHTML
(
$change
);
// no need to process the second key again.
unset
(
$changes
[
$secondKey
]
);
if
(
$limit
<=
0
)
{
// We need to restrict the changes per page per form submission
// limitations as well as performance.
$out
->
wrapWikiMsg
(
"<div class=warning>
\n
$1
\n
</div>"
,
'translate-smg-more'
);
break
;
}
}
}
private
function
formatRename
(
MessageGroup
$group
,
array
$addedMsg
,
array
$deletedMsg
,
string
$language
,
bool
$isEqual
,
int
&
$limit
):
string
{
$addedKey
=
$addedMsg
[
'key'
];
$deletedKey
=
$deletedMsg
[
'key'
];
$actions
=
''
;
$addedTitle
=
Title
::
makeTitleSafe
(
$group
->
getNamespace
(),
"$addedKey/$language"
);
$deletedTitle
=
Title
::
makeTitleSafe
(
$group
->
getNamespace
(),
"$deletedKey/$language"
);
$id
=
self
::
changeId
(
$group
->
getId
(),
$language
,
MessageSourceChange
::
RENAME
,
$addedKey
);
$addedTitleLink
=
$this
->
getLinkRenderer
()->
makeLink
(
$addedTitle
);
$deletedTitleLink
=
$this
->
getLinkRenderer
()->
makeLink
(
$deletedTitle
);
$renameSelected
=
true
;
if
(
$group
->
getSourceLanguage
()
===
$language
)
{
if
(
!
$isEqual
)
{
$renameSelected
=
false
;
$label
=
$this
->
msg
(
'translate-manage-action-rename-fuzzy'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"renamefuzzy"
,
true
);
}
$label
=
$this
->
msg
(
'translate-manage-action-rename'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"rename"
,
$renameSelected
);
}
else
{
$label
=
$this
->
msg
(
'translate-manage-action-import'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"import"
,
true
);
}
if
(
$group
->
getSourceLanguage
()
!==
$language
)
{
// Allow user to ignore changes to non-source languages.
$label
=
$this
->
msg
(
'translate-manage-action-ignore-change'
)->
text
();
$actions
.=
$this
->
radioLabel
(
$label
,
"msg/$id"
,
"ignore"
);
}
$limit
--;
$addedContent
=
ContentHandler
::
makeContent
(
(
string
)
$addedMsg
[
'content'
],
$addedTitle
);
$deletedContent
=
ContentHandler
::
makeContent
(
(
string
)
$deletedMsg
[
'content'
],
$deletedTitle
);
$this
->
diff
->
setContent
(
$deletedContent
,
$addedContent
);
$menu
=
''
;
if
(
$group
->
getSourceLanguage
()
===
$language
&&
$this
->
hasRight
)
{
// Only show rename and add as new option for source language.
$menu
=
Html
::
rawElement
(
'button'
,
[
'class'
=>
'smg-rename-actions'
,
'type'
=>
'button'
,
'data-group-id'
=>
$group
->
getId
(),
'data-msgkey'
=>
$addedKey
,
'data-msgtitle'
=>
$addedTitle
->
getFullText
()
]
);
}
$actions
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'smg-change-import-options'
],
$actions
);
$text
=
$this
->
diff
->
getDiff
(
$deletedTitleLink
,
$addedTitleLink
.
$menu
.
$actions
,
$isEqual
?
htmlspecialchars
(
$addedMsg
[
'content'
]
)
:
''
);
$hidden
=
Html
::
hidden
(
$id
,
1
);
$limit
--;
$text
.=
$hidden
;
return
Html
::
rawElement
(
'div'
,
[
'class'
=>
'mw-translate-smg-change smg-change-rename'
],
$text
);
}
private
function
getRenameJobParams
(
array
$currentMsg
,
MessageSourceChange
$sourceChanges
,
string
$languageCode
,
int
$groupNamespace
,
string
$selectedVal
,
bool
$isSourceLang
=
true
):
?
array
{
if
(
$selectedVal
===
'ignore'
)
{
return
null
;
}
$params
=
[];
$currentMsgKey
=
$currentMsg
[
'key'
];
$matchedMsg
=
$sourceChanges
->
getMatchedMessage
(
$languageCode
,
$currentMsgKey
);
if
(
$matchedMsg
===
null
)
{
throw
new
RuntimeException
(
"Could not find matched message for $currentMsgKey."
);
}
$matchedMsgKey
=
$matchedMsg
[
'key'
];
if
(
$sourceChanges
->
isPreviousState
(
$languageCode
,
$currentMsgKey
,
[
MessageSourceChange
::
ADDITION
,
MessageSourceChange
::
CHANGE
]
)
)
{
$params
[
'target'
]
=
$matchedMsgKey
;
$params
[
'replacement'
]
=
$currentMsgKey
;
$replacementContent
=
$currentMsg
[
'content'
];
}
else
{
$params
[
'target'
]
=
$currentMsgKey
;
$params
[
'replacement'
]
=
$matchedMsgKey
;
$replacementContent
=
$matchedMsg
[
'content'
];
}
$params
[
'fuzzy'
]
=
$selectedVal
===
'renamefuzzy'
;
$params
[
'content'
]
=
$replacementContent
;
if
(
$isSourceLang
)
{
$params
[
'targetTitle'
]
=
Title
::
newFromText
(
Utilities
::
title
(
$params
[
'target'
],
$languageCode
,
$groupNamespace
),
$groupNamespace
);
$params
[
'others'
]
=
[];
}
return
$params
;
}
private
function
handleRenameSubmit
(
MessageGroup
$group
,
MessageSourceChange
$sourceChanges
,
WebRequest
$req
,
string
$language
,
array
&
$postponed
,
array
&
$jobData
,
array
&
$modificationJobs
):
void
{
$groupId
=
$group
->
getId
();
$renames
=
$sourceChanges
->
getRenames
(
$language
);
$isSourceLang
=
$group
->
getSourceLanguage
()
===
$language
;
$groupNamespace
=
$group
->
getNamespace
();
foreach
(
$renames
as
$key
=>
$params
)
{
// Since we're removing items from the array within the loop add
// a check here to ensure that the current key is still set.
if
(
!
isset
(
$renames
[
$key
]
)
)
{
continue
;
}
$id
=
self
::
changeId
(
$groupId
,
$language
,
MessageSourceChange
::
RENAME
,
$key
);
[
$renameMissing
,
$isCurrentKeyPresent
]
=
$this
->
isRenameMissing
(
$req
,
$sourceChanges
,
$id
,
$key
,
$language
,
$groupId
,
$isSourceLang
);
if
(
$renameMissing
)
{
// we probably hit the limit with number of post parameters since neither
// addition nor deletion key is present.
$postponed
[
$groupId
][
$language
][
MessageSourceChange
::
RENAME
][
$key
]
=
$params
;
continue
;
}
if
(
!
$isCurrentKeyPresent
)
{
// still don't process this key, and wait for the matched rename
continue
;
}
$selectedVal
=
$req
->
getVal
(
"msg/$id"
);
$jobParams
=
$this
->
getRenameJobParams
(
$params
,
$sourceChanges
,
$language
,
$groupNamespace
,
$selectedVal
,
$isSourceLang
);
if
(
$jobParams
===
null
)
{
continue
;
}
$targetStr
=
$jobParams
[
'target'
];
if
(
$isSourceLang
)
{
$jobData
[
$targetStr
]
=
$jobParams
;
// Send notification for fuzzy items
if
(
isset
(
$jobParams
[
'targetTitle'
]
)
&&
(
$jobParams
[
'fuzzy'
]
??
false
)
)
{
$this
->
messageGroupSubscription
->
queueMessage
(
$jobParams
[
'targetTitle'
],
MessageGroupSubscription
::
STATE_UPDATED
,
[
$groupId
]
);
}
}
elseif
(
isset
(
$jobData
[
$targetStr
]
)
)
{
// We are grouping the source rename, and content changes in other languages
// for the message together into a single job in order to avoid race conditions
// since jobs are not guaranteed to be run in order.
$jobData
[
$targetStr
][
'others'
][
$language
]
=
$jobParams
[
'content'
];
}
else
{
// the source was probably ignored, we should add this as a modification instead,
// since the source is not going to be renamed.
$title
=
Title
::
newFromText
(
Utilities
::
title
(
$targetStr
,
$language
,
$groupNamespace
),
$groupNamespace
);
$modificationJobs
[]
=
UpdateMessageJob
::
newJob
(
$title
,
$jobParams
[
'content'
]
);
}
// remove the matched key in order to avoid double processing.
$matchedKey
=
$sourceChanges
->
getMatchedKey
(
$language
,
$key
);
unset
(
$renames
[
$matchedKey
]
);
}
}
private
function
handleModificationsSubmit
(
MessageGroup
$group
,
MessageSourceChange
$sourceChanges
,
WebRequest
$req
,
string
$language
,
array
&
$postponed
,
array
&
$messageUpdateJob
):
void
{
$groupId
=
$group
->
getId
();
$subChanges
=
$sourceChanges
->
getModificationsForLanguage
(
$language
);
$isSourceLanguage
=
$group
->
getSourceLanguage
()
===
$language
;
// Ignore renames
unset
(
$subChanges
[
MessageSourceChange
::
RENAME
]
);
// Handle additions, deletions, and changes.
foreach
(
$subChanges
as
$type
=>
$messages
)
{
foreach
(
$messages
as
$index
=>
$params
)
{
$key
=
$params
[
'key'
];
$id
=
self
::
changeId
(
$groupId
,
$language
,
$type
,
$key
);
$title
=
Title
::
makeTitleSafe
(
$group
->
getNamespace
(),
"$key/$language"
);
if
(
!
$this
->
isTitlePresent
(
$title
,
$type
)
)
{
continue
;
}
if
(
!
$req
->
getCheck
(
$id
)
)
{
// We probably hit the limit with number of post parameters.
$postponed
[
$groupId
][
$language
][
$type
][
$index
]
=
$params
;
continue
;
}
$selectedVal
=
$req
->
getVal
(
"msg/$id"
);
if
(
$type
===
MessageSourceChange
::
DELETION
||
$selectedVal
===
'ignore'
)
{
continue
;
}
$fuzzy
=
$selectedVal
===
'fuzzy'
;
$messageUpdateJob
[]
=
UpdateMessageJob
::
newJob
(
$title
,
$params
[
'content'
],
$fuzzy
);
if
(
$isSourceLanguage
)
{
$this
->
sendNotificationsForChangedMessages
(
$groupId
,
$title
,
$type
,
$fuzzy
);
}
}
}
}
/** @return UpdateMessageJob[][] */
private
function
createRenameJobs
(
array
$jobParams
):
array
{
$jobs
=
[];
foreach
(
$jobParams
as
$groupId
=>
$groupJobParams
)
{
$jobs
[
$groupId
]
??=
[];
foreach
(
$groupJobParams
as
$params
)
{
$jobs
[
$groupId
][]
=
UpdateMessageJob
::
newRenameJob
(
$params
[
'targetTitle'
],
$params
[
'target'
],
$params
[
'replacement'
],
$params
[
'fuzzy'
],
$params
[
'content'
],
$params
[
'others'
]
);
}
}
return
$jobs
;
}
/** Checks if a title still exists and can be processed. */
private
function
isTitlePresent
(
Title
$title
,
string
$type
):
bool
{
// phpcs:ignore SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn
if
(
(
$type
===
MessageSourceChange
::
DELETION
||
$type
===
MessageSourceChange
::
CHANGE
)
&&
!
$title
->
exists
()
)
{
// This means that this change was probably introduced due to a rename
// which removed the key. No need to process.
return
false
;
}
return
true
;
}
/**
* Checks if a renamed message key is missing from the user request submission.
* Checks the current key and the matched key. This is needed because as the
* keys in the wiki are not submitted along with the request, only the incoming
* modified keys are submitted.
* @return bool[]
* $response = [
* 0 => (bool) True if rename is missing, false otherwise.
* 1 => (bool) Was the current $id found?
* ]
*/
private
function
isRenameMissing
(
WebRequest
$req
,
MessageSourceChange
$sourceChanges
,
string
$id
,
string
$key
,
string
$language
,
string
$groupId
,
bool
$isSourceLang
):
array
{
if
(
$req
->
getCheck
(
$id
)
)
{
return
[
false
,
true
];
}
$isCurrentKeyPresent
=
false
;
// Checked the matched key is also missing to confirm if its truly missing
$matchedKey
=
$sourceChanges
->
getMatchedKey
(
$language
,
$key
);
$matchedId
=
self
::
changeId
(
$groupId
,
$language
,
MessageSourceChange
::
RENAME
,
$matchedKey
);
if
(
$req
->
getCheck
(
$matchedId
)
)
{
return
[
false
,
$isCurrentKeyPresent
];
}
// For non source language, if strings are equal, they are not shown on the UI
// and hence not submitted.
return
[
$isSourceLang
||
!
$sourceChanges
->
isEqual
(
$language
,
$matchedKey
),
$isCurrentKeyPresent
];
}
private
function
getProcessingErrorMessage
(
array
$errorGroups
,
int
$totalGroupCount
):
string
{
// Number of error groups, are less than the total groups processed.
if
(
count
(
$errorGroups
)
<
$totalGroupCount
)
{
$errorMsg
=
$this
->
msg
(
'translate-smg-submitted-with-failure'
)
->
numParams
(
count
(
$errorGroups
)
)
->
params
(
$this
->
getLanguage
()->
commaList
(
$errorGroups
),
$this
->
msg
(
'translate-smg-submitted-others-processing'
)
)->
parse
();
}
else
{
$errorMsg
=
trim
(
$this
->
msg
(
'translate-smg-submitted-with-failure'
)
->
numParams
(
count
(
$errorGroups
)
)
->
params
(
$this
->
getLanguage
()->
commaList
(
$errorGroups
),
''
)
->
parse
()
);
}
return
$errorMsg
;
}
/** @return array<int|string, MessageGroup> */
private
function
getGroupsFromCdb
(
Reader
$reader
):
array
{
$groups
=
[];
$groupIds
=
Utilities
::
deserialize
(
$reader
->
get
(
'#keys'
)
);
foreach
(
$groupIds
as
$id
)
{
$groups
[
$id
]
=
MessageGroups
::
getGroup
(
$id
);
}
return
array_filter
(
$groups
);
}
/**
* Add jobs to the queue, updates the interim cache, and start sync process for the group.
* @param UpdateMessageJob[][] $modificationJobs
* @param UpdateMessageJob[][] $renameJobs
*/
private
function
startSync
(
array
$modificationJobs
,
array
$renameJobs
):
void
{
// We are adding an empty array for groups that have no jobs. This is mainly done to
// avoid adding unnecessary checks. Remove those using array_filter
$modificationGroupIds
=
array_keys
(
array_filter
(
$modificationJobs
)
);
$renameGroupIds
=
array_keys
(
array_filter
(
$renameJobs
)
);
$uniqueGroupIds
=
array_unique
(
array_merge
(
$modificationGroupIds
,
$renameGroupIds
)
);
$jobQueueInstance
=
$this
->
jobQueueGroup
;
foreach
(
$uniqueGroupIds
as
$groupId
)
{
$messages
=
[];
$messageKeys
=
[];
$groupJobs
=
[];
$groupRenameJobs
=
$renameJobs
[
$groupId
]
??
[];
/** @var UpdateMessageJob $job */
foreach
(
$groupRenameJobs
as
$job
)
{
$groupJobs
[]
=
$job
;
$messageUpdateParam
=
MessageUpdateParameter
::
createFromJob
(
$job
);
$messages
[]
=
$messageUpdateParam
;
// Build the handle to add the message key in interim cache
$replacement
=
$messageUpdateParam
->
getReplacementValue
();
$targetTitle
=
Title
::
makeTitle
(
$job
->
getTitle
()->
getNamespace
(),
$replacement
);
$messageKeys
[]
=
(
new
MessageHandle
(
$targetTitle
)
)->
getKey
();
}
$groupModificationJobs
=
$modificationJobs
[
$groupId
]
??
[];
/** @var UpdateMessageJob $job */
foreach
(
$groupModificationJobs
as
$job
)
{
$groupJobs
[]
=
$job
;
$messageUpdateParam
=
MessageUpdateParameter
::
createFromJob
(
$job
);
$messages
[]
=
$messageUpdateParam
;
$messageKeys
[]
=
(
new
MessageHandle
(
$job
->
getTitle
()
)
)->
getKey
();
}
// Store all message keys in the interim cache - we're particularly interested in new
// and renamed messages, but it's cleaner to just store everything.
$group
=
MessageGroups
::
getGroup
(
$groupId
);
$this
->
messageIndex
->
storeInterim
(
$group
,
$messageKeys
);
if
(
$this
->
getConfig
()->
get
(
'TranslateGroupSynchronizationCache'
)
)
{
$this
->
synchronizationCache
->
addMessages
(
$groupId
,
...
$messages
);
$this
->
synchronizationCache
->
markGroupForSync
(
$groupId
);
LoggerFactory
::
getInstance
(
'Translate.GroupSynchronization'
)->
info
(
'['
.
__CLASS__
.
'] Synchronization started for {groupId} by {user}'
,
[
'groupId'
=>
$groupId
,
'user'
=>
$this
->
getUser
()->
getName
()
]
);
}
// There is possibility for a race condition here: the translate_cache table / group sync
// cache is not yet populated with the messages to be processed, but the jobs start
// running and try to remove the message from the cache. This results in a "Key not found"
// error. Avoid this condition by using a DeferredUpdate.
DeferredUpdates
::
addCallableUpdate
(
static
function
()
use
(
$jobQueueInstance
,
$groupJobs
)
{
$jobQueueInstance
->
push
(
$groupJobs
);
}
);
}
}
private
function
radioLabel
(
string
$label
,
string
$name
,
string
$value
,
bool
$checked
=
false
):
string
{
return
Html
::
rawElement
(
'label'
,
[],
Html
::
radio
(
$name
,
$checked
,
[
'value'
=>
$value
]
)
.
"
\u
{00A0}"
.
$label
);
}
private
function
sendNotificationsForChangedMessages
(
string
$groupId
,
Title
$title
,
$type
,
bool
$fuzzy
):
void
{
$subscriptionState
=
$type
===
MessageSourceChange
::
ADDITION
?
MessageGroupSubscription
::
STATE_ADDED
:
MessageGroupSubscription
::
STATE_UPDATED
;
if
(
$subscriptionState
===
MessageGroupSubscription
::
STATE_UPDATED
&&
!
$fuzzy
)
{
// If the state is updated, but the change has not been marked as fuzzy,
// lets not send a notification.
$subscriptionState
=
null
;
}
if
(
$subscriptionState
)
{
$this
->
messageGroupSubscription
->
queueMessage
(
$title
,
$subscriptionState
,
[
$groupId
]
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 14:43 (1 d, 3 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
b7/3b/de2065499b19d65e05b191c64893
Default Alt Text
ManageGroupsSpecialPage.php (36 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment