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