Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.42% |
135 / 179 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
ManageMessageGroupsActionApi | |
75.42% |
135 / 179 |
|
28.57% |
2 / 7 |
55.25 | |
0.00% |
0 / 1 |
execute | |
60.71% |
34 / 56 |
|
0.00% |
0 / 1 |
16.06 | |||
handleRename | |
76.47% |
39 / 51 |
|
0.00% |
0 / 1 |
10.06 | |||
handleSourceRename | |
80.00% |
24 / 30 |
|
0.00% |
0 / 1 |
7.39 | |||
handleNew | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
7.60 | |||
getAllowedParams | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
1 | |||
isInternal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use Exception; |
7 | use MediaWiki\Api\ApiBase; |
8 | use MediaWiki\Extension\Translate\MessageSync\MessageSourceChange; |
9 | use MediaWiki\Extension\Translate\Synchronization\MessageChangeStorage; |
10 | use MediaWiki\Extension\Translate\Utilities\StringComparators\SimpleStringComparator; |
11 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
12 | use MediaWiki\Json\FormatJson; |
13 | use MediaWiki\Title\Title; |
14 | use MessageGroup; |
15 | use Wikimedia\ParamValidator\ParamValidator; |
16 | |
17 | /** |
18 | * API module for managing message group changes. |
19 | * Marks message as a rename of another message or as a new message. |
20 | * Updates the cdb file. |
21 | * @author Abijeet Patro |
22 | * @since 2019.10 |
23 | * @license GPL-2.0-or-later |
24 | * @ingroup API TranslateAPI |
25 | */ |
26 | class ManageMessageGroupsActionApi extends ApiBase { |
27 | private const RIGHT = 'translate-manage'; |
28 | |
29 | public function execute(): void { |
30 | $this->checkUserRightsAny( self::RIGHT ); |
31 | $params = $this->extractRequestParams(); |
32 | |
33 | $groupId = $params['groupId']; |
34 | $op = $params['operation']; |
35 | $msgKey = $params['messageKey']; |
36 | $name = $params['changesetName'] ?? MessageChangeStorage::DEFAULT_NAME; |
37 | $changesetModifiedTime = $params['changesetModified']; |
38 | $keyToRename = null; |
39 | |
40 | if ( !MessageChangeStorage::isValidCdbName( $name ) ) { |
41 | $this->dieWithError( |
42 | [ 'apierror-translate-invalid-changeset-name', wfEscapeWikiText( $name ) ], |
43 | 'invalidchangeset' |
44 | ); |
45 | } |
46 | $cdbPath = MessageChangeStorage::getCdbPath( $name ); |
47 | |
48 | if ( !MessageChangeStorage::isModifiedSince( $cdbPath, $changesetModifiedTime ) ) { |
49 | // Changeset file has been modified since the time the page was generated. |
50 | $this->dieWithError( [ 'apierror-translate-changeset-modified' ] ); |
51 | } |
52 | |
53 | if ( $op === 'rename' ) { |
54 | if ( !isset( $params['renameMessageKey'] ) ) { |
55 | $this->dieWithError( [ 'apierror-missingparam', 'renameMessageKey' ] ); |
56 | } |
57 | $keyToRename = $params['renameMessageKey']; |
58 | } |
59 | |
60 | $sourceChanges = MessageChangeStorage::getGroupChanges( $cdbPath, $groupId ); |
61 | if ( $sourceChanges->getAllModifications() === [] ) { |
62 | $this->dieWithError( [ 'apierror-translate-smg-nochanges' ] ); |
63 | } |
64 | |
65 | $group = MessageGroups::getGroup( $groupId ); |
66 | if ( $group === null ) { |
67 | $this->dieWithError( 'apierror-translate-invalidgroup', 'invalidgroup' ); |
68 | } |
69 | |
70 | try { |
71 | if ( $op === 'rename' ) { |
72 | $this->handleRename( |
73 | $group, $sourceChanges, $msgKey, $keyToRename, $group->getSourceLanguage() |
74 | ); |
75 | } elseif ( $op === 'new' ) { |
76 | $this->handleNew( $sourceChanges, $msgKey, $group->getSourceLanguage() ); |
77 | } else { |
78 | $this->dieWithError( |
79 | [ 'apierror-translate-invalid-operation', wfEscapeWikiText( $op ), |
80 | wfEscapeWikiText( implode( '/', [ 'new', 'rename' ] ) ) ], |
81 | 'invalidoperation' |
82 | ); |
83 | } |
84 | } catch ( Exception $ex ) { |
85 | // Log necessary parameters and rethrow. |
86 | $data = [ |
87 | 'op' => $op, |
88 | 'newMsgKey' => $msgKey, |
89 | 'msgKey' => $keyToRename, |
90 | 'groupId' => $group->getId(), |
91 | 'group' => $group->getLabel(), |
92 | 'groupSourceLang' => $group->getSourceLanguage(), |
93 | 'exception' => $ex |
94 | ]; |
95 | |
96 | error_log( |
97 | "Error while running: ManageMessageGroupsActionApi::execute. Inputs: \n" . |
98 | FormatJson::encode( $data, true ) |
99 | ); |
100 | |
101 | throw $ex; |
102 | } |
103 | |
104 | // Write the source changes back to file. |
105 | MessageChangeStorage::writeGroupChanges( $sourceChanges, $groupId, $cdbPath ); |
106 | |
107 | $this->getResult()->addValue( null, $this->getModuleName(), [ |
108 | 'success' => 1 |
109 | ] ); |
110 | } |
111 | |
112 | /** Handles rename requests */ |
113 | protected function handleRename( |
114 | MessageGroup $group, |
115 | MessageSourceChange $sourceChanges, |
116 | string $msgKey, |
117 | string $keyToRename, |
118 | string $sourceLanguage |
119 | ): void { |
120 | $languages = $sourceChanges->getLanguages(); |
121 | |
122 | foreach ( $languages as $code ) { |
123 | $msgState = $renameMsgState = null; |
124 | |
125 | $isSourceLang = $sourceLanguage === $code; |
126 | if ( $isSourceLang ) { |
127 | $this->handleSourceRename( $sourceChanges, $code, $msgKey, $keyToRename ); |
128 | continue; |
129 | } |
130 | |
131 | // Check for changes with the new key, then with the old key. |
132 | // If there are no changes, we won't find anything at all, and |
133 | // can skip this languageCode. |
134 | $msg = $sourceChanges->findMessage( $code, $msgKey, [ |
135 | MessageSourceChange::ADDITION, |
136 | MessageSourceChange::RENAME |
137 | ], $msgState ); |
138 | |
139 | // This case will arise if the message key has been changed in the source |
140 | // language, but has not been modified in this language code. |
141 | // NOTE: We are also searching under deletions. This means that if the source |
142 | // language key is renamed, but one of the non source language keys is removed, |
143 | // renaming it will not remove the translation, but only rename it. This |
144 | // scenario is highly unlikely though. |
145 | $msg ??= $sourceChanges->findMessage( $code, $keyToRename, [ |
146 | MessageSourceChange::DELETION, |
147 | MessageSourceChange::CHANGE, |
148 | MessageSourceChange::RENAME |
149 | ], $msgState ); |
150 | |
151 | if ( $msg === null ) { |
152 | continue; |
153 | } |
154 | |
155 | // Check for the renamed message in the rename list, and deleted list. |
156 | $renameMsg = $sourceChanges->findMessage( |
157 | $code, |
158 | $keyToRename, |
159 | [ MessageSourceChange::RENAME, MessageSourceChange::DELETION ], |
160 | $renameMsgState |
161 | ); |
162 | |
163 | // content / msg will not be present if the message was deleted from the wiki or |
164 | // was for some reason unavailable during processing incoming changes. We're going |
165 | // to try and load it here again from the database. Very rare chance of this happening. |
166 | if ( $renameMsg === null || !isset( $renameMsg['content'] ) ) { |
167 | $title = Title::newFromText( |
168 | Utilities::title( $keyToRename, $code, $group->getNamespace() ), |
169 | $group->getNamespace() |
170 | ); |
171 | |
172 | $renameContent = Utilities::getContentForTitle( $title, true ) ?? ''; |
173 | |
174 | $renameMsg = [ |
175 | 'key' => $keyToRename, |
176 | 'content' => $renameContent |
177 | ]; |
178 | |
179 | // If the message was found in changes, this will be set, otherwise set it |
180 | // to none |
181 | $renameMsgState ??= MessageSourceChange::NONE; |
182 | } |
183 | |
184 | // Remove previous states |
185 | if ( $msgState === MessageSourceChange::RENAME ) { |
186 | $msgState = $sourceChanges->breakRename( $code, $msg['key'] ); |
187 | } else { |
188 | $sourceChanges->removeBasedOnType( $code, [ $msg['key'] ], $msgState ); |
189 | } |
190 | |
191 | if ( $renameMsgState === MessageSourceChange::RENAME ) { |
192 | $renameMsgState = $sourceChanges->breakRename( $code, $renameMsg['key'] ); |
193 | } elseif ( $renameMsgState !== MessageSourceChange::NONE ) { |
194 | $sourceChanges->removeBasedOnType( $code, [ $keyToRename ], $renameMsgState ); |
195 | } |
196 | |
197 | // This is done in case the key has not been renamed in the non-source language. |
198 | $msg['key'] = $msgKey; |
199 | |
200 | // Add as rename |
201 | $stringComparator = new SimpleStringComparator(); |
202 | $similarity = $stringComparator->getSimilarity( |
203 | $msg['content'], |
204 | $renameMsg['content'] |
205 | ); |
206 | $sourceChanges->addRename( $code, $msg, $renameMsg, $similarity ); |
207 | $sourceChanges->setRenameState( $code, $msgKey, $msgState ); |
208 | $sourceChanges->setRenameState( $code, $keyToRename, $renameMsgState ); |
209 | } |
210 | } |
211 | |
212 | protected function handleSourceRename( |
213 | MessageSourceChange $sourceChanges, |
214 | string $code, |
215 | string $msgKey, |
216 | string $keyToRename |
217 | ): void { |
218 | $msgState = $renameMsgState = null; |
219 | |
220 | $msg = $sourceChanges->findMessage( |
221 | $code, $msgKey, [ MessageSourceChange::ADDITION, MessageSourceChange::RENAME ], $msgState |
222 | ); |
223 | |
224 | $renameMsg = $sourceChanges->findMessage( |
225 | $code, |
226 | $keyToRename, |
227 | [ MessageSourceChange::DELETION, MessageSourceChange::RENAME ], |
228 | $renameMsgState |
229 | ); |
230 | |
231 | if ( $msg === null || $renameMsg === null ) { |
232 | $this->dieWithError( 'apierror-translate-rename-key-invalid' ); |
233 | } |
234 | |
235 | if ( $msgState === MessageSourceChange::RENAME ) { |
236 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141 |
237 | $msgState = $sourceChanges->breakRename( $code, $msg['key'] ); |
238 | } |
239 | |
240 | if ( $renameMsgState === MessageSourceChange::RENAME ) { |
241 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141 |
242 | $renameMsgState = $sourceChanges->breakRename( $code, $renameMsg['key'] ); |
243 | } |
244 | |
245 | // Ensure that one of them is an ADDITION, and one is DELETION |
246 | if ( $msgState !== MessageSourceChange::ADDITION || |
247 | $renameMsgState !== MessageSourceChange::DELETION ) { |
248 | $this->dieWithError( [ |
249 | 'apierror-translate-rename-state-invalid', |
250 | wfEscapeWikiText( $msgState ), wfEscapeWikiText( $renameMsgState ) |
251 | ] ); |
252 | } |
253 | |
254 | // Remove previous states |
255 | $sourceChanges->removeAdditions( $code, [ $msgKey ] ); |
256 | $sourceChanges->removeDeletions( $code, [ $keyToRename ] ); |
257 | |
258 | // Add as rename |
259 | $stringComparator = new SimpleStringComparator(); |
260 | $similarity = $stringComparator->getSimilarity( |
261 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141 |
262 | $msg['content'], |
263 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable T240141 |
264 | $renameMsg['content'] |
265 | ); |
266 | $sourceChanges->addRename( $code, $msg, $renameMsg, $similarity ); |
267 | } |
268 | |
269 | /** Handles add message as new request */ |
270 | protected function handleNew( |
271 | MessageSourceChange $sourceChanges, |
272 | string $msgKey, |
273 | string $sourceLang |
274 | ): void { |
275 | $msgState = null; |
276 | $languages = $sourceChanges->getLanguages(); |
277 | |
278 | foreach ( $languages as $code ) { |
279 | $msg = $sourceChanges->findMessage( |
280 | $code, $msgKey, [ MessageSourceChange::RENAME ], $msgState |
281 | ); |
282 | |
283 | if ( $code === $sourceLang && $msg === null ) { |
284 | $this->dieWithError( 'apierror-translate-addition-key-invalid' ); |
285 | } |
286 | |
287 | if ( $code === $sourceLang && $msgState !== MessageSourceChange::RENAME ) { |
288 | $this->dieWithError( 'apierror-translate-rename-msg-new' ); |
289 | } |
290 | |
291 | // For any other language, it's possible for the message to be not found. |
292 | if ( $msg === null ) { |
293 | continue; |
294 | } |
295 | |
296 | // breakRename will add the message back to its previous state, nothing more to do |
297 | $sourceChanges->breakRename( $code, $msg['key'] ); |
298 | } |
299 | } |
300 | |
301 | protected function getAllowedParams(): array { |
302 | return [ |
303 | 'groupId' => [ |
304 | ParamValidator::PARAM_TYPE => 'string', |
305 | ParamValidator::PARAM_REQUIRED => true, |
306 | ], |
307 | 'renameMessageKey' => [ |
308 | ParamValidator::PARAM_TYPE => 'string', |
309 | ParamValidator::PARAM_REQUIRED => false, |
310 | ], |
311 | 'messageKey' => [ |
312 | ParamValidator::PARAM_TYPE => 'string', |
313 | ParamValidator::PARAM_REQUIRED => true, |
314 | ], |
315 | 'operation' => [ |
316 | ParamValidator::PARAM_TYPE => [ 'rename', 'new' ], |
317 | ParamValidator::PARAM_ISMULTI => false, |
318 | ParamValidator::PARAM_REQUIRED => true, |
319 | ], |
320 | 'changesetName' => [ |
321 | ParamValidator::PARAM_TYPE => 'string', |
322 | ParamValidator::PARAM_DEFAULT => MessageChangeStorage::DEFAULT_NAME |
323 | ], |
324 | 'changesetModified' => [ |
325 | ParamValidator::PARAM_TYPE => 'integer', |
326 | ParamValidator::PARAM_REQUIRED => true, |
327 | ] |
328 | ]; |
329 | } |
330 | |
331 | public function isInternal(): bool { |
332 | return true; |
333 | } |
334 | |
335 | public function needsToken(): string { |
336 | return 'csrf'; |
337 | } |
338 | } |