Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
AggregateGroupsActionApi
0.00% covered (danger)
0.00%
0 / 164
0.00% covered (danger)
0.00%
0 / 8
1806
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 1
992
 getGroupsWithDifferentLanguage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 isWriteMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getIncludableGroups
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use AggregateMessageGroup;
7use JobQueueGroup;
8use ManualLogEntry;
9use MediaWiki\Api\ApiBase;
10use MediaWiki\Api\ApiMain;
11use MediaWiki\Extension\Translate\LogNames;
12use MediaWiki\Extension\Translate\MessageLoading\RebuildMessageIndexJob;
13use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
14use MediaWiki\Logger\LoggerFactory;
15use Wikimedia\ParamValidator\ParamValidator;
16
17/**
18 * API module for managing aggregate message groups
19 * Only supports aggregate message groups defined inside the wiki.
20 * Aggregate message group defined in YAML configuration cannot be altered.
21 * @author Santhosh Thottingal
22 * @author Niklas Laxström
23 * @copyright Copyright © 2012-2013, Santhosh Thottingal
24 * @license GPL-2.0-or-later
25 * @ingroup API TranslateAPI
26 */
27class AggregateGroupsActionApi extends ApiBase {
28    private JobQueueGroup $jobQueueGroup;
29    protected static string $right = 'translate-manage';
30    private MessageGroupMetadata $messageGroupMetadata;
31    private AggregateGroupManager $aggregateGroupManager;
32
33    private const NO_LANGUAGE_CODE = '-';
34
35    public function __construct(
36        ApiMain $main,
37        string $action,
38        JobQueueGroup $jobQueueGroup,
39        MessageGroupMetadata $messageGroupMetadata,
40        AggregateGroupManager $aggregateGroupManager
41    ) {
42        parent::__construct( $main, $action );
43        $this->jobQueueGroup = $jobQueueGroup;
44        $this->messageGroupMetadata = $messageGroupMetadata;
45        $this->aggregateGroupManager = $aggregateGroupManager;
46    }
47
48    public function execute(): void {
49        $this->checkUserRightsAny( self::$right );
50        $block = $this->getUser()->getBlock();
51        if ( $block && $block->isSitewide() ) {
52            $this->dieBlocked( $block );
53        }
54
55        $params = $this->extractRequestParams();
56        $action = $params['do'];
57        $output = [];
58        if ( $action === 'associate' || $action === 'dissociate' ) {
59            // Group or groups is mandatory only for these two actions
60            $this->requireOnlyOneParameter( $params, 'group', 'groups' );
61
62            if ( isset( $params['groups'] ) ) {
63                $subgroupIds = array_map( 'trim', $params['groups'] );
64            } else {
65                $subgroupIds = [ $params['group'] ];
66            }
67
68            if ( !isset( $params['aggregategroup'] ) ) {
69                $this->dieWithError( [ 'apierror-missingparam', 'aggregategroup' ] );
70            }
71
72            $aggregateGroupId = $params['aggregategroup'];
73
74            try {
75                if ( $action === 'associate' ) {
76                    // Not all subgroups passed maybe added, as some may already be part of the aggregate group
77                    $groupIdsToLog = $this->aggregateGroupManager->associate( $aggregateGroupId, $subgroupIds );
78                    $output[ 'groupUrls'] = [];
79                    foreach ( $groupIdsToLog as $subgroupId ) {
80                        $output[ 'groupUrls'][ $subgroupId ] =
81                            $this->aggregateGroupManager->getTargetTitleByGroupId( $subgroupId )->getFullURL();
82                    }
83                } else {
84                    $groupIdsToLog = $this->aggregateGroupManager->disassociate( $aggregateGroupId, $subgroupIds );
85                }
86            } catch (
87                AggregateGroupAssociationFailure |
88                AggregateGroupLanguageMismatchException |
89                AggregateGroupNotFoundException $e
90            ) {
91                $this->dieWithException( $e );
92            }
93
94            $logParams = [
95                'aggregategroup' => $this->messageGroupMetadata->get( $aggregateGroupId, 'name' ),
96                'aggregategroup-id' => $aggregateGroupId,
97            ];
98
99            /* To allow removing no longer existing groups from aggregate message groups,
100             * the message group object $group might not always be available.
101             * In this case, we need to fake some title. */
102            foreach ( $groupIdsToLog as $subgroupId ) {
103                $title = $this->aggregateGroupManager->getTargetTitleByGroupId( $subgroupId );
104                $entry = new ManualLogEntry( 'pagetranslation', $action );
105                $entry->setPerformer( $this->getUser() );
106                $entry->setTarget( $title );
107                // @todo
108                // $entry->setComment( $comment );
109                $entry->setParameters( $logParams );
110
111                $logId = $entry->insert();
112                $entry->publish( $logId );
113            }
114        } elseif ( $action === 'remove' ) {
115            if ( !isset( $params['aggregategroup'] ) ) {
116                $this->dieWithError( [ 'apierror-missingparam', 'aggregategroup' ] );
117            }
118
119            $aggregateGroupId = $params['aggregategroup'];
120            $group = MessageGroups::getGroup( $aggregateGroupId );
121            if ( !( $group instanceof AggregateMessageGroup ) ) {
122                $this->dieWithError(
123                    'apierror-translate-invalidaggregategroupname',
124                    'invalidaggregategroupname'
125                );
126            }
127
128            $this->messageGroupMetadata->deleteGroup( $params['aggregategroup'] );
129            $logger = LoggerFactory::getInstance( LogNames::MAIN );
130            $logger->info(
131                'Aggregate group {groupId} has been deleted.',
132                [ 'groupId' => $aggregateGroupId ]
133            );
134        } elseif ( $action === 'add' ) {
135            if ( !isset( $params['groupname'] ) ) {
136                $this->dieWithError( [ 'apierror-missingparam', 'groupname' ] );
137            }
138            $name = trim( $params['groupname'] );
139            if ( strlen( $name ) === 0 ) {
140                $this->dieWithError(
141                    'apierror-translate-invalidaggregategroupname',
142                    'invalidaggregategroupname'
143                );
144            }
145
146            $desc = trim( $params['groupdescription'] );
147
148            try {
149                $languageCode = $params['groupsourcelanguagecode'] === self::NO_LANGUAGE_CODE ?
150                    null : trim( $params['groupsourcelanguagecode'] );
151                $aggregateGroupId = $this->aggregateGroupManager->add( $name, $desc, $languageCode );
152            } catch ( DuplicateAggregateGroupException $e ) {
153                $this->dieWithException( $e );
154            }
155
156            // Once a new aggregate group is added, we need to show all the pages that can be added to that.
157            $output['groups'] = $this->getIncludableGroups();
158            $output['aggregategroupId'] = $aggregateGroupId;
159            // @todo Logging
160        } elseif ( $action === 'update' ) {
161            if ( !isset( $params['groupname'] ) ) {
162                $this->dieWithError( [ 'apierror-missingparam', 'groupname' ] );
163            }
164            $name = trim( $params['groupname'] );
165            if ( strlen( $name ) === 0 ) {
166                $this->dieWithError(
167                    'apierror-translate-invalidaggregategroupname',
168                    'invalidaggregategroupname'
169                );
170            }
171
172            $aggregateGroupId = $params['aggregategroup'];
173            $oldName = $this->messageGroupMetadata->get( $aggregateGroupId, 'name' );
174
175            // Error if the label exists already
176            $exists = MessageGroups::labelExists( $name );
177            if ( $exists && $oldName !== $name ) {
178                $this->dieWithException( new DuplicateAggregateGroupException( $name ) );
179            }
180
181            $desc = trim( $params['groupdescription'] );
182
183            $newLanguageCode = trim( $params['groupsourcelanguagecode'] );
184
185            $oldDesc = $this->messageGroupMetadata->get( $aggregateGroupId, 'description' );
186            $currentLanguageCode = $this->messageGroupMetadata->get( $aggregateGroupId, 'sourcelanguagecode' );
187
188            if ( $newLanguageCode !== self::NO_LANGUAGE_CODE && $newLanguageCode !== $currentLanguageCode ) {
189                $groupsWithDifferentLanguage =
190                    $this->getGroupsWithDifferentLanguage( $aggregateGroupId, $newLanguageCode );
191
192                if ( count( $groupsWithDifferentLanguage ) ) {
193                    $this->dieWithError( [
194                        'translate-error-aggregategroup-source-language-mismatch',
195                        implode( ', ', $groupsWithDifferentLanguage ),
196                        $newLanguageCode,
197                        count( $groupsWithDifferentLanguage )
198                    ] );
199                }
200            }
201
202            if (
203                $oldName === $name
204                && $oldDesc === $desc
205                && $newLanguageCode === $currentLanguageCode
206            ) {
207                $this->dieWithError( 'apierror-translate-invalidupdate', 'invalidupdate' );
208            }
209            $this->messageGroupMetadata->set( $aggregateGroupId, 'name', $name );
210            $this->messageGroupMetadata->set( $aggregateGroupId, 'description', $desc );
211            if ( $newLanguageCode === self::NO_LANGUAGE_CODE ) {
212                $this->messageGroupMetadata->clearMetadata( $aggregateGroupId, [ 'sourcelanguagecode' ] );
213            } else {
214                $this->messageGroupMetadata->set( $aggregateGroupId, 'sourcelanguagecode', $newLanguageCode );
215            }
216        }
217
218        // If we got this far, nothing has failed
219        $output['result'] = 'ok';
220        $this->getResult()->addValue( null, $this->getModuleName(), $output );
221        // Cache needs to be cleared after any changes to groups
222        MessageGroups::singleton()->recache();
223        $this->jobQueueGroup->push( RebuildMessageIndexJob::newJob() );
224    }
225
226    /**
227     * Aggregate groups have an explicit source language that should match with
228     * the associated message group source language. Thus, we check which of the subgroups
229     * of an aggregate group don't have a matching source language.
230     */
231    private function getGroupsWithDifferentLanguage(
232        string $aggregateGroupId,
233        string $sourceLanguageCode
234    ): array {
235        $groupsWithDifferentLanguage = [];
236        $subgroups = $this->messageGroupMetadata->getSubgroups( $aggregateGroupId );
237        foreach ( $subgroups as $group ) {
238            $messageGroup = MessageGroups::getGroup( $group );
239            $messageGroupLanguage = $messageGroup->getSourceLanguage();
240            if ( $messageGroupLanguage !== $sourceLanguageCode ) {
241                $groupsWithDifferentLanguage[] = $messageGroup->getLabel();
242            }
243        }
244
245        return $groupsWithDifferentLanguage;
246    }
247
248    public function isWriteMode(): bool {
249        return true;
250    }
251
252    public function needsToken(): string {
253        return 'csrf';
254    }
255
256    protected function getAllowedParams(): array {
257        return [
258            'do' => [
259                ParamValidator::PARAM_TYPE => [ 'associate', 'dissociate', 'remove', 'add', 'update' ],
260                ParamValidator::PARAM_REQUIRED => true,
261            ],
262            'aggregategroup' => [
263                ParamValidator::PARAM_TYPE => 'string',
264            ],
265            'group' => [
266                // For backward compatibility
267                ParamValidator::PARAM_TYPE => 'string',
268                ParamValidator::PARAM_DEPRECATED => true,
269            ],
270            'groups' => [
271                // Not providing list of values, to allow dissociation of unknown groups
272                ParamValidator::PARAM_TYPE => 'string',
273                ParamValidator::PARAM_ISMULTI => true,
274            ],
275            'groupname' => [
276                ParamValidator::PARAM_TYPE => 'string',
277            ],
278            'groupdescription' => [
279                ParamValidator::PARAM_TYPE => 'string',
280                ParamValidator::PARAM_DEFAULT => '',
281            ],
282            'groupsourcelanguagecode' => [
283                ParamValidator::PARAM_TYPE => 'string',
284                ParamValidator::PARAM_DEFAULT => self::NO_LANGUAGE_CODE,
285            ],
286        ];
287    }
288
289    protected function getExamplesMessages(): array {
290        return [
291            'action=aggregategroups&do=associate&groups=groupId1|groupId2&aggregategroup=aggregateGroupId'
292                => 'apihelp-aggregategroups-example-1',
293        ];
294    }
295
296    private function getIncludableGroups(): array {
297        $groups = MessageGroups::getAllGroups();
298        $pages = [];
299        foreach ( $groups as $group ) {
300            if ( $this->aggregateGroupManager->supportsAggregation( $group ) ) {
301                $pages[$group->getId()] = $group->getLabel( $this->getContext() );
302            }
303        }
304
305        return $pages;
306    }
307}