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