Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.06% covered (warning)
84.06%
116 / 138
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryLanguageinfo
84.06% covered (warning)
84.06%
116 / 138
60.00% covered (warning)
60.00%
3 / 5
23.96
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
91.49% covered (success)
91.49%
86 / 94
0.00% covered (danger)
0.00%
0 / 1
18.20
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\Languages\LanguageConverterFactory;
22use MediaWiki\Languages\LanguageFactory;
23use MediaWiki\Languages\LanguageFallback;
24use MediaWiki\Languages\LanguageNameUtils;
25use Wikimedia\ParamValidator\ParamValidator;
26use Wikimedia\Timestamp\ConvertibleTimestamp;
27
28/**
29 * API module to enumerate language information.
30 *
31 * @ingroup API
32 */
33class ApiQueryLanguageinfo extends ApiQueryBase {
34
35    /**
36     * The maximum time for {@link execute()};
37     * if execution takes longer than this, apply continuation.
38     *
39     * If the localization cache is used, this time is not expected to ever be
40     * exceeded; on the other hand, if it is not used, a typical request will
41     * not yield more than a handful of languages before the time is exceeded
42     * and continuation is applied, if one of the expensive props is requested.
43     */
44    private const MAX_EXECUTE_SECONDS = 3;
45
46    private LanguageFactory $languageFactory;
47    private LanguageNameUtils $languageNameUtils;
48    private LanguageFallback $languageFallback;
49    private LanguageConverterFactory $languageConverterFactory;
50
51    /**
52     * @param ApiQuery $queryModule
53     * @param string $moduleName
54     * @param LanguageFactory $languageFactory
55     * @param LanguageNameUtils $languageNameUtils
56     * @param LanguageFallback $languageFallback
57     * @param LanguageConverterFactory $languageConverterFactory
58     */
59    public function __construct(
60        ApiQuery $queryModule,
61        $moduleName,
62        LanguageFactory $languageFactory,
63        LanguageNameUtils $languageNameUtils,
64        LanguageFallback $languageFallback,
65        LanguageConverterFactory $languageConverterFactory
66    ) {
67        parent::__construct( $queryModule, $moduleName, 'li' );
68        $this->languageFactory = $languageFactory;
69        $this->languageNameUtils = $languageNameUtils;
70        $this->languageFallback = $languageFallback;
71        $this->languageConverterFactory = $languageConverterFactory;
72    }
73
74    public function execute() {
75        // ConvertibleTimestamp::time() used so we can fake the current time in tests
76        $endTime = ConvertibleTimestamp::time() + self::MAX_EXECUTE_SECONDS;
77
78        $props = array_fill_keys( $this->getParameter( 'prop' ), true );
79        $includeCode = isset( $props['code'] );
80        $includeBcp47 = isset( $props['bcp47'] );
81        $includeDir = isset( $props['dir'] );
82        $includeAutonym = isset( $props['autonym'] );
83        $includeName = isset( $props['name'] );
84        $includeVariantnames = isset( $props['variantnames'] );
85        $includeFallbacks = isset( $props['fallbacks'] );
86        $includeVariants = isset( $props['variants'] );
87
88        $targetLanguageCode = $this->getLanguage()->getCode();
89        $include = LanguageNameUtils::ALL;
90
91        $availableLanguageCodes = array_keys( $this->languageNameUtils->getLanguageNames(
92            // MediaWiki and extensions may return different sets of language codes
93            // when asked for language names in different languages;
94            // asking for English language names is most likely to give us the full set,
95            // even though we may not need those at all
96            'en',
97            $include
98        ) );
99        $selectedLanguageCodes = $this->getParameter( 'code' );
100        if ( $selectedLanguageCodes === [ '*' ] ) {
101            $languageCodes = $availableLanguageCodes;
102        } else {
103            $languageCodes = array_values( array_intersect(
104                $availableLanguageCodes,
105                $selectedLanguageCodes
106            ) );
107            $unrecognizedCodes = array_values( array_diff(
108                $selectedLanguageCodes,
109                $availableLanguageCodes
110            ) );
111            if ( $unrecognizedCodes !== [] ) {
112                $this->addWarning( [
113                    'apiwarn-unrecognizedvalues',
114                    $this->encodeParamName( 'code' ),
115                    Message::listParam( $unrecognizedCodes, 'comma' ),
116                    count( $unrecognizedCodes ),
117                ] );
118            }
119        }
120        // order of $languageCodes is guaranteed by LanguageNameUtils::getLanguageNames()
121        // and preserved by array_values() + array_intersect()
122
123        $continue = $this->getParameter( 'continue' ) ?? reset( $languageCodes );
124
125        $result = $this->getResult();
126        $rootPath = [
127            $this->getQuery()->getModuleName(),
128            $this->getModuleName(),
129        ];
130        $result->addArrayType( $rootPath, 'assoc' );
131
132        foreach ( $languageCodes as $languageCode ) {
133            if ( $languageCode < $continue ) {
134                continue;
135            }
136
137            $now = ConvertibleTimestamp::time();
138            if ( $now >= $endTime ) {
139                $this->setContinueEnumParameter( 'continue', $languageCode );
140                break;
141            }
142
143            $info = [];
144            ApiResult::setArrayType( $info, 'assoc' );
145
146            if ( $includeCode ) {
147                $info['code'] = $languageCode;
148            }
149
150            if ( $includeBcp47 ) {
151                $bcp47 = LanguageCode::bcp47( $languageCode );
152                $info['bcp47'] = $bcp47;
153            }
154
155            if ( $includeDir ) {
156                $dir = $this->languageFactory->getLanguage( $languageCode )->getDir();
157                $info['dir'] = $dir;
158            }
159
160            if ( $includeAutonym ) {
161                $autonym = $this->languageNameUtils->getLanguageName(
162                    $languageCode,
163                    LanguageNameUtils::AUTONYMS,
164                    $include
165                );
166                $info['autonym'] = $autonym;
167            }
168
169            if ( $includeName ) {
170                $name = $this->languageNameUtils->getLanguageName(
171                    $languageCode,
172                    $targetLanguageCode,
173                    $include
174                );
175                $info['name'] = $name;
176            }
177
178            if ( $includeFallbacks ) {
179                $fallbacks = $this->languageFallback->getAll(
180                    $languageCode,
181                    // allow users to distinguish between implicit and explicit 'en' fallbacks
182                    LanguageFallback::STRICT
183                );
184                ApiResult::setIndexedTagName( $fallbacks, 'fb' );
185                $info['fallbacks'] = $fallbacks;
186            }
187
188            if ( $includeVariants || $includeVariantnames ) {
189                $language = $this->languageFactory->getLanguage( $languageCode );
190                $converter = $this->languageConverterFactory->getLanguageConverter( $language );
191                $variants = $converter->getVariants();
192
193                if ( $includeVariants ) {
194                    $info['variants'] = $variants;
195                    ApiResult::setIndexedTagName( $info['variants'], 'var' );
196                }
197                if ( $includeVariantnames ) {
198                    $info['variantnames'] = [];
199                    foreach ( $variants as $variantCode ) {
200                        $info['variantnames'][$variantCode] = $language->getVariantname( $variantCode );
201                    }
202                }
203            }
204
205            $fit = $result->addValue( $rootPath, $languageCode, $info );
206            if ( !$fit ) {
207                $this->setContinueEnumParameter( 'continue', $languageCode );
208                break;
209            }
210        }
211    }
212
213    public function getCacheMode( $params ) {
214        return 'public';
215    }
216
217    public function getAllowedParams() {
218        return [
219            'prop' => [
220                ParamValidator::PARAM_DEFAULT => 'code',
221                ParamValidator::PARAM_ISMULTI => true,
222                ParamValidator::PARAM_TYPE => [
223                    'code',
224                    'bcp47',
225                    'dir',
226                    'autonym',
227                    'name',
228                    'variantnames',
229                    'fallbacks',
230                    'variants',
231                ],
232                self::PARAM_HELP_MSG_PER_VALUE => [],
233            ],
234            'code' => [
235                ParamValidator::PARAM_DEFAULT => '*',
236                ParamValidator::PARAM_ISMULTI => true,
237            ],
238            'continue' => [
239                self::PARAM_HELP_MSG => 'api-help-param-continue',
240            ],
241        ];
242    }
243
244    protected function getExamplesMessages() {
245        $pathUrl = 'action=' . $this->getQuery()->getModuleName() .
246            '&meta=' . $this->getModuleName();
247        $pathMsg = $this->getModulePath();
248        $prefix = $this->getModulePrefix();
249
250        return [
251            "$pathUrl"
252                => "apihelp-$pathMsg-example-simple",
253            "$pathUrl&{$prefix}prop=autonym|name&uselang=de"
254                => "apihelp-$pathMsg-example-autonym-name-de",
255            "$pathUrl&{$prefix}prop=fallbacks|variants&{$prefix}code=oc"
256                => "apihelp-$pathMsg-example-fallbacks-variants-oc",
257            "$pathUrl&{$prefix}prop=bcp47|dir"
258                => "apihelp-$pathMsg-example-bcp47-dir",
259        ];
260    }
261
262}