Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.41% covered (warning)
69.41%
118 / 170
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryMessageGroupsActionApi
69.41% covered (warning)
69.41%
118 / 170
42.86% covered (danger)
42.86%
3 / 7
145.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 execute
71.11% covered (warning)
71.11%
32 / 45
0.00% covered (danger)
0.00%
0 / 1
29.64
 formatGroup
60.00% covered (warning)
60.00%
30 / 50
0.00% covered (danger)
0.00%
0 / 1
56.86
 getWorkflowStates
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
 getAllowedParams
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
1
 getPropertyList
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use AggregateMessageGroup;
7use ApiBase;
8use ApiQuery;
9use ApiQueryBase;
10use MediaWiki\Extension\Translate\HookRunner;
11use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata;
12use MediaWiki\Extension\Translate\MessageProcessing\StringMatcher;
13use MediaWiki\Extension\Translate\Utilities\Utilities;
14use MessageGroup;
15use Wikimedia\ParamValidator\ParamValidator;
16
17/**
18 * Api module for querying MessageGroups.
19 * @author Niklas Laxström
20 * @author Harry Burt
21 * @copyright Copyright © 2012-2013, Harry Burt
22 * @license GPL-2.0-or-later
23 * @ingroup API TranslateAPI
24 */
25class QueryMessageGroupsActionApi extends ApiQueryBase {
26    private HookRunner $hookRunner;
27    private MessageGroupMetadata $messageGroupMetadata;
28    private MessageGroupSubscription $groupSubscription;
29
30    public function __construct(
31        ApiQuery $query,
32        string $moduleName,
33        HookRunner $hookRunner,
34        MessageGroupMetadata $messageGroupMetadata,
35        MessageGroupSubscription $groupSubscription
36    ) {
37        parent::__construct( $query, $moduleName, 'mg' );
38        $this->hookRunner = $hookRunner;
39        $this->messageGroupMetadata = $messageGroupMetadata;
40        $this->groupSubscription = $groupSubscription;
41    }
42
43    public function execute(): void {
44        $params = $this->extractRequestParams();
45        $filter = $params['filter'];
46
47        $groups = [];
48        $props = array_flip( $params['prop'] );
49
50        $needsMetadata = isset( $props['prioritylangs'] ) || isset( $props['priorityforce'] );
51
52        if ( $params['format'] === 'flat' ) {
53            if ( $params['root'] !== '' ) {
54                $group = MessageGroups::getGroup( $params['root'] );
55                if ( $group ) {
56                    $groups[$params['root']] = $group;
57                }
58            } else {
59                $groups = MessageGroups::getAllGroups();
60                usort( $groups, [ MessageGroups::class, 'groupLabelSort' ] );
61            }
62        } elseif ( $params['root'] !== '' ) {
63            // format=tree from now on, as it is the only other valid option
64            $group = MessageGroups::getGroup( $params['root'] );
65            if ( $group instanceof AggregateMessageGroup ) {
66                $childIds = [];
67                $groups = MessageGroups::subGroups( $group, $childIds );
68                // The parent group is the first, ignore it
69                array_shift( $groups );
70            }
71        } else {
72            $groups = MessageGroups::getGroupStructure();
73        }
74
75        if ( $params['root'] === '' ) {
76            $dynamicGroups = [];
77            foreach ( array_keys( MessageGroups::getDynamicGroups() ) as $id ) {
78                $dynamicGroups[$id] = MessageGroups::getGroup( $id );
79            }
80            // Have dynamic groups appear first in the list
81            $groups = $dynamicGroups + $groups;
82        }
83        '@phan-var (MessageGroup|array)[] $groups';
84
85        // Do not list the sandbox group. The code that knows it
86        // exists can access it directly.
87        if ( isset( $groups['!sandbox'] ) ) {
88            unset( $groups['!sandbox'] );
89        }
90
91        $result = $this->getResult();
92        $matcher = new StringMatcher( '', $filter );
93        /** @var MessageGroup|array $mixed */
94        foreach ( $groups as $index => $mixed ) {
95            // array when Format = tree
96            $group = is_array( $mixed ) ? reset( $mixed ) : $mixed;
97            if ( $filter !== [] && !$matcher->matches( $group->getId() ) ) {
98                unset( $groups[$index] );
99                continue;
100            }
101
102            if (
103                $params['languageFilter'] !== '' &&
104                $this->messageGroupMetadata->isExcluded( $group->getId(), $params['languageFilter'] )
105            ) {
106                unset( $groups[$index] );
107            }
108        }
109
110        if ( $needsMetadata && $groups ) {
111            // FIXME: This doesn't preload subgroups in a tree structure
112            $this->messageGroupMetadata->preloadGroups( array_keys( $groups ), __METHOD__ );
113        }
114
115        /** @var MessageGroup|array $mixed */
116        foreach ( $groups as $index => $mixed ) {
117            $a = $this->formatGroup( $mixed, $props );
118
119            $result->setIndexedTagName( $a, 'group' );
120
121            // @todo Add a continue?
122            $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $a );
123            if ( !$fit ) {
124                // Even if we're not going to give a continue, no point carrying on
125                // if the result is full
126                break;
127            }
128        }
129
130        $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'group' );
131    }
132
133    /**
134     * @param array|MessageGroup $mixed
135     * @param array $props List of props as the array keys
136     * @param int $depth
137     */
138    protected function formatGroup( $mixed, array $props, int $depth = 0 ): array {
139        $params = $this->extractRequestParams();
140        $context = $this->getContext();
141
142        // Default
143        $g = $mixed;
144        $subgroups = [];
145
146        // Format = tree and has subgroups
147        if ( is_array( $mixed ) ) {
148            $g = array_shift( $mixed );
149            $subgroups = $mixed;
150        }
151
152        $a = [];
153
154        $groupId = $g->getId();
155
156        if ( isset( $props['id'] ) ) {
157            $a['id'] = $groupId;
158        }
159
160        if ( isset( $props['label'] ) ) {
161            $a['label'] = $g->getLabel( $context );
162        }
163
164        if ( isset( $props['description'] ) ) {
165            $a['description'] = $g->getDescription( $context );
166        }
167
168        if ( isset( $props['class'] ) ) {
169            $a['class'] = get_class( $g );
170        }
171
172        if ( isset( $props['namespace'] ) ) {
173            $a['namespace'] = $g->getNamespace();
174        }
175
176        if ( isset( $props['exists'] ) ) {
177            $a['exists'] = $g->exists();
178        }
179
180        if ( isset( $props['icon'] ) ) {
181            $formats = Utilities::getIcon( $g, $params['iconsize'] );
182            if ( $formats ) {
183                $a['icon'] = $formats;
184            }
185        }
186
187        if ( isset( $props['priority'] ) ) {
188            $priority = MessageGroups::getPriority( $g );
189            $a['priority'] = $priority ?: 'default';
190        }
191
192        if ( isset( $props['prioritylangs'] ) ) {
193            $prioritylangs = $this->messageGroupMetadata->get( $groupId, 'prioritylangs' );
194            $a['prioritylangs'] = $prioritylangs ? explode( ',', $prioritylangs ) : false;
195        }
196
197        if ( isset( $props['priorityforce'] ) ) {
198            $a['priorityforce'] = ( $this->messageGroupMetadata->get( $groupId, 'priorityforce' ) === 'on' );
199        }
200
201        if ( isset( $props['workflowstates'] ) ) {
202            $a['workflowstates'] = $this->getWorkflowStates( $g );
203        }
204
205        if ( isset( $props['sourcelanguage'] ) ) {
206            $a['sourcelanguage'] = $g->getSourceLanguage();
207        }
208
209        if (
210            isset( $props['subscription'] ) &&
211            $this->groupSubscription->canUserSubscribeToGroup( $g, $this->getUser() )->isOK()
212        ) {
213            $a['subscription'] = $this->groupSubscription->isUserSubscribedTo( $g, $this->getUser() );
214        }
215
216        $this->hookRunner->onTranslateProcessAPIMessageGroupsProperties( $a, $props, $params, $g );
217
218        // Depth only applies to tree format
219        if ( $depth >= $params['depth'] && $params['format'] === 'tree' ) {
220            $a['groupcount'] = count( $subgroups );
221
222            // Prevent going further down in the three
223            return $a;
224        }
225
226        // Always empty array for flat format, only sometimes for tree format
227        if ( $subgroups !== [] ) {
228            foreach ( $subgroups as $sg ) {
229                $a['groups'][] = $this->formatGroup( $sg, $props );
230            }
231            $result = $this->getResult();
232            $result->setIndexedTagName( $a['groups'], 'group' );
233        }
234
235        return $a;
236    }
237
238    /**
239     * Get the workflow states applicable to the given message group
240     * @return bool|array Associative array with states as key and localized state
241     * labels as values
242     */
243    private function getWorkflowStates( MessageGroup $group ) {
244        if ( MessageGroups::isDynamic( $group ) ) {
245            return false;
246        }
247
248        $stateConfig = $group->getMessageGroupStates()->getStates();
249
250        if ( !is_array( $stateConfig ) || $stateConfig === [] ) {
251            return false;
252        }
253
254        $user = $this->getUser();
255
256        foreach ( $stateConfig as $state => $config ) {
257            if ( is_array( $config ) ) {
258                // Check if user is allowed to change states generally
259                $allowed = $user->isAllowed( 'translate-groupreview' );
260                // Check further restrictions
261                if ( $allowed && isset( $config['right'] ) ) {
262                    $allowed = $user->isAllowed( $config['right'] );
263                }
264
265                if ( $allowed ) {
266                    $stateConfig[$state]['canchange'] = 1;
267                }
268
269                $stateConfig[$state]['name'] =
270                    $this->msg( "translate-workflow-state-$state" )->text();
271            }
272        }
273
274        return $stateConfig;
275    }
276
277    protected function getAllowedParams(): array {
278        $allowedParams = [
279            'depth' => [
280                ParamValidator::PARAM_TYPE => 'integer',
281                ParamValidator::PARAM_DEFAULT => 100,
282            ],
283            'filter' => [
284                ParamValidator::PARAM_TYPE => 'string',
285                ParamValidator::PARAM_DEFAULT => '',
286                ParamValidator::PARAM_ISMULTI => true,
287            ],
288            'format' => [
289                ParamValidator::PARAM_TYPE => [ 'flat', 'tree' ],
290                ParamValidator::PARAM_DEFAULT => 'flat',
291            ],
292            'iconsize' => [
293                ParamValidator::PARAM_TYPE => 'integer',
294                ParamValidator::PARAM_DEFAULT => 64,
295            ],
296            'prop' => [
297                ParamValidator::PARAM_TYPE => array_keys( $this->getPropertyList() ),
298                ParamValidator::PARAM_DEFAULT => 'id|label|description|class|exists',
299                ParamValidator::PARAM_ISMULTI => true,
300                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
301            ],
302            'root' => [
303                ParamValidator::PARAM_TYPE => 'string',
304                ParamValidator::PARAM_DEFAULT => '',
305            ],
306            'languageFilter' => [
307                ParamValidator::PARAM_TYPE => 'string',
308                ParamValidator::PARAM_DEFAULT => '',
309            ]
310        ];
311        $this->hookRunner->onTranslateGetAPIMessageGroupsParameterList( $allowedParams );
312
313        return $allowedParams;
314    }
315
316    /**
317     * Returns an array of properties and their descriptions. Descriptions are ignored.
318     * Descriptions come from apihelp-query+messagegroups-param-prop and that is not
319     * extensible.
320     */
321    private function getPropertyList(): array {
322        $properties = array_flip( [
323            'id',
324            'label',
325            'description',
326            'class',
327            'namespace',
328            'exists',
329            'icon',
330            'priority',
331            'prioritylangs',
332            'priorityforce',
333            'workflowstates',
334            'sourcelanguage',
335            'subscription'
336        ] );
337
338        $this->hookRunner->onTranslateGetAPIMessageGroupsPropertyDescs( $properties );
339
340        return $properties;
341    }
342
343    protected function getExamplesMessages(): array {
344        return [
345            'action=query&meta=messagegroups' => 'apihelp-query+messagegroups-example-1',
346        ];
347    }
348}