Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ManageMessageGroupsActionApi.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use ApiBase;
7use Exception;
8use FormatJson;
12use MessageGroup;
14use Title;
16use Wikimedia\ParamValidator\ParamValidator;
17
27class ManageMessageGroupsActionApi extends ApiBase {
28 private const RIGHT = 'translate-manage';
29
30 public function execute(): void {
31 $this->checkUserRightsAny( self::RIGHT );
32 $params = $this->extractRequestParams();
33
34 $groupId = $params['groupId'];
35 $op = $params['operation'];
36 $msgKey = $params['messageKey'];
37 $name = $params['changesetName'] ?? MessageChangeStorage::DEFAULT_NAME;
38 $changesetModifiedTime = $params['changesetModified'];
39 $keyToRename = null;
40
41 if ( !MessageChangeStorage::isValidCdbName( $name ) ) {
42 $this->dieWithError(
43 [ 'apierror-translate-invalid-changeset-name', wfEscapeWikiText( $name ) ],
44 'invalidchangeset'
45 );
46 }
47 $cdbPath = MessageChangeStorage::getCdbPath( $name );
48
49 if ( !MessageChangeStorage::isModifiedSince( $cdbPath, $changesetModifiedTime ) ) {
50 // Changeset file has been modified since the time the page was generated.
51 $this->dieWithError( [ 'apierror-translate-changeset-modified' ] );
52 }
53
54 if ( $op === 'rename' ) {
55 if ( !isset( $params['renameMessageKey'] ) ) {
56 $this->dieWithError( [ 'apierror-missingparam', 'renameMessageKey' ] );
57 }
58 $keyToRename = $params['renameMessageKey'];
59 }
60
61 $sourceChanges = MessageChangeStorage::getGroupChanges( $cdbPath, $groupId );
62 if ( $sourceChanges->getAllModifications() === [] ) {
63 $this->dieWithError( [ 'apierror-translate-smg-nochanges' ] );
64 }
65
66 $group = MessageGroups::getGroup( $groupId );
67 if ( $group === null ) {
68 $this->dieWithError( 'apierror-translate-invalidgroup', 'invalidgroup' );
69 }
70
71 try {
72 if ( $op === 'rename' ) {
73 $this->handleRename(
74 $group, $sourceChanges, $msgKey, $keyToRename, $group->getSourceLanguage()
75 );
76 } elseif ( $op === 'new' ) {
77 $this->handleNew( $sourceChanges, $msgKey, $group->getSourceLanguage() );
78 } else {
79 $this->dieWithError(
80 [ 'apierror-translate-invalid-operation', wfEscapeWikiText( $op ),
81 wfEscapeWikiText( implode( '/', [ 'new' , 'rename' ] ) ) ],
82 'invalidoperation'
83 );
84 }
85 } catch ( Exception $ex ) {
86 // Log necessary parameters and rethrow.
87 $data = [
88 'op' => $op,
89 'newMsgKey' => $msgKey,
90 'msgKey' => $keyToRename,
91 'groupId' => $group->getId(),
92 'group' => $group->getLabel(),
93 'groupSourceLang' => $group->getSourceLanguage(),
94 'exception' => $ex
95 ];
96
97 error_log(
98 "Error while running: ManageMessageGroupsActionApi::execute. Inputs: \n" .
99 FormatJson::encode( $data, true )
100 );
101
102 throw $ex;
103 }
104
105 // Write the source changes back to file.
106 MessageChangeStorage::writeGroupChanges( $sourceChanges, $groupId, $cdbPath );
107
108 $this->getResult()->addValue( null, $this->getModuleName(), [
109 'success' => 1
110 ] );
111 }
112
114 protected function handleRename(
115 MessageGroup $group,
116 MessageSourceChange $sourceChanges,
117 string $msgKey,
118 string $keyToRename,
119 string $sourceLanguage
120 ): void {
121 $languages = $sourceChanges->getLanguages();
122
123 foreach ( $languages as $code ) {
124 $msgState = $renameMsgState = null;
125
126 $isSourceLang = $sourceLanguage === $code;
127 if ( $isSourceLang ) {
128 $this->handleSourceRename( $sourceChanges, $code, $msgKey, $keyToRename );
129 continue;
130 }
131
132 // Check for changes with the new key, then with the old key.
133 // If there are no changes, we won't find anything at all, and
134 // can skip this languageCode.
135 $msg = $sourceChanges->findMessage( $code, $msgKey, [
136 MessageSourceChange::ADDITION,
137 MessageSourceChange::RENAME
138 ], $msgState );
139
140 // This case will arise if the message key has been changed in the source
141 // language, but has not been modified in this language code.
142 // NOTE: We are also searching under deletions. This means that if the source
143 // language key is renamed, but one of the non source language keys is removed,
144 // renaming it will not remove the translation, but only rename it. This
145 // scenario is highly unlikely though.
146 $msg = $msg ?? $sourceChanges->findMessage( $code, $keyToRename, [
147 MessageSourceChange::DELETION,
148 MessageSourceChange::CHANGE,
149 MessageSourceChange::RENAME
150 ], $msgState );
151
152 if ( $msg === null ) {
153 continue;
154 }
155
156 // Check for the renamed message in the rename list, and deleted list.
157 $renameMsg = $sourceChanges->findMessage(
158 $code, $keyToRename, [ MessageSourceChange::RENAME, MessageSourceChange::DELETION ],
159 $renameMsgState
160 );
161
162 // content / msg will not be present if the message was deleted from the wiki or
163 // was for some reason unavailable during processing incoming changes. We're going
164 // to try and load it here again from the database. Very rare chance of this happening.
165 if ( $renameMsg === null || !isset( $renameMsg['content'] ) ) {
166 $title = Title::newFromText(
167 TranslateUtils::title( $keyToRename, $code, $group->getNamespace() ),
168 $group->getNamespace()
169 );
170
171 $renameContent = TranslateUtils::getContentForTitle( $title, true ) ?? '';
172
173 $renameMsg = [
174 'key' => $keyToRename,
175 'content' => $renameContent
176 ];
177
178 // If the message was found in changes, this will be set, otherwise set it
179 // to none
180 if ( $renameMsgState === null ) {
181 $renameMsgState = MessageSourceChange::NONE;
182 }
183 }
184
185 // Remove previous states
186 if ( $msgState === MessageSourceChange::RENAME ) {
187 $msgState = $sourceChanges->breakRename( $code, $msg['key'] );
188 } else {
189 $sourceChanges->removeBasedOnType( $code, [ $msg['key'] ], $msgState );
190 }
191
192 if ( $renameMsgState === MessageSourceChange::RENAME ) {
193 $renameMsgState = $sourceChanges->breakRename( $code, $renameMsg['key'] );
194 } elseif ( $renameMsgState !== MessageSourceChange::NONE ) {
195 $sourceChanges->removeBasedOnType( $code, [ $keyToRename ], $renameMsgState );
196 }
197
198 // This is done in case the key has not been renamed in the non-source language.
199 $msg['key'] = $msgKey;
200
201 // Add as rename
202 $stringComparator = new SimpleStringComparator();
203 $similarity = $stringComparator->getSimilarity(
204 $msg['content'],
205 $renameMsg['content']
206 );
207 $sourceChanges->addRename( $code, $msg, $renameMsg, $similarity );
208 $sourceChanges->setRenameState( $code, $msgKey, $msgState );
209 $sourceChanges->setRenameState( $code, $keyToRename, $renameMsgState );
210 }
211 }
212
213 protected function handleSourceRename(
214 MessageSourceChange $sourceChanges,
215 string $code,
216 string $msgKey,
217 string $keyToRename
218 ): void {
219 $msgState = $renameMsgState = null;
220
221 $msg = $sourceChanges->findMessage(
222 $code, $msgKey, [ MessageSourceChange::ADDITION, MessageSourceChange::RENAME ], $msgState
223 );
224
225 $renameMsg = $sourceChanges->findMessage(
226 $code,
227 $keyToRename,
228 [ MessageSourceChange::DELETION, MessageSourceChange::RENAME ],
229 $renameMsgState
230 );
231
232 if ( $msg === null || $renameMsg === null ) {
233 $this->dieWithError( 'apierror-translate-rename-key-invalid' );
234 }
235
236 if ( $msgState === MessageSourceChange::RENAME ) {
237 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141
238 $msgState = $sourceChanges->breakRename( $code, $msg['key'] );
239 }
240
241 if ( $renameMsgState === MessageSourceChange::RENAME ) {
242 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141
243 $renameMsgState = $sourceChanges->breakRename( $code, $renameMsg['key'] );
244 }
245
246 // Ensure that one of them is an ADDITION, and one is DELETION
247 if ( $msgState !== MessageSourceChange::ADDITION ||
248 $renameMsgState !== MessageSourceChange::DELETION ) {
249 $this->dieWithError( [
250 'apierror-translate-rename-state-invalid',
251 wfEscapeWikiText( $msgState ), wfEscapeWikiText( $renameMsgState )
252 ] );
253 }
254
255 // Remove previous states
256 $sourceChanges->removeAdditions( $code, [ $msgKey ] );
257 $sourceChanges->removeDeletions( $code, [ $keyToRename ] );
258
259 // Add as rename
260 $stringComparator = new SimpleStringComparator();
261 $similarity = $stringComparator->getSimilarity(
262 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141
263 $msg['content'],
264 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141
265 $renameMsg['content']
266 );
267 $sourceChanges->addRename( $code, $msg, $renameMsg, $similarity );
268 }
269
271 protected function handleNew(
272 MessageSourceChange $sourceChanges,
273 string $msgKey,
274 string $sourceLang
275 ): void {
276 $msgState = null;
277 $languages = $sourceChanges->getLanguages();
278
279 foreach ( $languages as $code ) {
280 $msg = $sourceChanges->findMessage(
281 $code, $msgKey, [ MessageSourceChange::RENAME ], $msgState
282 );
283
284 if ( $code === $sourceLang && $msg === null ) {
285 $this->dieWithError( 'apierror-translate-addition-key-invalid' );
286 }
287
288 if ( $code === $sourceLang && $msgState !== MessageSourceChange::RENAME ) {
289 $this->dieWithError( 'apierror-translate-rename-msg-new' );
290 }
291
292 // For any other language, its possible for the message to be not found.
293 if ( $msg === null ) {
294 continue;
295 }
296
297 // breakRename will add the message back to its previous state, nothing more to do
298 $sourceChanges->breakRename( $code, $msg['key'] );
299 }
300 }
301
302 protected function getAllowedParams(): array {
303 return [
304 'groupId' => [
305 ParamValidator::PARAM_TYPE => 'string',
306 ParamValidator::PARAM_REQUIRED => true,
307 ],
308 'renameMessageKey' => [
309 ParamValidator::PARAM_TYPE => 'string',
310 ParamValidator::PARAM_REQUIRED => false,
311 ],
312 'messageKey' => [
313 ParamValidator::PARAM_TYPE => 'string',
314 ParamValidator::PARAM_REQUIRED => true,
315 ],
316 'operation' => [
317 ParamValidator::PARAM_TYPE => [ 'rename', 'new' ],
318 ParamValidator::PARAM_ISMULTI => false,
319 ParamValidator::PARAM_REQUIRED => true,
320 ],
321 'changesetName' => [
322 ParamValidator::PARAM_TYPE => 'string',
323 ParamValidator::PARAM_DEFAULT => MessageChangeStorage::DEFAULT_NAME
324 ],
325 'changesetModified' => [
326 ParamValidator::PARAM_TYPE => 'integer',
327 ParamValidator::PARAM_REQUIRED => true,
328 ]
329 ];
330 }
331
332 public function isInternal(): bool {
333 return true;
334 }
335
336 public function needsToken(): string {
337 return 'csrf';
338 }
339}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
handleRename(MessageGroup $group, MessageSourceChange $sourceChanges, string $msgKey, string $keyToRename, string $sourceLanguage)
Handles rename requests.
handleNew(MessageSourceChange $sourceChanges, string $msgKey, string $sourceLang)
Handles add message as new request.
Class is use to track the changes made when importing messages from the remote sources using processM...
addRename( $language, $addedMessage, $deletedMessage, $similarity=0)
Adds a rename under a message group for a specific language.
getLanguages()
Get all language keys with modifications under the group.
findMessage( $language, $key, $possibleStates=[], &$modificationType=null)
Finds a message with the given key across different types of modifications.
removeBasedOnType( $language, $keysToRemove, $type)
Remove modifications based on the type.
breakRename( $languageCode, $msgKey)
Break renames, and put messages back into their previous state.
A simple string comparator, that compares two strings and determines if they are an exact match.
Factory class for accessing message groups individually by id or all of them as an list.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Interface for message groups.
getNamespace()
Returns the namespace where messages are placed.