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