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