Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
52.59% |
61 / 116 |
|
33.33% |
2 / 6 |
CRAP | |
0.00% |
0 / 1 |
TranslateEditAddons | |
52.59% |
61 / 116 |
|
33.33% |
2 / 6 |
133.43 | |
0.00% |
0 / 1 |
disallowLangTranslations | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
90 | |||
onSaveComplete | |
83.33% |
25 / 30 |
|
0.00% |
0 / 1 |
7.23 | |||
checkNeedsFuzzy | |
60.00% |
15 / 25 |
|
0.00% |
0 / 1 |
6.60 | |||
updateFuzzyTag | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
updateTransverTag | |
33.33% |
7 / 21 |
|
0.00% |
0 / 1 |
8.74 | |||
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 DeferredUpdates; |
7 | use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore; |
8 | use MediaWiki\Extension\Translate\MessageLoading\FatMessage; |
9 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
10 | use MediaWiki\Extension\Translate\PageTranslation\Hooks as PageTranslationHooks; |
11 | use MediaWiki\Extension\Translate\Services; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Revision\RevisionRecord; |
15 | use MediaWiki\Title\Title; |
16 | use MediaWiki\User\UserIdentity; |
17 | use MessageGroupStatesUpdaterJob; |
18 | use MessageGroupStats; |
19 | use ParserOptions; |
20 | use TextContent; |
21 | use TTMServer; |
22 | use User; |
23 | use 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 | */ |
34 | class 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 | } |