Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
56.82% |
100 / 176 |
|
50.00% |
5 / 10 |
CRAP | |
0.00% |
0 / 1 |
QueryMessageCollectionActionApi | |
56.82% |
100 / 176 |
|
50.00% |
5 / 10 |
154.27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
executeGenerator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateLanguageCode | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
run | |
50.00% |
45 / 90 |
|
0.00% |
0 / 1 |
76.12 | |||
getLanguageName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
extractMessageData | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
getAllowedParams | |
100.00% |
44 / 44 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageLoading; |
5 | |
6 | use ApiBase; |
7 | use ApiPageSet; |
8 | use ApiQuery; |
9 | use ApiQueryGeneratorBase; |
10 | use ApiResult; |
11 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupReviewStore; |
12 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
13 | use MediaWiki\Extension\Translate\Utilities\ConfigHelper; |
14 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
15 | use MediaWiki\Languages\LanguageNameUtils; |
16 | use MediaWiki\Title\Title; |
17 | use RecentMessageGroup; |
18 | use Wikimedia\ParamValidator\ParamValidator; |
19 | use Wikimedia\ParamValidator\TypeDef\EnumDef; |
20 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
21 | use Wikimedia\Rdbms\ILoadBalancer; |
22 | |
23 | /** |
24 | * Api module for querying MessageCollection. |
25 | * @author Niklas Laxström |
26 | * @license GPL-2.0-or-later |
27 | * @ingroup API TranslateAPI |
28 | */ |
29 | class QueryMessageCollectionActionApi extends ApiQueryGeneratorBase { |
30 | private ConfigHelper $configHelper; |
31 | private LanguageNameUtils $languageNameUtils; |
32 | private ILoadBalancer $loadBalancer; |
33 | private MessageGroupReviewStore $groupReviewStore; |
34 | |
35 | public function __construct( |
36 | ApiQuery $query, |
37 | string $moduleName, |
38 | ConfigHelper $configHelper, |
39 | LanguageNameUtils $languageNameUtils, |
40 | ILoadBalancer $loadBalancer, |
41 | MessageGroupReviewStore $groupReviewStore |
42 | ) { |
43 | parent::__construct( $query, $moduleName, 'mc' ); |
44 | $this->configHelper = $configHelper; |
45 | $this->languageNameUtils = $languageNameUtils; |
46 | $this->loadBalancer = $loadBalancer; |
47 | $this->groupReviewStore = $groupReviewStore; |
48 | } |
49 | |
50 | public function execute(): void { |
51 | $this->run(); |
52 | } |
53 | |
54 | /** @inheritDoc */ |
55 | public function getCacheMode( $params ): string { |
56 | return 'public'; |
57 | } |
58 | |
59 | /** @inheritDoc */ |
60 | public function executeGenerator( $resultPageSet ): void { |
61 | $this->run( $resultPageSet ); |
62 | } |
63 | |
64 | private function validateLanguageCode( string $code ): void { |
65 | if ( !Utilities::isSupportedLanguageCode( $code ) ) { |
66 | $this->dieWithError( [ 'apierror-translate-invalidlanguage', $code ] ); |
67 | } |
68 | } |
69 | |
70 | private function run( ApiPageSet $resultPageSet = null ): void { |
71 | $params = $this->extractRequestParams(); |
72 | |
73 | $group = MessageGroups::getGroup( $params['group'] ); |
74 | if ( !$group ) { |
75 | $this->dieWithError( [ 'apierror-badparameter', 'mcgroup' ] ); |
76 | } |
77 | |
78 | $languageCode = $params[ 'language' ]; |
79 | $this->validateLanguageCode( $languageCode ); |
80 | $sourceLanguageCode = $group->getSourceLanguage(); |
81 | |
82 | // Even though translation to source language maybe disabled, we still want to |
83 | // fetch the message collections for the source language. |
84 | if ( $sourceLanguageCode === $languageCode ) { |
85 | $name = $this->getLanguageName( $languageCode ); |
86 | $this->addWarning( [ 'apiwarn-translate-language-disabled-source', wfEscapeWikiText( $name ) ] ); |
87 | } else { |
88 | $languages = $group->getTranslatableLanguages(); |
89 | if ( $languages === null ) { |
90 | $checks = [ |
91 | $group->getId(), |
92 | strtok( $group->getId(), '-' ), |
93 | '*' |
94 | ]; |
95 | |
96 | $disabledLanguages = $this->configHelper->getDisabledTargetLanguages(); |
97 | foreach ( $checks as $check ) { |
98 | if ( isset( $disabledLanguages[ $check ][ $languageCode ] ) ) { |
99 | $name = $this->getLanguageName( $languageCode ); |
100 | $reason = $disabledLanguages[ $check ][ $languageCode ]; |
101 | $this->dieWithError( [ 'apierror-translate-language-disabled-reason', $name, $reason ] ); |
102 | } |
103 | } |
104 | } elseif ( !isset( $languages[ $languageCode ] ) ) { |
105 | // Not a translatable language |
106 | $name = $this->getLanguageName( $languageCode ); |
107 | $this->dieWithError( [ 'apierror-translate-language-disabled', $name ] ); |
108 | } |
109 | |
110 | // A check for cases where the source language of group messages |
111 | // is a variant of the target language being translated into. |
112 | if ( strtok( $sourceLanguageCode, '-' ) === strtok( $languageCode, '-' ) ) { |
113 | $sourceLanguageName = $this->getLanguageName( $sourceLanguageCode ); |
114 | $targetLanguageName = $this->getLanguageName( $languageCode ); |
115 | $this->addWarning( [ |
116 | 'apiwarn-translate-language-targetlang-variant-of-source', |
117 | wfEscapeWikiText( $targetLanguageName ), |
118 | wfEscapeWikiText( $sourceLanguageName ) ] |
119 | ); |
120 | } |
121 | } |
122 | |
123 | if ( MessageGroups::isDynamic( $group ) ) { |
124 | /** @var RecentMessageGroup $group */ |
125 | // @phan-suppress-next-line PhanUndeclaredMethod |
126 | $group->setLanguage( $params['language'] ); |
127 | } |
128 | |
129 | $messages = $group->initCollection( $params['language'] ); |
130 | |
131 | foreach ( $params['filter'] as $filter ) { |
132 | if ( $filter === '' || $filter === null ) { |
133 | continue; |
134 | } |
135 | |
136 | $value = null; |
137 | if ( str_contains( $filter, ':' ) ) { |
138 | [ $filter, $value ] = explode( ':', $filter, 2 ); |
139 | } |
140 | /* The filtering params here are swapped wrt MessageCollection. |
141 | * There (fuzzy) means do not show fuzzy, which is the same as !fuzzy |
142 | * here and fuzzy here means (fuzzy, false) there. */ |
143 | try { |
144 | $value = $value === null ? $value : (int)$value; |
145 | if ( str_starts_with( $filter, '!' ) ) { |
146 | $messages->filter( substr( $filter, 1 ), true, $value ); |
147 | } else { |
148 | $messages->filter( $filter, false, $value ); |
149 | } |
150 | } catch ( InvalidFilterException $e ) { |
151 | $this->dieWithError( |
152 | [ 'apierror-translate-invalidfilter', wfEscapeWikiText( $e->getMessage() ) ], |
153 | 'invalidfilter' |
154 | ); |
155 | } |
156 | } |
157 | |
158 | $resultSize = count( $messages ); |
159 | $offsets = $messages->slice( $params['offset'], $params['limit'] ); |
160 | $batchSize = count( $messages ); |
161 | [ /*$backwardsOffset*/, $forwardsOffset, $startOffset ] = $offsets; |
162 | |
163 | $result = $this->getResult(); |
164 | $result->addValue( |
165 | [ 'query', 'metadata' ], |
166 | 'state', |
167 | $this->groupReviewStore->getWorkflowState( $group->getId(), $params['language'] ) |
168 | ); |
169 | |
170 | $result->addValue( [ 'query', 'metadata' ], 'resultsize', $resultSize ); |
171 | $result->addValue( |
172 | [ 'query', 'metadata' ], |
173 | 'remaining', |
174 | $resultSize - $startOffset - $batchSize |
175 | ); |
176 | |
177 | $messages->loadTranslations(); |
178 | |
179 | $pages = []; |
180 | |
181 | if ( $forwardsOffset !== false ) { |
182 | $this->setContinueEnumParameter( 'offset', $forwardsOffset ); |
183 | } |
184 | |
185 | $props = array_flip( $params['prop'] ); |
186 | |
187 | /** @var Title $title */ |
188 | foreach ( $messages->keys() as $mkey => $titleValue ) { |
189 | $title = Title::newFromLinkTarget( $titleValue ); |
190 | |
191 | if ( $resultPageSet === null ) { |
192 | $data = $this->extractMessageData( $result, $props, $messages[$mkey] ); |
193 | $data['title'] = $title->getPrefixedText(); |
194 | $data['targetLanguage'] = $messages->getLanguage(); |
195 | |
196 | $handle = new MessageHandle( $title ); |
197 | |
198 | if ( $handle->isValid() ) { |
199 | $data['primaryGroup'] = $handle->getGroup()->getId(); |
200 | } |
201 | |
202 | $result->addValue( [ 'query', $this->getModuleName() ], null, $data ); |
203 | } else { |
204 | $pages[] = $title; |
205 | } |
206 | } |
207 | |
208 | if ( $resultPageSet === null ) { |
209 | $result->addIndexedTagName( |
210 | [ 'query', $this->getModuleName() ], |
211 | 'message' |
212 | ); |
213 | } else { |
214 | $resultPageSet->populateFromTitles( $pages ); |
215 | } |
216 | } |
217 | |
218 | private function getLanguageName( string $languageCode ): string { |
219 | return $this |
220 | ->languageNameUtils |
221 | ->getLanguageName( $languageCode, $this->getLanguage()->getCode() ); |
222 | } |
223 | |
224 | private function extractMessageData( |
225 | ApiResult $result, |
226 | array $props, |
227 | Message $message |
228 | ): array { |
229 | $data = [ 'key' => $message->key() ]; |
230 | |
231 | if ( isset( $props['definition'] ) ) { |
232 | $data['definition'] = $message->definition(); |
233 | } |
234 | if ( isset( $props['translation'] ) ) { |
235 | // Remove !!FUZZY!! from translation if present. |
236 | $translation = $message->translation(); |
237 | if ( $translation !== null ) { |
238 | $translation = str_replace( TRANSLATE_FUZZY, '', $translation ); |
239 | } |
240 | $data['translation'] = $translation; |
241 | } |
242 | if ( isset( $props['tags'] ) ) { |
243 | $data['tags'] = $message->getTags(); |
244 | $result->setIndexedTagName( $data['tags'], 'tag' ); |
245 | } |
246 | // BC |
247 | if ( isset( $props['revision'] ) ) { |
248 | $data['revision'] = $message->getProperty( 'revision' ); |
249 | } |
250 | if ( isset( $props['properties'] ) ) { |
251 | foreach ( $message->getPropertyNames() as $prop ) { |
252 | $data['properties'][$prop] = $message->getProperty( $prop ); |
253 | ApiResult::setIndexedTagNameRecursive( $data['properties'], 'val' ); |
254 | } |
255 | } |
256 | |
257 | return $data; |
258 | } |
259 | |
260 | /** @inheritDoc */ |
261 | protected function getAllowedParams(): array { |
262 | return [ |
263 | 'group' => [ |
264 | ParamValidator::PARAM_TYPE => 'string', |
265 | ParamValidator::PARAM_REQUIRED => true, |
266 | ], |
267 | 'language' => [ |
268 | ParamValidator::PARAM_TYPE => 'string', |
269 | ParamValidator::PARAM_DEFAULT => 'en', |
270 | ], |
271 | 'limit' => [ |
272 | ParamValidator::PARAM_DEFAULT => 500, |
273 | ParamValidator::PARAM_TYPE => 'limit', |
274 | IntegerDef::PARAM_MIN => 1, |
275 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG2, |
276 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2, |
277 | ], |
278 | 'offset' => [ |
279 | ParamValidator::PARAM_DEFAULT => '', |
280 | ParamValidator::PARAM_TYPE => 'string', |
281 | ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', |
282 | ], |
283 | 'filter' => [ |
284 | ParamValidator::PARAM_TYPE => 'string', |
285 | ParamValidator::PARAM_DEFAULT => '!optional|!ignored', |
286 | ParamValidator::PARAM_ISMULTI => true, |
287 | ], |
288 | 'prop' => [ |
289 | ParamValidator::PARAM_TYPE => [ |
290 | 'definition', |
291 | 'translation', |
292 | 'tags', |
293 | 'properties', |
294 | 'revision', |
295 | ], |
296 | ParamValidator::PARAM_DEFAULT => 'definition|translation', |
297 | ParamValidator::PARAM_ISMULTI => true, |
298 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [ |
299 | 'translation' => [ 'apihelp-query+messagecollection-paramvalue-prop-translation', TRANSLATE_FUZZY ], |
300 | ], |
301 | EnumDef::PARAM_DEPRECATED_VALUES => [ |
302 | 'revision' => true, |
303 | ], |
304 | ], |
305 | ]; |
306 | } |
307 | |
308 | /** @inheritDoc */ |
309 | protected function getExamplesMessages(): array { |
310 | return [ |
311 | 'action=query&meta=siteinfo&siprop=languages' |
312 | => 'apihelp-query+messagecollection-example-1', |
313 | 'action=query&list=messagecollection&mcgroup=page-Example' |
314 | => 'apihelp-query+messagecollection-example-2', |
315 | 'action=query&list=messagecollection&mcgroup=page-Example&mclanguage=fi&' . |
316 | 'mcprop=definition|translation|tags&mcfilter=optional' |
317 | => 'apihelp-query+messagecollection-example-3', |
318 | 'action=query&generator=messagecollection&gmcgroup=page-Example&gmclanguage=nl&prop=revisions' |
319 | => 'apihelp-query+messagecollection-example-4', |
320 | ]; |
321 | } |
322 | } |