Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.05% |
116 / 168 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
QueryMessageGroupsActionApi | |
69.05% |
116 / 168 |
|
42.86% |
3 / 7 |
148.99 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
71.11% |
32 / 45 |
|
0.00% |
0 / 1 |
29.64 | |||
formatGroup | |
60.00% |
30 / 50 |
|
0.00% |
0 / 1 |
56.86 | |||
getWorkflowStates | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
90 | |||
getAllowedParams | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
1 | |||
getPropertyList | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageGroupProcessing; |
5 | |
6 | use AggregateMessageGroup; |
7 | use MediaWiki\Api\ApiBase; |
8 | use MediaWiki\Api\ApiQuery; |
9 | use MediaWiki\Api\ApiQueryBase; |
10 | use MediaWiki\Extension\Translate\HookRunner; |
11 | use MediaWiki\Extension\Translate\MessageProcessing\MessageGroupMetadata; |
12 | use MediaWiki\Extension\Translate\MessageProcessing\StringMatcher; |
13 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
14 | use MessageGroup; |
15 | use 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 | */ |
25 | class 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 | |
312 | return $allowedParams; |
313 | } |
314 | |
315 | /** |
316 | * Returns an array of properties and their descriptions. Descriptions are ignored. |
317 | * Descriptions come from apihelp-query+messagegroups-param-prop and that is not |
318 | * extensible. |
319 | */ |
320 | private function getPropertyList(): array { |
321 | $properties = array_flip( [ |
322 | 'id', |
323 | 'label', |
324 | 'description', |
325 | 'class', |
326 | 'namespace', |
327 | 'exists', |
328 | 'icon', |
329 | 'priority', |
330 | 'prioritylangs', |
331 | 'priorityforce', |
332 | 'workflowstates', |
333 | 'sourcelanguage', |
334 | 'subscription' |
335 | ] ); |
336 | |
337 | return $properties; |
338 | } |
339 | |
340 | protected function getExamplesMessages(): array { |
341 | return [ |
342 | 'action=query&meta=messagegroups' => 'apihelp-query+messagegroups-example-1', |
343 | ]; |
344 | } |
345 | } |