Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 190 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
AggregateGroupsActionApi | |
0.00% |
0 / 190 |
|
0.00% |
0 / 9 |
2450 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 138 |
|
0.00% |
0 / 1 |
1332 | |||
getGroupsWithDifferentLanguage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
generateAggregateGroupId | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isWriteMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAllowedParams | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getAllPages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use AggregateMessageGroup; |
7 | use ApiBase; |
8 | use ApiMain; |
9 | use JobQueueGroup; |
10 | use ManualLogEntry; |
11 | use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\Title\Title; |
14 | use MessageIndexRebuildJob; |
15 | use Wikimedia\ParamValidator\ParamValidator; |
16 | use WikiPageMessageGroup; |
17 | |
18 | /** |
19 | * API module for managing aggregate message groups |
20 | * Only supports aggregate message groups defined inside the wiki. |
21 | * Aggregate message group defined in YAML configuration cannot be altered. |
22 | * @author Santhosh Thottingal |
23 | * @author Niklas Laxström |
24 | * @copyright Copyright © 2012-2013, Santhosh Thottingal |
25 | * @license GPL-2.0-or-later |
26 | * @ingroup API TranslateAPI |
27 | */ |
28 | class AggregateGroupsActionApi extends ApiBase { |
29 | private JobQueueGroup $jobQueueGroup; |
30 | protected static string $right = 'translate-manage'; |
31 | private const NO_LANGUAGE_CODE = '-'; |
32 | private MessageGroupMetadata $messageGroupMetadata; |
33 | |
34 | public function __construct( |
35 | ApiMain $main, |
36 | string $action, |
37 | JobQueueGroup $jobQueueGroup, |
38 | MessageGroupMetadata $messageGroupMetadata |
39 | ) { |
40 | parent::__construct( $main, $action ); |
41 | $this->jobQueueGroup = $jobQueueGroup; |
42 | $this->messageGroupMetadata = $messageGroupMetadata; |
43 | } |
44 | |
45 | public function execute(): void { |
46 | $this->checkUserRightsAny( self::$right ); |
47 | $block = $this->getUser()->getBlock(); |
48 | if ( $block && $block->isSitewide() ) { |
49 | $this->dieBlocked( $block ); |
50 | } |
51 | |
52 | $params = $this->extractRequestParams(); |
53 | $action = $params['do']; |
54 | $output = []; |
55 | if ( $action === 'associate' || $action === 'dissociate' ) { |
56 | // Group is mandatory only for these two actions |
57 | if ( !isset( $params['group'] ) ) { |
58 | $this->dieWithError( [ 'apierror-missingparam', 'group' ] ); |
59 | } |
60 | if ( !isset( $params['aggregategroup'] ) ) { |
61 | $this->dieWithError( [ 'apierror-missingparam', 'aggregategroup' ] ); |
62 | } |
63 | $aggregateGroup = $params['aggregategroup']; |
64 | $subgroups = $this->messageGroupMetadata->getSubgroups( $aggregateGroup ); |
65 | if ( $subgroups === null ) { |
66 | // For a newly created aggregate group, it may contain no subgroups, but null |
67 | // means the group does not exist or something has gone wrong. |
68 | |
69 | $this->dieWithError( 'apierror-translate-invalidaggregategroup', 'invalidaggregategroup' ); |
70 | } |
71 | |
72 | $subgroupId = $params['group']; |
73 | $group = MessageGroups::getGroup( $subgroupId ); |
74 | |
75 | // Add or remove from the list |
76 | if ( $action === 'associate' ) { |
77 | if ( !$group instanceof WikiPageMessageGroup ) { |
78 | $this->dieWithError( 'apierror-translate-invalidgroup', 'invalidgroup' ); |
79 | } |
80 | |
81 | $messageGroupLanguage = $group->getSourceLanguage(); |
82 | $aggregateGroupLanguage = $this->messageGroupMetadata->get( $aggregateGroup, 'sourcelanguagecode' ); |
83 | // If source language is not set, user shouldn't be prevented from associating a message group |
84 | if ( $aggregateGroupLanguage !== false && $messageGroupLanguage !== $aggregateGroupLanguage ) { |
85 | $this->dieWithError( [ |
86 | 'apierror-translate-grouplanguagemismatch', |
87 | $messageGroupLanguage, |
88 | $aggregateGroupLanguage |
89 | ] ); |
90 | } |
91 | $subgroups[] = $subgroupId; |
92 | $subgroups = array_unique( $subgroups ); |
93 | } elseif ( $action === 'dissociate' ) { |
94 | // Allow removal of non-existing groups |
95 | $subgroups = array_flip( $subgroups ); |
96 | unset( $subgroups[$subgroupId] ); |
97 | $subgroups = array_flip( $subgroups ); |
98 | } |
99 | |
100 | $this->messageGroupMetadata->setSubgroups( $aggregateGroup, $subgroups ); |
101 | |
102 | $logParams = [ |
103 | 'aggregategroup' => $this->messageGroupMetadata->get( $aggregateGroup, 'name' ), |
104 | 'aggregategroup-id' => $aggregateGroup, |
105 | ]; |
106 | |
107 | /* Note that to allow removing no longer existing groups from |
108 | * aggregate message groups, the message group object $group |
109 | * might not always be available. In this case we need to fake |
110 | * some title. */ |
111 | $title = $group instanceof WikiPageMessageGroup ? |
112 | $group->getTitle() : |
113 | Title::newFromText( "Special:Translate/$subgroupId" ); |
114 | |
115 | $entry = new ManualLogEntry( 'pagetranslation', $action ); |
116 | $entry->setPerformer( $this->getUser() ); |
117 | $entry->setTarget( $title ); |
118 | // @todo |
119 | // $entry->setComment( $comment ); |
120 | $entry->setParameters( $logParams ); |
121 | |
122 | $logid = $entry->insert(); |
123 | $entry->publish( $logid ); |
124 | } elseif ( $action === 'remove' ) { |
125 | if ( !isset( $params['aggregategroup'] ) ) { |
126 | $this->dieWithError( [ 'apierror-missingparam', 'aggregategroup' ] ); |
127 | } |
128 | |
129 | $aggregateGroupId = $params['aggregategroup']; |
130 | $group = MessageGroups::getGroup( $aggregateGroupId ); |
131 | if ( !( $group instanceof AggregateMessageGroup ) ) { |
132 | $this->dieWithError( |
133 | 'apierror-translate-invalidaggregategroupname', |
134 | 'invalidaggregategroupname' |
135 | ); |
136 | } |
137 | |
138 | $this->messageGroupMetadata->deleteGroup( $params['aggregategroup'] ); |
139 | $logger = LoggerFactory::getInstance( 'Translate' ); |
140 | $logger->info( |
141 | 'Aggregate group {groupId} has been deleted.', |
142 | [ 'groupId' => $aggregateGroupId ] |
143 | ); |
144 | } elseif ( $action === 'add' ) { |
145 | if ( !isset( $params['groupname'] ) ) { |
146 | $this->dieWithError( [ 'apierror-missingparam', 'groupname' ] ); |
147 | } |
148 | $name = trim( $params['groupname'] ); |
149 | if ( strlen( $name ) === 0 ) { |
150 | $this->dieWithError( |
151 | 'apierror-translate-invalidaggregategroupname', |
152 | 'invalidaggregategroupname' |
153 | ); |
154 | } |
155 | |
156 | if ( !isset( $params['groupdescription'] ) ) { |
157 | $this->dieWithError( [ 'apierror-missingparam', 'groupdescription' ] ); |
158 | } |
159 | $desc = trim( $params['groupdescription'] ); |
160 | |
161 | $aggregateGroupId = self::generateAggregateGroupId( $name ); |
162 | |
163 | // Throw error if group already exists |
164 | $nameExists = MessageGroups::labelExists( $name ); |
165 | if ( $nameExists ) { |
166 | $this->dieWithError( 'apierror-translate-duplicateaggregategroup', 'duplicateaggregategroup' ); |
167 | } |
168 | |
169 | // ID already exists: Generate a new ID by adding a number to it. |
170 | $idExists = MessageGroups::getGroup( $aggregateGroupId ); |
171 | if ( $idExists ) { |
172 | $i = 1; |
173 | do { |
174 | $tempId = $aggregateGroupId . '-' . $i; |
175 | $idExists = MessageGroups::getGroup( $tempId ); |
176 | $i++; |
177 | } while ( $idExists ); |
178 | $aggregateGroupId = $tempId; |
179 | } |
180 | $sourceLanguageCode = trim( $params['groupsourcelanguagecode'] ); |
181 | |
182 | $this->messageGroupMetadata->set( $aggregateGroupId, 'name', $name ); |
183 | $this->messageGroupMetadata->set( $aggregateGroupId, 'description', $desc ); |
184 | if ( $sourceLanguageCode !== self::NO_LANGUAGE_CODE ) { |
185 | $this->messageGroupMetadata->set( $aggregateGroupId, 'sourcelanguagecode', $sourceLanguageCode ); |
186 | } |
187 | $this->messageGroupMetadata->setSubgroups( $aggregateGroupId, [] ); |
188 | |
189 | // Once new aggregate group added, we need to show all the pages that can be added to that. |
190 | $output['groups'] = self::getAllPages(); |
191 | $output['aggregategroupId'] = $aggregateGroupId; |
192 | // @todo Logging |
193 | } elseif ( $action === 'update' ) { |
194 | if ( !isset( $params['groupname'] ) ) { |
195 | $this->dieWithError( [ 'apierror-missingparam', 'groupname' ] ); |
196 | } |
197 | $name = trim( $params['groupname'] ); |
198 | if ( strlen( $name ) === 0 ) { |
199 | $this->dieWithError( |
200 | 'apierror-translate-invalidaggregategroupname', |
201 | 'invalidaggregategroupname' |
202 | ); |
203 | } |
204 | $desc = trim( $params['groupdescription'] ); |
205 | $aggregateGroupId = $params['aggregategroup']; |
206 | $newLanguageCode = trim( $params['groupsourcelanguagecode'] ); |
207 | |
208 | $oldName = $this->messageGroupMetadata->get( $aggregateGroupId, 'name' ); |
209 | $oldDesc = $this->messageGroupMetadata->get( $aggregateGroupId, 'description' ); |
210 | $currentLanguageCode = $this->messageGroupMetadata->get( $aggregateGroupId, 'sourcelanguagecode' ); |
211 | |
212 | if ( $newLanguageCode !== self::NO_LANGUAGE_CODE && $newLanguageCode !== $currentLanguageCode ) { |
213 | $groupsWithDifferentLanguage = |
214 | $this->getGroupsWithDifferentLanguage( $aggregateGroupId, $newLanguageCode ); |
215 | |
216 | if ( count( $groupsWithDifferentLanguage ) ) { |
217 | $this->dieWithError( [ |
218 | 'apierror-translate-messagegroup-aggregategrouplanguagemismatch', |
219 | implode( ', ', $groupsWithDifferentLanguage ), |
220 | $newLanguageCode, |
221 | count( $groupsWithDifferentLanguage ) |
222 | ] ); |
223 | } |
224 | } |
225 | |
226 | // Error if the label exists already |
227 | $exists = MessageGroups::labelExists( $name ); |
228 | if ( $exists && $oldName !== $name ) { |
229 | $this->dieWithError( 'apierror-translate-duplicateaggregategroup', 'duplicateaggregategroup' ); |
230 | } |
231 | |
232 | if ( |
233 | $oldName === $name |
234 | && $oldDesc === $desc |
235 | && $newLanguageCode === $currentLanguageCode |
236 | ) { |
237 | $this->dieWithError( 'apierror-translate-invalidupdate', 'invalidupdate' ); |
238 | } |
239 | $this->messageGroupMetadata->set( $aggregateGroupId, 'name', $name ); |
240 | $this->messageGroupMetadata->set( $aggregateGroupId, 'description', $desc ); |
241 | if ( $newLanguageCode === self::NO_LANGUAGE_CODE ) { |
242 | $this->messageGroupMetadata->clearMetadata( $aggregateGroupId, [ 'sourcelanguagecode' ] ); |
243 | } else { |
244 | $this->messageGroupMetadata->set( $aggregateGroupId, 'sourcelanguagecode', $newLanguageCode ); |
245 | } |
246 | } |
247 | |
248 | // If we got this far, nothing has failed |
249 | $output['result'] = 'ok'; |
250 | $this->getResult()->addValue( null, $this->getModuleName(), $output ); |
251 | // Cache needs to be cleared after any changes to groups |
252 | MessageGroups::singleton()->recache(); |
253 | $this->jobQueueGroup->push( MessageIndexRebuildJob::newJob() ); |
254 | } |
255 | |
256 | /** |
257 | * Aggregate groups have an explicit source language that should match with |
258 | * the associated message group source language. Thus, we check which of the subgroups |
259 | * of an aggregate group don't have a matching source language. |
260 | */ |
261 | private function getGroupsWithDifferentLanguage( |
262 | string $aggregateGroupId, |
263 | string $sourceLanguageCode |
264 | ): array { |
265 | $groupsWithDifferentLanguage = []; |
266 | $subgroups = $this->messageGroupMetadata->getSubgroups( $aggregateGroupId ); |
267 | foreach ( $subgroups as $group ) { |
268 | $messageGroup = MessageGroups::getGroup( $group ); |
269 | $messageGroupLanguage = $messageGroup->getSourceLanguage(); |
270 | if ( $messageGroupLanguage !== $sourceLanguageCode ) { |
271 | $groupsWithDifferentLanguage[] = $messageGroup->getLabel(); |
272 | } |
273 | } |
274 | |
275 | return $groupsWithDifferentLanguage; |
276 | } |
277 | |
278 | protected function generateAggregateGroupId( string $aggregateGroupName, string $prefix = 'agg-' ): string { |
279 | // The database field has a maximum limit of 200 bytes |
280 | if ( strlen( $aggregateGroupName ) + strlen( $prefix ) >= 200 ) { |
281 | return $prefix . substr( sha1( $aggregateGroupName ), 0, 5 ); |
282 | } else { |
283 | $pattern = '/[\x00-\x1f\x23\x27\x2c\x2e\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/i'; |
284 | return $prefix . preg_replace( $pattern, '_', $aggregateGroupName ); |
285 | } |
286 | } |
287 | |
288 | public function isWriteMode(): bool { |
289 | return true; |
290 | } |
291 | |
292 | public function needsToken(): string { |
293 | return 'csrf'; |
294 | } |
295 | |
296 | protected function getAllowedParams(): array { |
297 | return [ |
298 | 'do' => [ |
299 | ParamValidator::PARAM_TYPE => [ 'associate', 'dissociate', 'remove', 'add', 'update' ], |
300 | ParamValidator::PARAM_REQUIRED => true, |
301 | ], |
302 | 'aggregategroup' => [ |
303 | ParamValidator::PARAM_TYPE => 'string', |
304 | ], |
305 | 'group' => [ |
306 | // Not providing list of values, to allow dissociation of unknown groups |
307 | ParamValidator::PARAM_TYPE => 'string', |
308 | ], |
309 | 'groupname' => [ |
310 | ParamValidator::PARAM_TYPE => 'string', |
311 | ], |
312 | 'groupdescription' => [ |
313 | ParamValidator::PARAM_TYPE => 'string', |
314 | ], |
315 | 'groupsourcelanguagecode' => [ |
316 | ParamValidator::PARAM_TYPE => 'string', |
317 | ], |
318 | 'token' => [ |
319 | ParamValidator::PARAM_TYPE => 'string', |
320 | ParamValidator::PARAM_REQUIRED => true, |
321 | ], |
322 | ]; |
323 | } |
324 | |
325 | protected function getExamplesMessages(): array { |
326 | return [ |
327 | 'action=aggregategroups&do=associate&group=groupId&aggregategroup=aggregateGroupId' |
328 | => 'apihelp-aggregategroups-example-1', |
329 | ]; |
330 | } |
331 | |
332 | public static function getAllPages(): array { |
333 | $groups = MessageGroups::getAllGroups(); |
334 | $pages = []; |
335 | foreach ( $groups as $group ) { |
336 | if ( $group instanceof WikiPageMessageGroup ) { |
337 | $pages[$group->getId()] = $group->getTitle()->getPrefixedText(); |
338 | } |
339 | } |
340 | |
341 | return $pages; |
342 | } |
343 | } |