Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MoveTranslatableBundleMaintenanceScript.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use Closure;
7use MalformedTitleException;
10use MediaWiki\MediaWikiServices;
11use Message;
12use SplObjectStorage;
13use Status;
14use Title;
15use TitleParser;
16
19 private $bundleMover;
21 private $titleParser;
22
23 public function __construct() {
24 parent::__construct();
25 $this->addDescription( 'Review and move translatable bundles including their subpages' );
26
27 $this->addArg(
28 'current-page',
29 ' Current name of the page representing a translatable bundle',
30 self::REQUIRED
31 );
32
33 $this->addArg(
34 'new-page',
35 'New translatable bundle name',
36 self::REQUIRED
37 );
38
39 $this->addArg(
40 'user',
41 'User performing the move',
42 self::REQUIRED
43 );
44
45 $this->addOption(
46 'reason',
47 'Reason for moving the translatable bundle',
48 self::OPTIONAL,
49 self::HAS_ARG
50 );
51
52 $this->addOption(
53 'skip-subpages',
54 'Skip moving subpages under the current page'
55 );
56
57 $this->addOption(
58 'skip-talkpages',
59 'Skip moving talk pages under pages being moved'
60 );
61
62 $this->requireExtension( 'Translate' );
63 }
64
66 public function execute() {
67 $this->bundleMover = Services::getInstance()->getTranslatableBundleMover();
68
69 $mwService = MediaWikiServices::getInstance();
70 $this->titleParser = $mwService->getTitleParser();
71
72 $currentBundleName = $this->getArg( 0 );
73 $newBundleName = $this->getArg( 1 );
74 $username = $this->getArg( 2 );
75 $reason = $this->getOption( 'reason', '' );
76 $moveSubpages = !$this->hasOption( 'skip-subpages' );
77 $moveTalkpages = !$this->hasOption( 'skip-talkpages' );
78
79 $userFactory = $mwService->getUserFactory();
80 $user = $userFactory->newFromName( $username );
81
82 if ( $user === null || !$user->isRegistered() ) {
83 $this->fatalError( "User $username does not exist." );
84 }
85
86 $outputMsg = "Check if '$currentBundleName' can be moved to '$newBundleName'";
87 $subpageMsg = 'excluding subpages';
88 if ( $moveSubpages ) {
89 $subpageMsg = 'including subpages';
90 }
91
92 $talkpageMsg = 'excluding talkpages';
93 if ( $moveTalkpages ) {
94 $talkpageMsg = 'including talkpages';
95 }
96
97 $this->output( "$outputMsg ($subpageMsg; $talkpageMsg)\n" );
98
99 try {
100 $currentTitle = $this->getTitleFromInput( $currentBundleName ?? '' );
101 $newTitle = $this->getTitleFromInput( $newBundleName ?? '' );
102 } catch ( MalformedTitleException $e ) {
103 $this->error( 'Invalid title: current-bundle or new-bundle' );
104 $this->fatalError( $e->getMessageObject()->text() );
105 }
106
107 // When moving translatable bundles from script, remove all limits on the number of
108 // pages that can be moved
109 $this->bundleMover->disablePageMoveLimit();
110 try {
111 $pageCollection = $this->bundleMover->getPageMoveCollection(
112 $currentTitle,
113 $newTitle,
114 $user,
115 $reason,
116 $moveSubpages,
117 $moveTalkpages
118 );
119 } catch ( ImpossiblePageMove $e ) {
120 $fatalErrorMsg = $this->parseErrorMessage( $e->getBlockers() );
121 $this->fatalError( $fatalErrorMsg );
122 }
123
124 $this->displayPagesToMove( $pageCollection );
125
126 $haveConfirmation = $this->getConfirmation();
127 if ( !$haveConfirmation ) {
128 $this->output( "Exiting...\n" );
129 return;
130 }
131
132 $this->output( "Starting page move\n" );
133
134 $pagesToMove = $pageCollection->getListOfPages();
135
136 $this->bundleMover->moveSynchronously(
137 $currentTitle,
138 $newTitle,
139 $pagesToMove,
140 $user,
141 $reason,
142 Closure::fromCallable( [ $this, 'progressCallback' ] )
143 );
144
145 $this->logSeparator();
146 $this->output( "Finished moving '$currentBundleName' to '$newBundleName' $subpageMsg\n" );
147 }
148
149 private function parseErrorMessage( SplObjectStorage $errors ): string {
150 $errorMsg = wfMessage( 'pt-movepage-blockers', count( $errors ) )->text() . "\n";
151 foreach ( $errors as $title ) {
152 $titleText = $title->getPrefixedText();
153 $errorMsg .= "$titleText\n";
154 $errorMsg .= $errors[ $title ]->getWikiText( false, 'pt-movepage-error-placeholder', 'en' );
155 $errorMsg .= "\n";
156 }
157
158 return $errorMsg;
159 }
160
161 private function progressCallback( Title $previous, Title $new, Status $status, int $total, int $processed ): void {
162 $previousTitleText = $previous->getPrefixedText();
163 $newTitleText = $new->getPrefixedText();
164 $paddedProcessed = str_pad( (string)$processed, strlen( (string)$total ), ' ', STR_PAD_LEFT );
165 $progressCounter = "($paddedProcessed/$total)";
166
167 if ( $status->isOK() ) {
168 $this->output( "$progressCounter $previousTitleText --> $newTitleText\n" );
169 } else {
170 $this->output( "$progressCounter Failed to move $previousTitleText to $newTitleText\n" );
171 $this->output( "\tReason:" . $status->getWikiText() . "\n" );
172 }
173 }
174
175 private function displayPagesToMove( PageMoveCollection $pageCollection ): void {
176 $infoMessage = "\nThe following pages will be moved:\n";
177 $count = 0;
178 $subpagesCount = 0;
179 $talkpagesCount = 0;
180
182 $pagesToMove = [
183 'pt-movepage-list-pages' => [ $pageCollection->getTranslatablePage() ],
184 'pt-movepage-list-translation' => $pageCollection->getTranslationPagesPair(),
185 'pt-movepage-list-section' => $pageCollection->getUnitPagesPair()
186 ];
187
188 $subpages = $pageCollection->getSubpagesPair();
189 if ( $subpages ) {
190 $pagesToMove[ 'pt-movepage-list-other'] = $subpages;
191 }
192
193 foreach ( $pagesToMove as $type => $pages ) {
194 $lines = [];
195 $infoMessage .= $this->getSectionHeader( $type, $pages );
196 if ( !count( $pages ) ) {
197 continue;
198 }
199
200 foreach ( $pages as $pagePairs ) {
201 $count++;
202
203 if ( $type === 'pt-movepage-list-other' ) {
204 $subpagesCount++;
205 }
206
207 $old = $pagePairs->getOldTitle();
208 $new = $pagePairs->getNewTitle();
209
210 if ( $new ) {
211 $line = '* ' . $old->getPrefixedText() . ' → ' . $new->getPrefixedText();
212 if ( $pagePairs->hasTalkpage() ) {
213 $count++;
214 $talkpagesCount++;
215 $line .= ' ' . $this->message( 'pt-movepage-talkpage-exists' )->text();
216 }
217
218 $lines[] = $line;
219 }
220 }
221
222 $infoMessage .= implode( "\n", $lines ) . "\n";
223 }
224
225 $translatableSubpages = $pageCollection->getTranslatableSubpages();
226 $infoMessage .= $this->getSectionHeader( 'pt-movepage-list-translatable', $translatableSubpages );
227
228 if ( $translatableSubpages ) {
229 $lines = [];
230 $infoMessage .= $this->message( 'pt-movepage-list-translatable-note' )->text() . "\n";
231 foreach ( $translatableSubpages as $page ) {
232 $lines[] = '* ' . $page->getPrefixedText();
233 }
234
235 $infoMessage .= implode( "\n", $lines ) . "\n";
236 }
237
238 $this->output( $infoMessage );
239
240 $this->logSeparator();
241 $this->output(
242 $this->message( 'pt-movepage-list-count' )
243 ->numParams( $count, $subpagesCount, $talkpagesCount )
244 ->text() . "\n"
245 );
246 $this->logSeparator();
247 $this->output( "\n" );
248 }
249
250 private function getSectionHeader( string $type, array $pages ): string {
251 $infoMessage = $this->getSeparator();
252 $pageCount = count( $pages );
253
254 // $type can be: pt-movepage-list-pages, pt-movepage-list-translation, pt-movepage-list-section
255 // pt-movepage-list-other
256 $infoMessage .= $this->message( $type )->numParams( $pageCount )->text() . "\n\n";
257 if ( !$pageCount ) {
258 $infoMessage .= $this->message( 'pt-movepage-list-no-pages' )->text() . "\n";
259 }
260
261 return $infoMessage;
262 }
263
264 private function getConfirmation(): bool {
265 $line = self::readconsole( 'Type "MOVE" to begin the move operation: ' );
266 return strtolower( $line ) === 'move';
267 }
268
269 private function getSeparator( int $width = 15 ): string {
270 return str_repeat( '-', $width ) . "\n";
271 }
272
273 private function logSeparator( int $width = 15 ): void {
274 $this->output( $this->getSeparator( $width ) );
275 }
276
277 private function message( string $key ): Message {
278 return ( new Message( $key ) )->inLanguage( 'en' );
279 }
280
281 private function getTitleFromInput( string $pageName ): Title {
282 $titleValue = $this->titleParser->parseTitle( $pageName );
283 return Title::newFromLinkTarget( $titleValue );
284 }
285}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Exception thrown when a translatable page move is not possible.
Minimal service container.
Definition Services.php:44
Constants for making code for maintenance scripts more readable.