Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 610 |
|
0.00% |
0 / 24 |
CRAP | |
0.00% |
0 / 1 |
ManageGroupsSpecialPage | |
0.00% |
0 / 610 |
|
0.00% |
0 / 24 |
18906 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
110 | |||
getLimit | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getLegend | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
showChanges | |
0.00% |
0 / 79 |
|
0.00% |
0 / 1 |
240 | |||
formatChange | |
0.00% |
0 / 91 |
|
0.00% |
0 / 1 |
650 | |||
processSubmit | |
0.00% |
0 / 76 |
|
0.00% |
0 / 1 |
240 | |||
changeId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tabify | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
56 | |||
isMessageDefinitionPresent | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
showRenames | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
72 | |||
formatRename | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
56 | |||
getRenameJobParams | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
handleRenameSubmit | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
72 | |||
handleModificationsSubmit | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
createRenameJobs | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
isTitlePresent | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
isRenameMissing | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getProcessingErrorMessage | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
getGroupsFromCdb | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
startSync | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\Synchronization; |
5 | |
6 | use ContentHandler; |
7 | use DeferredUpdates; |
8 | use DifferenceEngine; |
9 | use DisabledSpecialPage; |
10 | use Exception; |
11 | use FileBasedMessageGroup; |
12 | use JobQueueGroup; |
13 | use Language; |
14 | use MediaWiki\Cache\LinkBatchFactory; |
15 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
16 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
17 | use MediaWiki\Extension\Translate\MessageLoading\MessageIndex; |
18 | use MediaWiki\Extension\Translate\MessageSync\MessageSourceChange; |
19 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
20 | use MediaWiki\Html\Html; |
21 | use MediaWiki\Logger\LoggerFactory; |
22 | use MediaWiki\MediaWikiServices; |
23 | use MediaWiki\Revision\RevisionLookup; |
24 | use MediaWiki\Revision\SlotRecord; |
25 | use MediaWiki\Title\Title; |
26 | use MessageGroup; |
27 | use MessageUpdateJob; |
28 | use NamespaceInfo; |
29 | use OOUI\ButtonInputWidget; |
30 | use OutputPage; |
31 | use PermissionsError; |
32 | use RuntimeException; |
33 | use Skin; |
34 | use SpecialPage; |
35 | use TextContent; |
36 | use UserBlockedError; |
37 | use WebRequest; |
38 | use Xml; |
39 | |
40 | /** |
41 | * Class for special page Special:ManageMessageGroups. On this special page |
42 | * file based message groups can be managed (FileBasedMessageGroup). This page |
43 | * allows updating of the file cache, import and fuzzy for source language |
44 | * messages, as well as import/update of messages in other languages. |
45 | * |
46 | * @author Niklas Laxström |
47 | * @author Siebrand Mazeland |
48 | * @ingroup SpecialPage TranslateSpecialPage |
49 | * @license GPL-2.0-or-later |
50 | */ |
51 | class ManageGroupsSpecialPage extends SpecialPage { |
52 | private const GROUP_SYNC_INFO_WRAPPER_CLASS = 'smg-group-sync-cache-info'; |
53 | private const RIGHT = 'translate-manage'; |
54 | /** @var DifferenceEngine */ |
55 | protected $diff; |
56 | /** @var string Path to the change cdb file. */ |
57 | protected $cdb; |
58 | /** @var bool Has the necessary right specified by the RIGHT constant */ |
59 | protected $hasRight = false; |
60 | /** @var Language */ |
61 | private $contLang; |
62 | /** @var NamespaceInfo */ |
63 | private $nsInfo; |
64 | /** @var RevisionLookup */ |
65 | private $revLookup; |
66 | /** @var GroupSynchronizationCache */ |
67 | private $synchronizationCache; |
68 | /** @var DisplayGroupSynchronizationInfo */ |
69 | private $displayGroupSyncInfo; |
70 | /** @var JobQueueGroup */ |
71 | private $jobQueueGroup; |
72 | /** @var MessageIndex */ |
73 | private $messageIndex; |
74 | /** @var LinkBatchFactory */ |
75 | private $linkBatchFactory; |
76 | |
77 | public function __construct( |
78 | Language $contLang, |
79 | NamespaceInfo $nsInfo, |
80 | RevisionLookup $revLookup, |
81 | GroupSynchronizationCache $synchronizationCache, |
82 | JobQueueGroup $jobQueueGroup, |
83 | MessageIndex $messageIndex, |
84 | LinkBatchFactory $linkBatchFactory |
85 | ) { |
86 | // Anyone is allowed to see, but actions are restricted |
87 | parent::__construct( 'ManageMessageGroups' ); |
88 | $this->contLang = $contLang; |
89 | $this->nsInfo = $nsInfo; |
90 | $this->revLookup = $revLookup; |
91 | $this->synchronizationCache = $synchronizationCache; |
92 | $this->displayGroupSyncInfo = new DisplayGroupSynchronizationInfo( $this, $this->getLinkRenderer() ); |
93 | $this->jobQueueGroup = $jobQueueGroup; |
94 | $this->messageIndex = $messageIndex; |
95 | $this->linkBatchFactory = $linkBatchFactory; |
96 | } |
97 | |
98 | public function doesWrites() { |
99 | return true; |
100 | } |
101 | |
102 | protected function getGroupName() { |
103 | return 'translation'; |
104 | } |
105 | |
106 | public function getDescription() { |
107 | // Backward compatibility for < 1.41 |
108 | if ( version_compare( MW_VERSION, '1.41', '<' ) ) { |
109 | return $this->msg( 'managemessagegroups' )->text(); |
110 | } |
111 | return $this->msg( 'managemessagegroups' ); |
112 | } |
113 | |
114 | public function execute( $par ) { |
115 | $this->setHeaders(); |
116 | |
117 | $out = $this->getOutput(); |
118 | $out->addModuleStyles( 'ext.translate.specialpages.styles' ); |
119 | $out->addModules( 'ext.translate.special.managegroups' ); |
120 | $out->addHelpLink( 'Help:Extension:Translate/Group_management' ); |
121 | |
122 | $name = $par ?: MessageChangeStorage::DEFAULT_NAME; |
123 | |
124 | $this->cdb = MessageChangeStorage::getCdbPath( $name ); |
125 | if ( !MessageChangeStorage::isValidCdbName( $name ) || !file_exists( $this->cdb ) ) { |
126 | if ( $this->getConfig()->get( 'TranslateGroupSynchronizationCache' ) ) { |
127 | $out->addHTML( |
128 | $this->displayGroupSyncInfo->getGroupsInSyncHtml( |
129 | $this->synchronizationCache->getGroupsInSync(), |
130 | self::GROUP_SYNC_INFO_WRAPPER_CLASS |
131 | ) |
132 | ); |
133 | |
134 | $out->addHTML( |
135 | $this->displayGroupSyncInfo->getHtmlForGroupsWithError( |
136 | $this->synchronizationCache, |
137 | self::GROUP_SYNC_INFO_WRAPPER_CLASS, |
138 | $this->getLanguage() |
139 | ) |
140 | ); |
141 | } |
142 | |
143 | // @todo Tell them when changes was last checked/process |
144 | // or how to initiate recheck. |
145 | $out->addWikiMsg( 'translate-smg-nochanges' ); |
146 | |
147 | return; |
148 | } |
149 | |
150 | $user = $this->getUser(); |
151 | $this->hasRight = $user->isAllowed( self::RIGHT ); |
152 | |
153 | $req = $this->getRequest(); |
154 | if ( !$req->wasPosted() ) { |
155 | $this->showChanges( $this->getLimit() ); |
156 | |
157 | return; |
158 | } |
159 | |
160 | $block = $user->getBlock(); |
161 | if ( $block && $block->isSitewide() ) { |
162 | throw new UserBlockedError( |
163 | $block, |
164 | $user, |
165 | $this->getLanguage(), |
166 | $req->getIP() |
167 | ); |
168 | } |
169 | |
170 | $csrfTokenSet = $this->getContext()->getCsrfTokenSet(); |
171 | if ( !$this->hasRight || !$csrfTokenSet->matchTokenField( 'token' ) ) { |
172 | throw new PermissionsError( self::RIGHT ); |
173 | } |
174 | |
175 | $this->processSubmit(); |
176 | } |
177 | |
178 | /** How many changes can be shown per page. */ |
179 | protected function getLimit(): int { |
180 | $limits = [ |
181 | 1000, // Default max |
182 | ini_get( 'max_input_vars' ), |
183 | ini_get( 'suhosin.post.max_vars' ), |
184 | ini_get( 'suhosin.request.max_vars' ) |
185 | ]; |
186 | // Ignore things not set |
187 | $limits = array_filter( $limits ); |
188 | return (int)min( $limits ); |
189 | } |
190 | |
191 | protected function getLegend(): string { |
192 | $text = $this->diff->addHeader( |
193 | '', |
194 | $this->msg( 'translate-smg-left' )->escaped(), |
195 | $this->msg( 'translate-smg-right' )->escaped() |
196 | ); |
197 | |
198 | return Html::rawElement( 'div', [ 'class' => 'mw-translate-smg-header' ], $text ); |
199 | } |
200 | |
201 | protected function showChanges( int $limit ): void { |
202 | $diff = new DifferenceEngine( $this->getContext() ); |
203 | $diff->showDiffStyle(); |
204 | $diff->setReducedLineNumbers(); |
205 | $this->diff = $diff; |
206 | |
207 | $out = $this->getOutput(); |
208 | $out->addHTML( |
209 | Html::openElement( 'form', [ 'method' => 'post' ] ) . |
210 | Html::hidden( 'title', $this->getPageTitle()->getPrefixedText(), [ |
211 | 'id' => 'smgPageTitle' |
212 | ] ) . |
213 | Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() ) . |
214 | Html::hidden( 'changesetModifiedTime', |
215 | MessageChangeStorage::getLastModifiedTime( $this->cdb ) ) . |
216 | $this->getLegend() |
217 | ); |
218 | |
219 | // The above count as three |
220 | $limit -= 3; |
221 | |
222 | $groupSyncCacheEnabled = $this->getConfig()->get( 'TranslateGroupSynchronizationCache' ); |
223 | if ( $groupSyncCacheEnabled ) { |
224 | $out->addHTML( |
225 | $this->displayGroupSyncInfo->getGroupsInSyncHtml( |
226 | $this->synchronizationCache->getGroupsInSync(), |
227 | self::GROUP_SYNC_INFO_WRAPPER_CLASS |
228 | ) |
229 | ); |
230 | |
231 | $out->addHTML( |
232 | $this->displayGroupSyncInfo->getHtmlForGroupsWithError( |
233 | $this->synchronizationCache, |
234 | self::GROUP_SYNC_INFO_WRAPPER_CLASS, |
235 | $this->getLanguage() |
236 | ) |
237 | ); |
238 | } |
239 | |
240 | $reader = \Cdb\Reader::open( $this->cdb ); |
241 | $groups = $this->getGroupsFromCdb( $reader ); |
242 | foreach ( $groups as $id => $group ) { |
243 | $sourceChanges = MessageSourceChange::loadModifications( |
244 | Utilities::deserialize( $reader->get( $id ) ) |
245 | ); |
246 | $out->addHTML( Html::element( 'h2', [], $group->getLabel() ) ); |
247 | |
248 | if ( $groupSyncCacheEnabled && $this->synchronizationCache->groupHasErrors( $id ) ) { |
249 | $out->addHTML( |
250 | Html::warningBox( $this->msg( 'translate-smg-group-sync-error-warn' )->escaped(), 'center' ) |
251 | ); |
252 | } |
253 | |
254 | // Reduce page existance queries to one per group |
255 | $lb = $this->linkBatchFactory->newLinkBatch(); |
256 | $ns = $group->getNamespace(); |
257 | $isCap = $this->nsInfo->isCapitalized( $ns ); |
258 | $languages = $sourceChanges->getLanguages(); |
259 | |
260 | foreach ( $languages as $language ) { |
261 | $languageChanges = $sourceChanges->getModificationsForLanguage( $language ); |
262 | foreach ( $languageChanges as $type => $changes ) { |
263 | foreach ( $changes as $params ) { |
264 | // Constructing title objects is way slower |
265 | $key = $params['key']; |
266 | if ( $isCap ) { |
267 | $key = $this->contLang->ucfirst( $key ); |
268 | } |
269 | $lb->add( $ns, "$key/$language" ); |
270 | } |
271 | } |
272 | } |
273 | $lb->execute(); |
274 | |
275 | foreach ( $languages as $language ) { |
276 | // Handle and generate UI for additions, deletions, change |
277 | $changes = []; |
278 | $changes[ MessageSourceChange::ADDITION ] = $sourceChanges->getAdditions( $language ); |
279 | $changes[ MessageSourceChange::DELETION ] = $sourceChanges->getDeletions( $language ); |
280 | $changes[ MessageSourceChange::CHANGE ] = $sourceChanges->getChanges( $language ); |
281 | |
282 | foreach ( $changes as $type => $messages ) { |
283 | foreach ( $messages as $params ) { |
284 | $change = $this->formatChange( $group, $sourceChanges, $language, $type, $params, $limit ); |
285 | $out->addHTML( $change ); |
286 | |
287 | if ( $limit <= 0 ) { |
288 | // We need to restrict the changes per page per form submission |
289 | // limitations as well as performance. |
290 | $out->wrapWikiMsg( "<div class=warning>\n$1\n</div>", 'translate-smg-more' ); |
291 | break 4; |
292 | } |
293 | } |
294 | } |
295 | |
296 | // Handle and generate UI for renames |
297 | $this->showRenames( $group, $sourceChanges, $out, $language, $limit ); |
298 | } |
299 | } |
300 | |
301 | $out->enableOOUI(); |
302 | $button = new ButtonInputWidget( [ |
303 | 'type' => 'submit', |
304 | 'label' => $this->msg( 'translate-smg-submit' )->plain(), |
305 | 'disabled' => !$this->hasRight ? 'disabled' : null, |
306 | 'classes' => [ 'mw-translate-smg-submit' ], |
307 | 'title' => !$this->hasRight ? $this->msg( 'translate-smg-notallowed' )->plain() : null, |
308 | 'flags' => [ 'primary', 'progressive' ], |
309 | ] ); |
310 | $out->addHTML( $button ); |
311 | $out->addHTML( Html::closeElement( 'form' ) ); |
312 | } |
313 | |
314 | protected function formatChange( |
315 | MessageGroup $group, |
316 | MessageSourceChange $changes, |
317 | string $language, |
318 | string $type, |
319 | array $params, |
320 | int &$limit |
321 | ): string { |
322 | $key = $params['key']; |
323 | $title = Title::makeTitleSafe( $group->getNamespace(), "$key/$language" ); |
324 | $id = self::changeId( $group->getId(), $language, $type, $key ); |
325 | $noticeHtml = ''; |
326 | $isReusedKey = false; |
327 | |
328 | if ( $title && $type === 'addition' && $title->exists() ) { |
329 | // The message has for some reason dropped out from cache |
330 | // or perhaps it is being reused. In any case treat it |
331 | // as a change for display, so the admin can see if |
332 | // action is needed and let the message be processed. |
333 | // Otherwise it will end up in the postponed category |
334 | // forever and will prevent rebuilding the cache, which |
335 | // leads to many other annoying problems. |
336 | $type = 'change'; |
337 | $noticeHtml .= Html::warningBox( $this->msg( 'translate-manage-key-reused' )->text() ); |
338 | $isReusedKey = true; |
339 | } elseif ( $title && ( $type === 'deletion' || $type === 'change' ) && !$title->exists() ) { |
340 | // This happens if a message key has been renamed |
341 | // The change can be ignored. |
342 | return ''; |
343 | } |
344 | |
345 | $text = ''; |
346 | $titleLink = $this->getLinkRenderer()->makeLink( $title ); |
347 | |
348 | if ( $type === 'deletion' ) { |
349 | $revTitle = $this->revLookup->getRevisionByTitle( $title ); |
350 | if ( !$revTitle ) { |
351 | wfWarn( "[ManageGroupSpecialPage] No revision associated with {$title->getPrefixedText()}" ); |
352 | } |
353 | $content = $revTitle ? $revTitle->getContent( SlotRecord::MAIN ) : null; |
354 | $wiki = ( $content instanceof TextContent ) ? $content->getText() : ''; |
355 | |
356 | if ( $wiki === '' ) { |
357 | $noticeHtml .= Html::warningBox( |
358 | $this->msg( 'translate-manage-empty-content' )->text() |
359 | ); |
360 | } |
361 | |
362 | $oldContent = ContentHandler::makeContent( (string)$wiki, $title ); |
363 | $newContent = ContentHandler::makeContent( '', $title ); |
364 | $this->diff->setContent( $oldContent, $newContent ); |
365 | $text = $this->diff->getDiff( $titleLink, '', $noticeHtml ); |
366 | } elseif ( $type === 'addition' ) { |
367 | $menu = ''; |
368 | $sourceLanguage = $group->getSourceLanguage(); |
369 | if ( $sourceLanguage === $language ) { |
370 | if ( $this->hasRight ) { |
371 | $menu = Html::rawElement( |
372 | 'button', |
373 | [ |
374 | 'class' => 'smg-rename-actions', |
375 | 'type' => 'button', |
376 | 'data-group-id' => $group->getId(), |
377 | 'data-lang' => $language, |
378 | 'data-msgkey' => $key, |
379 | 'data-msgtitle' => $title->getFullText() |
380 | ], |
381 | '' |
382 | ); |
383 | } |
384 | } elseif ( !self::isMessageDefinitionPresent( $group, $changes, $key ) ) { |
385 | $noticeHtml .= Html::warningBox( |
386 | $this->msg( 'translate-manage-source-message-not-found' )->text(), |
387 | 'mw-translate-smg-notice-important' |
388 | ); |
389 | |
390 | // Automatically ignore messages that don't have a definitions |
391 | $menu = Html::hidden( "msg/$id", 'ignore', [ 'id' => "i/$id" ] ); |
392 | $limit--; |
393 | } |
394 | |
395 | if ( $params['content'] === '' ) { |
396 | $noticeHtml .= Html::warningBox( |
397 | $this->msg( 'translate-manage-empty-content' )->text() |
398 | ); |
399 | } |
400 | |
401 | $oldContent = ContentHandler::makeContent( '', $title ); |
402 | $newContent = ContentHandler::makeContent( (string)$params['content'], $title ); |
403 | $this->diff->setContent( $oldContent, $newContent ); |
404 | $text = $this->diff->getDiff( '', $titleLink . $menu, $noticeHtml ); |
405 | } elseif ( $type === 'change' ) { |
406 | $wiki = Utilities::getContentForTitle( $title, true ); |
407 | |
408 | $actions = ''; |
409 | $sourceLanguage = $group->getSourceLanguage(); |
410 | |
411 | // Option to fuzzy is only available for source languages, and should be used |
412 | // if content has changed. |
413 | $shouldFuzzy = $sourceLanguage === $language && $wiki !== $params['content']; |
414 | |
415 | if ( $sourceLanguage === $language ) { |
416 | $label = $this->msg( 'translate-manage-action-fuzzy' )->text(); |
417 | $actions .= Xml::radioLabel( $label, "msg/$id", "fuzzy", "f/$id", $shouldFuzzy ); |
418 | } |
419 | |
420 | if ( |
421 | $sourceLanguage !== $language && |
422 | $isReusedKey && |
423 | !self::isMessageDefinitionPresent( $group, $changes, $key ) |
424 | ) { |
425 | $noticeHtml .= Html::warningBox( |
426 | $this->msg( 'translate-manage-source-message-not-found' )->text(), |
427 | 'mw-translate-smg-notice-important' |
428 | ); |
429 | |
430 | // Automatically ignore messages that don't have a definitions |
431 | $actions .= Html::hidden( "msg/$id", 'ignore', [ 'id' => "i/$id" ] ); |
432 | $limit--; |
433 | } else { |
434 | $label = $this->msg( 'translate-manage-action-import' )->text(); |
435 | $actions .= Xml::radioLabel( $label, "msg/$id", "import", "imp/$id", !$shouldFuzzy ); |
436 | |
437 | $label = $this->msg( 'translate-manage-action-ignore' )->text(); |
438 | $actions .= Xml::radioLabel( $label, "msg/$id", "ignore", "i/$id" ); |
439 | $limit--; |
440 | } |
441 | |
442 | $oldContent = ContentHandler::makeContent( (string)$wiki, $title ); |
443 | $newContent = ContentHandler::makeContent( (string)$params['content'], $title ); |
444 | |
445 | $this->diff->setContent( $oldContent, $newContent ); |
446 | $text .= $this->diff->getDiff( $titleLink, $actions, $noticeHtml ); |
447 | } |
448 | |
449 | $hidden = Html::hidden( $id, 1 ); |
450 | $limit--; |
451 | $text .= $hidden; |
452 | $classes = "mw-translate-smg-change smg-change-$type"; |
453 | |
454 | if ( $limit < 0 ) { |
455 | // Don't add if one of the fields might get dropped of at submission |
456 | return ''; |
457 | } |
458 | |
459 | return Html::rawElement( 'div', [ 'class' => $classes ], $text ); |
460 | } |
461 | |
462 | protected function processSubmit(): void { |
463 | $req = $this->getRequest(); |
464 | $out = $this->getOutput(); |
465 | $errorGroups = []; |
466 | |
467 | $modificationJobs = $renameJobData = []; |
468 | $lastModifiedTime = intval( $req->getVal( 'changesetModifiedTime' ) ); |
469 | |
470 | if ( !MessageChangeStorage::isModifiedSince( $this->cdb, $lastModifiedTime ) ) { |
471 | $out->addWikiMsg( 'translate-smg-changeset-modified' ); |
472 | return; |
473 | } |
474 | |
475 | $reader = \Cdb\Reader::open( $this->cdb ); |
476 | $groups = $this->getGroupsFromCdb( $reader ); |
477 | $groupSyncCacheEnabled = $this->getConfig()->get( 'TranslateGroupSynchronizationCache' ); |
478 | $postponed = []; |
479 | |
480 | foreach ( $groups as $groupId => $group ) { |
481 | try { |
482 | if ( !$group instanceof FileBasedMessageGroup ) { |
483 | throw new RuntimeException( "Expected $groupId to be FileBasedMessageGroup, got " |
484 | . get_class( $group ) |
485 | . " instead." |
486 | ); |
487 | } |
488 | $changes = Utilities::deserialize( $reader->get( $groupId ) ); |
489 | if ( $groupSyncCacheEnabled && $this->synchronizationCache->groupHasErrors( $groupId ) ) { |
490 | $postponed[$groupId] = $changes; |
491 | continue; |
492 | } |
493 | |
494 | $sourceChanges = MessageSourceChange::loadModifications( $changes ); |
495 | $groupModificationJobs = []; |
496 | $groupRenameJobData = []; |
497 | $languages = $sourceChanges->getLanguages(); |
498 | foreach ( $languages as $language ) { |
499 | // Handle changes, additions, deletions |
500 | $this->handleModificationsSubmit( |
501 | $group, |
502 | $sourceChanges, |
503 | $req, |
504 | $language, |
505 | $postponed, |
506 | $groupModificationJobs |
507 | ); |
508 | |
509 | // Handle renames, this might also add modification jobs based on user selection. |
510 | $this->handleRenameSubmit( |
511 | $group, |
512 | $sourceChanges, |
513 | $req, |
514 | $language, |
515 | $postponed, |
516 | $groupRenameJobData, |
517 | $groupModificationJobs |
518 | ); |
519 | |
520 | if ( !isset( $postponed[$groupId][$language] ) ) { |
521 | $group->getMessageGroupCache( $language )->create(); |
522 | } |
523 | } |
524 | |
525 | if ( $groupSyncCacheEnabled && !isset( $postponed[ $groupId ] ) ) { |
526 | $this->synchronizationCache->markGroupAsReviewed( $groupId ); |
527 | } |
528 | |
529 | $modificationJobs[$groupId] = $groupModificationJobs; |
530 | $renameJobData[$groupId] = $groupRenameJobData; |
531 | } catch ( Exception $e ) { |
532 | error_log( |
533 | "ManageGroupsSpecialPage: Error in processSubmit. Group: $groupId\n" . |
534 | "Exception: $e" |
535 | ); |
536 | |
537 | $errorGroups[] = $group->getLabel(); |
538 | } |
539 | } |
540 | |
541 | $renameJobs = $this->createRenameJobs( $renameJobData ); |
542 | $this->startSync( $modificationJobs, $renameJobs ); |
543 | |
544 | $reader->close(); |
545 | rename( $this->cdb, $this->cdb . '-' . wfTimestamp() ); |
546 | |
547 | if ( $errorGroups ) { |
548 | $errorMsg = $this->getProcessingErrorMessage( $errorGroups, count( $groups ) ); |
549 | $out->addHTML( |
550 | Html::warningBox( |
551 | $errorMsg, |
552 | 'mw-translate-smg-submitted' |
553 | ) |
554 | ); |
555 | } |
556 | |
557 | if ( count( $postponed ) ) { |
558 | $postponedSourceChanges = []; |
559 | foreach ( $postponed as $groupId => $changes ) { |
560 | $postponedSourceChanges[$groupId] = MessageSourceChange::loadModifications( $changes ); |
561 | } |
562 | MessageChangeStorage::writeChanges( $postponedSourceChanges, $this->cdb ); |
563 | |
564 | $this->showChanges( $this->getLimit() ); |
565 | } elseif ( $errorGroups === [] ) { |
566 | $out->addWikiMsg( 'translate-smg-submitted' ); |
567 | } |
568 | } |
569 | |
570 | protected static function changeId( |
571 | string $groupId, |
572 | string $language, |
573 | string $type, |
574 | string $key |
575 | ): string { |
576 | return 'smg/' . substr( sha1( "$groupId/$language/$type/$key" ), 0, 7 ); |
577 | } |
578 | |
579 | /** |
580 | * Adds the task-based tabs on Special:Translate and few other special pages. |
581 | * Hook: SkinTemplateNavigation::Universal |
582 | */ |
583 | public static function tabify( Skin $skin, array &$tabs ): void { |
584 | $title = $skin->getTitle(); |
585 | if ( !$title->isSpecialPage() ) { |
586 | return; |
587 | } |
588 | $specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
589 | [ $alias, ] = $specialPageFactory->resolveAlias( $title->getText() ); |
590 | |
591 | $pagesInGroup = [ |
592 | 'ManageMessageGroups' => 'namespaces', |
593 | 'AggregateGroups' => 'namespaces', |
594 | 'SupportedLanguages' => 'views', |
595 | 'TranslationStats' => 'views', |
596 | ]; |
597 | if ( !isset( $pagesInGroup[$alias] ) ) { |
598 | return; |
599 | } |
600 | |
601 | $tabs['namespaces'] = []; |
602 | foreach ( $pagesInGroup as $spName => $section ) { |
603 | $spClass = $specialPageFactory->getPage( $spName ); |
604 | |
605 | if ( $spClass === null || $spClass instanceof DisabledSpecialPage ) { |
606 | continue; // Page explicitly disabled |
607 | } |
608 | $spTitle = $spClass->getPageTitle(); |
609 | |
610 | $tabs[$section][strtolower( $spName )] = [ |
611 | 'text' => $spClass->getDescription(), |
612 | 'href' => $spTitle->getLocalURL(), |
613 | 'class' => $alias === $spName ? 'selected' : '', |
614 | ]; |
615 | } |
616 | } |
617 | |
618 | /** |
619 | * Check if the message definition is present as an incoming addition |
620 | * OR exists already on the wiki |
621 | */ |
622 | private static function isMessageDefinitionPresent( |
623 | MessageGroup $group, |
624 | MessageSourceChange $changes, |
625 | string $msgKey |
626 | ): bool { |
627 | $sourceLanguage = $group->getSourceLanguage(); |
628 | if ( $changes->findMessage( $sourceLanguage, $msgKey, [ MessageSourceChange::ADDITION ] ) ) { |
629 | return true; |
630 | } |
631 | |
632 | $namespace = $group->getNamespace(); |
633 | $sourceHandle = new MessageHandle( Title::makeTitle( $namespace, $msgKey ) ); |
634 | return $sourceHandle->isValid(); |
635 | } |
636 | |
637 | private function showRenames( |
638 | MessageGroup $group, |
639 | MessageSourceChange $sourceChanges, |
640 | OutputPage $out, |
641 | string $language, |
642 | int &$limit |
643 | ): void { |
644 | $changes = $sourceChanges->getRenames( $language ); |
645 | foreach ( $changes as $key => $params ) { |
646 | // Since we're removing items from the array within the loop add |
647 | // a check here to ensure that the current key is still set. |
648 | if ( !isset( $changes[ $key ] ) ) { |
649 | continue; |
650 | } |
651 | |
652 | if ( $group->getSourceLanguage() !== $language && |
653 | $sourceChanges->isEqual( $language, $key ) ) { |
654 | // This is a translation rename, that does not have any changes. |
655 | // We can group this along with the source rename. |
656 | continue; |
657 | } |
658 | |
659 | // Determine added key, and corresponding removed key. |
660 | $firstMsg = $params; |
661 | $secondKey = $sourceChanges->getMatchedKey( $language, $key ) ?? ''; |
662 | $secondMsg = $sourceChanges->getMatchedMessage( $language, $key ); |
663 | if ( $secondMsg === null ) { |
664 | throw new RuntimeException( "Could not find matched message for $key" ); |
665 | } |
666 | |
667 | if ( |
668 | $sourceChanges->isPreviousState( |
669 | $language, |
670 | $key, |
671 | [ MessageSourceChange::ADDITION, MessageSourceChange::CHANGE ] |
672 | ) |
673 | ) { |
674 | $addedMsg = $firstMsg; |
675 | $deletedMsg = $secondMsg; |
676 | } else { |
677 | $addedMsg = $secondMsg; |
678 | $deletedMsg = $firstMsg; |
679 | } |
680 | |
681 | $change = $this->formatRename( |
682 | $group, |
683 | $addedMsg, |
684 | $deletedMsg, |
685 | $language, |
686 | $sourceChanges->isEqual( $language, $key ), |
687 | $limit |
688 | ); |
689 | $out->addHTML( $change ); |
690 | |
691 | // no need to process the second key again. |
692 | unset( $changes[$secondKey] ); |
693 | |
694 | if ( $limit <= 0 ) { |
695 | // We need to restrict the changes per page per form submission |
696 | // limitations as well as performance. |
697 | $out->wrapWikiMsg( "<div class=warning>\n$1\n</div>", 'translate-smg-more' ); |
698 | break; |
699 | } |
700 | } |
701 | } |
702 | |
703 | private function formatRename( |
704 | MessageGroup $group, |
705 | array $addedMsg, |
706 | array $deletedMsg, |
707 | string $language, |
708 | bool $isEqual, |
709 | int &$limit |
710 | ): string { |
711 | $addedKey = $addedMsg['key']; |
712 | $deletedKey = $deletedMsg['key']; |
713 | $actions = ''; |
714 | |
715 | $addedTitle = Title::makeTitleSafe( $group->getNamespace(), "$addedKey/$language" ); |
716 | $deletedTitle = Title::makeTitleSafe( $group->getNamespace(), "$deletedKey/$language" ); |
717 | $id = self::changeId( $group->getId(), $language, MessageSourceChange::RENAME, $addedKey ); |
718 | |
719 | $addedTitleLink = $this->getLinkRenderer()->makeLink( $addedTitle ); |
720 | $deletedTitleLink = $this->getLinkRenderer()->makeLink( $deletedTitle ); |
721 | |
722 | $renameSelected = true; |
723 | if ( $group->getSourceLanguage() === $language ) { |
724 | if ( !$isEqual ) { |
725 | $renameSelected = false; |
726 | $label = $this->msg( 'translate-manage-action-rename-fuzzy' )->text(); |
727 | $actions .= Xml::radioLabel( $label, "msg/$id", "renamefuzzy", "rf/$id", true ); |
728 | } |
729 | |
730 | $label = $this->msg( 'translate-manage-action-rename' )->text(); |
731 | $actions .= Xml::radioLabel( $label, "msg/$id", "rename", "imp/$id", $renameSelected ); |
732 | } else { |
733 | $label = $this->msg( 'translate-manage-action-import' )->text(); |
734 | $actions .= Xml::radioLabel( $label, "msg/$id", "import", "imp/$id", true ); |
735 | } |
736 | |
737 | if ( $group->getSourceLanguage() !== $language ) { |
738 | // Allow user to ignore changes to non-source languages. |
739 | $label = $this->msg( 'translate-manage-action-ignore-change' )->text(); |
740 | $actions .= Xml::radioLabel( $label, "msg/$id", "ignore", "i/$id" ); |
741 | } |
742 | $limit--; |
743 | |
744 | $addedContent = ContentHandler::makeContent( (string)$addedMsg['content'], $addedTitle ); |
745 | $deletedContent = ContentHandler::makeContent( (string)$deletedMsg['content'], $deletedTitle ); |
746 | $this->diff->setContent( $deletedContent, $addedContent ); |
747 | |
748 | $menu = ''; |
749 | if ( $group->getSourceLanguage() === $language && $this->hasRight ) { |
750 | // Only show rename and add as new option for source language. |
751 | $menu = Html::rawElement( |
752 | 'button', |
753 | [ |
754 | 'class' => 'smg-rename-actions', |
755 | 'type' => 'button', |
756 | 'data-group-id' => $group->getId(), |
757 | 'data-msgkey' => $addedKey, |
758 | 'data-msgtitle' => $addedTitle->getFullText() |
759 | ], '' |
760 | ); |
761 | } |
762 | |
763 | $actions = Html::rawElement( 'div', [ 'class' => 'smg-change-import-options' ], $actions ); |
764 | |
765 | $text = $this->diff->getDiff( |
766 | $deletedTitleLink, |
767 | $addedTitleLink . $menu . $actions, |
768 | $isEqual ? htmlspecialchars( $addedMsg['content'] ) : '' |
769 | ); |
770 | |
771 | $hidden = Html::hidden( $id, 1 ); |
772 | $limit--; |
773 | $text .= $hidden; |
774 | |
775 | return Html::rawElement( |
776 | 'div', |
777 | [ 'class' => 'mw-translate-smg-change smg-change-rename' ], |
778 | $text |
779 | ); |
780 | } |
781 | |
782 | private function getRenameJobParams( |
783 | array $currentMsg, |
784 | MessageSourceChange $sourceChanges, |
785 | string $languageCode, |
786 | int $groupNamespace, |
787 | string $selectedVal, |
788 | bool $isSourceLang = true |
789 | ): ?array { |
790 | if ( $selectedVal === 'ignore' ) { |
791 | return null; |
792 | } |
793 | |
794 | $params = []; |
795 | $replacementContent = ''; |
796 | $currentMsgKey = $currentMsg['key']; |
797 | $matchedMsg = $sourceChanges->getMatchedMessage( $languageCode, $currentMsgKey ); |
798 | if ( $matchedMsg === null ) { |
799 | throw new RuntimeException( "Could not find matched message for $currentMsgKey." ); |
800 | } |
801 | $matchedMsgKey = $matchedMsg['key']; |
802 | |
803 | if ( |
804 | $sourceChanges->isPreviousState( |
805 | $languageCode, |
806 | $currentMsgKey, |
807 | [ MessageSourceChange::ADDITION, MessageSourceChange::CHANGE ] |
808 | ) |
809 | ) { |
810 | $params['target'] = $matchedMsgKey; |
811 | $params['replacement'] = $currentMsgKey; |
812 | $replacementContent = $currentMsg['content']; |
813 | } else { |
814 | $params['target'] = $currentMsgKey; |
815 | $params['replacement'] = $matchedMsgKey; |
816 | $replacementContent = $matchedMsg['content']; |
817 | } |
818 | |
819 | $params['fuzzy'] = $selectedVal === 'renamefuzzy'; |
820 | |
821 | $params['content'] = $replacementContent; |
822 | |
823 | if ( $isSourceLang ) { |
824 | $params['targetTitle'] = Title::newFromText( |
825 | Utilities::title( $params['target'], $languageCode, $groupNamespace ), |
826 | $groupNamespace |
827 | ); |
828 | $params['others'] = []; |
829 | } |
830 | |
831 | return $params; |
832 | } |
833 | |
834 | private function handleRenameSubmit( |
835 | MessageGroup $group, |
836 | MessageSourceChange $sourceChanges, |
837 | WebRequest $req, |
838 | string $language, |
839 | array &$postponed, |
840 | array &$jobData, |
841 | array &$modificationJobs |
842 | ): void { |
843 | $groupId = $group->getId(); |
844 | $renames = $sourceChanges->getRenames( $language ); |
845 | $isSourceLang = $group->getSourceLanguage() === $language; |
846 | $groupNamespace = $group->getNamespace(); |
847 | |
848 | foreach ( $renames as $key => $params ) { |
849 | // Since we're removing items from the array within the loop add |
850 | // a check here to ensure that the current key is still set. |
851 | if ( !isset( $renames[$key] ) ) { |
852 | continue; |
853 | } |
854 | |
855 | $id = self::changeId( $groupId, $language, MessageSourceChange::RENAME, $key ); |
856 | |
857 | [ $renameMissing, $isCurrentKeyPresent ] = $this->isRenameMissing( |
858 | $req, |
859 | $sourceChanges, |
860 | $id, |
861 | $key, |
862 | $language, |
863 | $groupId, |
864 | $isSourceLang |
865 | ); |
866 | |
867 | if ( $renameMissing ) { |
868 | // we probably hit the limit with number of post parameters since neither |
869 | // addition nor deletion key is present. |
870 | $postponed[$groupId][$language][MessageSourceChange::RENAME][$key] = $params; |
871 | continue; |
872 | } |
873 | |
874 | if ( !$isCurrentKeyPresent ) { |
875 | // still don't process this key, and wait for the matched rename |
876 | continue; |
877 | } |
878 | |
879 | $selectedVal = $req->getVal( "msg/$id" ); |
880 | $jobParams = $this->getRenameJobParams( |
881 | $params, |
882 | $sourceChanges, |
883 | $language, |
884 | $groupNamespace, |
885 | $selectedVal, |
886 | $isSourceLang |
887 | ); |
888 | |
889 | if ( $jobParams === null ) { |
890 | continue; |
891 | } |
892 | |
893 | $targetStr = $jobParams[ 'target' ]; |
894 | if ( $isSourceLang ) { |
895 | $jobData[ $targetStr ] = $jobParams; |
896 | } elseif ( isset( $jobData[ $targetStr ] ) ) { |
897 | // We are grouping the source rename, and content changes in other languages |
898 | // for the message together into a single job in order to avoid race conditions |
899 | // since jobs are not guaranteed to be run in order. |
900 | $jobData[ $targetStr ][ 'others' ][ $language ] = $jobParams[ 'content' ]; |
901 | } else { |
902 | // the source was probably ignored, we should add this as a modification instead, |
903 | // since the source is not going to be renamed. |
904 | $title = Title::newFromText( |
905 | Utilities::title( $targetStr, $language, $groupNamespace ), |
906 | $groupNamespace |
907 | ); |
908 | $modificationJobs[] = MessageUpdateJob::newJob( $title, $jobParams['content'] ); |
909 | } |
910 | |
911 | // remove the matched key in order to avoid double processing. |
912 | $matchedKey = $sourceChanges->getMatchedKey( $language, $key ); |
913 | unset( $renames[$matchedKey] ); |
914 | } |
915 | } |
916 | |
917 | private function handleModificationsSubmit( |
918 | MessageGroup $group, |
919 | MessageSourceChange $sourceChanges, |
920 | WebRequest $req, |
921 | string $language, |
922 | array &$postponed, |
923 | array &$messageUpdateJob |
924 | ): void { |
925 | $groupId = $group->getId(); |
926 | $subchanges = $sourceChanges->getModificationsForLanguage( $language ); |
927 | |
928 | // Ignore renames |
929 | unset( $subchanges[ MessageSourceChange::RENAME ] ); |
930 | |
931 | // Handle additions, deletions, and changes. |
932 | foreach ( $subchanges as $type => $messages ) { |
933 | foreach ( $messages as $index => $params ) { |
934 | $key = $params['key']; |
935 | $id = self::changeId( $groupId, $language, $type, $key ); |
936 | $title = Title::makeTitleSafe( $group->getNamespace(), "$key/$language" ); |
937 | |
938 | if ( !$this->isTitlePresent( $title, $type ) ) { |
939 | continue; |
940 | } |
941 | |
942 | if ( !$req->getCheck( $id ) ) { |
943 | // We probably hit the limit with number of post parameters. |
944 | $postponed[$groupId][$language][$type][$index] = $params; |
945 | continue; |
946 | } |
947 | |
948 | $selectedVal = $req->getVal( "msg/$id" ); |
949 | if ( $type === MessageSourceChange::DELETION || $selectedVal === 'ignore' ) { |
950 | continue; |
951 | } |
952 | |
953 | $fuzzy = $selectedVal === 'fuzzy'; |
954 | $messageUpdateJob[] = MessageUpdateJob::newJob( $title, $params['content'], $fuzzy ); |
955 | } |
956 | } |
957 | } |
958 | |
959 | /** @return MessageUpdateJob[][] */ |
960 | private function createRenameJobs( array $jobParams ): array { |
961 | $jobs = []; |
962 | foreach ( $jobParams as $groupId => $groupJobParams ) { |
963 | $jobs[$groupId] ??= []; |
964 | foreach ( $groupJobParams as $params ) { |
965 | $jobs[$groupId][] = MessageUpdateJob::newRenameJob( |
966 | $params['targetTitle'], |
967 | $params['target'], |
968 | $params['replacement'], |
969 | $params['fuzzy'], |
970 | $params['content'], |
971 | $params['others'] |
972 | ); |
973 | } |
974 | } |
975 | |
976 | return $jobs; |
977 | } |
978 | |
979 | /** Checks if a title still exists and can be processed. */ |
980 | private function isTitlePresent( Title $title, string $type ): bool { |
981 | // phpcs:ignore SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn |
982 | if ( |
983 | ( $type === MessageSourceChange::DELETION || $type === MessageSourceChange::CHANGE ) && |
984 | !$title->exists() |
985 | ) { |
986 | // This means that this change was probably introduced due to a rename |
987 | // which removed the key. No need to process. |
988 | return false; |
989 | } |
990 | return true; |
991 | } |
992 | |
993 | /** |
994 | * Checks if a renamed message key is missing from the user request submission. |
995 | * Checks the current key and the matched key. This is needed because as the |
996 | * keys in the wiki are not submitted along with the request, only the incoming |
997 | * modified keys are submitted. |
998 | * @return bool[] |
999 | * $response = [ |
1000 | * 0 => (bool) True if rename is missing, false otherwise. |
1001 | * 1 => (bool) Was the current $id found? |
1002 | * ] |
1003 | */ |
1004 | private function isRenameMissing( |
1005 | WebRequest $req, |
1006 | MessageSourceChange $sourceChanges, |
1007 | string $id, |
1008 | string $key, |
1009 | string $language, |
1010 | string $groupId, |
1011 | bool $isSourceLang |
1012 | ): array { |
1013 | if ( $req->getCheck( $id ) ) { |
1014 | return [ false, true ]; |
1015 | } |
1016 | |
1017 | $isCurrentKeyPresent = false; |
1018 | |
1019 | // Checked the matched key is also missing to confirm if its truly missing |
1020 | $matchedKey = $sourceChanges->getMatchedKey( $language, $key ); |
1021 | $matchedId = self::changeId( $groupId, $language, MessageSourceChange::RENAME, $matchedKey ); |
1022 | if ( $req->getCheck( $matchedId ) ) { |
1023 | return [ false, $isCurrentKeyPresent ]; |
1024 | } |
1025 | |
1026 | // For non source language, if strings are equal, they are not shown on the UI |
1027 | // and hence not submitted. |
1028 | return [ |
1029 | $isSourceLang || !$sourceChanges->isEqual( $language, $matchedKey ), |
1030 | $isCurrentKeyPresent |
1031 | ]; |
1032 | } |
1033 | |
1034 | private function getProcessingErrorMessage( array $errorGroups, int $totalGroupCount ): string { |
1035 | // Number of error groups, are less than the total groups processed. |
1036 | if ( count( $errorGroups ) < $totalGroupCount ) { |
1037 | $errorMsg = $this->msg( 'translate-smg-submitted-with-failure' ) |
1038 | ->numParams( count( $errorGroups ) ) |
1039 | ->params( |
1040 | $this->getLanguage()->commaList( $errorGroups ), |
1041 | $this->msg( 'translate-smg-submitted-others-processing' ) |
1042 | )->text(); |
1043 | } else { |
1044 | $errorMsg = trim( |
1045 | $this->msg( 'translate-smg-submitted-with-failure' ) |
1046 | ->numParams( count( $errorGroups ) ) |
1047 | ->params( $this->getLanguage()->commaList( $errorGroups ), '' ) |
1048 | ->text() |
1049 | ); |
1050 | } |
1051 | |
1052 | return $errorMsg; |
1053 | } |
1054 | |
1055 | /** @return associative-array<int|string,\MessageGroup> */ |
1056 | private function getGroupsFromCdb( \Cdb\Reader $reader ): array { |
1057 | $groups = []; |
1058 | $groupIds = Utilities::deserialize( $reader->get( '#keys' ) ); |
1059 | foreach ( $groupIds as $id ) { |
1060 | $groups[$id] = MessageGroups::getGroup( $id ); |
1061 | } |
1062 | return array_filter( $groups ); |
1063 | } |
1064 | |
1065 | /** |
1066 | * Add jobs to the queue, updates the interim cache, and start sync process for the group. |
1067 | * @param MessageUpdateJob[][] $modificationJobs |
1068 | * @param MessageUpdateJob[][] $renameJobs |
1069 | */ |
1070 | private function startSync( array $modificationJobs, array $renameJobs ): void { |
1071 | // We are adding an empty array for groups that have no jobs. This is mainly done to |
1072 | // avoid adding unnecessary checks. Remove those using array_filter |
1073 | $modificationGroupIds = array_keys( array_filter( $modificationJobs ) ); |
1074 | $renameGroupIds = array_keys( array_filter( $renameJobs ) ); |
1075 | $uniqueGroupIds = array_unique( array_merge( $modificationGroupIds, $renameGroupIds ) ); |
1076 | $jobQueueInstance = $this->jobQueueGroup; |
1077 | |
1078 | foreach ( $uniqueGroupIds as $groupId ) { |
1079 | $messages = []; |
1080 | $messageKeys = []; |
1081 | $groupJobs = []; |
1082 | |
1083 | $groupRenameJobs = $renameJobs[$groupId] ?? []; |
1084 | /** @var MessageUpdateJob $job */ |
1085 | foreach ( $groupRenameJobs as $job ) { |
1086 | $groupJobs[] = $job; |
1087 | $messageUpdateParam = MessageUpdateParameter::createFromJob( $job ); |
1088 | $messages[] = $messageUpdateParam; |
1089 | |
1090 | // Build the handle to add the message key in interim cache |
1091 | $replacement = $messageUpdateParam->getReplacementValue(); |
1092 | $targetTitle = Title::makeTitle( $job->getTitle()->getNamespace(), $replacement ); |
1093 | $messageKeys[] = ( new MessageHandle( $targetTitle ) )->getKey(); |
1094 | } |
1095 | |
1096 | $groupModificationJobs = $modificationJobs[$groupId] ?? []; |
1097 | /** @var MessageUpdateJob $job */ |
1098 | foreach ( $groupModificationJobs as $job ) { |
1099 | $groupJobs[] = $job; |
1100 | $messageUpdateParam = MessageUpdateParameter::createFromJob( $job ); |
1101 | $messages[] = $messageUpdateParam; |
1102 | |
1103 | $messageKeys[] = ( new MessageHandle( $job->getTitle() ) )->getKey(); |
1104 | } |
1105 | |
1106 | // Store all message keys in the interim cache - we're particularly interested in new |
1107 | // and renamed messages, but it's cleaner to just store everything. |
1108 | $group = MessageGroups::getGroup( $groupId ); |
1109 | $this->messageIndex->storeInterim( $group, $messageKeys ); |
1110 | |
1111 | if ( $this->getConfig()->get( 'TranslateGroupSynchronizationCache' ) ) { |
1112 | $this->synchronizationCache->addMessages( $groupId, ...$messages ); |
1113 | $this->synchronizationCache->markGroupForSync( $groupId ); |
1114 | |
1115 | LoggerFactory::getInstance( 'Translate.GroupSynchronization' )->info( |
1116 | '[' . __CLASS__ . '] Synchronization started for {groupId} by {user}', |
1117 | [ |
1118 | 'groupId' => $groupId, |
1119 | 'user' => $this->getUser()->getName() |
1120 | ] |
1121 | ); |
1122 | } |
1123 | |
1124 | // There is posibility for a race condition here: the translate_cache table / group sync |
1125 | // cache is not yet populated with the messages to be processed, but the jobs start |
1126 | // running and try to remove the message from the cache. This results in a "Key not found" |
1127 | // error. Avoid this condition by using a DeferredUpdate. |
1128 | DeferredUpdates::addCallableUpdate( |
1129 | static function () use ( $jobQueueInstance, $groupJobs ) { |
1130 | $jobQueueInstance->push( $groupJobs ); |
1131 | } |
1132 | ); |
1133 | |
1134 | } |
1135 | } |
1136 | } |