29 private const LOCK_TIMEOUT = 3600 * 2;
30 private const FETCH_TRANSLATABLE_SUBPAGES =
true;
32 private $movePageFactory;
34 private $pageMoveLimit;
38 private $linkBatchFactory;
40 private $bundleFactory;
42 private $subpageBuilder;
44 private $pageMoveLimitEnabled =
true;
46 public function __construct(
47 MovePageFactory $movePageFactory,
48 JobQueueGroup $jobQueue,
49 LinkBatchFactory $linkBatchFactory,
54 $this->movePageFactory = $movePageFactory;
55 $this->jobQueue = $jobQueue;
56 $this->pageMoveLimit = $pageMoveLimit;
57 $this->linkBatchFactory = $linkBatchFactory;
58 $this->bundleFactory = $bundleFactory;
59 $this->subpageBuilder = $subpageBuilder;
62 public function getPageMoveCollection(
70 $blockers =
new SplObjectStorage();
73 $blockers[$source] = Status::newFatal(
'pt-movepage-block-base-invalid' );
77 if ( $target->inNamespaces( NS_MEDIAWIKI, NS_TRANSLATIONS ) ) {
78 $blockers[$source] = Status::newFatal(
'immobile-target-namespace', $target->getNsText() );
82 $movePage = $this->movePageFactory->newMovePage( $source, $target );
83 $status = $movePage->isValidMove();
84 $status->merge( $movePage->checkPermissions( $user, $reason ) );
85 if ( !$status->isOK() ) {
86 $blockers[$source] = $status;
90 if ( count( $blockers ) ) {
94 $pageCollection = $this->getPagesToMove(
95 $source, $target, $moveSubPages, self::FETCH_TRANSLATABLE_SUBPAGES, $moveTalkPages
100 'tp' => $pageCollection->getTranslationPagesPair(),
101 'subpage' => $pageCollection->getSubpagesPair(),
102 'section' => $pageCollection->getUnitPagesPair()
107 $lb = $this->linkBatchFactory->newLinkBatch();
108 foreach ( $titles as $type => $list ) {
109 $moveCount += count( $list );
113 foreach ( $list as $pair ) {
114 $old = $pair->getOldTitle();
115 $new = $pair->getNewTitle();
117 if ( $new ===
null ) {
118 $blockers[$old] = $this->getRenameMoveBlocker( $old, $type, $pair->getRenameErrorCode() );
126 if ( $this->pageMoveLimitEnabled ) {
127 if ( $this->pageMoveLimit !==
null && $moveCount > $this->pageMoveLimit ) {
128 $blockers[$source] = Status::newFatal(
129 'pt-movepage-page-count-limit',
130 Message::numParam( $this->pageMoveLimit )
136 if ( count( $blockers ) ) {
141 $lb->setCaller( __METHOD__ )->execute();
142 foreach ( $titles as $type => $list ) {
143 foreach ( $list as $pair ) {
144 $old = $pair->getOldTitle();
145 $new = $pair->getNewTitle();
151 $movePage = $this->movePageFactory->newMovePage( $old, $new );
152 $status = $movePage->isValidMove();
155 if ( !$status->isOK() ) {
156 $blockers[$old] = $status;
162 if ( $type ===
'section' ) {
168 if ( count( $blockers ) ) {
172 return $pageCollection;
175 public function moveAsynchronously(
183 $pageCollection = $this->getPagesToMove(
184 $source, $target, $moveSubPages, !self::FETCH_TRANSLATABLE_SUBPAGES, $moveTalkPages
186 $pagesToMove = $pageCollection->getListOfPages();
188 $job = MoveTranslatableBundleJob::newJob( $source, $target, $pagesToMove, $summary, $user );
189 $this->lock( array_keys( $pagesToMove ) );
190 $this->lock( array_values( $pagesToMove ) );
192 $this->jobQueue->push( $job );
209 callable $progressCallback =
null
211 $sourceBundle = $this->bundleFactory->getValidBundle( $source );
213 $this->move( $sourceBundle, $performer, $pagesToMove, $summary, $progressCallback );
215 $this->bundleFactory->getStore( $sourceBundle )->move( $source, $target );
217 $this->bundleFactory->getPageMoveLogger( $sourceBundle )
218 ->logSuccess( $performer, $target );
221 public function disablePageMoveLimit(): void {
222 $this->pageMoveLimitEnabled = false;
225 public function enablePageMoveLimit(): void {
226 $this->pageMoveLimitEnabled = true;
229 private function getPagesToMove(
233 bool $fetchTranslatableSubpages,
235 ): PageMoveCollection {
236 $sourceBundle = $this->bundleFactory->getValidBundle( $source );
238 $classifiedSubpages = $this->subpageBuilder->getSubpagesPerType( $sourceBundle, $moveTalkPages );
240 $talkPages = $moveTalkPages ? $classifiedSubpages[
'talkPages'] : [];
241 $subpages = $moveSubPages ? $classifiedSubpages[
'normalSubpages'] : [];
242 $relatedTranslatablePageList = [];
243 if ( $fetchTranslatableSubpages ) {
244 $relatedTranslatablePageList = array_merge(
245 $classifiedSubpages[
'translatableSubpages'],
246 $classifiedSubpages[
'translatableTalkPages']
250 $pageTitleRenamer =
new PageTitleRenamer( $source, $target );
251 $createOps =
static function ( array $pages ) use ( $pageTitleRenamer, $talkPages ) {
253 foreach ( $pages as $from ) {
254 $to = $pageTitleRenamer->getNewTitle( $from );
255 $op =
new PageMoveOperation( $from, $to );
257 $talkPage = $talkPages[ $from->getPrefixedDBkey() ] ??
null;
259 $op->setTalkpage( $talkPage, $pageTitleRenamer->getNewTitle( $talkPage ) );
267 return new PageMoveCollection(
268 $createOps( [ $source ] )[0],
269 $createOps( $classifiedSubpages[
'translationPages'] ),
270 $createOps( $classifiedSubpages[
'translationUnitPages'] ),
271 $createOps( $subpages ),
272 $relatedTranslatablePageList
277 private function lock( array $titles ): void {
278 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
280 foreach ( $titles as $title ) {
281 $data[$cache->makeKey(
'pt-lock', sha1( $title ) )] =
'locked';
287 $cache->setMulti( $data, self::LOCK_TIMEOUT );
291 private function unlock( array $titles ): void {
292 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
293 foreach ( $titles as $title ) {
294 $cache->delete( $cache->makeKey(
'pt-lock', sha1( $title ) ) );
306 private function move(
307 TranslatableBundle $sourceBundle,
311 callable $progressCallback =
null
313 $fuzzybot = FuzzyBot::getUser();
315 Hooks::$allowTargetEdit =
true;
318 foreach ( $pagesToMove as $source => $target ) {
319 $sourceTitle = Title::newFromText( $source );
320 $targetTitle = Title::newFromText( $target );
322 if ( $source === $sourceBundle->getTitle()->getPrefixedText() ) {
328 $mover = $this->movePageFactory->newMovePage( $sourceTitle, $targetTitle );
329 $status = $mover->move( $user, $summary,
false );
332 if ( $progressCallback ) {
337 count( $pagesToMove ),
342 if ( !$status->isOK() ) {
343 $this->bundleFactory->getPageMoveLogger( $sourceBundle )
344 ->logError( $performer, $sourceTitle, $targetTitle, $status );
347 $this->unlock( [ $source, $target ] );
350 Hooks::$allowTargetEdit =
false;
353 private function getRenameMoveBlocker( Title $old,
string $pageType,
int $renameError ): Status {
354 if ( $renameError === PageTitleRenamer::NO_ERROR ) {
355 throw new LogicException(
356 'Trying to fetch MoveBlocker when there was no error during rename. Title: ' .
357 $old->getPrefixedText() .
', page type: ' . $pageType
361 if ( $renameError === PageTitleRenamer::UNKNOWN_PAGE ) {
362 $status = Status::newFatal(
'pt-movepage-block-unknown-page', $old->getPrefixedText() );
363 } elseif ( $renameError === PageTitleRenamer::NS_TALK_UNSUPPORTED ) {
364 $status = Status::newFatal(
'pt-movepage-block-ns-talk-unsupported', $old->getPrefixedText() );
365 } elseif ( $renameError === PageTitleRenamer::RENAME_FAILED ) {
366 $status = Status::newFatal(
'pt-movepage-block-rename-failed', $old->getPrefixedText() );
368 return Status::newFatal(
"pt-movepage-block-$pageType-invalid", $old->getPrefixedText() );