Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageHandle
0.00% covered (danger)
0.00%
0 / 127
0.00% covered (danger)
0.00%
0 / 19
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isMessageNamespace
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 figureMessage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getKey
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCode
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getEffectiveLanguage
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 isDoc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isPageTranslation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupIds
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getGroup
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isValid
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleForLanguage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleForBase
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 hasFuzzyString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeFuzzyString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isFuzzy
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 getInternalKey
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 needsFuzzy
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageLoading;
5
6use BadMethodCallException;
7use MediaWiki\Extension\Translate\LogNames;
8use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
9use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
10use MediaWiki\Extension\Translate\Services;
11use MediaWiki\Language\Language;
12use MediaWiki\Linker\LinkTarget;
13use MediaWiki\Logger\LoggerFactory;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Title\Title;
16use MessageGroup;
17use RuntimeException;
18
19/**
20 * Class for pointing to messages, like Title class is for titles.
21 * Also enhances Title with stuff related to message groups
22 * @author Niklas Laxström
23 * @copyright Copyright © 2011-2013 Niklas Laxström
24 * @license GPL-2.0-or-later
25 */
26class MessageHandle {
27    private LinkTarget $title;
28    private ?string $key = null;
29    private ?string $languageCode = null;
30    /** @var string[]|null */
31    private ?array $groupIds = null;
32    private MessageIndex $messageIndex;
33
34    public function __construct( LinkTarget $title ) {
35        $this->title = $title;
36        $this->messageIndex = Services::getInstance()->getMessageIndex();
37    }
38
39    /** Check if this handle is in a message namespace. */
40    public function isMessageNamespace(): bool {
41        global $wgTranslateMessageNamespaces;
42        $namespace = $this->title->getNamespace();
43
44        return in_array( $namespace, $wgTranslateMessageNamespaces );
45    }
46
47    /**
48     * Recommended to use getCode and getKey instead.
49     * @return string[] Array of the message key and the language code
50     */
51    public function figureMessage(): array {
52        if ( $this->key === null ) {
53            // Check if this is a valid message first
54            $this->key = $this->title->getDBkey();
55            $known = $this->messageIndex->getGroupIds( $this ) !== [];
56
57            $pos = strrpos( $this->key, '/' );
58            if ( $known || $pos === false ) {
59                $this->languageCode = '';
60            } else {
61                // For keys like Foo/, substr returns false instead of ''
62                $this->languageCode = (string)( substr( $this->key, $pos + 1 ) );
63                $this->key = substr( $this->key, 0, $pos );
64            }
65        }
66
67        return [ $this->key, $this->languageCode ];
68    }
69
70    /** Returns the identified or guessed message key. */
71    public function getKey(): string {
72        $this->figureMessage();
73
74        return $this->key;
75    }
76
77    /**
78     * Returns the language code.
79     * For language codeless source messages will return empty string.
80     */
81    public function getCode(): string {
82        $this->figureMessage();
83
84        return $this->languageCode;
85    }
86
87    /**
88     * Return the Language object for the assumed language of the content, which might
89     * be different from the subpage code (qqq, no subpage).
90     */
91    public function getEffectiveLanguage(): Language {
92        $code = $this->getCode();
93        $mwServices = MediaWikiServices::getInstance();
94        if ( !$mwServices->getLanguageNameUtils()->isKnownLanguageTag( $code ) ||
95            $this->isDoc()
96        ) {
97            return $mwServices->getContentLanguage();
98        }
99
100        return $mwServices->getLanguageFactory()->getLanguage( $code );
101    }
102
103    /** Determine whether the current handle is for message documentation. */
104    public function isDoc(): bool {
105        global $wgTranslateDocumentationLanguageCode;
106
107        return $this->getCode() === $wgTranslateDocumentationLanguageCode;
108    }
109
110    /**
111     * Determine whether the current handle is for page translation feature.
112     * This does not consider whether the handle corresponds to any message.
113     */
114    public function isPageTranslation(): bool {
115        return $this->title->inNamespace( NS_TRANSLATIONS );
116    }
117
118    /**
119     * Returns all message group ids this message belongs to.
120     * The primary message group id is always the first one.
121     * If the handle does not correspond to any message, the returned array
122     * is empty.
123     * @return string[]
124     */
125    public function getGroupIds() {
126        if ( $this->groupIds === null ) {
127            $this->groupIds = $this->messageIndex->getGroupIds( $this );
128        }
129
130        return $this->groupIds;
131    }
132
133    /**
134     * Get the primary MessageGroup this message belongs to.
135     * You should check first that the handle is valid.
136     */
137    public function getGroup(): ?MessageGroup {
138        $ids = $this->getGroupIds();
139        if ( !isset( $ids[0] ) ) {
140            throw new BadMethodCallException( 'called before isValid' );
141        }
142        return MessageGroups::getGroup( $ids[0] );
143    }
144
145    /** Checks if the handle corresponds to a known message. */
146    public function isValid(): bool {
147        static $jobHasBeenScheduled = false;
148
149        if ( !$this->isMessageNamespace() ) {
150            return false;
151        }
152
153        $groups = $this->getGroupIds();
154        if ( !$groups ) {
155            return false;
156        }
157
158        // Do another check that the group actually exists
159        $group = $this->getGroup();
160        if ( !$group ) {
161            $logger = LoggerFactory::getInstance( LogNames::MAIN );
162            $logger->warning(
163                '[MessageHandle] MessageIndex is out of date. Page {pagename} refers to ' .
164                'unknown group {messagegroup}',
165                [
166                    'pagename' => $this->getTitle()->getPrefixedText(),
167                    'messagegroup' => $groups[0],
168                    'exception' => new RuntimeException(),
169                    'hasJobBeenScheduled' => $jobHasBeenScheduled
170                ]
171            );
172
173            if ( !$jobHasBeenScheduled ) {
174                // Schedule a job in the job queue (with deduplication)
175                $job = RebuildMessageIndexJob::newJob( __METHOD__ );
176                MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $job );
177                $jobHasBeenScheduled = true;
178            }
179
180            return false;
181        }
182
183        return true;
184    }
185
186    /** Get the original title. */
187    public function getTitle(): Title {
188        return Title::newFromLinkTarget( $this->title );
189    }
190
191    /** Get the original title with the passed language code. */
192    public function getTitleForLanguage( string $languageCode ): Title {
193        return Title::makeTitle(
194            $this->title->getNamespace(),
195            $this->getKey() . "/$languageCode"
196        );
197    }
198
199    /** Get the title for the page base. */
200    public function getTitleForBase(): Title {
201        return Title::makeTitle(
202            $this->title->getNamespace(),
203            $this->getKey()
204        );
205    }
206
207    /**
208     * Check if a string contains the fuzzy string.
209     * @param string $text Arbitrary text
210     * @return bool If string contains fuzzy string.
211     */
212    public static function hasFuzzyString( string $text ): bool {
213        return str_contains( $text, TRANSLATE_FUZZY );
214    }
215
216    /** Check if a string has fuzzy string and if not, add it */
217    public static function makeFuzzyString( string $text ): string {
218        return self::hasFuzzyString( $text ) ? $text : TRANSLATE_FUZZY . $text;
219    }
220
221    /** Check if a title is marked as fuzzy. */
222    public function isFuzzy(): bool {
223        $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
224
225        $res = $dbr->newSelectQueryBuilder()
226            ->select( 'rt_type' )
227            ->from( 'page' )
228            ->join( 'revtag', null, [
229                'page_id=rt_page',
230                'page_latest=rt_revision',
231                'rt_type' => RevTagStore::FUZZY_TAG,
232            ] )
233            ->where( [
234                'page_namespace' => $this->title->getNamespace(),
235                'page_title' => $this->title->getDBkey(),
236            ] )
237            ->caller( __METHOD__ )
238            ->fetchField();
239
240        return $res !== false;
241    }
242
243    /**
244     * This returns the key that can be used for showMessage parameter for Special:Translate
245     * for regular message groups. It is not possible to automatically determine this key
246     * from the title alone.
247     */
248    public function getInternalKey(): string {
249        $mwServices = MediaWikiServices::getInstance();
250        $nsInfo = $mwServices->getNamespaceInfo();
251        $contentLanguage = $mwServices->getContentLanguage();
252
253        $key = $this->getKey();
254        $group = $this->getGroup();
255        $groupKeys = $group->getKeys();
256
257        if ( in_array( $key, $groupKeys, true ) ) {
258            return $key;
259        }
260
261        $namespace = $this->title->getNamespace();
262        if ( $nsInfo->isCapitalized( $namespace ) ) {
263            $lowercaseKey = $contentLanguage->lcfirst( $key );
264            if ( in_array( $lowercaseKey, $groupKeys, true ) ) {
265                return $lowercaseKey;
266            }
267        }
268
269        // Brute force all the keys to find the one. This one should always find a match
270        // if there is one.
271        foreach ( $groupKeys as $haystackKey ) {
272            $normalizedHaystackKey = Title::makeTitleSafe( $namespace, $haystackKey )->getDBkey();
273            if ( $normalizedHaystackKey === $key ) {
274                return $haystackKey;
275            }
276        }
277
278        return "BUG:$key";
279    }
280
281    /** Returns true if message is fuzzy, OR fails checks OR fails validations (error OR warning). */
282    public function needsFuzzy( string $text ): bool {
283        // Docs are exempt for checks
284        if ( $this->isDoc() ) {
285            return false;
286        }
287
288        // Check for explicit tag.
289        if ( self::hasFuzzyString( $text ) ) {
290            return true;
291        }
292
293        // Not all groups have validators
294        $group = $this->getGroup();
295        $validator = $group->getValidator();
296
297        // no validator set
298        if ( !$validator ) {
299            return false;
300        }
301
302        $code = $this->getCode();
303        $key = $this->getKey();
304        $en = $group->getMessage( $key, $group->getSourceLanguage() );
305        $message = new FatMessage( $key, $en );
306        // Take the contents from edit field as a translation.
307        $message->setTranslation( $text );
308        if ( $message->definition() === null ) {
309            // This should NOT happen, but add a check since it seems to be happening
310            // See: https://phabricator.wikimedia.org/T255669
311            LoggerFactory::getInstance( LogNames::MAIN )->warning(
312                'Message definition is empty! Title: {title}, group: {group}, key: {key}',
313                [
314                    'title' => $this->getTitle()->getPrefixedText(),
315                    'group' => $group->getId(),
316                    'key' => $key
317                ]
318            );
319            return false;
320        }
321
322        $validationResult = $validator->quickValidate( $message, $code );
323        return $validationResult->hasIssues();
324    }
325}