Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 180 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
MoveTranslatableBundleMaintenanceScript | |
0.00% |
0 / 180 |
|
0.00% |
0 / 11 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
90 | |||
parseErrorMessage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
progressCallback | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
displayPagesToMove | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
110 | |||
getSectionHeader | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getConfirmation | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getSeparator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
logSeparator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
message | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitleFromInput | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\PageTranslation; |
5 | |
6 | use Closure; |
7 | use MalformedTitleException; |
8 | use MediaWiki\Extension\Translate\Services; |
9 | use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript; |
10 | use MediaWiki\MediaWikiServices; |
11 | use MediaWiki\Title\Title; |
12 | use Message; |
13 | use SplObjectStorage; |
14 | use Status; |
15 | use TitleParser; |
16 | |
17 | class MoveTranslatableBundleMaintenanceScript extends BaseMaintenanceScript { |
18 | private TranslatableBundleMover $bundleMover; |
19 | private TitleParser $titleParser; |
20 | |
21 | public function __construct() { |
22 | parent::__construct(); |
23 | $this->addDescription( 'Review and move translatable bundles including their subpages' ); |
24 | |
25 | $this->addArg( |
26 | 'current-page', |
27 | ' Current name of the page representing a translatable bundle', |
28 | self::REQUIRED |
29 | ); |
30 | |
31 | $this->addArg( |
32 | 'new-page', |
33 | 'New translatable bundle name', |
34 | self::REQUIRED |
35 | ); |
36 | |
37 | $this->addArg( |
38 | 'user', |
39 | 'User performing the move', |
40 | self::REQUIRED |
41 | ); |
42 | |
43 | $this->addOption( |
44 | 'reason', |
45 | 'Reason for moving the translatable bundle', |
46 | self::OPTIONAL, |
47 | self::HAS_ARG |
48 | ); |
49 | |
50 | $this->addOption( |
51 | 'skip-redirect', |
52 | 'Skip leaving a redirect behind for translatable bundle, subpages and related talk pages', |
53 | ); |
54 | |
55 | $this->addOption( |
56 | 'skip-subpages', |
57 | 'Skip moving subpages under the current page' |
58 | ); |
59 | |
60 | $this->addOption( |
61 | 'skip-talkpages', |
62 | 'Skip moving talk pages under pages being moved' |
63 | ); |
64 | |
65 | $this->requireExtension( 'Translate' ); |
66 | } |
67 | |
68 | /** @inheritDoc */ |
69 | public function execute() { |
70 | $this->bundleMover = Services::getInstance()->getTranslatableBundleMover(); |
71 | |
72 | $mwService = MediaWikiServices::getInstance(); |
73 | $this->titleParser = $mwService->getTitleParser(); |
74 | |
75 | $currentBundleName = $this->getArg( 0 ); |
76 | $newBundleName = $this->getArg( 1 ); |
77 | $username = $this->getArg( 2 ); |
78 | $reason = $this->getOption( 'reason', '' ); |
79 | $leaveRedirect = !$this->hasOption( 'skip-redirect' ); |
80 | $moveSubpages = !$this->hasOption( 'skip-subpages' ); |
81 | $moveTalkpages = !$this->hasOption( 'skip-talkpages' ); |
82 | |
83 | $userFactory = $mwService->getUserFactory(); |
84 | $user = $userFactory->newFromName( $username ); |
85 | |
86 | if ( $user === null || !$user->isRegistered() ) { |
87 | $this->fatalError( "User $username does not exist." ); |
88 | } |
89 | |
90 | $outputMsg = "Check if '$currentBundleName' can be moved to '$newBundleName'"; |
91 | $subpageMsg = 'excluding subpages'; |
92 | if ( $moveSubpages ) { |
93 | $subpageMsg = 'including subpages'; |
94 | } |
95 | |
96 | $talkpageMsg = 'excluding talkpages'; |
97 | if ( $moveTalkpages ) { |
98 | $talkpageMsg = 'including talkpages'; |
99 | } |
100 | |
101 | $leaveRedirectMsg = 'without leaving redirects'; |
102 | if ( $leaveRedirect ) { |
103 | $leaveRedirectMsg = 'leaving redirects'; |
104 | } |
105 | |
106 | $this->output( "$outputMsg ($subpageMsg; $talkpageMsg; $leaveRedirectMsg)\n" ); |
107 | |
108 | try { |
109 | $currentTitle = $this->getTitleFromInput( $currentBundleName ?? '' ); |
110 | $newTitle = $this->getTitleFromInput( $newBundleName ?? '' ); |
111 | } catch ( MalformedTitleException $e ) { |
112 | $this->error( 'Invalid title: current-bundle or new-bundle' ); |
113 | $this->fatalError( $e->getMessageObject()->text() ); |
114 | } |
115 | |
116 | // When moving translatable bundles from script, remove all limits on the number of |
117 | // pages that can be moved |
118 | $this->bundleMover->disablePageMoveLimit(); |
119 | try { |
120 | $pageCollection = $this->bundleMover->getPageMoveCollection( |
121 | $currentTitle, |
122 | $newTitle, |
123 | $user, |
124 | $reason, |
125 | $moveSubpages, |
126 | $moveTalkpages, |
127 | $leaveRedirect |
128 | ); |
129 | } catch ( ImpossiblePageMove $e ) { |
130 | $fatalErrorMsg = $this->parseErrorMessage( $e->getBlockers() ); |
131 | $this->fatalError( $fatalErrorMsg ); |
132 | } |
133 | |
134 | $this->displayPagesToMove( $pageCollection, $leaveRedirect ); |
135 | |
136 | $haveConfirmation = $this->getConfirmation(); |
137 | if ( !$haveConfirmation ) { |
138 | $this->output( "Exiting...\n" ); |
139 | return; |
140 | } |
141 | |
142 | $this->output( "Starting page move\n" ); |
143 | $pagesToMove = $pageCollection->getListOfPages(); |
144 | $pagesToRedirect = $pageCollection->getListOfPagesToRedirect(); |
145 | |
146 | $this->bundleMover->moveSynchronously( |
147 | $currentTitle, |
148 | $newTitle, |
149 | $pagesToMove, |
150 | $pagesToRedirect, |
151 | $user, |
152 | $reason, |
153 | Closure::fromCallable( [ $this, 'progressCallback' ] ) |
154 | ); |
155 | |
156 | $this->logSeparator(); |
157 | $this->output( "Finished moving '$currentBundleName' to '$newBundleName' $subpageMsg\n" ); |
158 | } |
159 | |
160 | private function parseErrorMessage( SplObjectStorage $errors ): string { |
161 | $errorMsg = wfMessage( 'pt-movepage-blockers', count( $errors ) )->text() . "\n"; |
162 | foreach ( $errors as $title ) { |
163 | $titleText = $title->getPrefixedText(); |
164 | $errorMsg .= "$titleText\n"; |
165 | $errorMsg .= $errors[ $title ]->getWikiText( false, 'pt-movepage-error-placeholder', 'en' ); |
166 | $errorMsg .= "\n"; |
167 | } |
168 | |
169 | return $errorMsg; |
170 | } |
171 | |
172 | private function progressCallback( Title $previous, Title $new, Status $status, int $total, int $processed ): void { |
173 | $previousTitleText = $previous->getPrefixedText(); |
174 | $newTitleText = $new->getPrefixedText(); |
175 | $paddedProcessed = str_pad( (string)$processed, strlen( (string)$total ), ' ', STR_PAD_LEFT ); |
176 | $progressCounter = "($paddedProcessed/$total)"; |
177 | |
178 | if ( $status->isOK() ) { |
179 | $this->output( "$progressCounter $previousTitleText --> $newTitleText\n" ); |
180 | } else { |
181 | $this->output( "$progressCounter Failed to move $previousTitleText to $newTitleText\n" ); |
182 | $this->output( "\tReason:" . $status->getWikiText() . "\n" ); |
183 | } |
184 | } |
185 | |
186 | private function displayPagesToMove( PageMoveCollection $pageCollection, bool $leaveRedirect ): void { |
187 | $infoMessage = "\nThe following pages will be moved:\n"; |
188 | $count = 0; |
189 | $subpagesCount = 0; |
190 | $talkpagesCount = 0; |
191 | |
192 | /** @var PageMoveOperation[][] */ |
193 | $pagesToMove = [ |
194 | 'pt-movepage-list-source' => [ $pageCollection->getTranslatablePage() ], |
195 | 'pt-movepage-list-translation' => $pageCollection->getTranslationPagesPair(), |
196 | 'pt-movepage-list-section' => $pageCollection->getUnitPagesPair() |
197 | ]; |
198 | |
199 | $subpages = $pageCollection->getSubpagesPair(); |
200 | if ( $subpages ) { |
201 | $pagesToMove[ 'pt-movepage-list-other'] = $subpages; |
202 | } |
203 | |
204 | foreach ( $pagesToMove as $type => $pages ) { |
205 | $lines = []; |
206 | $infoMessage .= $this->getSectionHeader( $type, $pages, $leaveRedirect ); |
207 | if ( !count( $pages ) ) { |
208 | continue; |
209 | } |
210 | |
211 | foreach ( $pages as $pagePairs ) { |
212 | $count++; |
213 | |
214 | if ( $type === 'pt-movepage-list-other' ) { |
215 | $subpagesCount++; |
216 | } |
217 | |
218 | $old = $pagePairs->getOldTitle(); |
219 | $new = $pagePairs->getNewTitle(); |
220 | |
221 | if ( $new ) { |
222 | $line = '* ' . $old->getPrefixedText() . ' → ' . $new->getPrefixedText(); |
223 | if ( $pagePairs->hasTalkpage() ) { |
224 | $count++; |
225 | $talkpagesCount++; |
226 | $line .= ' ' . $this->message( 'pt-movepage-talkpage-exists' )->text(); |
227 | } |
228 | |
229 | $lines[] = $line; |
230 | } |
231 | } |
232 | |
233 | $infoMessage .= implode( "\n", $lines ) . "\n"; |
234 | } |
235 | |
236 | $translatableSubpages = $pageCollection->getTranslatableSubpages(); |
237 | $infoMessage .= $this->getSectionHeader( |
238 | 'pt-movepage-list-translatable', $translatableSubpages, $leaveRedirect |
239 | ); |
240 | |
241 | if ( $translatableSubpages ) { |
242 | $lines = []; |
243 | $infoMessage .= $this->message( 'pt-movepage-list-translatable-note' )->text() . "\n"; |
244 | foreach ( $translatableSubpages as $page ) { |
245 | $lines[] = '* ' . $page->getPrefixedText(); |
246 | } |
247 | |
248 | $infoMessage .= implode( "\n", $lines ) . "\n"; |
249 | } |
250 | |
251 | $this->output( $infoMessage ); |
252 | |
253 | $this->logSeparator(); |
254 | $this->output( |
255 | $this->message( 'pt-movepage-list-count' ) |
256 | ->numParams( $count, $subpagesCount, $talkpagesCount ) |
257 | ->text() . "\n" |
258 | ); |
259 | $this->logSeparator(); |
260 | $this->output( "\n" ); |
261 | } |
262 | |
263 | private function getSectionHeader( string $type, array $pages, bool $leaveRedirect ): string { |
264 | $infoMessage = $this->getSeparator(); |
265 | $pageCount = count( $pages ); |
266 | $shouldRedirect = TranslatableBundleMover::shouldLeaveRedirect( $type, $leaveRedirect ); |
267 | |
268 | // $type can be: pt-movepage-list-source, pt-movepage-list-translation, pt-movepage-list-section |
269 | // pt-movepage-list-other |
270 | $infoMessage .= $this->message( $type )->numParams( $pageCount )->text() . ' '; |
271 | |
272 | if ( $shouldRedirect ) { |
273 | $infoMessage .= '(leave redirect)'; |
274 | } |
275 | |
276 | $infoMessage .= "\n\n"; |
277 | |
278 | if ( !$pageCount ) { |
279 | $infoMessage .= $this->message( 'pt-movepage-list-no-pages' )->text() . "\n"; |
280 | } |
281 | |
282 | return $infoMessage; |
283 | } |
284 | |
285 | private function getConfirmation(): bool { |
286 | $line = self::readconsole( 'Type "MOVE" to begin the move operation: ' ); |
287 | return strtolower( $line ) === 'move'; |
288 | } |
289 | |
290 | private function getSeparator( int $width = 15 ): string { |
291 | return str_repeat( '-', $width ) . "\n"; |
292 | } |
293 | |
294 | private function logSeparator( int $width = 15 ): void { |
295 | $this->output( $this->getSeparator( $width ) ); |
296 | } |
297 | |
298 | private function message( string $key ): Message { |
299 | return ( new Message( $key ) )->inLanguage( 'en' ); |
300 | } |
301 | |
302 | private function getTitleFromInput( string $pageName ): Title { |
303 | $titleValue = $this->titleParser->parseTitle( $pageName ); |
304 | return Title::newFromLinkTarget( $titleValue ); |
305 | } |
306 | } |