Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
LanguageBabelBox
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 8
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 getText
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 addCategories
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 addCategory
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 getCategoryName
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 getCategoryLink
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Contains code for language boxes.
4 *
5 * @file
6 * @author Robert Leverington
7 * @author Robin Pepermans
8 * @author Niklas Laxström
9 * @author Brian Wolff
10 * @author Purodha Blissenbach
11 * @author Sam Reed
12 * @author Siebrand Mazeland
13 * @license GPL-2.0-or-later
14 */
15
16declare( strict_types = 1 );
17
18namespace MediaWiki\Babel\BabelBox;
19
20use MediaWiki\Babel\BabelAutoCreate;
21use MediaWiki\Babel\BabelLanguageCodes;
22use MediaWiki\Babel\BabelServices;
23use MediaWiki\Config\Config;
24use MediaWiki\Language\Language;
25use MediaWiki\Language\LanguageCode;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\Page\PageReference;
28use MediaWiki\Parser\ParserOutput;
29use MediaWiki\Title\Title;
30
31/**
32 * Class for babel language boxes.
33 */
34class LanguageBabelBox implements BabelBox {
35
36    private Config $config;
37    private PageReference $page;
38    private Language $targetLanguage;
39    private string $code;
40    private string $level;
41
42    /**
43     * Construct a babel box for the given language and level.
44     *
45     * @param Config $config
46     * @param PageReference $page
47     * @param Language $targetLanguage Target language of the parse.
48     * @param string $code Language code to use.
49     *   This is a mediawiki-internal code (not necessarily a valid BCP-47 code)
50     * @param string $level Level of ability to use.
51     */
52    public function __construct(
53        Config $config,
54        PageReference $page,
55        Language $targetLanguage,
56        string $code,
57        string $level
58    ) {
59        $this->config = $config;
60        $this->page = $page;
61        $this->targetLanguage = $targetLanguage;
62        $this->code = BabelLanguageCodes::getCode( $code ) ?? $code;
63        $this->level = $level;
64    }
65
66    /**
67     * Get a Config instance to use
68     *
69     * @todo Use proper Dependency Injection.
70     * @return Config
71     */
72    private static function getConfig(): Config {
73        return BabelServices::wrap( MediaWikiServices::getInstance() )->getConfig();
74    }
75
76    /**
77     * Return the babel box code.
78     *
79     * @return string A babel box for the given language and level.
80     */
81    public function render(): string {
82        $code = $this->code;
83        $catCode = BabelLanguageCodes::getCategoryCode( $code );
84        $bcp47 = LanguageCode::bcp47( $code );
85
86        $portal = wfMessage( 'babel-portal', $catCode )->inContentLanguage()->text();
87        if ( $portal !== '' ) {
88            $portal = "[[$portal|$catCode]]";
89        } else {
90            $portal = $catCode;
91        }
92        $header = "$portal<span class=\"mw-babel-box-level-{$this->level}\">-{$this->level}</span>";
93
94        $name = BabelLanguageCodes::getName( $code );
95        $text = self::getText( $this->page, $name, $code, $this->level );
96
97        $dir_current = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $code )->getDir();
98
99        $dir_head = $this->targetLanguage->getDir();
100
101        return <<<EOT
102<div class="mw-babel-box mw-babel-box-{$this->level} mw-babel-box-{$catCode} notheme" dir="$dir_head">
103{|
104! dir="$dir_head" | $header
105| dir="$dir_current" lang="$bcp47" | $text
106|}
107</div>
108EOT;
109    }
110
111    /**
112     * Get the text to display in the language box for specific language and
113     * level. If MediaWiki:Babel-<level>-n (the message that includes the
114     * language autonym) is translated into the given language, use that
115     * otherwise use MediaWiki:Babel-<level> (the message that takes the
116     * language name as a parameter)
117     *
118     * @param PageReference $page
119     * @param string $name
120     * @param string $code Mediawiki-internal language code of language to use.
121     * @param string $level Level to use.
122     * @return string Text for display, in wikitext format.
123     */
124    private static function getText(
125        PageReference $page,
126        string $name,
127        string $code,
128        string $level
129    ): string {
130        $categoryLevel = self::getCategoryLink( $page, $level, $code );
131        $categoryMain = self::getCategoryLink( $page, null, $code );
132
133        // Give grep a chance to find the usages:
134        // babel-0-n, babel-1-n, babel-2-n, babel-3-n, babel-4-n, babel-5-n, babel-N-n
135        $text = wfMessage( "babel-$level-n",
136            $categoryLevel, $categoryMain, '', $page->getDBkey()
137        )->inLanguage( $code )->text();
138
139        $fallbackLanguage = MediaWikiServices::getInstance()->getLanguageFallback()->getFirst( $code );
140        // Because of T75473, the above wfMessage call will ignore any
141        // MediaWiki namespace overrides for fallback languages. Hence, we
142        // must explicitly ignore them here, or else the comparison will fail,
143        // resulting in a message claiming that the user knows the fallback
144        // language (probably English), rather than the language
145        // they actually specified.
146        $fallback = wfMessage( "babel-$level-n",
147            $categoryLevel, $categoryMain, '', $page->getDBkey()
148        )->useDatabase( false )->inLanguage( $fallbackLanguage ?? $code )->text();
149
150        // Give grep a chance to find the usages:
151        // babel-0, babel-1, babel-2, babel-3, babel-4, babel-5, babel-N
152        if ( $text == $fallback ) {
153            $text = wfMessage( "babel-$level",
154                $categoryLevel, $categoryMain, $name, $page->getDBkey()
155            )->inLanguage( $code )->text();
156        }
157
158        return $text;
159    }
160
161    /**
162     * Add appropriate categories for the language box to the given parser output
163     *
164     * @param ParserOutput $parserOutput Output to add categories to
165     */
166    public function addCategories( ParserOutput $parserOutput ): void {
167        $namespaces = $this->config->get( 'BabelCategorizeNamespaces' );
168        if (
169            $namespaces !== null &&
170            !Title::newFromPageReference( $this->page )->inNamespaces( $namespaces )
171        ) {
172            return;
173        }
174
175        $footerPage = Title::newFromText( wfMessage( 'babel-footer-url' )->inContentLanguage()->text() );
176        if ( $footerPage != null && $footerPage->inNamespace( NS_CATEGORY ) ) {
177            $footerCategory = $footerPage->getDBkey();
178        } else {
179            $footerCategory = null;
180        }
181        # Add main category
182        if ( $this->level !== '0' ) {
183            $mainCategory = $this->addCategory( $parserOutput, $this->code, null, $this->level, $footerCategory );
184        } else {
185            $mainCategory = null;
186        }
187
188        # Add level category
189        $this->addCategory( $parserOutput, $this->code, $this->level, false, $mainCategory );
190    }
191
192    /**
193     * Adds one category to the given ParserOutput, and arranges for its creation if it doesn't exist.
194     *
195     * @param ParserOutput $parserOutput Parser output to use
196     * @param string $code Code of language that the category is for.
197     * @param string|null $level Level that the category is for.
198     * @param string|bool $sortkey The sortkey to use for the category, or false to use the default sort
199     * @param string|null $parent An eventual parent category to add to the newly-created category if one is created.
200     * @return string|null The name of the category that was eventually added
201     */
202    private function addCategory( ParserOutput $parserOutput,
203        string $code, ?string $level, $sortkey, ?string $parent
204    ): ?string {
205        $isOverridden = false;
206        $category = self::getCategoryName( $level, $code, $isOverridden );
207        if ( $category === null ) {
208            return null;
209        }
210        if ( $sortkey === false ) {
211            $sortkey = $parserOutput->getPageProperty( 'defaultsort' );
212        }
213        $parserOutput->addCategory( $category, $sortkey ?? '' );
214
215        if ( $this->config->get( 'BabelAutoCreate' ) ) {
216            // Now arrange for autocreation (in LinksUpdate hook) unless the category was overridden locally
217            // (to reduce the risk if a compromised admin edits MediaWiki:Babel-category-override)
218            $title = Title::makeTitleSafe( NS_CATEGORY, $category );
219            $text = BabelAutoCreate::getCategoryText( $code, $level, $parent );
220            if ( !$isOverridden && !$title->exists() ) {
221                $parserOutput->appendExtensionData( "babel-tocreate", $category );
222                $parserOutput->setExtensionData( "babel-category-text-$category", $text );
223            }
224        }
225        return $category;
226    }
227
228    /**
229     * Replace the placeholder variables from the category names configuration
230     * array with actual values.
231     *
232     * @param ?string $level Level of babel category in question, or null for the main category
233     * @param string $code Mediawiki-internal language code of category.
234     * @param bool &$isOverridden Output parameter. Set to true if the category is overridden on-wiki
235     * so that the caller knows not to create categories.
236     * @return string|null Category name with variables replaced and possibly
237     * overridden by the wiki, or null if no category is desired.
238     */
239    private static function getCategoryName( ?string $level, string $code, bool &$isOverridden ): ?string {
240        global $wgLanguageCode;
241
242        $categoryDef = $level !== null ? self::getConfig()->get( 'BabelCategoryNames' )[$level] :
243            self::getConfig()->get( 'BabelMainCategory' );
244        if ( $categoryDef === false || $categoryDef === '' ) {
245            return null;
246        }
247
248        $category = strtr( $categoryDef, [
249            '%code%' => BabelLanguageCodes::getCategoryCode( $code ),
250            '%wikiname%' => BabelLanguageCodes::getName( $code, $wgLanguageCode ),
251            '%nativename%' => BabelLanguageCodes::getName( $code )
252        ] );
253
254        $oldCategory = $category;
255
256        // Chance to locally override categorization
257        if ( self::getConfig()->get( 'BabelAllowOverride' ) ) {
258            $category = wfMessage( "babel-category-override",
259                $category, $code, $level ?? ''
260            )->inContentLanguage()->text();
261            if ( $category !== $oldCategory ) {
262                $isOverridden = true;
263            }
264        }
265
266        // Normalize using Title
267        $title = Title::makeTitleSafe( NS_CATEGORY, $category );
268        if ( !$title ) {
269            // babel-category-override returned an invalid page name
270            return null;
271        }
272
273        return $title->getDBkey();
274    }
275
276    /**
277     * Returns the right link target for a category (either the category itself or the
278     * title given to get a self-link)
279     * @param PageReference $page The page to point the self-link to
280     * @param ?string $level Level of babel category in question, or null for the main category
281     * @param string $code Mediawiki-internal language code of category.
282     * @return string Link target to use for the given category
283     */
284    private static function getCategoryLink( PageReference $page, ?string $level, string $code ): string {
285        $isOverridden = false;
286        $category = self::getCategoryName( $level, $code, $isOverridden );
287        if ( $category !== null ) {
288            return ":Category:" . $category;
289        }
290        return ":" . MediaWikiServices::getInstance()->getTitleFormatter()->getPrefixedText( $page );
291    }
292}