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