Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 180
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
MoveTranslatableBundleMaintenanceScript
0.00% covered (danger)
0.00%
0 / 180
0.00% covered (danger)
0.00%
0 / 11
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
90
 parseErrorMessage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 progressCallback
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 displayPagesToMove
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
110
 getSectionHeader
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getConfirmation
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSeparator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logSeparator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 message
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleFromInput
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use Closure;
7use MalformedTitleException;
8use MediaWiki\Extension\Translate\Services;
9use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\Title\Title;
12use Message;
13use SplObjectStorage;
14use Status;
15use TitleParser;
16
17class 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}