Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.82% covered (warning)
56.82%
100 / 176
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryMessageCollectionActionApi
56.82% covered (warning)
56.82%
100 / 176
50.00% covered (danger)
50.00%
5 / 10
154.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateLanguageCode
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 run
50.00% covered (danger)
50.00%
45 / 90
0.00% covered (danger)
0.00%
0 / 1
76.12
 getLanguageName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extractMessageData
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 getAllowedParams
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageLoading;
5
6use ApiBase;
7use ApiPageSet;
8use ApiQuery;
9use ApiQueryGeneratorBase;
10use ApiResult;
11use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupReviewStore;
12use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
13use MediaWiki\Extension\Translate\Utilities\ConfigHelper;
14use MediaWiki\Extension\Translate\Utilities\Utilities;
15use MediaWiki\Languages\LanguageNameUtils;
16use MediaWiki\Title\Title;
17use RecentMessageGroup;
18use Wikimedia\ParamValidator\ParamValidator;
19use Wikimedia\ParamValidator\TypeDef\EnumDef;
20use Wikimedia\ParamValidator\TypeDef\IntegerDef;
21use 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 */
29class 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}