Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5975921
TranslatableBundleMover.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
TranslatableBundleMover.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
MediaWiki\Extension\Translate\PageTranslation
;
use
BagOStuff
;
use
JobQueueGroup
;
use
LogicException
;
use
MediaWiki\Cache\LinkBatchFactory
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\MoveTranslatableBundleJob
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\SubpageListBuilder
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundle
;
use
MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleFactory
;
use
MediaWiki\Extension\Translate\SystemUsers\FuzzyBot
;
use
MediaWiki\Message\Message
;
use
MediaWiki\Page\MovePageFactory
;
use
MediaWiki\Status\Status
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\User
;
use
ObjectCacheFactory
;
use
SplObjectStorage
;
use
Wikimedia\Rdbms\IConnectionProvider
;
/**
* Contains the core logic to validate and move translatable bundles
* @author Abijeet Patro
* @license GPL-2.0-or-later
* @since 2021.03
*/
class
TranslatableBundleMover
{
private
const
LOCK_TIMEOUT
=
3600
*
2
;
private
const
FETCH_TRANSLATABLE_SUBPAGES
=
true
;
private
MovePageFactory
$movePageFactory
;
private
?
int
$pageMoveLimit
;
private
JobQueueGroup
$jobQueue
;
private
LinkBatchFactory
$linkBatchFactory
;
private
TranslatableBundleFactory
$bundleFactory
;
private
SubpageListBuilder
$subpageBuilder
;
private
IConnectionProvider
$dbProvider
;
private
BagOStuff
$cache
;
private
bool
$pageMoveLimitEnabled
=
true
;
private
const
REDIRECTABLE_PAGE_TYPES
=
[
'pt-movepage-list-source'
=>
true
,
'pt-movepage-list-section'
=>
false
,
'pt-movepage-list-nonmovable'
=>
false
,
'pt-movepage-list-translatable'
=>
false
,
'pt-movepage-list-translation'
=>
false
,
'pt-movepage-list-other'
=>
true
];
public
function
__construct
(
MovePageFactory
$movePageFactory
,
JobQueueGroup
$jobQueue
,
LinkBatchFactory
$linkBatchFactory
,
TranslatableBundleFactory
$bundleFactory
,
SubpageListBuilder
$subpageBuilder
,
IConnectionProvider
$dbProvider
,
ObjectCacheFactory
$objectCacheFactory
,
?
int
$pageMoveLimit
)
{
$this
->
movePageFactory
=
$movePageFactory
;
$this
->
jobQueue
=
$jobQueue
;
$this
->
pageMoveLimit
=
$pageMoveLimit
;
$this
->
linkBatchFactory
=
$linkBatchFactory
;
$this
->
bundleFactory
=
$bundleFactory
;
$this
->
subpageBuilder
=
$subpageBuilder
;
$this
->
dbProvider
=
$dbProvider
;
$this
->
cache
=
$objectCacheFactory
->
getInstance
(
CACHE_ANYTHING
);
}
public
function
getPageMoveCollection
(
Title
$source
,
?
Title
$target
,
User
$user
,
string
$reason
,
bool
$moveSubPages
,
bool
$moveTalkPages
,
bool
$leaveRedirect
):
PageMoveCollection
{
$blockers
=
new
SplObjectStorage
();
if
(
!
$target
)
{
$blockers
[
$source
]
=
Status
::
newFatal
(
'pt-movepage-block-base-invalid'
);
throw
new
ImpossiblePageMove
(
$blockers
);
}
if
(
$target
->
inNamespaces
(
NS_MEDIAWIKI
,
NS_TRANSLATIONS
)
)
{
$blockers
[
$source
]
=
Status
::
newFatal
(
'immobile-target-namespace'
,
$target
->
getNsText
()
);
throw
new
ImpossiblePageMove
(
$blockers
);
}
$movePage
=
$this
->
movePageFactory
->
newMovePage
(
$source
,
$target
);
$status
=
$movePage
->
isValidMove
();
$status
->
merge
(
$movePage
->
probablyCanMove
(
$user
,
$reason
)
);
if
(
!
$status
->
isOK
()
)
{
$blockers
[
$source
]
=
$status
;
}
// Don't spam the same errors for all pages if base page fails
if
(
count
(
$blockers
)
)
{
throw
new
ImpossiblePageMove
(
$blockers
);
}
$pageCollection
=
$this
->
getPagesToMove
(
$source
,
$target
,
$moveSubPages
,
self
::
FETCH_TRANSLATABLE_SUBPAGES
,
$moveTalkPages
,
$leaveRedirect
);
// Collect all the old and new titles for checks
$titles
=
[
'tp'
=>
$pageCollection
->
getTranslationPagesPair
(),
'subpage'
=>
$pageCollection
->
getSubpagesPair
(),
'section'
=>
$pageCollection
->
getUnitPagesPair
()
];
// Check that all new titles are valid and count them. Add 1 for source page.
$moveCount
=
1
;
$lb
=
$this
->
linkBatchFactory
->
newLinkBatch
();
foreach
(
$titles
as
$type
=>
$list
)
{
$moveCount
+=
count
(
$list
);
// Give grep a chance to find the usages:
// pt-movepage-block-tp-invalid, pt-movepage-block-section-invalid,
// pt-movepage-block-subpage-invalid
foreach
(
$list
as
$pair
)
{
$old
=
$pair
->
getOldTitle
();
$new
=
$pair
->
getNewTitle
();
if
(
$new
===
null
)
{
$blockers
[
$old
]
=
$this
->
getRenameMoveBlocker
(
$old
,
$type
,
$pair
->
getRenameErrorCode
()
);
continue
;
}
$lb
->
addObj
(
$old
);
$lb
->
addObj
(
$new
);
}
}
if
(
$this
->
pageMoveLimitEnabled
)
{
if
(
$this
->
pageMoveLimit
!==
null
&&
$moveCount
>
$this
->
pageMoveLimit
)
{
$blockers
[
$source
]
=
Status
::
newFatal
(
'pt-movepage-page-count-limit'
,
Message
::
numParam
(
$this
->
pageMoveLimit
)
);
}
}
// Stop further validation if there are blockers already.
if
(
count
(
$blockers
)
)
{
throw
new
ImpossiblePageMove
(
$blockers
);
}
// Check that there are no move blockers
$lb
->
setCaller
(
__METHOD__
)->
execute
();
foreach
(
$titles
as
$type
=>
$list
)
{
foreach
(
$list
as
$pair
)
{
$old
=
$pair
->
getOldTitle
();
$new
=
$pair
->
getNewTitle
();
/* This method has terrible performance:
* - 2 queries by core
* - 3 queries by lqt
* - and no obvious way to preload the data! */
$movePage
=
$this
->
movePageFactory
->
newMovePage
(
$old
,
$new
);
$status
=
$movePage
->
isValidMove
();
// Do not check for permissions here, as these pages are not editable/movable
// in regular use
if
(
!
$status
->
isOK
()
)
{
$blockers
[
$old
]
=
$status
;
}
/* Because of the poor performance, check only one of the possibly thousands
* of section pages and assume rest are fine. This assumes section pages are
* listed last in the array. */
if
(
$type
===
'section'
)
{
break
;
}
}
}
if
(
count
(
$blockers
)
)
{
throw
new
ImpossiblePageMove
(
$blockers
);
}
return
$pageCollection
;
}
public
function
moveAsynchronously
(
Title
$source
,
Title
$target
,
bool
$moveSubPages
,
User
$user
,
string
$moveReason
,
bool
$moveTalkPages
,
bool
$leaveRedirect
):
void
{
$pageCollection
=
$this
->
getPagesToMove
(
$source
,
$target
,
$moveSubPages
,
!
self
::
FETCH_TRANSLATABLE_SUBPAGES
,
$moveTalkPages
,
$leaveRedirect
);
$pagesToMove
=
$pageCollection
->
getListOfPages
();
$pagesToLeaveRedirect
=
$pageCollection
->
getListOfPagesToRedirect
();
$job
=
MoveTranslatableBundleJob
::
newJob
(
$source
,
$target
,
$pagesToMove
,
$pagesToLeaveRedirect
,
$moveReason
,
$user
,
);
$this
->
lock
(
array_keys
(
$pagesToMove
)
);
$this
->
lock
(
array_values
(
$pagesToMove
)
);
$this
->
jobQueue
->
push
(
$job
);
}
/**
* @param Title $source
* @param Title $target
* @param string[] $pagesToMove
* @param array<string,bool> $pagesToRedirect
* @param User $performer
* @param string $moveReason
* @param ?callable $progressCallback
*/
public
function
moveSynchronously
(
Title
$source
,
Title
$target
,
array
$pagesToMove
,
array
$pagesToRedirect
,
User
$performer
,
string
$moveReason
,
?
callable
$progressCallback
=
null
):
void
{
$sourceBundle
=
$this
->
bundleFactory
->
getValidBundle
(
$source
);
$this
->
move
(
$sourceBundle
,
$performer
,
$pagesToMove
,
$pagesToRedirect
,
$moveReason
,
$progressCallback
);
$this
->
bundleFactory
->
getStore
(
$sourceBundle
)->
move
(
$source
,
$target
);
$this
->
bundleFactory
->
getPageMoveLogger
(
$sourceBundle
)
->
logSuccess
(
$performer
,
$target
,
$moveReason
);
}
public
function
disablePageMoveLimit
():
void
{
$this
->
pageMoveLimitEnabled
=
false
;
}
public
function
enablePageMoveLimit
():
void
{
$this
->
pageMoveLimitEnabled
=
true
;
}
public
static
function
shouldLeaveRedirect
(
string
$pageType
,
bool
$leaveRedirect
):
bool
{
return
self
::
REDIRECTABLE_PAGE_TYPES
[
$pageType
]
&&
$leaveRedirect
;
}
private
function
getPagesToMove
(
Title
$source
,
Title
$target
,
bool
$moveSubPages
,
bool
$fetchTranslatableSubpages
,
bool
$moveTalkPages
,
bool
$leaveRedirect
):
PageMoveCollection
{
$sourceBundle
=
$this
->
bundleFactory
->
getValidBundle
(
$source
);
$classifiedSubpages
=
$this
->
subpageBuilder
->
getSubpagesPerType
(
$sourceBundle
,
$moveTalkPages
);
$talkPages
=
$moveTalkPages
?
$classifiedSubpages
[
'talkPages'
]
:
[];
$subpages
=
$moveSubPages
?
$classifiedSubpages
[
'normalSubpages'
]
:
[];
$nonMovableSubpages
=
[];
$relatedTranslatablePageList
=
[];
if
(
$fetchTranslatableSubpages
)
{
$relatedTranslatablePageList
=
array_merge
(
$classifiedSubpages
[
'translatableSubpages'
],
$classifiedSubpages
[
'translatableTalkPages'
]
);
}
$movePageFactory
=
$this
->
movePageFactory
;
$pageTitleRenamer
=
new
PageTitleRenamer
(
$source
,
$target
);
$createOps
=
function
(
array
$pages
,
string
$pageType
)
use
(
$pageTitleRenamer
,
$talkPages
,
&
$nonMovableSubpages
,
$leaveRedirect
,
$movePageFactory
)
{
$leaveRedirect
=
self
::
shouldLeaveRedirect
(
$pageType
,
$leaveRedirect
);
$ops
=
[];
foreach
(
$pages
as
$from
)
{
$to
=
$pageTitleRenamer
->
getNewTitle
(
$from
);
$op
=
new
PageMoveOperation
(
$from
,
$to
);
if
(
$pageType
===
'pt-movepage-list-other'
)
{
// TODO: In the future, think of moving all checks regarding whether a page
// is movable into this method. Currently its' being checked in two places
// making things slow.
$movePage
=
$movePageFactory
->
newMovePage
(
$from
,
$to
);
$status
=
$movePage
->
isValidMove
();
// Remove non movable subpages
if
(
!
$status
->
isOK
()
)
{
$nonMovableSubpages
[
$from
->
getPrefixedText
()
]
=
$status
;
continue
;
}
}
$op
->
setLeaveRedirect
(
$leaveRedirect
);
$talkPage
=
$talkPages
[
$from
->
getPrefixedDBkey
()
]
??
null
;
if
(
$talkPage
)
{
$op
->
setTalkpage
(
$talkPage
,
$pageTitleRenamer
->
getNewTitle
(
$talkPage
)
);
}
$ops
[]
=
$op
;
}
return
$ops
;
};
return
new
PageMoveCollection
(
$createOps
(
[
$source
],
'pt-movepage-list-source'
)[
0
],
$createOps
(
$classifiedSubpages
[
'translationPages'
],
'pt-movepage-list-translation'
),
$createOps
(
$classifiedSubpages
[
'translationUnitPages'
],
'pt-movepage-list-section'
),
$createOps
(
$subpages
,
'pt-movepage-list-other'
),
$nonMovableSubpages
,
$relatedTranslatablePageList
);
}
/** @param string[] $titles */
private
function
lock
(
array
$titles
):
void
{
$data
=
[];
foreach
(
$titles
as
$title
)
{
$data
[
$this
->
cache
->
makeKey
(
'pt-lock'
,
sha1
(
$title
)
)]
=
'locked'
;
}
// Do not lock pages indefinitely during translatable page moves since
// they can fail. Add a timeout so that the locks expire by themselves.
// Timeout value has been chosen by a gut feeling
$this
->
cache
->
setMulti
(
$data
,
self
::
LOCK_TIMEOUT
);
}
/** @param string[] $titles */
private
function
unlock
(
array
$titles
):
void
{
foreach
(
$titles
as
$title
)
{
$this
->
cache
->
delete
(
$this
->
cache
->
makeKey
(
'pt-lock'
,
sha1
(
$title
)
)
);
}
}
/**
* @param TranslatableBundle $sourceBundle
* @param User $performer
* @param string[] $pagesToMove
* @param array<string,bool> $pagesToRedirect
* @param string $reason
* @param ?callable $progressCallback
*/
private
function
move
(
TranslatableBundle
$sourceBundle
,
User
$performer
,
array
$pagesToMove
,
array
$pagesToRedirect
,
string
$reason
,
?
callable
$progressCallback
=
null
):
void
{
$fuzzyBot
=
FuzzyBot
::
getUser
();
Hooks
::
$allowTargetEdit
=
true
;
$processed
=
0
;
$this
->
dbProvider
->
getPrimaryDatabase
()->
startAtomic
(
__METHOD__
);
foreach
(
$pagesToMove
as
$source
=>
$target
)
{
$sourceTitle
=
Title
::
newFromText
(
$source
);
$targetTitle
=
Title
::
newFromText
(
$target
);
if
(
$source
===
$sourceBundle
->
getTitle
()->
getPrefixedText
()
)
{
$user
=
$performer
;
$moveSummary
=
$reason
;
}
else
{
$user
=
$fuzzyBot
;
$moveSummary
=
wfMessage
(
'pt-movepage-logreason'
,
$sourceBundle
->
getTitle
()->
getPrefixedText
()
)->
text
();
}
$mover
=
$this
->
movePageFactory
->
newMovePage
(
$sourceTitle
,
$targetTitle
);
$status
=
$mover
->
move
(
$user
,
$moveSummary
,
$pagesToRedirect
[
$source
]
??
false
);
$processed
++;
if
(
$progressCallback
)
{
$progressCallback
(
$sourceTitle
,
$targetTitle
,
$status
,
count
(
$pagesToMove
),
$processed
);
}
if
(
!
$status
->
isOK
()
)
{
$this
->
bundleFactory
->
getPageMoveLogger
(
$sourceBundle
)
->
logError
(
$performer
,
$sourceTitle
,
$targetTitle
,
$status
);
}
$this
->
unlock
(
[
$source
,
$target
]
);
}
$this
->
dbProvider
->
getPrimaryDatabase
()->
endAtomic
(
__METHOD__
);
Hooks
::
$allowTargetEdit
=
false
;
}
private
function
getRenameMoveBlocker
(
Title
$old
,
string
$pageType
,
int
$renameError
):
Status
{
if
(
$renameError
===
PageTitleRenamer
::
NO_ERROR
)
{
throw
new
LogicException
(
'Trying to fetch MoveBlocker when there was no error during rename. Title: '
.
$old
->
getPrefixedText
()
.
', page type: '
.
$pageType
);
}
if
(
$renameError
===
PageTitleRenamer
::
UNKNOWN_PAGE
)
{
$status
=
Status
::
newFatal
(
'pt-movepage-block-unknown-page'
,
$old
->
getPrefixedText
()
);
}
elseif
(
$renameError
===
PageTitleRenamer
::
NS_TALK_UNSUPPORTED
)
{
$status
=
Status
::
newFatal
(
'pt-movepage-block-ns-talk-unsupported'
,
$old
->
getPrefixedText
()
);
}
elseif
(
$renameError
===
PageTitleRenamer
::
RENAME_FAILED
)
{
$status
=
Status
::
newFatal
(
'pt-movepage-block-rename-failed'
,
$old
->
getPrefixedText
()
);
}
else
{
return
Status
::
newFatal
(
"pt-movepage-block-$pageType-invalid"
,
$old
->
getPrefixedText
()
);
}
return
$status
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 18:40 (1 d, 2 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
94/13/e27ac79fda7e4286f969c9e563d0
Default Alt Text
TranslatableBundleMover.php (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment