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, $moveReason, $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, $moveReason, $progressCallback );
215 $this->bundleFactory->getStore( $sourceBundle )->move( $source, $target );
217 $this->bundleFactory->getPageMoveLogger( $sourceBundle )
218 ->logSuccess( $performer, $target, $moveReason );
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 ) ) );
305 private function move(
306 TranslatableBundle $sourceBundle,
310 ?callable $progressCallback =
null
312 $fuzzyBot = FuzzyBot::getUser();
314 Hooks::$allowTargetEdit =
true;
317 foreach ( $pagesToMove as $source => $target ) {
318 $sourceTitle = Title::newFromText( $source );
319 $targetTitle = Title::newFromText( $target );
321 if ( $source === $sourceBundle->getTitle()->getPrefixedText() ) {
323 $moveSummary = $reason;
326 $moveSummary = wfMessage(
327 'pt-movepage-logreason', $sourceBundle->getTitle()->getPrefixedText()
331 $mover = $this->movePageFactory->newMovePage( $sourceTitle, $targetTitle );
332 $status = $mover->move( $user, $moveSummary,
false );
335 if ( $progressCallback ) {
340 count( $pagesToMove ),
345 if ( !$status->isOK() ) {
346 $this->bundleFactory->getPageMoveLogger( $sourceBundle )
347 ->logError( $performer, $sourceTitle, $targetTitle, $status );
350 $this->unlock( [ $source, $target ] );
353 Hooks::$allowTargetEdit =
false;
356 private function getRenameMoveBlocker( Title $old,
string $pageType,
int $renameError ): Status {
357 if ( $renameError === PageTitleRenamer::NO_ERROR ) {
358 throw new LogicException(
359 'Trying to fetch MoveBlocker when there was no error during rename. Title: ' .
360 $old->getPrefixedText() .
', page type: ' . $pageType
364 if ( $renameError === PageTitleRenamer::UNKNOWN_PAGE ) {
365 $status = Status::newFatal(
'pt-movepage-block-unknown-page', $old->getPrefixedText() );
366 } elseif ( $renameError === PageTitleRenamer::NS_TALK_UNSUPPORTED ) {
367 $status = Status::newFatal(
'pt-movepage-block-ns-talk-unsupported', $old->getPrefixedText() );
368 } elseif ( $renameError === PageTitleRenamer::RENAME_FAILED ) {
369 $status = Status::newFatal(
'pt-movepage-block-rename-failed', $old->getPrefixedText() );
371 return Status::newFatal(
"pt-movepage-block-$pageType-invalid", $old->getPrefixedText() );