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