Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
44.32% |
191 / 431 |
|
25.93% |
7 / 27 |
CRAP | |
0.00% |
0 / 1 |
HookHandler | |
44.32% |
191 / 431 |
|
25.93% |
7 / 27 |
1553.43 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setupTranslate | |
57.44% |
112 / 195 |
|
0.00% |
0 / 1 |
17.71 | |||
registerHookHandlers | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
5.58 | |||
onUserGetReservedNames | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
onAbuseFilterAlterVariables | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
onAbuseFilterComputeVariable | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
onAbuseFilterBuilder | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setupParserHooks | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onPageContentLanguage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
translateMessageDocumentationLanguage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
searchProfile | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
2.01 | |||
searchProfileForm | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
42 | |||
searchProfileSetupEngine | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
preventCategorization | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
5.01 | |||
showFakeCategories | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
addConfig | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
onAdminLinks | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onMergeAccountFromTo | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
onDeleteAccount | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onAbortEmailNotificationReview | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onTitleIsAlwaysKnown | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
onParserFirstCallInit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
translateRenderParserFunction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
validateMessage | |
84.85% |
28 / 33 |
|
0.00% |
0 / 1 |
10.35 | |||
onRevisionRecordInserted | |
13.04% |
3 / 23 |
|
0.00% |
0 / 1 |
21.44 | |||
onListDefinedTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onChangeTagsListActive | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types=1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate; |
5 | |
6 | use ALItem; |
7 | use ALTree; |
8 | use ApiRawMessage; |
9 | use Config; |
10 | use ConfigException; |
11 | use Content; |
12 | use ExtensionRegistry; |
13 | use IContextSource; |
14 | use Language; |
15 | use LogFormatter; |
16 | use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook; |
17 | use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook; |
18 | use MediaWiki\Config\ServiceOptions; |
19 | use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder; |
20 | use MediaWiki\Extension\Translate\LogFormatter as TranslateLogFormatter; |
21 | use MediaWiki\Extension\Translate\MessageGroupProcessing\DeleteTranslatableBundleJob; |
22 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupSubscriptionHookHandler; |
23 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupSubscriptionNotificationJob; |
24 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MoveTranslatableBundleJob; |
25 | use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore; |
26 | use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleLogFormatter; |
27 | use MediaWiki\Extension\Translate\MessageLoading\FatMessage; |
28 | use MediaWiki\Extension\Translate\MessageLoading\MessageHandle; |
29 | use MediaWiki\Extension\Translate\PageTranslation\DeleteTranslatableBundleSpecialPage; |
30 | use MediaWiki\Extension\Translate\PageTranslation\Hooks; |
31 | use MediaWiki\Extension\Translate\PageTranslation\MigrateTranslatablePageSpecialPage; |
32 | use MediaWiki\Extension\Translate\PageTranslation\PageTranslationSpecialPage; |
33 | use MediaWiki\Extension\Translate\PageTranslation\PrepareTranslatablePageSpecialPage; |
34 | use MediaWiki\Extension\Translate\PageTranslation\RenderTranslationPageJob; |
35 | use MediaWiki\Extension\Translate\PageTranslation\UpdateTranslatablePageJob; |
36 | use MediaWiki\Extension\Translate\Statistics\RebuildMessageGroupStatsJob; |
37 | use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot; |
38 | use MediaWiki\Extension\Translate\SystemUsers\TranslateUserManager; |
39 | use MediaWiki\Extension\Translate\TranslatorInterface\TranslateEditAddons; |
40 | use MediaWiki\Extension\Translate\TranslatorSandbox\ManageTranslatorSandboxSpecialPage; |
41 | use MediaWiki\Extension\Translate\TranslatorSandbox\TranslateSandboxEmailJob; |
42 | use MediaWiki\Extension\Translate\TranslatorSandbox\TranslationStashActionApi; |
43 | use MediaWiki\Extension\Translate\TranslatorSandbox\TranslationStashSpecialPage; |
44 | use MediaWiki\Extension\Translate\TranslatorSandbox\TranslatorSandboxActionApi; |
45 | use MediaWiki\Extension\Translate\TtmServer\SearchableTtmServer; |
46 | use MediaWiki\Extension\Translate\Utilities\Utilities; |
47 | use MediaWiki\Hook\ParserFirstCallInitHook; |
48 | use MediaWiki\Html\Html; |
49 | use MediaWiki\Languages\LanguageNameUtils; |
50 | use MediaWiki\MediaWikiServices; |
51 | use MediaWiki\Revision\Hook\RevisionRecordInsertedHook; |
52 | use MediaWiki\Revision\RevisionLookup; |
53 | use MediaWiki\Settings\SettingsBuilder; |
54 | use MediaWiki\StubObject\StubUserLang; |
55 | use MediaWiki\Title\Title; |
56 | use MediaWiki\User\Hook\UserGetReservedNamesHook; |
57 | use OutputPage; |
58 | use Parser; |
59 | use ParserOutput; |
60 | use RecentChange; |
61 | use SearchEngine; |
62 | use SpecialPage; |
63 | use SpecialSearch; |
64 | use Status; |
65 | use TextContent; |
66 | use TitleValue; |
67 | use User; |
68 | use Wikimedia\Rdbms\ILoadBalancer; |
69 | use Xml; |
70 | use XmlSelect; |
71 | |
72 | /** |
73 | * Hooks for Translate extension. |
74 | * Contains class with basic non-feature specific hooks. |
75 | * Main subsystems, like page translation, should have their own hook handler. * |
76 | * Most of the hooks on this class are still old style static functions, but new new hooks should |
77 | * use the new style hook handlers with interfaces. |
78 | * |
79 | * @author Niklas Laxström |
80 | * @license GPL-2.0-or-later |
81 | */ |
82 | class HookHandler implements |
83 | ChangeTagsListActiveHook, |
84 | ListDefinedTagsHook, |
85 | ParserFirstCallInitHook, |
86 | RevisionRecordInsertedHook, |
87 | UserGetReservedNamesHook |
88 | { |
89 | /** |
90 | * Any user of this list should make sure that the tables |
91 | * actually exist, since they may be optional |
92 | */ |
93 | private const USER_MERGE_TABLES = [ |
94 | 'translate_stash' => 'ts_user', |
95 | 'translate_reviews' => 'trr_user', |
96 | ]; |
97 | private RevisionLookup $revisionLookup; |
98 | private ILoadBalancer $loadBalancer; |
99 | private Config $config; |
100 | private LanguageNameUtils $languageNameUtils; |
101 | |
102 | public function __construct( |
103 | RevisionLookup $revisionLookup, |
104 | ILoadBalancer $loadBalancer, |
105 | Config $config, |
106 | LanguageNameUtils $languageNameUtils |
107 | ) { |
108 | $this->revisionLookup = $revisionLookup; |
109 | $this->loadBalancer = $loadBalancer; |
110 | $this->config = $config; |
111 | $this->languageNameUtils = $languageNameUtils; |
112 | } |
113 | |
114 | /** Do late setup that depends on configuration. */ |
115 | public static function setupTranslate(): void { |
116 | global $wgTranslateYamlLibrary; |
117 | $hooks = []; |
118 | |
119 | /* |
120 | * Text that will be shown in translations if the translation is outdated. |
121 | * Must be something that does not conflict with actual content. |
122 | */ |
123 | if ( !defined( 'TRANSLATE_FUZZY' ) ) { |
124 | define( 'TRANSLATE_FUZZY', '!!FUZZY!!' ); |
125 | } |
126 | |
127 | if ( $wgTranslateYamlLibrary === null ) { |
128 | $wgTranslateYamlLibrary = function_exists( 'yaml_parse' ) ? 'phpyaml' : 'spyc'; |
129 | } |
130 | |
131 | $hooks['PageSaveComplete'][] = [ TranslateEditAddons::class, 'onSaveComplete' ]; |
132 | |
133 | // TODO Remove after MLEB 2024.01 release |
134 | global $wgJobClasses; |
135 | $wgJobClasses['MessageGroupStatsRebuildJob'] = RebuildMessageGroupStatsJob::class; |
136 | |
137 | // Page translation setup check and init if enabled. |
138 | global $wgEnablePageTranslation; |
139 | if ( $wgEnablePageTranslation ) { |
140 | // Special page and the right to use it |
141 | global $wgSpecialPages, $wgAvailableRights; |
142 | $wgSpecialPages['PageTranslation'] = [ |
143 | 'class' => PageTranslationSpecialPage::class, |
144 | 'services' => [ |
145 | 'LanguageFactory', |
146 | 'LinkBatchFactory', |
147 | 'JobQueueGroup', |
148 | 'Translate:TranslatablePageMarker', |
149 | 'Translate:TranslatablePageParser', |
150 | 'Translate:MessageGroupMetadata' |
151 | ] |
152 | ]; |
153 | $wgSpecialPages['PageTranslationDeletePage'] = [ |
154 | 'class' => DeleteTranslatableBundleSpecialPage::class, |
155 | 'services' => [ |
156 | 'PermissionManager', |
157 | 'Translate:TranslatableBundleDeleter', |
158 | 'Translate:TranslatableBundleFactory', |
159 | ] |
160 | ]; |
161 | |
162 | // right-pagetranslation action-pagetranslation |
163 | $wgAvailableRights[] = 'pagetranslation'; |
164 | |
165 | $wgSpecialPages['PageMigration'] = MigrateTranslatablePageSpecialPage::class; |
166 | $wgSpecialPages['PagePreparation'] = PrepareTranslatablePageSpecialPage::class; |
167 | |
168 | global $wgActionFilteredLogs, $wgLogActionsHandlers, $wgLogTypes; |
169 | |
170 | // log-description-pagetranslation log-name-pagetranslation logentry-pagetranslation-mark |
171 | // logentry-pagetranslation-unmark logentry-pagetranslation-moveok |
172 | // logentry-pagetranslation-movenok logentry-pagetranslation-deletefok |
173 | // logentry-pagetranslation-deletefnok logentry-pagetranslation-deletelok |
174 | // logentry-pagetranslation-deletelnok logentry-pagetranslation-encourage |
175 | // logentry-pagetranslation-discourage logentry-pagetranslation-prioritylanguages |
176 | // logentry-pagetranslation-associate logentry-pagetranslation-dissociate |
177 | $wgLogTypes[] = 'pagetranslation'; |
178 | $wgLogActionsHandlers['pagetranslation/mark'] = TranslatableBundleLogFormatter::class; |
179 | $wgLogActionsHandlers['pagetranslation/unmark'] = TranslatableBundleLogFormatter::class; |
180 | $wgLogActionsHandlers['pagetranslation/moveok'] = TranslatableBundleLogFormatter::class; |
181 | $wgLogActionsHandlers['pagetranslation/movenok'] = TranslatableBundleLogFormatter::class; |
182 | $wgLogActionsHandlers['pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class; |
183 | $wgLogActionsHandlers['pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class; |
184 | $wgLogActionsHandlers['pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class; |
185 | $wgLogActionsHandlers['pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class; |
186 | $wgLogActionsHandlers['pagetranslation/encourage'] = TranslatableBundleLogFormatter::class; |
187 | $wgLogActionsHandlers['pagetranslation/discourage'] = TranslatableBundleLogFormatter::class; |
188 | $wgLogActionsHandlers['pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class; |
189 | $wgLogActionsHandlers['pagetranslation/associate'] = TranslatableBundleLogFormatter::class; |
190 | $wgLogActionsHandlers['pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class; |
191 | $wgActionFilteredLogs['pagetranslation'] = [ |
192 | 'mark' => [ 'mark' ], |
193 | 'unmark' => [ 'unmark' ], |
194 | 'move' => [ 'moveok', 'movenok' ], |
195 | 'delete' => [ 'deletefok', 'deletefnok', 'deletelok', 'deletelnok' ], |
196 | 'encourage' => [ 'encourage' ], |
197 | 'discourage' => [ 'discourage' ], |
198 | 'prioritylanguages' => [ 'prioritylanguages' ], |
199 | 'aggregategroups' => [ 'associate', 'dissociate' ], |
200 | ]; |
201 | |
202 | $wgLogTypes[] = 'messagebundle'; |
203 | $wgLogActionsHandlers['messagebundle/moveok'] = TranslatableBundleLogFormatter::class; |
204 | $wgLogActionsHandlers['messagebundle/movenok'] = TranslatableBundleLogFormatter::class; |
205 | $wgLogActionsHandlers['messagebundle/deletefok'] = TranslatableBundleLogFormatter::class; |
206 | $wgLogActionsHandlers['messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class; |
207 | $wgActionFilteredLogs['messagebundle'] = [ |
208 | 'move' => [ 'moveok', 'movenok' ], |
209 | 'delete' => [ 'deletefok', 'deletefnok' ], |
210 | ]; |
211 | |
212 | $wgLogActionsHandlers['import/translatable-bundle'] = TranslatableBundleLogFormatter::class; |
213 | |
214 | $wgJobClasses['RenderTranslationPageJob'] = RenderTranslationPageJob::class; |
215 | $wgJobClasses['NonPrioritizedRenderTranslationPageJob'] = RenderTranslationPageJob::class; |
216 | $wgJobClasses['MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class; |
217 | $wgJobClasses['DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class; |
218 | $wgJobClasses['UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class; |
219 | |
220 | // Namespaces |
221 | global $wgNamespacesWithSubpages, $wgNamespaceProtection; |
222 | global $wgTranslateMessageNamespaces; |
223 | |
224 | $wgNamespacesWithSubpages[NS_TRANSLATIONS] = true; |
225 | $wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] = true; |
226 | |
227 | // Standard protection and register it for filtering |
228 | $wgNamespaceProtection[NS_TRANSLATIONS] = [ 'translate' ]; |
229 | $wgTranslateMessageNamespaces[] = NS_TRANSLATIONS; |
230 | |
231 | /// Page translation hooks |
232 | |
233 | /// Register our CSS and metadata |
234 | $hooks['BeforePageDisplay'][] = [ Hooks::class, 'onBeforePageDisplay' ]; |
235 | |
236 | // Disable VE |
237 | $hooks['VisualEditorBeforeEditor'][] = [ Hooks::class, 'onVisualEditorBeforeEditor' ]; |
238 | |
239 | // Check syntax for \<translate> |
240 | $hooks['MultiContentSave'][] = [ Hooks::class, 'tpSyntaxCheck' ]; |
241 | $hooks['EditFilterMergedContent'][] = |
242 | [ Hooks::class, 'tpSyntaxCheckForEditContent' ]; |
243 | |
244 | // Add transtag to page props for discovery |
245 | $hooks['PageSaveComplete'][] = [ Hooks::class, 'addTranstagAfterSave' ]; |
246 | |
247 | $hooks['RevisionRecordInserted'][] = [ Hooks::class, 'updateTranstagOnNullRevisions' ]; |
248 | |
249 | // Register different ways to show language links |
250 | $hooks['ParserFirstCallInit'][] = [ self::class, 'setupParserHooks' ]; |
251 | $hooks['LanguageLinks'][] = [ Hooks::class, 'addLanguageLinks' ]; |
252 | $hooks['SkinTemplateGetLanguageLink'][] = [ Hooks::class, 'formatLanguageLink' ]; |
253 | |
254 | // Allow templates to query whether they are transcluded in a translatable/translated page |
255 | $hooks['GetMagicVariableIDs'][] = [ Hooks::class, 'onGetMagicVariableIDs' ]; |
256 | $hooks['ParserGetVariableValueSwitch'][] = [ Hooks::class, 'onParserGetVariableValueSwitch' ]; |
257 | |
258 | // Strip \<translate> tags etc. from source pages when rendering |
259 | $hooks['ParserBeforeInternalParse'][] = [ Hooks::class, 'renderTagPage' ]; |
260 | // Strip \<translate> tags etc. from source pages when preprocessing |
261 | $hooks['ParserBeforePreprocess'][] = [ Hooks::class, 'preprocessTagPage' ]; |
262 | $hooks['ParserOutputPostCacheTransform'][] = |
263 | [ Hooks::class, 'onParserOutputPostCacheTransform' ]; |
264 | |
265 | $hooks['BeforeParserFetchTemplateRevisionRecord'][] = |
266 | [ Hooks::class, 'fetchTranslatableTemplateAndTitle' ]; |
267 | |
268 | // Set the page content language |
269 | $hooks['PageContentLanguage'][] = [ Hooks::class, 'onPageContentLanguage' ]; |
270 | |
271 | // Prevent editing of certain pages in translations namespace |
272 | $hooks['getUserPermissionsErrorsExpensive'][] = |
273 | [ Hooks::class, 'onGetUserPermissionsErrorsExpensive' ]; |
274 | // Prevent editing of translation pages directly |
275 | $hooks['getUserPermissionsErrorsExpensive'][] = |
276 | [ Hooks::class, 'preventDirectEditing' ]; |
277 | |
278 | // Our custom header for translation pages |
279 | $hooks['ArticleViewHeader'][] = [ Hooks::class, 'translatablePageHeader' ]; |
280 | |
281 | // Edit notice shown on translatable pages |
282 | $hooks['TitleGetEditNotices'][] = [ Hooks::class, 'onTitleGetEditNotices' ]; |
283 | |
284 | // Custom move page that can move all the associated pages too |
285 | $hooks['SpecialPage_initList'][] = [ Hooks::class, 'replaceMovePage' ]; |
286 | // Locking during page moves |
287 | $hooks['getUserPermissionsErrorsExpensive'][] = |
288 | [ Hooks::class, 'lockedPagesCheck' ]; |
289 | // Disable action=delete |
290 | $hooks['ArticleConfirmDelete'][] = [ Hooks::class, 'disableDelete' ]; |
291 | |
292 | // Replace subpage logic behavior |
293 | $hooks['SkinSubPageSubtitle'][] = [ Hooks::class, 'replaceSubtitle' ]; |
294 | |
295 | // Replaced edit tab with translation tab for translation pages |
296 | $hooks['SkinTemplateNavigation::Universal'][] = [ Hooks::class, 'translateTab' ]; |
297 | |
298 | // Update translated page when translation unit is moved |
299 | $hooks['PageMoveComplete'][] = [ Hooks::class, 'onMovePageTranslationUnits' ]; |
300 | |
301 | // Update translated page when translation unit is deleted |
302 | $hooks['ArticleDeleteComplete'][] = [ Hooks::class, 'onDeleteTranslationUnit' ]; |
303 | |
304 | // Prevent editing of translation pages. |
305 | $hooks['ReplaceTextFilterPageTitlesForEdit'][] = [ Hooks::class, 'onReplaceTextFilterPageTitlesForEdit' ]; |
306 | // Prevent renaming of translatable pages and their translation and translation units |
307 | $hooks['ReplaceTextFilterPageTitlesForRename'][] = |
308 | [ Hooks::class, 'onReplaceTextFilterPageTitlesForRename' ]; |
309 | } |
310 | |
311 | global $wgTranslateUseSandbox; |
312 | if ( $wgTranslateUseSandbox ) { |
313 | global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions; |
314 | |
315 | $wgSpecialPages['ManageTranslatorSandbox'] = [ |
316 | 'class' => ManageTranslatorSandboxSpecialPage::class, |
317 | 'services' => [ |
318 | 'Translate:TranslationStashReader', |
319 | 'UserOptionsLookup', |
320 | 'Translate:TranslateSandbox', |
321 | ], |
322 | 'args' => [ |
323 | static function () { |
324 | return new ServiceOptions( |
325 | ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS, |
326 | MediaWikiServices::getInstance()->getMainConfig() |
327 | ); |
328 | } |
329 | ] |
330 | ]; |
331 | $wgSpecialPages['TranslationStash'] = [ |
332 | 'class' => TranslationStashSpecialPage::class, |
333 | 'services' => [ |
334 | 'LanguageNameUtils', |
335 | 'Translate:TranslationStashReader', |
336 | 'UserOptionsLookup', |
337 | 'LanguageFactory', |
338 | ], |
339 | 'args' => [ |
340 | static function () { |
341 | return new ServiceOptions( |
342 | TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS, |
343 | MediaWikiServices::getInstance()->getMainConfig() |
344 | ); |
345 | } |
346 | ] |
347 | ]; |
348 | $wgDefaultUserOptions['translate-sandbox'] = ''; |
349 | // right-translate-sandboxmanage action-translate-sandboxmanage |
350 | $wgAvailableRights[] = 'translate-sandboxmanage'; |
351 | |
352 | global $wgLogTypes, $wgLogActionsHandlers; |
353 | // log-name-translatorsandbox log-description-translatorsandbox |
354 | $wgLogTypes[] = 'translatorsandbox'; |
355 | // logentry-translatorsandbox-promoted logentry-translatorsandbox-rejected |
356 | $wgLogActionsHandlers['translatorsandbox/promoted'] = TranslateLogFormatter::class; |
357 | $wgLogActionsHandlers['translatorsandbox/rejected'] = TranslateLogFormatter::class; |
358 | |
359 | // This is no longer used for new entries since 2016.07. |
360 | // logentry-newusers-tsbpromoted |
361 | $wgLogActionsHandlers['newusers/tsbpromoted'] = LogFormatter::class; |
362 | |
363 | global $wgJobClasses; |
364 | $wgJobClasses['TranslateSandboxEmailJob'] = TranslateSandboxEmailJob::class; |
365 | |
366 | global $wgAPIModules; |
367 | $wgAPIModules['translationstash'] = [ |
368 | 'class' => TranslationStashActionApi::class, |
369 | 'services' => [ |
370 | 'DBLoadBalancer', |
371 | 'UserFactory' |
372 | ] |
373 | ]; |
374 | $wgAPIModules['translatesandbox'] = [ |
375 | 'class' => TranslatorSandboxActionApi::class, |
376 | 'services' => [ |
377 | 'UserFactory', |
378 | 'UserNameUtils', |
379 | 'UserOptionsManager', |
380 | 'WikiPageFactory', |
381 | 'UserOptionsLookup', |
382 | 'Translate:TranslateSandbox', |
383 | ], |
384 | 'args' => [ |
385 | static function () { |
386 | return new ServiceOptions( |
387 | TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS, |
388 | MediaWikiServices::getInstance()->getMainConfig() |
389 | ); |
390 | } |
391 | ] |
392 | ]; |
393 | } |
394 | |
395 | global $wgNamespaceRobotPolicies; |
396 | $wgNamespaceRobotPolicies[NS_TRANSLATIONS] = 'noindex'; |
397 | |
398 | // If no service has been configured, we use a built-in fallback. |
399 | global $wgTranslateTranslationDefaultService, |
400 | $wgTranslateTranslationServices; |
401 | if ( $wgTranslateTranslationDefaultService === true ) { |
402 | $wgTranslateTranslationDefaultService = 'TTMServer'; |
403 | if ( !isset( $wgTranslateTranslationServices['TTMServer'] ) ) { |
404 | $wgTranslateTranslationServices['TTMServer'] = [ |
405 | 'database' => false, |
406 | 'cutoff' => 0.75, |
407 | 'type' => 'ttmserver', |
408 | 'public' => false, |
409 | ]; |
410 | } |
411 | } |
412 | |
413 | global $wgTranslateEnableMessageGroupSubscription; |
414 | if ( $wgTranslateEnableMessageGroupSubscription ) { |
415 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) { |
416 | throw new ConfigException( |
417 | 'Translate: Message group subscriptions (TranslateEnableMessageGroupSubscription) are ' . |
418 | 'enabled but Echo extension is not installed' |
419 | ); |
420 | } |
421 | MessageGroupSubscriptionHookHandler::registerHooks( $hooks ); |
422 | $wgJobClasses['MessageGroupSubscriptionNotificationJob'] = MessageGroupSubscriptionNotificationJob::class; |
423 | } |
424 | |
425 | static::registerHookHandlers( $hooks ); |
426 | } |
427 | |
428 | private static function registerHookHandlers( array $hooks ): void { |
429 | if ( defined( 'MW_PHPUNIT_TEST' ) && MediaWikiServices::hasInstance() ) { |
430 | // When called from a test case's setUp() method, |
431 | // we can use HookContainer, but we cannot use SettingsBuilder. |
432 | $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); |
433 | foreach ( $hooks as $name => $handlers ) { |
434 | foreach ( $handlers as $h ) { |
435 | $hookContainer->register( $name, $h ); |
436 | } |
437 | } |
438 | } else { |
439 | $settingsBuilder = SettingsBuilder::getInstance(); |
440 | $settingsBuilder->registerHookHandlers( $hooks ); |
441 | } |
442 | } |
443 | |
444 | /** |
445 | * Prevents anyone from registering or logging in as FuzzyBot |
446 | * @inheritDoc |
447 | */ |
448 | public function onUserGetReservedNames( &$reservedUsernames ): void { |
449 | $reservedUsernames[] = FuzzyBot::getName(); |
450 | $reservedUsernames[] = TranslateUserManager::getName(); |
451 | } |
452 | |
453 | /** Used for setting an AbuseFilter variable. */ |
454 | public static function onAbuseFilterAlterVariables( |
455 | VariableHolder &$vars, Title $title, User $user |
456 | ): void { |
457 | $handle = new MessageHandle( $title ); |
458 | |
459 | // Only set this variable if we are in a proper namespace to avoid |
460 | // unnecessary overhead in non-translation pages |
461 | if ( $handle->isMessageNamespace() ) { |
462 | $vars->setLazyLoadVar( |
463 | 'translate_source_text', |
464 | 'translate-get-source', |
465 | [ 'handle' => $handle ] |
466 | ); |
467 | $vars->setLazyLoadVar( |
468 | 'translate_target_language', |
469 | 'translate-get-target-language', |
470 | [ 'handle' => $handle ] |
471 | ); |
472 | } |
473 | } |
474 | |
475 | /** Computes the translate_source_text and translate_target_language AbuseFilter variables */ |
476 | public static function onAbuseFilterComputeVariable( |
477 | string $method, |
478 | VariableHolder $vars, |
479 | array $parameters, |
480 | ?string &$result |
481 | ): bool { |
482 | if ( $method !== 'translate-get-source' && $method !== 'translate-get-target-language' ) { |
483 | return true; |
484 | } |
485 | |
486 | $handle = $parameters['handle']; |
487 | $value = ''; |
488 | if ( $handle->isValid() ) { |
489 | if ( $method === 'translate-get-source' ) { |
490 | $group = $handle->getGroup(); |
491 | $value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() ); |
492 | } else { |
493 | $value = $handle->getCode(); |
494 | } |
495 | } |
496 | |
497 | $result = $value; |
498 | |
499 | return false; |
500 | } |
501 | |
502 | /** Register AbuseFilter variables provided by Translate. */ |
503 | public static function onAbuseFilterBuilder( array &$builderValues ): void { |
504 | // The following messages are generated here: |
505 | // * abusefilter-edit-builder-vars-translate-source-text |
506 | // * abusefilter-edit-builder-vars-translate-target-language |
507 | $builderValues['vars']['translate_source_text'] = 'translate-source-text'; |
508 | $builderValues['vars']['translate_target_language'] = 'translate-target-language'; |
509 | } |
510 | |
511 | /** |
512 | * Hook: ParserFirstCallInit |
513 | * Registers \<languages> tag with the parser. |
514 | */ |
515 | public static function setupParserHooks( Parser $parser ): void { |
516 | // For nice language list in-page |
517 | $parser->setHook( 'languages', [ Hooks::class, 'languages' ] ); |
518 | } |
519 | |
520 | /** |
521 | * Hook: PageContentLanguage |
522 | * Set the correct page content language for translation units. |
523 | * @param Title $title |
524 | * @param Language|StubUserLang|string &$pageLang |
525 | */ |
526 | public static function onPageContentLanguage( Title $title, &$pageLang ): void { |
527 | $handle = new MessageHandle( $title ); |
528 | if ( $handle->isMessageNamespace() ) { |
529 | $pageLang = $handle->getEffectiveLanguage(); |
530 | } |
531 | } |
532 | |
533 | /** |
534 | * Hook: LanguageGetTranslatedLanguageNames |
535 | * Hook: TranslateSupportedLanguages |
536 | */ |
537 | public static function translateMessageDocumentationLanguage( array &$names, ?string $code ): void { |
538 | global $wgTranslateDocumentationLanguageCode; |
539 | if ( $wgTranslateDocumentationLanguageCode ) { |
540 | // Special case the autonyms |
541 | if ( |
542 | $wgTranslateDocumentationLanguageCode === $code || |
543 | $code === null |
544 | ) { |
545 | $code = 'en'; |
546 | } |
547 | |
548 | $names[$wgTranslateDocumentationLanguageCode] = |
549 | wfMessage( 'translate-documentation-language' )->inLanguage( $code )->plain(); |
550 | } |
551 | } |
552 | |
553 | /** Hook: SpecialSearchProfiles */ |
554 | public static function searchProfile( array &$profiles ): void { |
555 | global $wgTranslateMessageNamespaces; |
556 | $insert = []; |
557 | $insert['translation'] = [ |
558 | 'message' => 'translate-searchprofile', |
559 | 'tooltip' => 'translate-searchprofile-tooltip', |
560 | 'namespaces' => $wgTranslateMessageNamespaces, |
561 | ]; |
562 | |
563 | // Insert translations before 'all' |
564 | $index = array_search( 'all', array_keys( $profiles ) ); |
565 | |
566 | // Or just at the end if all is not found |
567 | if ( $index === false ) { |
568 | wfWarn( '"all" not found in search profiles' ); |
569 | $index = count( $profiles ); |
570 | } |
571 | |
572 | $profiles = array_merge( |
573 | array_slice( $profiles, 0, $index ), |
574 | $insert, |
575 | array_slice( $profiles, $index ) |
576 | ); |
577 | } |
578 | |
579 | /** Hook: SpecialSearchProfileForm */ |
580 | public static function searchProfileForm( |
581 | SpecialSearch $search, |
582 | string &$form, |
583 | string $profile, |
584 | string $term, |
585 | array $opts |
586 | ): bool { |
587 | if ( $profile !== 'translation' ) { |
588 | return true; |
589 | } |
590 | |
591 | if ( Services::getInstance()->getTtmServerFactory()->getDefaultForQuerying() instanceof SearchableTtmServer ) { |
592 | $href = SpecialPage::getTitleFor( 'SearchTranslations' ) |
593 | ->getFullUrl( [ 'query' => $term ] ); |
594 | $form = Html::successBox( |
595 | $search->msg( 'translate-searchprofile-note', $href )->parse(), |
596 | 'plainlinks' |
597 | ); |
598 | |
599 | return false; |
600 | } |
601 | |
602 | if ( !$search->getSearchEngine()->supports( 'title-suffix-filter' ) ) { |
603 | return false; |
604 | } |
605 | |
606 | $hidden = ''; |
607 | foreach ( $opts as $key => $value ) { |
608 | $hidden .= Html::hidden( $key, $value ); |
609 | } |
610 | |
611 | $context = $search->getContext(); |
612 | $code = $context->getLanguage()->getCode(); |
613 | $selected = $context->getRequest()->getVal( 'languagefilter' ); |
614 | |
615 | $languages = Utilities::getLanguageNames( $code ); |
616 | ksort( $languages ); |
617 | |
618 | $selector = new XmlSelect( 'languagefilter', 'languagefilter' ); |
619 | $selector->setDefault( $selected ); |
620 | $selector->addOption( wfMessage( 'translate-search-nofilter' )->text(), '-' ); |
621 | foreach ( $languages as $code => $name ) { |
622 | $selector->addOption( "$code - $name", $code ); |
623 | } |
624 | |
625 | $selector = $selector->getHTML(); |
626 | |
627 | $label = Xml::label( |
628 | wfMessage( 'translate-search-languagefilter' )->text(), |
629 | 'languagefilter' |
630 | ) . "\u{00A0}"; |
631 | $params = [ 'id' => 'mw-searchoptions' ]; |
632 | |
633 | $form = Xml::fieldset( false, false, $params ) . |
634 | $hidden . $label . $selector . |
635 | Html::closeElement( 'fieldset' ); |
636 | |
637 | return false; |
638 | } |
639 | |
640 | /** Hook: SpecialSearchSetupEngine */ |
641 | public static function searchProfileSetupEngine( |
642 | SpecialSearch $search, |
643 | string $profile, |
644 | SearchEngine $engine |
645 | ): void { |
646 | if ( $profile !== 'translation' ) { |
647 | return; |
648 | } |
649 | |
650 | $context = $search->getContext(); |
651 | $selected = $context->getRequest()->getVal( 'languagefilter' ); |
652 | if ( $selected !== '-' && $selected ) { |
653 | $engine->setFeatureData( 'title-suffix-filter', "/$selected" ); |
654 | $search->setExtraParam( 'languagefilter', $selected ); |
655 | } |
656 | } |
657 | |
658 | /** Hook: ParserAfterTidy */ |
659 | public static function preventCategorization( Parser $parser, string &$html ): void { |
660 | $pageReference = $parser->getPage(); |
661 | if ( !$pageReference ) { |
662 | return; |
663 | } |
664 | |
665 | $linkTarget = TitleValue::newFromPage( $pageReference ); |
666 | $handle = new MessageHandle( $linkTarget ); |
667 | if ( $handle->isMessageNamespace() && !$handle->isDoc() ) { |
668 | $parserOutput = $parser->getOutput(); |
669 | $names = $parserOutput->getCategoryNames(); |
670 | $parserCategories = []; |
671 | foreach ( $names as $name ) { |
672 | $parserCategories[$name] = $parserOutput->getCategorySortKey( $name ); |
673 | } |
674 | $parserOutput->setExtensionData( 'translate-fake-categories', $parserCategories ); |
675 | $parserOutput->setCategories( [] ); |
676 | } |
677 | } |
678 | |
679 | /** Hook: OutputPageParserOutput */ |
680 | public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ): void { |
681 | $fakeCategories = $parserOutput->getExtensionData( 'translate-fake-categories' ); |
682 | if ( $fakeCategories ) { |
683 | $outputPage->setCategoryLinks( $fakeCategories ); |
684 | } |
685 | } |
686 | |
687 | /** |
688 | * Hook: MakeGlobalVariablesScript |
689 | * Adds $wgTranslateDocumentationLanguageCode to ResourceLoader configuration |
690 | * when Special:Translate is shown. |
691 | */ |
692 | public static function addConfig( array &$vars, OutputPage $out ): void { |
693 | global $wgTranslateDocumentationLanguageCode, |
694 | $wgTranslatePermissionUrl, |
695 | $wgTranslateUseSandbox; |
696 | |
697 | $title = $out->getTitle(); |
698 | if ( $title->isSpecial( 'Translate' ) || |
699 | $title->isSpecial( 'TranslationStash' ) || |
700 | $title->isSpecial( 'SearchTranslations' ) |
701 | ) { |
702 | $user = $out->getUser(); |
703 | $vars['TranslateRight'] = $user->isAllowed( 'translate' ); |
704 | $vars['TranslateMessageReviewRight'] = $user->isAllowed( 'translate-messagereview' ); |
705 | $vars['DeleteRight'] = $user->isAllowed( 'delete' ); |
706 | $vars['TranslateManageRight'] = $user->isAllowed( 'translate-manage' ); |
707 | $vars['wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode; |
708 | $vars['wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl; |
709 | $vars['wgTranslateUseSandbox'] = $wgTranslateUseSandbox; |
710 | } |
711 | } |
712 | |
713 | /** Hook: AdminLinks */ |
714 | public static function onAdminLinks( ALTree $tree ): void { |
715 | global $wgTranslateUseSandbox; |
716 | |
717 | if ( $wgTranslateUseSandbox ) { |
718 | $sectionLabel = wfMessage( 'adminlinks_users' )->text(); |
719 | $row = $tree->getSection( $sectionLabel )->getRow( 'main' ); |
720 | $row->addItem( ALItem::newFromSpecialPage( 'TranslateSandbox' ) ); |
721 | } |
722 | } |
723 | |
724 | /** |
725 | * Hook: MergeAccountFromTo |
726 | * For UserMerge extension. |
727 | */ |
728 | public static function onMergeAccountFromTo( User $oldUser, User $newUser ): void { |
729 | $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY ); |
730 | |
731 | // Update the non-duplicate rows, we'll just delete |
732 | // the duplicate ones later |
733 | foreach ( self::USER_MERGE_TABLES as $table => $field ) { |
734 | if ( $dbw->tableExists( $table, __METHOD__ ) ) { |
735 | $dbw->update( |
736 | $table, |
737 | [ $field => $newUser->getId() ], |
738 | [ $field => $oldUser->getId() ], |
739 | __METHOD__, |
740 | [ 'IGNORE' ] |
741 | ); |
742 | } |
743 | } |
744 | } |
745 | |
746 | /** |
747 | * Hook: DeleteAccount |
748 | * For UserMerge extension. |
749 | */ |
750 | public static function onDeleteAccount( User $oldUser ): void { |
751 | $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY ); |
752 | |
753 | // Delete any remaining rows that didn't get merged |
754 | foreach ( self::USER_MERGE_TABLES as $table => $field ) { |
755 | if ( $dbw->tableExists( $table, __METHOD__ ) ) { |
756 | $dbw->delete( |
757 | $table, |
758 | [ $field => $oldUser->getId() ], |
759 | __METHOD__ |
760 | ); |
761 | } |
762 | } |
763 | } |
764 | |
765 | /** Hook: AbortEmailNotification */ |
766 | public static function onAbortEmailNotificationReview( |
767 | User $editor, |
768 | Title $title, |
769 | RecentChange $rc |
770 | ): bool { |
771 | return $rc->getAttribute( 'rc_log_type' ) !== 'translationreview'; |
772 | } |
773 | |
774 | /** |
775 | * Hook: TitleIsAlwaysKnown |
776 | * Make Special:MyLanguage links red if the target page doesn't exist. |
777 | * A bit hacky because the core code is not so flexible. |
778 | * @param Title $target Title object that is being checked |
779 | * @param bool|null &$isKnown Whether MediaWiki currently thinks this page is known |
780 | * @return bool True or no return value to continue or false to abort |
781 | */ |
782 | public static function onTitleIsAlwaysKnown( $target, &$isKnown ): bool { |
783 | if ( !$target->inNamespace( NS_SPECIAL ) ) { |
784 | return true; |
785 | } |
786 | |
787 | [ $name, $subpage ] = MediaWikiServices::getInstance() |
788 | ->getSpecialPageFactory()->resolveAlias( $target->getDBkey() ); |
789 | if ( $name !== 'MyLanguage' || (string)$subpage === '' ) { |
790 | return true; |
791 | } |
792 | |
793 | $realTarget = Title::newFromText( $subpage ); |
794 | if ( !$realTarget || !$realTarget->exists() ) { |
795 | $isKnown = false; |
796 | |
797 | return false; |
798 | } |
799 | |
800 | return true; |
801 | } |
802 | |
803 | public function onParserFirstCallInit( $parser ) { |
804 | $parser->setFunctionHook( 'translation', [ $this, 'translateRenderParserFunction' ] ); |
805 | } |
806 | |
807 | public function translateRenderParserFunction( Parser $parser ): string { |
808 | $pageReference = $parser->getPage(); |
809 | if ( !$pageReference ) { |
810 | return ''; |
811 | } |
812 | $linkTarget = TitleValue::newFromPage( $pageReference ); |
813 | $handle = new MessageHandle( $linkTarget ); |
814 | $code = $handle->getCode(); |
815 | if ( $this->languageNameUtils->isKnownLanguageTag( $code ) ) { |
816 | return '/' . $code; |
817 | } |
818 | return ''; |
819 | } |
820 | |
821 | /** |
822 | * Runs the configured validator to ensure that the message meets the required criteria. |
823 | * Hook: EditFilterMergedContent |
824 | * @return bool true if message is valid, false otherwise. |
825 | */ |
826 | public static function validateMessage( |
827 | IContextSource $context, |
828 | Content $content, |
829 | Status $status, |
830 | string $summary, |
831 | User $user |
832 | ): bool { |
833 | if ( !$content instanceof TextContent ) { |
834 | // Not interested |
835 | return true; |
836 | } |
837 | |
838 | $text = $content->getText(); |
839 | $title = $context->getTitle(); |
840 | $handle = new MessageHandle( $title ); |
841 | |
842 | if ( !$handle->isValid() ) { |
843 | return true; |
844 | } |
845 | |
846 | // Don't bother validating if FuzzyBot or translation admin are saving. |
847 | if ( $user->isAllowed( 'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) { |
848 | return true; |
849 | } |
850 | |
851 | // Check the namespace, and perform validations for all messages excluding documentation. |
852 | if ( $handle->isMessageNamespace() && !$handle->isDoc() ) { |
853 | $group = $handle->getGroup(); |
854 | |
855 | if ( method_exists( $group, 'getMessageContent' ) ) { |
856 | // @phan-suppress-next-line PhanUndeclaredMethod |
857 | $definition = $group->getMessageContent( $handle ); |
858 | } else { |
859 | $definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() ); |
860 | } |
861 | |
862 | $message = new FatMessage( $handle->getKey(), $definition ); |
863 | $message->setTranslation( $text ); |
864 | |
865 | $messageValidator = $group->getValidator(); |
866 | if ( !$messageValidator ) { |
867 | return true; |
868 | } |
869 | |
870 | $validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() ); |
871 | if ( $validationResponse->hasErrors() ) { |
872 | $status->fatal( new ApiRawMessage( |
873 | $context->msg( 'translate-syntax-error' )->parse(), |
874 | 'translate-validation-failed', |
875 | [ |
876 | 'validation' => [ |
877 | 'errors' => $validationResponse->getDescriptiveErrors( $context ), |
878 | 'warnings' => $validationResponse->getDescriptiveWarnings( $context ) |
879 | ] |
880 | ] |
881 | ) ); |
882 | return false; |
883 | } |
884 | } |
885 | |
886 | return true; |
887 | } |
888 | |
889 | /** @inheritDoc */ |
890 | public function onRevisionRecordInserted( $revisionRecord ): void { |
891 | $parentId = $revisionRecord->getParentId(); |
892 | if ( $parentId === 0 || $parentId === null ) { |
893 | // No parent, bail out. |
894 | return; |
895 | } |
896 | |
897 | $prevRev = $this->revisionLookup->getRevisionById( $parentId ); |
898 | if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) { |
899 | // Not a null revision, bail out. |
900 | return; |
901 | } |
902 | |
903 | // List of tags that should be copied over when updating |
904 | // tp:tag and tp:mark handling is in Hooks::updateTranstagOnNullRevisions. |
905 | $tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ]; |
906 | |
907 | $db = $this->loadBalancer->getConnection( DB_PRIMARY ); |
908 | $db->insertSelect( |
909 | 'revtag', |
910 | 'revtag', |
911 | [ |
912 | 'rt_type' => 'rt_type', |
913 | 'rt_page' => 'rt_page', |
914 | 'rt_revision' => $revisionRecord->getId(), |
915 | 'rt_value' => 'rt_value', |
916 | |
917 | ], |
918 | [ |
919 | 'rt_type' => $tagsToCopy, |
920 | 'rt_revision' => $parentId, |
921 | ], |
922 | __METHOD__ |
923 | ); |
924 | } |
925 | |
926 | /** @inheritDoc */ |
927 | public function onListDefinedTags( &$tags ): void { |
928 | $tags[] = 'translate-translation-pages'; |
929 | } |
930 | |
931 | /** @inheritDoc */ |
932 | public function onChangeTagsListActive( &$tags ): void { |
933 | if ( $this->config->get( 'EnablePageTranslation' ) ) { |
934 | $tags[] = 'translate-translation-pages'; |
935 | } |
936 | } |
937 | } |