Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
UpdateMessageJob.php
1<?php
2declare( strict_types = 1 );
3
5
7use MediaWiki\CommentStore\CommentStoreComment;
8use MediaWiki\Content\ContentHandler;
9use MediaWiki\Content\TextContent;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\RecentChanges\RecentChange;
20use MediaWiki\Revision\SlotRecord;
21use MediaWiki\Storage\PageUpdater;
22use MediaWiki\Title\Title;
23use MediaWiki\User\User;
24
34 private User $fuzzyBot;
35
37 public static function newJob(
38 Title $target, string $content, bool $fuzzy = false
39 ): self {
40 $params = [
41 'content' => $content,
42 'fuzzy' => $fuzzy,
43 ];
44
45 return new self( $target, $params );
46 }
47
58 public static function newRenameJob(
59 Title $target,
60 string $targetStr,
61 string $replacement,
62 $fuzzy,
63 string $content,
64 array $otherLangContents = []
65 ): self {
66 $params = [
67 'target' => $targetStr,
68 'replacement' => $replacement,
69 'fuzzy' => $fuzzy,
70 'rename' => 'rename',
71 'content' => $content,
72 'otherLangs' => $otherLangContents
73 ];
74
75 return new self( $target, $params );
76 }
77
78 public function __construct( Title $title, array $params = [] ) {
79 parent::__construct( 'UpdateMessageJob', $title, $params );
80 }
81
82 public function run(): bool {
83 $params = $this->params;
84 $isRename = $params['rename'] ?? false;
85 $isFuzzy = $params['fuzzy'] ?? false;
86 $otherLangs = $params['otherLangs'] ?? [];
87 $originalTitle = Title::newFromLinkTarget( $this->title->getTitleValue(), Title::NEW_CLONE );
88
89 if ( $isRename ) {
90 $renamedTitle = $this->handleRename( $params['target'], $params['replacement'] );
91 if ( $renamedTitle === null ) {
92 // There was a failure, return true, but don't proceed further.
93 $this->logWarning(
94 'Rename process could not find the source title.',
95 [
96 'replacement' => $params['replacement'],
97 'target' => $params['target']
98 ]
99 );
100
101 $this->removeFromCache( $originalTitle );
102 return true;
103 }
104 $this->title = $renamedTitle;
105 }
106 $title = $this->title;
107 $baseRevId = $title->getLatestRevId();
108 $updater = $this->fuzzyBotEdit( $title, $params['content'] );
109 if ( !$updater->getStatus()->isOK() ) {
110 $this->logError(
111 'Failed to update content for source message',
112 [
113 'content' => ContentHandler::makeContent( $params['content'], $this->title ),
114 'errors' => $updater->getStatus()->getMessages()
115 ]
116 );
117 }
118
119 if ( $isRename ) {
120 // Update other language content if present.
121 $this->processTranslationChanges( $otherLangs, $params['replacement'], $params['namespace'] );
122 }
123
124 $this->handleFuzzy( $title, $isFuzzy, $updater, $baseRevId );
125
126 $this->removeFromCache( $originalTitle );
127 return true;
128 }
129
130 private function handleRename( string $target, string $replacement ): ?Title {
131 $newSourceTitle = null;
132
133 $sourceMessageHandle = new MessageHandle( $this->title );
134 $movableTitles = TranslateReplaceTitle::getTitlesForMove( $sourceMessageHandle, $replacement );
135
136 if ( $movableTitles === [] ) {
137 $this->logError(
138 'No movable titles found with target text.',
139 [
140 'title' => $this->title->getPrefixedText(),
141 'replacement' => $replacement,
142 'target' => $target
143 ]
144 );
145 return null;
146 }
147
148 $renameSummary = wfMessage( 'translate-manage-import-rename-summary' )
149 ->inContentLanguage()->plain();
150
151 foreach ( $movableTitles as [ $sourceTitle, $replacementTitle ] ) {
152 $mv = MediaWikiServices::getInstance()
153 ->getMovePageFactory()
154 ->newMovePage( $sourceTitle, $replacementTitle );
155
156 $status = $mv->move( $this->getFuzzyBot(), $renameSummary, false );
157 if ( !$status->isOK() ) {
158 $this->logError(
159 'Error moving message',
160 [
161 'target' => $sourceTitle->getPrefixedText(),
162 'replacement' => $replacementTitle->getPrefixedText(),
163 'errors' => $status->getMessages()
164 ]
165 );
166 }
167
168 [ , $targetCode ] = Utilities::figureMessage( $replacementTitle->getText() );
169 if ( !$newSourceTitle && $sourceMessageHandle->getCode() === $targetCode ) {
170 $newSourceTitle = $replacementTitle;
171 }
172 }
173
174 if ( $newSourceTitle ) {
175 return $newSourceTitle;
176 } else {
177 // This means that the old source Title was never moved
178 // which is not possible but handle it.
179 $this->logError(
180 'Source title was not in the list of movable titles.',
181 [ 'title' => $this->title->getPrefixedText() ]
182 );
183 return null;
184 }
185 }
186
197 private function handleFuzzy( Title $title, bool $invalidate, PageUpdater $updater, int $baseTranver ): void {
198 global $wgTranslateDocumentationLanguageCode;
199 $editResult = $updater->getEditResult();
200 if ( !$invalidate && !$editResult->isExactRevert() ) {
201 return;
202 }
203 $oldRevId = $editResult->getOriginalRevisionId();
204 $handle = new MessageHandle( $title );
205
206 $languages = Utilities::getLanguageNames( 'en' );
207
208 // Don't fuzzy the message documentation or the source language
209 unset( $languages[$wgTranslateDocumentationLanguageCode] );
210 unset( $languages[$handle->getCode()] );
211
212 $languages = array_keys( $languages );
213
214 $fuzzies = [];
215 $unfuzzies = [];
216 $mwInstance = MediaWikiServices::getInstance();
217 $revTagStore = Services::getInstance()->getRevTagStore();
218 $revStore = $mwInstance->getRevisionStore();
219
220 if ( $oldRevId || $invalidate ) {
221 // We'll need to check if each possible tunit exists later on, so do that now
222 // as a batch
223 $batch = $mwInstance->getLinkBatchFactory()->newLinkBatch();
224 $batch->setCaller( __METHOD__ );
225 foreach ( $languages as $code ) {
226 $batch->addObj( $handle->getTitleForLanguage( $code ) );
227 }
228 $batch->execute();
229 }
230 $newRevision = $updater->getNewRevision();
231 // $newRevision can be null if a change is made to only tvars and then the fuzzy checkbox is manually turned on
232 $targetSha = $newRevision ? $newRevision->getSha1() : null;
233
234 foreach ( $languages as $code ) {
235 $otherTitle = $handle->getTitleForLanguage( $code );
236 $shouldUnfuzzy = false;
237 if ( !$otherTitle->exists() ) {
238 // Don't care about fuzzy status for nonexistent tunits
239 continue;
240 }
241 $transverId = $revTagStore->getTransver( $otherTitle );
242 if ( !$transverId ) {
243 // The page doesn't have a tp:transver at all
244 // This shouldn't happen, but it does in some edge cases like importing translations across wikis
245 $latest = $otherTitle->getLatestRevID();
246 if ( $invalidate && !$revTagStore->isRevIdFuzzy( $otherTitle->getId(), $latest ) && $newRevision ) {
247 // If the (latest revision of the) translation isn't fuzzy and the source tunit was actually changed
248 // then assume the translation pertains to the latest revision of the source tunit
249 // (before the update that triggered this job and marked it fuzzy)
250 // and set its transver to that so "show differences" has something to show
251 $revTagStore->setTransver( $otherTitle, $latest, $baseTranver );
252 }
253 // Don't do any revert checking
254 } elseif ( $oldRevId && $newRevision && $editResult->isExactRevert() ) {
255 // Only try to do revert analysis if the edit succeeded and is truly an exact revert
256 $transver = $revStore->getRevisionById( $transverId, 0, $title );
257 if ( $oldRevId == $transverId ) {
258 // It's a straightforward revert
259 $shouldUnfuzzy = true;
260 } elseif ( $transver ) {
261 $transverSha = $transver->getSha1();
262 if ( $transverSha == $targetSha ) {
263 // It's a deeper revert or otherwise wasn't detected by MediaWiki's builtin revert detection
264 $shouldUnfuzzy = true;
265 } // Else it's not a revert at all so leave shouldUnfuzzy false
266 }
267 // Else should never happen (it means tp:transver is corrupt) but it could concievably happen
268 // in some edge cases so do nothing (and fuzzy the tunit) rather than crashing the entire job
269 }
270 if ( $shouldUnfuzzy ) {
271 // In principle it's a revert so should unfuzzy, first check for validation failures
272 // or manual fuzzying
273 $otherHandle = new MessageHandle( $otherTitle );
274 $wikiPage = $mwInstance->getWikiPageFactory()->newFromTitle( $otherTitle );
275 $content = $wikiPage->getContent();
276 if ( !$content instanceof TextContent ) {
277 // This should never happen (translation units should always be wikitext) but Phan complains
278 // otherwise
279 continue;
280 }
281 $text = $content->getText();
282 if ( $otherHandle->isFuzzy() && !$otherHandle->needsFuzzy( $text ) ) {
283 $unfuzzies[] = $otherTitle;
284 }
285 // If it's not already fuzzy then that means the original change was done without invalidating
286 // translations and while the new change probably should have been done that way as well
287 // even if it wasn't it never makes sense to re-fuzzy in that case so just leave the fuzzy status alone
288 } elseif ( $invalidate ) {
289 $fuzzies[] = $otherTitle;
290 }
291 }
292
293 $dbw = $mwInstance->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
294
295 if ( $fuzzies !== [] ) {
296 $inserts = [];
297 foreach ( $fuzzies as $otherTitle ) {
298 $inserts[] = [
299 'rt_type' => RevTagStore::FUZZY_TAG,
300 'rt_page' => $otherTitle->getId(),
301 'rt_revision' => $otherTitle->getLatestRevID(),
302 ];
303 }
304 $dbw->newReplaceQueryBuilder()
305 ->replaceInto( 'revtag' )
306 ->uniqueIndexFields( [ 'rt_type', 'rt_page', 'rt_revision' ] )
307 ->rows( $inserts )
308 ->caller( __METHOD__ )
309 ->execute();
310 }
311 if ( $unfuzzies !== [] ) {
312 foreach ( $unfuzzies as $otherTitle ) {
313 $dbw->newDeleteQueryBuilder()
314 ->deleteFrom( 'revtag' )
315 ->where( [
316 'rt_type' => RevTagStore::FUZZY_TAG,
317 'rt_page' => $otherTitle->getId(),
318 'rt_revision' => $otherTitle->getLatestRevID(),
319 ] )
320 ->caller( __METHOD__ )
321 ->execute();
322 }
323 }
324 }
325
327 private function processTranslationChanges(
328 array $langChanges,
329 string $baseTitle,
330 int $groupNamespace
331 ): void {
332 foreach ( $langChanges as $code => $contentStr ) {
333 $titleStr = Utilities::title( $baseTitle, $code, $groupNamespace );
334 $title = Title::newFromText( $titleStr, $groupNamespace );
335 $updater = $this->fuzzyBotEdit( $title, $contentStr );
336
337 if ( !$updater->getStatus()->isOK() ) {
338 $this->logError(
339 'Failed to update content for non-source message',
340 [
341 'title' => $title->getPrefixedText(),
342 'errors' => $updater->getStatus()->getMessages()
343 ]
344 );
345 }
346 }
347 }
348
349 private function removeFromCache( Title $title ): void {
350 $config = MediaWikiServices::getInstance()->getMainConfig();
351
352 if ( !$config->get( 'TranslateGroupSynchronizationCache' ) ) {
353 return;
354 }
355
356 $currentTitle = $title;
357 // Check if the current title, is equal to the title passed. This condition will be
358 // true in case of rename where the old title would have been renamed.
359 if ( $this->title && $this->title->getPrefixedDBkey() !== $title->getPrefixedDBkey() ) {
360 $currentTitle = $this->title;
361 }
362
363 $sourceMessageHandle = new MessageHandle( $currentTitle );
364 $groupIds = $sourceMessageHandle->getGroupIds();
365 if ( !$groupIds ) {
366 $this->logWarning(
367 "Could not find group Id for message title: {$currentTitle->getPrefixedDBkey()}",
368 $this->getParams()
369 );
370 return;
371 }
372
373 $groupId = $groupIds[0];
374 $group = MessageGroups::getGroup( $groupId );
375
376 if ( !$group instanceof FileBasedMessageGroup ) {
377 return;
378 }
379
380 $groupSyncCache = Services::getInstance()->getGroupSynchronizationCache();
381 $messageKey = $title->getPrefixedDBkey();
382
383 if ( $groupSyncCache->isMessageBeingProcessed( $groupId, $messageKey ) ) {
384 $groupSyncCache->removeMessages( $groupId, $messageKey );
385 $groupSyncCache->extendGroupExpiryTime( $groupId );
386 } else {
387 $this->logWarning(
388 "Did not find key: $messageKey; in group: $groupId in group sync cache",
389 $this->getParams()
390 );
391 }
392 }
393
394 private function fuzzyBotEdit( Title $title, string $content ): PageUpdater {
395 $wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
396 $content = ContentHandler::makeContent( $content, $title );
397 $page = $wikiPageFactory->newFromTitle( $title );
398 $updater = $page->newPageUpdater( $this->getFuzzyBot() )
399 ->setContent( SlotRecord::MAIN, $content );
400
401 if ( $this->getFuzzyBot()->authorizeWrite( 'autopatrol', $title ) ) {
402 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
403 }
404
405 $summary = wfMessage( 'translate-manage-import-summary' )
406 ->inContentLanguage()->plain();
407 $updater->saveRevision(
408 CommentStoreComment::newUnsavedComment( $summary ),
409 EDIT_FORCE_BOT
410 );
411 return $updater;
412 }
413
414 private function getFuzzyBot(): User {
415 $this->fuzzyBot ??= FuzzyBot::getUser();
416 return $this->fuzzyBot;
417 }
418}
This class implements default behavior for file based message groups.
Factory class for accessing message groups individually by id or all of them as a list.
Class to manage revision tags for translatable bundles.
Class for pointing to messages, like Title class is for titles.
Helper class that contains utility methods to help with identifying and replace titles.
Minimal service container.
Definition Services.php:60
Job for updating translation pages when translation or message definition changes.
static newRenameJob(Title $target, string $targetStr, string $replacement, $fuzzy, string $content, array $otherLangContents=[])
Create a message update job containing a rename process.
static newJob(Title $target, string $content, bool $fuzzy=false)
Create a normal message update job without a rename process.
FuzzyBot - the misunderstood workhorse.
Definition FuzzyBot.php:15
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:30
Finds external changes for file based message groups.