Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.13% |
88 / 122 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
TranslateEditAddons | |
72.13% |
88 / 122 |
|
40.00% |
2 / 5 |
59.02 | |
0.00% |
0 / 1 |
disallowLangTranslations | |
7.69% |
2 / 26 |
|
0.00% |
0 / 1 |
72.71 | |||
onSaveComplete | |
91.80% |
56 / 61 |
|
0.00% |
0 / 1 |
15.12 | |||
updateFuzzyTag | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
2 | |||
updateTransverTag | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
5.16 | |||
disablePreSaveTransform | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\TranslatorInterface; |
5 | |
6 | use ManualLogEntry; |
7 | use MediaWiki\CommentStore\CommentStoreComment; |
8 | use MediaWiki\Content\TextContent; |
9 | use MediaWiki\Deferred\DeferredUpdates; |
10 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupStatesUpdaterJob; |
11 | use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore; |
12 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
13 | use MediaWiki\Extension\Translate\PageTranslation\Hooks as PageTranslationHooks; |
14 | use MediaWiki\Extension\Translate\Services; |
15 | use MediaWiki\Extension\Translate\Statistics\MessageGroupStats; |
16 | use MediaWiki\Extension\Translate\TtmServer\TtmServer; |
17 | use MediaWiki\MediaWikiServices; |
18 | use MediaWiki\Parser\ParserOptions; |
19 | use MediaWiki\Revision\RevisionRecord; |
20 | use MediaWiki\Storage\EditResult; |
21 | use MediaWiki\Title\Title; |
22 | use MediaWiki\User\User; |
23 | use MediaWiki\User\UserIdentity; |
24 | use 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 | */ |
35 | class 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 | } |