Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.59% covered (warning)
52.59%
61 / 116
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslateEditAddons
52.59% covered (warning)
52.59%
61 / 116
33.33% covered (danger)
33.33%
2 / 6
133.43
0.00% covered (danger)
0.00%
0 / 1
 disallowLangTranslations
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 onSaveComplete
83.33% covered (warning)
83.33%
25 / 30
0.00% covered (danger)
0.00%
0 / 1
7.23
 checkNeedsFuzzy
60.00% covered (warning)
60.00%
15 / 25
0.00% covered (danger)
0.00%
0 / 1
6.60
 updateFuzzyTag
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 updateTransverTag
33.33% covered (danger)
33.33%
7 / 21
0.00% covered (danger)
0.00%
0 / 1
8.74
 disablePreSaveTransform
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorInterface;
5
6use DeferredUpdates;
7use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
8use MediaWiki\Extension\Translate\MessageLoading\FatMessage;
9use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
10use MediaWiki\Extension\Translate\PageTranslation\Hooks as PageTranslationHooks;
11use MediaWiki\Extension\Translate\Services;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Revision\RevisionRecord;
15use MediaWiki\Title\Title;
16use MediaWiki\User\UserIdentity;
17use MessageGroupStatesUpdaterJob;
18use MessageGroupStats;
19use ParserOptions;
20use TextContent;
21use TTMServer;
22use User;
23use WikiPage;
24
25/**
26 * Various editing enhancements to the edit page interface.
27 * Partly succeeded by the new ajax-enhanced editor but kept for compatibility.
28 * Also has code that is still relevant, like the hooks on save.
29 *
30 * @author Niklas Laxström
31 * @author Siebrand Mazeland
32 * @license GPL-2.0-or-later
33 */
34class TranslateEditAddons {
35    /**
36     * Prevent translations to non-translatable languages for the group
37     * Hook: getUserPermissionsErrorsExpensive
38     *
39     * @param Title $title
40     * @param User $user
41     * @param string $action
42     * @param mixed &$result
43     */
44    public static function disallowLangTranslations(
45        Title $title,
46        User $user,
47        string $action,
48        &$result
49    ): bool {
50        if ( $action !== 'edit' ) {
51            return true;
52        }
53
54        $handle = new MessageHandle( $title );
55        if ( !$handle->isValid() ) {
56            return true;
57        }
58
59        if ( $user->isAllowed( 'translate-manage' ) ) {
60            return true;
61        }
62
63        $group = $handle->getGroup();
64        $languages = $group->getTranslatableLanguages();
65        $langCode = $handle->getCode();
66        if ( $languages !== null && $langCode && !isset( $languages[$langCode] ) ) {
67            $result = [ 'translate-language-disabled' ];
68            return false;
69        }
70
71        $groupId = $group->getId();
72        $checks = [
73            $groupId,
74            strtok( $groupId, '-' ),
75            '*'
76        ];
77
78        $disabledLanguages = Services::getInstance()->getConfigHelper()->getDisabledTargetLanguages();
79        foreach ( $checks as $check ) {
80            if ( isset( $disabledLanguages[$check][$langCode] ) ) {
81                $reason = $disabledLanguages[$check][$langCode];
82                $result = [ 'translate-page-disabled', $reason ];
83                return false;
84            }
85        }
86
87        return true;
88    }
89
90    /**
91     * Runs message checks, adds tp:transver tags and updates statistics.
92     * Hook: PageSaveComplete
93     */
94    public static function onSaveComplete(
95        WikiPage $wikiPage,
96        UserIdentity $userIdentity,
97        string $summary,
98        int $flags,
99        RevisionRecord $revisionRecord
100    ): void {
101        global $wgEnablePageTranslation;
102
103        $content = $wikiPage->getContent();
104
105        if ( !$content instanceof TextContent ) {
106            // Screw it, not interested
107            return;
108        }
109
110        $text = $content->getText();
111        $title = $wikiPage->getTitle();
112        $handle = new MessageHandle( $title );
113
114        if ( !$handle->isValid() ) {
115            return;
116        }
117
118        // Update it.
119        $revId = $revisionRecord->getId();
120
121        $fuzzy = self::checkNeedsFuzzy( $handle, $text );
122        self::updateFuzzyTag( $title, $revId, $fuzzy );
123
124        $group = $handle->getGroup();
125        // Update translation stats - source language should always be up to date
126        if ( $handle->getCode() !== $group->getSourceLanguage() ) {
127            // This will update in-process cache immediately, but the value is saved
128            // to the database in a deferred update. See MessageGroupStats::queueUpdates.
129            // In case an error happens before that, the stats may be stale, but that
130            // would be fixed by the next update or purge.
131            MessageGroupStats::clear( $handle );
132        }
133
134        // This job asks for stats, however the updated stats are written in a deferred update.
135        // To make it less likely that the job would be executed before the updated stats are
136        // written, create the job inside a deferred update too.
137        DeferredUpdates::addCallableUpdate(
138            static function () use ( $handle ) {
139                MessageGroupStatesUpdaterJob::onChange( $handle );
140            }
141        );
142        $mwServices = MediaWikiServices::getInstance();
143        $user = $mwServices->getUserFactory()
144            ->newFromId( $userIdentity->getId() );
145
146        if ( !$fuzzy ) {
147            Services::getInstance()->getHookRunner()
148                ->onTranslate_newTranslation( $handle, $revId, $text, $user );
149        }
150
151        TTMServer::onChange( $handle );
152
153        if ( $wgEnablePageTranslation && $handle->isPageTranslation() ) {
154            // Updates for translatable pages only
155            $minor = (bool)( $flags & EDIT_MINOR );
156            PageTranslationHooks::onSectionSave( $wikiPage, $user, $content,
157                $summary, $minor, $flags, $handle );
158        }
159    }
160
161    /** Returns true if message is fuzzy, OR fails checks OR fails validations (error OR warning). */
162    private static function checkNeedsFuzzy( MessageHandle $handle, string $text ): bool {
163        // Docs are exempt for checks
164        if ( $handle->isDoc() ) {
165            return false;
166        }
167
168        // Check for explicit tag.
169        if ( MessageHandle::hasFuzzyString( $text ) ) {
170            return true;
171        }
172
173        // Not all groups have validators
174        $group = $handle->getGroup();
175        $validator = $group->getValidator();
176
177        // no validator set
178        if ( !$validator ) {
179            return false;
180        }
181
182        $code = $handle->getCode();
183        $key = $handle->getKey();
184        $en = $group->getMessage( $key, $group->getSourceLanguage() );
185        $message = new FatMessage( $key, $en );
186        // Take the contents from edit field as a translation.
187        $message->setTranslation( $text );
188        if ( $message->definition() === null ) {
189            // This should NOT happen, but add a check since it seems to be happening
190            // See: https://phabricator.wikimedia.org/T255669
191            LoggerFactory::getInstance( 'Translate' )->warning(
192                'Message definition is empty! Title: {title}, group: {group}, key: {key}',
193                [
194                    'title' => $handle->getTitle()->getPrefixedText(),
195                    'group' => $group->getId(),
196                    'key' => $key
197                ]
198            );
199            return false;
200        }
201
202        $validationResult = $validator->quickValidate( $message, $code );
203        return $validationResult->hasIssues();
204    }
205
206    /**
207     * @param Title $title
208     * @param int $revision
209     * @param bool $fuzzy Whether to fuzzy or not
210     */
211    private static function updateFuzzyTag( Title $title, int $revision, bool $fuzzy ): void {
212        $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_PRIMARY );
213
214        $conds = [
215            'rt_page' => $title->getArticleID(),
216            'rt_type' => RevTagStore::FUZZY_TAG,
217            'rt_revision' => $revision
218        ];
219
220        // Replace the existing fuzzy tag, if any
221        if ( $fuzzy ) {
222            $index = array_keys( $conds );
223            $dbw->replace( 'revtag', [ $index ], $conds, __METHOD__ );
224        } else {
225            $dbw->delete( 'revtag', $conds, __METHOD__ );
226        }
227    }
228
229    /**
230     * Adds tag which identifies the revision of source message at that time.
231     * This is used to show diff against current version of source message
232     * when updating a translation.
233     * Hook: Translate:newTranslation
234     */
235    public static function updateTransverTag(
236        MessageHandle $handle,
237        int $revision,
238        string $text,
239        User $user
240    ): bool {
241        if ( $user->isAllowed( 'bot' ) ) {
242            return false;
243        }
244
245        $group = $handle->getGroup();
246
247        $title = $handle->getTitle();
248        $name = $handle->getKey() . '/' . $group->getSourceLanguage();
249        $definitionTitle = Title::makeTitleSafe( $title->getNamespace(), $name );
250        if ( !$definitionTitle || !$definitionTitle->exists() ) {
251            return true;
252        }
253
254        $definitionRevision = $definitionTitle->getLatestRevID();
255        $dbw = MediaWikiServices::getInstance()
256            ->getDBLoadBalancer()
257            ->getConnection( DB_PRIMARY );
258
259        $conds = [
260            'rt_page' => $title->getArticleID(),
261            'rt_type' => RevTagStore::TRANSVER_PROP,
262            'rt_revision' => $revision,
263            'rt_value' => $definitionRevision,
264        ];
265        $index = [ 'rt_type', 'rt_page', 'rt_revision' ];
266        $dbw->replace( 'revtag', [ $index ], $conds, __METHOD__ );
267
268        return true;
269    }
270
271    /** Hook: ArticlePrepareTextForEdit */
272    public static function disablePreSaveTransform(
273        WikiPage $wikiPage,
274        ParserOptions $popts
275    ): void {
276        global $wgTranslateUsePreSaveTransform;
277
278        if ( !$wgTranslateUsePreSaveTransform ) {
279            $handle = new MessageHandle( $wikiPage->getTitle() );
280            if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
281                $popts->setPreSaveTransform( false );
282            }
283        }
284    }
285}