Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 191 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
ImportTranslatableBundleMaintenanceScript | |
0.00% |
0 / 191 |
|
0.00% |
0 / 10 |
650 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
6 | |||
logPageImportComplete | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getPathOfFileToImport | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getImportUser | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getInterwikiPrefix | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getPriorityLanguages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
markPageForTranslation | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
getTranslatablePageSettings | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getTargetPageName | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use MediaWiki\Context\RequestContext; |
7 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageMarkException; |
8 | use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageSettings; |
9 | use MediaWiki\Extension\Translate\Services; |
10 | use MediaWiki\Extension\Translate\Utilities\BaseMaintenanceScript; |
11 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Title\ForeignTitle; |
14 | use MediaWiki\Title\MalformedTitleException; |
15 | use MediaWiki\Title\Title; |
16 | use MediaWiki\User\UserIdentity; |
17 | use 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 | */ |
25 | class 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 | } |