Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.13% covered (warning)
72.13%
88 / 122
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslateEditAddons
72.13% covered (warning)
72.13%
88 / 122
40.00% covered (danger)
40.00%
2 / 5
59.02
0.00% covered (danger)
0.00%
0 / 1
 disallowLangTranslations
7.69% covered (danger)
7.69%
2 / 26
0.00% covered (danger)
0.00%
0 / 1
72.71
 onSaveComplete
91.80% covered (success)
91.80%
56 / 61
0.00% covered (danger)
0.00%
0 / 1
15.12
 updateFuzzyTag
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 updateTransverTag
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
5.16
 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 ManualLogEntry;
7use MediaWiki\CommentStore\CommentStoreComment;
8use MediaWiki\Content\TextContent;
9use MediaWiki\Deferred\DeferredUpdates;
10use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupStatesUpdaterJob;
11use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
12use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
13use MediaWiki\Extension\Translate\PageTranslation\Hooks as PageTranslationHooks;
14use MediaWiki\Extension\Translate\Services;
15use MediaWiki\Extension\Translate\Statistics\MessageGroupStats;
16use MediaWiki\Extension\Translate\TtmServer\TtmServer;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Parser\ParserOptions;
19use MediaWiki\Revision\RevisionRecord;
20use MediaWiki\Storage\EditResult;
21use MediaWiki\Title\Title;
22use MediaWiki\User\User;
23use MediaWiki\User\UserIdentity;
24use WikiPage;
25
26/**
27 * Various editing enhancements to the edit page interface.
28 * Partly succeeded by the new ajax-enhanced editor but kept for compatibility.
29 * Also has code that is still relevant, like the hooks on save.
30 *
31 * @author Niklas Laxström
32 * @author Siebrand Mazeland
33 * @license GPL-2.0-or-later
34 */
35class TranslateEditAddons {
36    /**
37     * Prevent translations to non-translatable languages for the group
38     * Hook: getUserPermissionsErrorsExpensive
39     *
40     * @param Title $title
41     * @param User $user
42     * @param string $action
43     * @param mixed &$result
44     */
45    public static function disallowLangTranslations(
46        Title $title,
47        User $user,
48        string $action,
49        &$result
50    ): bool {
51        if ( $action !== 'edit' ) {
52            return true;
53        }
54
55        $handle = new MessageHandle( $title );
56        if ( !$handle->isValid() ) {
57            return true;
58        }
59
60        if ( $user->isAllowed( 'translate-manage' ) ) {
61            return true;
62        }
63
64        $group = $handle->getGroup();
65        $languages = $group->getTranslatableLanguages();
66        $langCode = $handle->getCode();
67        if ( $languages !== null && $langCode && !isset( $languages[$langCode] ) ) {
68            $result = [ 'translate-language-disabled' ];
69            return false;
70        }
71
72        $groupId = $group->getId();
73        $checks = [
74            $groupId,
75            strtok( $groupId, '-' ),
76            '*'
77        ];
78
79        $disabledLanguages = Services::getInstance()->getConfigHelper()->getDisabledTargetLanguages();
80        foreach ( $checks as $check ) {
81            if ( isset( $disabledLanguages[$check][$langCode] ) ) {
82                $reason = $disabledLanguages[$check][$langCode];
83                $result = [ 'translate-page-disabled', $reason ];
84                return false;
85            }
86        }
87
88        return true;
89    }
90
91    /**
92     * Runs message checks, adds tp:transver tags and updates statistics.
93     * Hook: PageSaveComplete
94     */
95    public static function onSaveComplete(
96        WikiPage $wikiPage,
97        UserIdentity $userIdentity,
98        string $summary,
99        int $flags,
100        RevisionRecord $revisionRecord,
101        EditResult $editResult
102    ): void {
103        global $wgEnablePageTranslation;
104
105        $content = $wikiPage->getContent();
106
107        if ( !$content instanceof TextContent ) {
108            // Screw it, not interested
109            return;
110        }
111
112        $text = $content->getText();
113        $title = $wikiPage->getTitle();
114        $handle = new MessageHandle( $title );
115
116        if ( !$handle->isValid() ) {
117            return;
118        }
119
120        // Update it.
121        $revId = $revisionRecord->getId();
122        $mwServices = MediaWikiServices::getInstance();
123
124        $fuzzy = $handle->needsFuzzy( $text );
125        $parentId = $revisionRecord->getParentId();
126        if ( $editResult->isNullEdit() || $parentId == 0 ) {
127            // In this case the page_latest hasn't changed so we can rely on its fuzzy status
128            $wasFuzzy = $handle->isFuzzy();
129        } else {
130            // In this case the page_latest will (probably) have changed. The above might work by chance
131            // since it reads from a replica database which might not have gotten the update yet, but
132            // don't trust it and read the fuzzy status of the parent ID from the database instead
133            $revTagStore = Services::getInstance()->getRevTagStore();
134            $wasFuzzy = $revTagStore->isRevIdFuzzy( $title->getArticleID(), $parentId );
135        }
136        if ( !$fuzzy && $wasFuzzy ) {
137            $title = $mwServices->getTitleFactory()->castFromPageIdentity( $wikiPage );
138            $user = $mwServices->getUserFactory()->newFromUserIdentity( $userIdentity );
139
140            if ( !$mwServices->getPermissionManager()->userCan( 'unfuzzy', $user, $title ) ) {
141                // No permission to unfuzzy this unit so leave it fuzzy
142                $fuzzy = true;
143            } elseif ( $editResult->isNullEdit() ) {
144                $entry = new ManualLogEntry( 'translationreview', 'unfuzzy' );
145                // Generate a log entry and null revision for the otherwise
146                // invisible unfuzzying
147                $dbw = $mwServices->getDBLoadBalancer()->getConnection( DB_PRIMARY );
148                $nullRevision = $mwServices->getRevisionStore()->newNullRevision(
149                    $dbw,
150                    $wikiPage,
151                    CommentStoreComment::newUnsavedComment(
152                        $summary !== '' ? $summary : wfMessage( "translate-unfuzzy-comment" )
153                    ),
154                    false,
155                    $userIdentity
156                );
157                if ( $nullRevision ) {
158                    $nullRevision = $mwServices->getRevisionStore()->insertRevisionOn( $nullRevision, $dbw );
159                    // Overwrite $revId so the revision ID of the null revision rather than the previous parent
160                    // revision is used for any further edits
161                    $revId = $nullRevision->getId();
162                    $wikiPage->updateRevisionOn( $dbw, $nullRevision, $nullRevision->getParentId() );
163                    $entry->setAssociatedRevId( $revId );
164                }
165
166                $entry->setPerformer( $userIdentity );
167                $entry->setTarget( $title );
168                $logId = $entry->insert();
169                $entry->publish( $logId );
170            }
171        }
172        self::updateFuzzyTag( $title, $revId, $fuzzy );
173
174        $group = $handle->getGroup();
175        // Update translation stats - source language should always be up to date
176        if ( $handle->getCode() !== $group->getSourceLanguage() ) {
177            // This will update in-process cache immediately, but the value is saved
178            // to the database in a deferred update. See MessageGroupStats::queueUpdates.
179            // In case an error happens before that, the stats may be stale, but that
180            // would be fixed by the next update or purge.
181            MessageGroupStats::clear( $handle );
182        }
183
184        // This job asks for stats, however the updated stats are written in a deferred update.
185        // To make it less likely that the job would be executed before the updated stats are
186        // written, create the job inside a deferred update too.
187        DeferredUpdates::addCallableUpdate(
188            static function () use ( $handle ) {
189                MessageGroupStatesUpdaterJob::onChange( $handle );
190            }
191        );
192        $user = $mwServices->getUserFactory()
193            ->newFromId( $userIdentity->getId() );
194
195        if ( !$fuzzy ) {
196            Services::getInstance()->getHookRunner()
197                ->onTranslate_newTranslation( $handle, $revId, $text, $user );
198        }
199
200        TtmServer::onChange( $handle );
201
202        if ( $wgEnablePageTranslation && $handle->isPageTranslation() ) {
203            // Updates for translatable pages only
204            $minor = (bool)( $flags & EDIT_MINOR );
205            PageTranslationHooks::onSectionSave( $wikiPage, $user, $content,
206                $summary, $minor, $flags, $handle );
207        }
208    }
209
210    /**
211     * @param Title $title
212     * @param int $revision
213     * @param bool $fuzzy Whether to fuzzy or not
214     */
215    private static function updateFuzzyTag( Title $title, int $revision, bool $fuzzy ): void {
216        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
217
218        $conds = [
219            'rt_page' => $title->getArticleID(),
220            'rt_type' => RevTagStore::FUZZY_TAG,
221            'rt_revision' => $revision
222        ];
223
224        // Replace the existing fuzzy tag, if any
225        if ( $fuzzy ) {
226            $index = array_keys( $conds );
227            $dbw->newReplaceQueryBuilder()
228                ->replaceInto( 'revtag' )
229                ->uniqueIndexFields( $index )
230                ->row( $conds )
231                ->caller( __METHOD__ )
232                ->execute();
233        } else {
234            $dbw->newDeleteQueryBuilder()
235                ->deleteFrom( 'revtag' )
236                ->where( $conds )
237                ->caller( __METHOD__ )
238                ->execute();
239        }
240    }
241
242    /**
243     * Adds tag which identifies the revision of source message at that time.
244     * This is used to show diff against current version of source message
245     * when updating a translation.
246     * Hook: Translate:newTranslation
247     */
248    public static function updateTransverTag(
249        MessageHandle $handle,
250        int $revision,
251        string $text,
252        User $user
253    ): bool {
254        if ( $user->isAllowed( 'bot' ) ) {
255            return false;
256        }
257
258        $group = $handle->getGroup();
259
260        $title = $handle->getTitle();
261        $name = $handle->getKey() . '/' . $group->getSourceLanguage();
262        $definitionTitle = Title::makeTitleSafe( $title->getNamespace(), $name );
263        if ( !$definitionTitle || !$definitionTitle->exists() ) {
264            return true;
265        }
266
267        $definitionRevision = $definitionTitle->getLatestRevID();
268        $revTagStore = Services::getInstance()->getRevTagStore();
269        $revTagStore->setTransver( $title, $revision, $definitionRevision );
270
271        return true;
272    }
273
274    /** Hook: ArticlePrepareTextForEdit */
275    public static function disablePreSaveTransform(
276        WikiPage $wikiPage,
277        ParserOptions $popts
278    ): void {
279        global $wgTranslateUsePreSaveTransform;
280
281        if ( !$wgTranslateUsePreSaveTransform ) {
282            $handle = new MessageHandle( $wikiPage->getTitle() );
283            if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
284                $popts->setPreSaveTransform( false );
285            }
286        }
287    }
288}