Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportTranslatableBundleMaintenanceScript
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 10
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
6
 logPageImportComplete
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getPathOfFileToImport
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getImportUser
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getInterwikiPrefix
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getPriorityLanguages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 markPageForTranslation
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 getTranslatablePageSettings
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getTargetPageName
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use MediaWiki\Context\RequestContext;
7use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageMarkException;
8use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageSettings;
9use MediaWiki\Extension\Translate\Services;
10use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript;
11use MediaWiki\Extension\Translate\Utilities\Utilities;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Title\ForeignTitle;
14use MediaWiki\Title\MalformedTitleException;
15use MediaWiki\Title\Title;
16use MediaWiki\User\UserIdentity;
17use Wikimedia\Rdbms\IDBAccessObject;
18
19/**
20 * Script to import a translatable bundle from a script exported via WikiExporter.
21 * @since 2023.05
22 * @license GPL-2.0-or-later
23 * @author Abijeet Patro
24 */
25class ImportTranslatableBundleMaintenanceScript extends BaseMaintenanceScript {
26    private int $pageImportCount = 0;
27    private int $totalPagesBeingImported = 0;
28
29    public function __construct() {
30        parent::__construct();
31        $this->addArg(
32            'xml-path',
33            'Path to the XML file to be imported',
34            self::REQUIRED
35        );
36        $this->addOption(
37            'user',
38            'Name of the user performing the import',
39            self::REQUIRED,
40            self::HAS_ARG
41        );
42        $this->addOption(
43            'interwiki-prefix',
44            'Prefix to apply to unknown (and possibly also known) usernames',
45            self::REQUIRED,
46            self::HAS_ARG
47        );
48        $this->addOption(
49            'comment',
50            'Comment added to the log for the import',
51            self::OPTIONAL,
52            self::HAS_ARG
53        );
54        $this->addOption(
55            'assign-known-users',
56            'Whether to apply the prefix to usernames that exist locally',
57            self::OPTIONAL
58        );
59        $this->addOption(
60            'target-name',
61            'Target page name to import the page to',
62            self::OPTIONAL,
63            self::HAS_ARG
64        );
65        $this->addOption(
66            'override',
67            'Override existing target page if it exists',
68            self::OPTIONAL
69        );
70
71        // Options related to marking a page for translation
72        $this->addOption(
73            'skip-translating-title',
74            'Should translation of title be skipped',
75            self::OPTIONAL
76        );
77        $this->addOption(
78            'priority-languages',
79            'Comma separated list of priority language codes',
80            self::OPTIONAL,
81            self::HAS_ARG
82        );
83        $this->addOption(
84            'priority-languages-reason',
85            'Reason for setting the priority languages',
86            self::OPTIONAL,
87            self::HAS_ARG
88        );
89        $this->addOption(
90            'force-priority-languages',
91            'Only allow translations to the priority languages',
92            self::OPTIONAL
93        );
94        $this->addOption(
95            'disallow-transclusion',
96            'Disable translation aware transclusion for this page',
97            self::OPTIONAL
98        );
99        $this->addOption(
100            'use-old-syntax-version',
101            'Use the old syntax version for translatable pages',
102            self::OPTIONAL
103        );
104
105        $this->requireExtension( 'Translate' );
106    }
107
108    /** @inheritDoc */
109    public function execute() {
110        $this->pageImportCount = 0;
111        $importFilePath = $this->getPathOfFileToImport();
112        $importUser = $this->getImportUser();
113        $comment = $this->getOption( 'comment' );
114        $interwikiPrefix = $this->getInterwikiPrefix();
115        $assignKnownUsers = $this->hasOption( 'assign-known-users' );
116        $targetPage = $this->getTargetPageName();
117        $translatablePageSettings = $this->getTranslatablePageSettings();
118
119        // First import the page
120        try {
121            $this->totalPagesBeingImported = substr_count( file_get_contents( $importFilePath ), '</page>' );
122            $importer = Services::getInstance()->getTranslatableBundleImporter();
123            $this->output( "Starting import for file '$importFilePath'...\n" );
124            $importer->setPageImportCompleteCallback( [ $this, 'logPageImportComplete' ] );
125            $bundleTitle = $importer->import(
126                $importFilePath,
127                $interwikiPrefix,
128                $assignKnownUsers,
129                $importUser,
130                RequestContext::getMain(),
131                $targetPage,
132                $comment
133            );
134        } catch ( TranslatableBundleImportException $e ) {
135            $this->error( "An error occurred during import: {$e->getMessage()}\n" );
136            $this->error( "Stacktrace: {$e->getTraceAsString()} .\n" );
137            $this->fatalError( 'Stopping import.' );
138        }
139
140        $this->output(
141            "Translatable bundle {$bundleTitle->getPrefixedText()} was imported. " .
142            "Total {$this->pageImportCount} page(s) were created\n"
143        );
144
145        $this->output( "Now marking {$bundleTitle->getPrefixedText()} for translation...\n" );
146        // Try to mark the page for translation
147        $this->markPageForTranslation( $bundleTitle, $translatablePageSettings, $importUser );
148    }
149
150    public function logPageImportComplete( Title $title, ForeignTitle $foreignTitle ): void {
151        ++$this->pageImportCount;
152        $currentProgress = str_pad(
153            (string)$this->pageImportCount,
154            strlen( (string)$this->totalPagesBeingImported ),
155            ' ',
156            STR_PAD_LEFT
157        );
158
159        $progressCounter = "($currentProgress/$this->totalPagesBeingImported)";
160        $this->output( "$progressCounter {$foreignTitle->getFullText()} --> {$title->getFullText()}\n" );
161    }
162
163    private function getPathOfFileToImport(): string {
164        $xmlPath = $this->getArg( 'xml-path' );
165        if ( !file_exists( $xmlPath ) ) {
166            $this->fatalError( "File '$xmlPath' does not exist" );
167        }
168
169        if ( !is_readable( $xmlPath ) ) {
170            $this->fatalError( "File '$xmlPath' is not readable" );
171        }
172
173        return $xmlPath;
174    }
175
176    private function getImportUser(): UserIdentity {
177        $username = $this->getOption( 'user' );
178
179        $userFactory = MediaWikiServices::getInstance()->getUserFactory();
180        $user = $userFactory->newFromName( $username );
181
182        if ( $user === null || !$user->isNamed() ) {
183            $this->fatalError( "User $username does not exist." );
184        }
185
186        return $user;
187    }
188
189    private function getInterwikiPrefix(): string {
190        $interwikiPrefix = trim( $this->getOption( 'interwiki-prefix', '' ) );
191        if ( $interwikiPrefix === '' ) {
192            $this->fatalError( 'Argument interwiki-prefix cannot be empty.' );
193        }
194
195        return $interwikiPrefix;
196    }
197
198    private function getPriorityLanguages(): array {
199        $priorityLanguageCodes = self::commaList2Array( $this->getOption( 'priority-languages' ) ?? '' );
200        $knownLanguageCodes = array_keys( Utilities::getLanguageNames( 'en' ) );
201        $invalidLanguageCodes = array_diff( $priorityLanguageCodes, $knownLanguageCodes );
202
203        if ( $invalidLanguageCodes ) {
204            $this->fatalError(
205                'Unknown priority language code(s): ' . implode( ', ', $invalidLanguageCodes )
206            );
207        }
208
209        return $priorityLanguageCodes;
210    }
211
212    private function markPageForTranslation(
213        Title $bundleTitle,
214        TranslatablePageSettings $translatablePageSettings,
215        UserIdentity $importUser
216    ): void {
217        $translatablePageMarker = Services::getInstance()->getTranslatablePageMarker();
218        $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $importUser );
219        try {
220            $operation = $translatablePageMarker->getMarkOperation(
221                $bundleTitle->toPageRecord( IDBAccessObject::READ_LATEST ),
222                null,
223                $translatablePageSettings->shouldTranslateTitle()
224            );
225        } catch ( TranslatablePageMarkException $e ) {
226            $this->error( "Error while marking page {$bundleTitle->getPrefixedText()} for translation.\n" );
227            $this->error( "Fix the issues and mark the page for translation using Special:PageTranslation.\n\n" );
228            $this->fatalError( wfMessage( $e->getMessageObject() )->text() );
229        }
230
231        $statusFormatter = MediaWikiServices::getInstance()
232            ->getFormatterFactory()
233            ->getStatusFormatter( RequestContext::getMain() );
234
235        $unitNameValidationResult = $operation->getUnitValidationStatus();
236        if ( !$unitNameValidationResult->isOK() ) {
237            $this->output( "Unit validation failed for {$bundleTitle->getPrefixedText()}.\n" );
238            $this->fatalError( $statusFormatter->getMessage( $unitNameValidationResult )->text() );
239        }
240
241        try {
242            $translatablePageMarker->markForTranslation(
243                $operation,
244                $translatablePageSettings,
245                RequestContext::getMain(),
246                $user
247            );
248            $this->output( "The page {$bundleTitle->getPrefixedText()} has been marked for translation.\n" );
249        } catch ( TranslatablePageMarkException $e ) {
250            $this->error( "Error while marking page {$bundleTitle->getPrefixedText()} for translation.\n" );
251            $this->error( "Fix the issues and mark the page for translation using Special:PageTranslation.\n\n" );
252            $this->fatalError( $e->getMessageObject()->text() );
253        }
254    }
255
256    private function getTranslatablePageSettings(): TranslatablePageSettings {
257        return new TranslatablePageSettings(
258            $this->getPriorityLanguages(),
259            $this->hasOption( 'force-priority-languages' ),
260            $this->getOption( 'priority-languages-reason' ) ?? '',
261            [],
262            !$this->hasOption( 'skip-translating-title' ),
263            !$this->hasOption( 'use-old-syntax-version' ),
264            !$this->hasOption( 'disallow-transclusion' ),
265        );
266    }
267
268    private function getTargetPageName(): ?Title {
269        $targetPage = $this->getOption( 'target-name' );
270        if ( $targetPage === null ) {
271            return null;
272        }
273
274        try {
275            $targetPageTitle = MediaWikiServices::getInstance()->getTitleFactory()->newFromTextThrow( $targetPage );
276        } catch ( MalformedTitleException $e ) {
277            $this->fatalError(
278                "Target page name $targetPage does not appear to be valid: {$e->getMessage()}"
279            );
280        }
281
282        $shouldOverride = $this->hasOption( 'override' );
283        if ( $targetPageTitle->exists() && !$shouldOverride ) {
284            $this->fatalError(
285                "Specified target page $targetPage already exists. Use '--override' if you still want to import"
286            );
287        }
288
289        if ( !$targetPageTitle->canExist() ) {
290            $this->fatalError( "The target page name $targetPage cannot be created" );
291        }
292
293        return $targetPageTitle;
294    }
295}