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