src/HookHandler.php
<?php
declare( strict_types=1 );
namespace MediaWiki\Extension\Translate;
use ALItem;
use ALTree;
use ApiRawMessage;
use Content;
use ExtensionRegistry;
use Language;
use LogFormatter;
use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
use MediaWiki\Config\Config;
use MediaWiki\Config\ConfigException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Context\IContextSource;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Extension\Translate\LogFormatter as TranslateLogFormatter;
use MediaWiki\Extension\Translate\MessageBundleTranslation\ScribuntoHookHandler;
use MediaWiki\Extension\Translate\MessageGroupProcessing\DeleteTranslatableBundleJob;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupSubscriptionHookHandler;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroupSubscriptionNotificationJob;
use MediaWiki\Extension\Translate\MessageGroupProcessing\MoveTranslatableBundleJob;
use MediaWiki\Extension\Translate\MessageGroupProcessing\RevTagStore;
use MediaWiki\Extension\Translate\MessageGroupProcessing\TranslatableBundleLogFormatter;
use MediaWiki\Extension\Translate\MessageLoading\FatMessage;
use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
use MediaWiki\Extension\Translate\MessageLoading\RebuildMessageIndexJob;
use MediaWiki\Extension\Translate\PageTranslation\DeleteTranslatableBundleSpecialPage;
use MediaWiki\Extension\Translate\PageTranslation\Hooks;
use MediaWiki\Extension\Translate\PageTranslation\MarkForTranslationActionApi;
use MediaWiki\Extension\Translate\PageTranslation\MigrateTranslatablePageSpecialPage;
use MediaWiki\Extension\Translate\PageTranslation\PageTranslationSpecialPage;
use MediaWiki\Extension\Translate\PageTranslation\PrepareTranslatablePageSpecialPage;
use MediaWiki\Extension\Translate\PageTranslation\RenderTranslationPageJob;
use MediaWiki\Extension\Translate\PageTranslation\UpdateTranslatablePageJob;
use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot;
use MediaWiki\Extension\Translate\SystemUsers\TranslateUserManager;
use MediaWiki\Extension\Translate\TranslatorInterface\TranslateEditAddons;
use MediaWiki\Extension\Translate\TranslatorSandbox\ManageTranslatorSandboxSpecialPage;
use MediaWiki\Extension\Translate\TranslatorSandbox\TranslateSandboxEmailJob;
use MediaWiki\Extension\Translate\TranslatorSandbox\TranslationStashActionApi;
use MediaWiki\Extension\Translate\TranslatorSandbox\TranslationStashSpecialPage;
use MediaWiki\Extension\Translate\TranslatorSandbox\TranslatorSandboxActionApi;
use MediaWiki\Extension\Translate\TtmServer\SearchableTtmServer;
use MediaWiki\Extension\Translate\Utilities\Utilities;
use MediaWiki\Hook\ParserFirstCallInitHook;
use MediaWiki\Html\Html;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\MediaWikiServices;
use MediaWiki\Output\OutputPage;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Revision\Hook\RevisionRecordInsertedHook;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Settings\SettingsBuilder;
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Specials\SpecialSearch;
use MediaWiki\Status\Status;
use MediaWiki\StubObject\StubUserLang;
use MediaWiki\Title\Title;
use MediaWiki\Title\TitleValue;
use MediaWiki\User\Hook\UserGetReservedNamesHook;
use MediaWiki\User\User;
use RecentChange;
use SearchEngine;
use TextContent;
use Wikimedia\Rdbms\ILoadBalancer;
use XmlSelect;
/**
* Hooks for Translate extension.
* Contains class with basic non-feature specific hooks.
* Main subsystems, like page translation, should have their own hook handler. *
* Most of the hooks on this class are still old style static functions, but new new hooks should
* use the new style hook handlers with interfaces.
*
* @author Niklas Laxström
* @license GPL-2.0-or-later
*/
class HookHandler implements
ChangeTagsListActiveHook,
ListDefinedTagsHook,
ParserFirstCallInitHook,
RevisionRecordInsertedHook,
UserGetReservedNamesHook
{
/**
* Any user of this list should make sure that the tables
* actually exist, since they may be optional
*/
private const USER_MERGE_TABLES = [
'translate_stash' => 'ts_user',
'translate_reviews' => 'trr_user',
];
private RevisionLookup $revisionLookup;
private ILoadBalancer $loadBalancer;
private Config $config;
private LanguageNameUtils $languageNameUtils;
public function __construct(
RevisionLookup $revisionLookup,
ILoadBalancer $loadBalancer,
Config $config,
LanguageNameUtils $languageNameUtils
) {
$this->revisionLookup = $revisionLookup;
$this->loadBalancer = $loadBalancer;
$this->config = $config;
$this->languageNameUtils = $languageNameUtils;
}
/** Do late setup that depends on configuration. */
public static function setupTranslate(): void {
global $wgTranslateYamlLibrary, $wgLogTypes;
$hooks = [];
/*
* Text that will be shown in translations if the translation is outdated.
* Must be something that does not conflict with actual content.
*/
if ( !defined( 'TRANSLATE_FUZZY' ) ) {
define( 'TRANSLATE_FUZZY', '!!FUZZY!!' );
}
$wgTranslateYamlLibrary ??= function_exists( 'yaml_parse' ) ? 'phpyaml' : 'spyc';
$hooks['PageSaveComplete'][] = [ TranslateEditAddons::class, 'onSaveComplete' ];
global $wgJobClasses;
$wgJobClasses['MessageIndexRebuildJob'] = RebuildMessageIndexJob::class;
$wgJobClasses['RebuildMessageIndexJob'] = RebuildMessageIndexJob::class;
// Page translation setup check and init if enabled.
global $wgEnablePageTranslation;
if ( $wgEnablePageTranslation ) {
// Special page and the right to use it
global $wgSpecialPages, $wgAvailableRights;
$wgSpecialPages['PageTranslation'] = [
'class' => PageTranslationSpecialPage::class,
'services' => [
'LanguageFactory',
'LinkBatchFactory',
'JobQueueGroup',
'PermissionManager',
'Translate:TranslatablePageMarker',
'Translate:TranslatablePageParser',
'Translate:MessageGroupMetadata',
'Translate:TranslatablePageView',
'Translate:TranslatablePageStateStore'
]
];
$wgSpecialPages['PageTranslationDeletePage'] = [
'class' => DeleteTranslatableBundleSpecialPage::class,
'services' => [
'PermissionManager',
'Translate:TranslatableBundleDeleter',
'Translate:TranslatableBundleFactory',
]
];
// right-pagetranslation action-pagetranslation
$wgAvailableRights[] = 'pagetranslation';
$wgSpecialPages['PageMigration'] = MigrateTranslatablePageSpecialPage::class;
$wgSpecialPages['PagePreparation'] = PrepareTranslatablePageSpecialPage::class;
global $wgActionFilteredLogs, $wgLogActionsHandlers;
// log-description-pagetranslation log-name-pagetranslation logentry-pagetranslation-mark
// logentry-pagetranslation-unmark logentry-pagetranslation-moveok
// logentry-pagetranslation-movenok logentry-pagetranslation-deletefok
// logentry-pagetranslation-deletefnok logentry-pagetranslation-deletelok
// logentry-pagetranslation-deletelnok logentry-pagetranslation-encourage
// logentry-pagetranslation-discourage logentry-pagetranslation-prioritylanguages
// logentry-pagetranslation-associate logentry-pagetranslation-dissociate
if ( !in_array( 'pagetranslation', $wgLogTypes ) ) {
$wgLogTypes[] = 'pagetranslation';
}
$wgLogActionsHandlers['pagetranslation/mark'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/unmark'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/moveok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/movenok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/deletelok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/deletefok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/deletelnok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/deletefnok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/encourage'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/discourage'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/prioritylanguages'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/associate'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['pagetranslation/dissociate'] = TranslatableBundleLogFormatter::class;
$wgActionFilteredLogs['pagetranslation'] = [
'mark' => [ 'mark' ],
'unmark' => [ 'unmark' ],
'move' => [ 'moveok', 'movenok' ],
'delete' => [ 'deletefok', 'deletefnok', 'deletelok', 'deletelnok' ],
'encourage' => [ 'encourage' ],
'discourage' => [ 'discourage' ],
'prioritylanguages' => [ 'prioritylanguages' ],
'aggregategroups' => [ 'associate', 'dissociate' ],
];
if ( !in_array( 'messagebundle', $wgLogTypes ) ) {
$wgLogTypes[] = 'messagebundle';
}
$wgLogActionsHandlers['messagebundle/moveok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['messagebundle/movenok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['messagebundle/deletefok'] = TranslatableBundleLogFormatter::class;
$wgLogActionsHandlers['messagebundle/deletefnok'] = TranslatableBundleLogFormatter::class;
$wgActionFilteredLogs['messagebundle'] = [
'move' => [ 'moveok', 'movenok' ],
'delete' => [ 'deletefok', 'deletefnok' ],
];
$wgLogActionsHandlers['import/translatable-bundle'] = TranslatableBundleLogFormatter::class;
$wgJobClasses['RenderTranslationPageJob'] = RenderTranslationPageJob::class;
$wgJobClasses['NonPrioritizedRenderTranslationPageJob'] = RenderTranslationPageJob::class;
$wgJobClasses['MoveTranslatableBundleJob'] = MoveTranslatableBundleJob::class;
$wgJobClasses['DeleteTranslatableBundleJob'] = DeleteTranslatableBundleJob::class;
$wgJobClasses['UpdateTranslatablePageJob'] = UpdateTranslatablePageJob::class;
// API modules
global $wgAPIModules;
$wgAPIModules['markfortranslation'] = [
'class' => MarkForTranslationActionApi::class,
'services' => [
'Translate:TranslatablePageMarker',
'Translate:MessageGroupMetadata',
]
];
// Namespaces
global $wgNamespacesWithSubpages, $wgNamespaceProtection;
global $wgTranslateMessageNamespaces;
$wgNamespacesWithSubpages[NS_TRANSLATIONS] = true;
$wgNamespacesWithSubpages[NS_TRANSLATIONS_TALK] = true;
// Standard protection and register it for filtering
$wgNamespaceProtection[NS_TRANSLATIONS] = [ 'translate' ];
$wgTranslateMessageNamespaces[] = NS_TRANSLATIONS;
/// Page translation hooks
/// Register our CSS and metadata
$hooks['BeforePageDisplay'][] = [ Hooks::class, 'onBeforePageDisplay' ];
// Disable VE
$hooks['VisualEditorBeforeEditor'][] = [ Hooks::class, 'onVisualEditorBeforeEditor' ];
// Check syntax for \<translate>
$hooks['MultiContentSave'][] = [ Hooks::class, 'tpSyntaxCheck' ];
$hooks['EditFilterMergedContent'][] =
[ Hooks::class, 'tpSyntaxCheckForEditContent' ];
// Add transtag to page props for discovery
$hooks['PageSaveComplete'][] = [ Hooks::class, 'addTranstagAfterSave' ];
$hooks['RevisionRecordInserted'][] = [ Hooks::class, 'updateTranstagOnNullRevisions' ];
// Register different ways to show language links
$hooks['ParserFirstCallInit'][] = [ self::class, 'setupParserHooks' ];
$hooks['LanguageLinks'][] = [ Hooks::class, 'addLanguageLinks' ];
$hooks['SkinTemplateGetLanguageLink'][] = [ Hooks::class, 'formatLanguageLink' ];
// Allow templates to query whether they are transcluded in a translatable/translated page
$hooks['GetMagicVariableIDs'][] = [ Hooks::class, 'onGetMagicVariableIDs' ];
$hooks['ParserGetVariableValueSwitch'][] = [ Hooks::class, 'onParserGetVariableValueSwitch' ];
// Strip \<translate> tags etc. from source pages when rendering
$hooks['ParserBeforeInternalParse'][] = [ Hooks::class, 'renderTagPage' ];
// Strip \<translate> tags etc. from source pages when preprocessing
$hooks['ParserBeforePreprocess'][] = [ Hooks::class, 'preprocessTagPage' ];
$hooks['ParserOutputPostCacheTransform'][] =
[ Hooks::class, 'onParserOutputPostCacheTransform' ];
$hooks['BeforeParserFetchTemplateRevisionRecord'][] =
[ Hooks::class, 'fetchTranslatableTemplateAndTitle' ];
// Set the page content language
$hooks['PageContentLanguage'][] = [ Hooks::class, 'onPageContentLanguage' ];
// Prevent editing of certain pages in translations namespace
$hooks['getUserPermissionsErrorsExpensive'][] =
[ Hooks::class, 'onGetUserPermissionsErrorsExpensive' ];
// Prevent editing of translation pages directly
$hooks['getUserPermissionsErrorsExpensive'][] =
[ Hooks::class, 'preventDirectEditing' ];
// Our custom header for translation pages
$hooks['ArticleViewHeader'][] = [ Hooks::class, 'translatablePageHeader' ];
// Edit notice shown on translatable pages
$hooks['TitleGetEditNotices'][] = [ Hooks::class, 'onTitleGetEditNotices' ];
// Custom move page that can move all the associated pages too
$hooks['SpecialPage_initList'][] = [ Hooks::class, 'replaceMovePage' ];
// Locking during page moves
$hooks['getUserPermissionsErrorsExpensive'][] =
[ Hooks::class, 'lockedPagesCheck' ];
// Disable action=delete
$hooks['ArticleConfirmDelete'][] = [ Hooks::class, 'disableDelete' ];
// Replace subpage logic behavior
$hooks['SkinSubPageSubtitle'][] = [ Hooks::class, 'replaceSubtitle' ];
// Replaced edit tab with translation tab for translation pages
$hooks['SkinTemplateNavigation::Universal'][] = [ Hooks::class, 'translateTab' ];
// Update translated page when translation unit is moved
$hooks['PageMoveComplete'][] = [ Hooks::class, 'onMovePageTranslationUnits' ];
// Update translated page when translation unit is deleted
$hooks['ArticleDeleteComplete'][] = [ Hooks::class, 'onDeleteTranslationUnit' ];
// Prevent editing of translation pages.
$hooks['ReplaceTextFilterPageTitlesForEdit'][] = [ Hooks::class, 'onReplaceTextFilterPageTitlesForEdit' ];
// Prevent renaming of translatable pages and their translation and translation units
$hooks['ReplaceTextFilterPageTitlesForRename'][] =
[ Hooks::class, 'onReplaceTextFilterPageTitlesForRename' ];
}
global $wgTranslateUseSandbox;
if ( $wgTranslateUseSandbox ) {
global $wgSpecialPages, $wgAvailableRights, $wgDefaultUserOptions;
$wgSpecialPages['ManageTranslatorSandbox'] = [
'class' => ManageTranslatorSandboxSpecialPage::class,
'services' => [
'Translate:TranslationStashReader',
'UserOptionsLookup',
'Translate:TranslateSandbox',
],
'args' => [
static function () {
return new ServiceOptions(
ManageTranslatorSandboxSpecialPage::CONSTRUCTOR_OPTIONS,
MediaWikiServices::getInstance()->getMainConfig()
);
}
]
];
$wgSpecialPages['TranslationStash'] = [
'class' => TranslationStashSpecialPage::class,
'services' => [
'LanguageNameUtils',
'Translate:TranslationStashReader',
'UserOptionsLookup',
'LanguageFactory',
],
'args' => [
static function () {
return new ServiceOptions(
TranslationStashSpecialPage::CONSTRUCTOR_OPTIONS,
MediaWikiServices::getInstance()->getMainConfig()
);
}
]
];
$wgDefaultUserOptions['translate-sandbox'] = '';
// right-translate-sandboxmanage action-translate-sandboxmanage
$wgAvailableRights[] = 'translate-sandboxmanage';
global $wgLogTypes, $wgLogActionsHandlers;
// log-name-translatorsandbox log-description-translatorsandbox
if ( !in_array( 'translatorsandbox', $wgLogTypes ) ) {
$wgLogTypes[] = 'translatorsandbox';
}
// logentry-translatorsandbox-promoted logentry-translatorsandbox-rejected
$wgLogActionsHandlers['translatorsandbox/promoted'] = TranslateLogFormatter::class;
$wgLogActionsHandlers['translatorsandbox/rejected'] = TranslateLogFormatter::class;
// This is no longer used for new entries since 2016.07.
// logentry-newusers-tsbpromoted
$wgLogActionsHandlers['newusers/tsbpromoted'] = LogFormatter::class;
$wgJobClasses['TranslateSandboxEmailJob'] = TranslateSandboxEmailJob::class;
global $wgAPIModules;
$wgAPIModules['translationstash'] = [
'class' => TranslationStashActionApi::class,
'services' => [
'DBLoadBalancerFactory',
'UserFactory',
'Translate:MessageIndex'
]
];
$wgAPIModules['translatesandbox'] = [
'class' => TranslatorSandboxActionApi::class,
'services' => [
'UserFactory',
'UserNameUtils',
'UserOptionsManager',
'WikiPageFactory',
'UserOptionsLookup',
'Translate:TranslateSandbox',
],
'args' => [
static function () {
return new ServiceOptions(
TranslatorSandboxActionApi::CONSTRUCTOR_OPTIONS,
MediaWikiServices::getInstance()->getMainConfig()
);
}
]
];
}
global $wgNamespaceRobotPolicies;
$wgNamespaceRobotPolicies[NS_TRANSLATIONS] = 'noindex';
// If no service has been configured, we use a built-in fallback.
global $wgTranslateTranslationDefaultService,
$wgTranslateTranslationServices;
if ( $wgTranslateTranslationDefaultService === true ) {
$wgTranslateTranslationDefaultService = 'TTMServer';
if ( !isset( $wgTranslateTranslationServices['TTMServer'] ) ) {
$wgTranslateTranslationServices['TTMServer'] = [
'database' => false,
'cutoff' => 0.75,
'type' => 'ttmserver',
'public' => false,
];
}
}
global $wgTranslateEnableMessageGroupSubscription;
if ( $wgTranslateEnableMessageGroupSubscription ) {
if ( !ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) ) {
throw new ConfigException(
'Translate: Message group subscriptions (TranslateEnableMessageGroupSubscription) are ' .
'enabled but Echo extension is not installed'
);
}
MessageGroupSubscriptionHookHandler::registerHooks( $hooks );
$wgJobClasses['MessageGroupSubscriptionNotificationJob'] = MessageGroupSubscriptionNotificationJob::class;
}
global $wgTranslateEnableEventLogging;
if ( $wgTranslateEnableEventLogging ) {
if ( !ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) {
throw new ConfigException(
'Translate: Event logging (TranslateEnableEventLogging) is ' .
'enabled but EventLogging extension is not installed'
);
}
}
global $wgTranslateEnableLuaIntegration;
if ( $wgTranslateEnableLuaIntegration ) {
if ( ExtensionRegistry::getInstance()->isLoaded( 'Scribunto' ) ) {
$hooks[ 'ScribuntoExternalLibraries' ][] = static function ( string $engine, array &$extraLibraries ) {
$scribuntoHookHandler = new ScribuntoHookHandler();
$scribuntoHookHandler->onScribuntoExternalLibraries( $engine, $extraLibraries );
};
} else {
wfLogWarning(
'Translate: Lua integration (TranslateEnableLuaIntegration) is ' .
'enabled but Scribunto extension is not installed'
);
}
}
static::registerHookHandlers( $hooks );
}
private static function registerHookHandlers( array $hooks ): void {
if ( defined( 'MW_PHPUNIT_TEST' ) && MediaWikiServices::hasInstance() ) {
// When called from a test case's setUp() method,
// we can use HookContainer, but we cannot use SettingsBuilder.
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
foreach ( $hooks as $name => $handlers ) {
foreach ( $handlers as $h ) {
$hookContainer->register( $name, $h );
}
}
} else {
$settingsBuilder = SettingsBuilder::getInstance();
$settingsBuilder->registerHookHandlers( $hooks );
}
}
/**
* Prevents anyone from registering or logging in as FuzzyBot
* @inheritDoc
*/
public function onUserGetReservedNames( &$reservedUsernames ): void {
$reservedUsernames[] = FuzzyBot::getName();
$reservedUsernames[] = TranslateUserManager::getName();
}
/** Used for setting an AbuseFilter variable. */
public static function onAbuseFilterAlterVariables(
VariableHolder &$vars, Title $title, User $user
): void {
$handle = new MessageHandle( $title );
// Only set this variable if we are in a proper namespace to avoid
// unnecessary overhead in non-translation pages
if ( $handle->isMessageNamespace() ) {
$vars->setLazyLoadVar(
'translate_source_text',
'translate-get-source',
[ 'handle' => $handle ]
);
$vars->setLazyLoadVar(
'translate_target_language',
'translate-get-target-language',
[ 'handle' => $handle ]
);
}
}
/** Computes the translate_source_text and translate_target_language AbuseFilter variables */
public static function onAbuseFilterComputeVariable(
string $method,
VariableHolder $vars,
array $parameters,
?string &$result
): bool {
if ( $method !== 'translate-get-source' && $method !== 'translate-get-target-language' ) {
return true;
}
$handle = $parameters['handle'];
$value = '';
if ( $handle->isValid() ) {
if ( $method === 'translate-get-source' ) {
$group = $handle->getGroup();
$value = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
} else {
$value = $handle->getCode();
}
}
$result = $value;
return false;
}
/** Register AbuseFilter variables provided by Translate. */
public static function onAbuseFilterBuilder( array &$builderValues ): void {
// The following messages are generated here:
// * abusefilter-edit-builder-vars-translate-source-text
// * abusefilter-edit-builder-vars-translate-target-language
$builderValues['vars']['translate_source_text'] = 'translate-source-text';
$builderValues['vars']['translate_target_language'] = 'translate-target-language';
}
/**
* Hook: ParserFirstCallInit
* Registers \<languages> tag with the parser.
*/
public static function setupParserHooks( Parser $parser ): void {
// For nice language list in-page
$parser->setHook( 'languages', [ Hooks::class, 'languages' ] );
}
/**
* Hook: PageContentLanguage
* Set the correct page content language for translation units.
* @param Title $title
* @param Language|StubUserLang|string &$pageLang
*/
public static function onPageContentLanguage( Title $title, &$pageLang ): void {
$handle = new MessageHandle( $title );
if ( $handle->isMessageNamespace() ) {
$pageLang = $handle->getEffectiveLanguage();
}
}
/**
* Hook: LanguageGetTranslatedLanguageNames
* Hook: TranslateSupportedLanguages
*/
public static function translateMessageDocumentationLanguage( array &$names, ?string $code ): void {
global $wgTranslateDocumentationLanguageCode;
if ( $wgTranslateDocumentationLanguageCode ) {
// Special case the autonyms
if (
$wgTranslateDocumentationLanguageCode === $code ||
$code === null
) {
$code = 'en';
}
$names[$wgTranslateDocumentationLanguageCode] =
wfMessage( 'translate-documentation-language' )->inLanguage( $code )->plain();
}
}
/** Hook: SpecialSearchProfiles */
public static function searchProfile( array &$profiles ): void {
global $wgTranslateMessageNamespaces;
$insert = [];
$insert['translation'] = [
'message' => 'translate-searchprofile',
'tooltip' => 'translate-searchprofile-tooltip',
'namespaces' => $wgTranslateMessageNamespaces,
];
// Insert translations before 'all'
$index = array_search( 'all', array_keys( $profiles ) );
// Or just at the end if all is not found
if ( $index === false ) {
wfWarn( '"all" not found in search profiles' );
$index = count( $profiles );
}
$profiles = array_merge(
array_slice( $profiles, 0, $index ),
$insert,
array_slice( $profiles, $index )
);
}
/** Hook: SpecialSearchProfileForm */
public static function searchProfileForm(
SpecialSearch $search,
string &$form,
string $profile,
string $term,
array $opts
): void {
if ( $profile !== 'translation' ) {
return;
}
if ( Services::getInstance()->getTtmServerFactory()->getDefaultForQuerying() instanceof SearchableTtmServer ) {
$href = SpecialPage::getTitleFor( 'SearchTranslations' )
->getFullUrl( [ 'query' => $term ] );
$form = Html::successBox(
$search->msg( 'translate-searchprofile-note', $href )->parse(),
'plainlinks'
);
return;
}
if ( !$search->getSearchEngine()->supports( 'title-suffix-filter' ) ) {
return;
}
$hidden = '';
foreach ( $opts as $key => $value ) {
$hidden .= Html::hidden( $key, $value );
}
$context = $search->getContext();
$code = $context->getLanguage()->getCode();
$selected = $context->getRequest()->getVal( 'languagefilter' );
$languages = Utilities::getLanguageNames( $code );
ksort( $languages );
$selector = new XmlSelect( 'languagefilter', 'languagefilter' );
$selector->setDefault( $selected );
$selector->addOption( wfMessage( 'translate-search-nofilter' )->text(), '-' );
foreach ( $languages as $code => $name ) {
$selector->addOption( "$code - $name", $code );
}
$selector = $selector->getHTML();
$label = Html::label(
wfMessage( 'translate-search-languagefilter' )->text(),
'languagefilter'
) . "\u{00A0}";
$form .= Html::rawElement(
'fieldset',
[ 'id' => 'mw-searchoptions' ],
$hidden . $label . $selector
);
}
/** Hook: SpecialSearchSetupEngine */
public static function searchProfileSetupEngine(
SpecialSearch $search,
string $profile,
SearchEngine $engine
): void {
if ( $profile !== 'translation' ) {
return;
}
$context = $search->getContext();
$selected = $context->getRequest()->getVal( 'languagefilter' );
if ( $selected !== '-' && $selected ) {
$engine->setFeatureData( 'title-suffix-filter', "/$selected" );
$search->setExtraParam( 'languagefilter', $selected );
}
}
/** Hook: ParserAfterTidy */
public static function preventCategorization( Parser $parser, string &$html ): void {
if ( $parser->getOptions()->getInterfaceMessage() ) {
return;
}
$pageReference = $parser->getPage();
if ( !$pageReference ) {
return;
}
$linkTarget = TitleValue::newFromPage( $pageReference );
$handle = new MessageHandle( $linkTarget );
if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
$parserOutput = $parser->getOutput();
$names = $parserOutput->getCategoryNames();
$parserCategories = [];
foreach ( $names as $name ) {
$parserCategories[$name] = $parserOutput->getCategorySortKey( $name );
}
$parserOutput->setExtensionData( 'translate-fake-categories', $parserCategories );
$parserOutput->setCategories( [] );
}
}
/** Hook: OutputPageParserOutput */
public static function showFakeCategories( OutputPage $outputPage, ParserOutput $parserOutput ): void {
$fakeCategories = $parserOutput->getExtensionData( 'translate-fake-categories' );
if ( $fakeCategories ) {
$outputPage->addCategoryLinks( $fakeCategories );
}
}
/**
* Hook: MakeGlobalVariablesScript
* Adds $wgTranslateDocumentationLanguageCode to ResourceLoader configuration
* when Special:Translate is shown.
*/
public static function addConfig( array &$vars, OutputPage $out ): void {
global $wgTranslateDocumentationLanguageCode,
$wgTranslatePermissionUrl,
$wgTranslateUseSandbox;
$title = $out->getTitle();
if ( $title->isSpecial( 'Translate' ) ||
$title->isSpecial( 'TranslationStash' ) ||
$title->isSpecial( 'SearchTranslations' )
) {
$user = $out->getUser();
$vars['TranslateRight'] = $user->isAllowed( 'translate' );
$vars['TranslateMessageReviewRight'] = $user->isAllowed( 'translate-messagereview' );
$vars['DeleteRight'] = $user->isAllowed( 'delete' );
$vars['TranslateManageRight'] = $user->isAllowed( 'translate-manage' );
$vars['wgTranslateDocumentationLanguageCode'] = $wgTranslateDocumentationLanguageCode;
$vars['wgTranslatePermissionUrl'] = $wgTranslatePermissionUrl;
$vars['wgTranslateUseSandbox'] = $wgTranslateUseSandbox;
}
}
/** Hook: AdminLinks */
public static function onAdminLinks( ALTree $tree ): void {
global $wgTranslateUseSandbox;
if ( $wgTranslateUseSandbox ) {
$sectionLabel = wfMessage( 'adminlinks_users' )->text();
$row = $tree->getSection( $sectionLabel )->getRow( 'main' );
$row->addItem( ALItem::newFromSpecialPage( 'TranslateSandbox' ) );
}
}
/**
* Hook: MergeAccountFromTo
* For UserMerge extension.
*/
public static function onMergeAccountFromTo( User $oldUser, User $newUser ): void {
$dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
// Update the non-duplicate rows, we'll just delete
// the duplicate ones later
foreach ( self::USER_MERGE_TABLES as $table => $field ) {
if ( $dbw->tableExists( $table, __METHOD__ ) ) {
$dbw->newUpdateQueryBuilder()
->update( $table )
->ignore()
->set( [ $field => $newUser->getId() ] )
->where( [ $field => $oldUser->getId() ] )
->caller( __METHOD__ )
->execute();
}
}
}
/**
* Hook: DeleteAccount
* For UserMerge extension.
*/
public static function onDeleteAccount( User $oldUser ): void {
$dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY );
// Delete any remaining rows that didn't get merged
foreach ( self::USER_MERGE_TABLES as $table => $field ) {
if ( $dbw->tableExists( $table, __METHOD__ ) ) {
$dbw->newDeleteQueryBuilder()
->deleteFrom( $table )
->where( [ $field => $oldUser->getId() ] )
->caller( __METHOD__ )
->execute();
}
}
}
/** Hook: AbortEmailNotification */
public static function onAbortEmailNotificationReview(
User $editor,
Title $title,
RecentChange $rc
): bool {
return $rc->getAttribute( 'rc_log_type' ) !== 'translationreview';
}
/**
* Hook: TitleIsAlwaysKnown
* Make Special:MyLanguage links red if the target page doesn't exist.
* A bit hacky because the core code is not so flexible.
* @param Title $target Title object that is being checked
* @param bool|null &$isKnown Whether MediaWiki currently thinks this page is known
* @return bool True or no return value to continue or false to abort
*/
public static function onTitleIsAlwaysKnown( $target, &$isKnown ): bool {
if ( !$target->inNamespace( NS_SPECIAL ) ) {
return true;
}
[ $name, $subpage ] = MediaWikiServices::getInstance()
->getSpecialPageFactory()->resolveAlias( $target->getDBkey() );
if ( $name !== 'MyLanguage' || (string)$subpage === '' ) {
return true;
}
$realTarget = Title::newFromText( $subpage );
if ( !$realTarget || !$realTarget->exists() ) {
$isKnown = false;
return false;
}
return true;
}
public function onParserFirstCallInit( $parser ) {
$parser->setFunctionHook( 'translation', [ $this, 'translateRenderParserFunction' ] );
}
public function translateRenderParserFunction( Parser $parser ): string {
if ( $parser->getOptions()->getInterfaceMessage() ) {
return '';
}
$pageReference = $parser->getPage();
if ( !$pageReference ) {
return '';
}
$linkTarget = TitleValue::newFromPage( $pageReference );
$handle = new MessageHandle( $linkTarget );
$code = $handle->getCode();
if ( $this->languageNameUtils->isKnownLanguageTag( $code ) ) {
return '/' . $code;
}
return '';
}
/**
* Runs the configured validator to ensure that the message meets the required criteria.
* Hook: EditFilterMergedContent
* @return bool true if message is valid, false otherwise.
*/
public static function validateMessage(
IContextSource $context,
Content $content,
Status $status,
string $summary,
User $user
): bool {
if ( !$content instanceof TextContent ) {
// Not interested
return true;
}
$text = $content->getText();
$title = $context->getTitle();
$handle = new MessageHandle( $title );
if ( !$handle->isValid() ) {
return true;
}
// Don't bother validating if FuzzyBot or translation admin are saving.
if ( $user->isAllowed( 'translate-manage' ) || $user->equals( FuzzyBot::getUser() ) ) {
return true;
}
// Check the namespace, and perform validations for all messages excluding documentation.
if ( $handle->isMessageNamespace() && !$handle->isDoc() ) {
$group = $handle->getGroup();
if ( method_exists( $group, 'getMessageContent' ) ) {
// @phan-suppress-next-line PhanUndeclaredMethod
$definition = $group->getMessageContent( $handle );
} else {
$definition = $group->getMessage( $handle->getKey(), $group->getSourceLanguage() );
}
$message = new FatMessage( $handle->getKey(), $definition );
$message->setTranslation( $text );
$messageValidator = $group->getValidator();
if ( !$messageValidator ) {
return true;
}
$validationResponse = $messageValidator->validateMessage( $message, $handle->getCode() );
if ( $validationResponse->hasErrors() ) {
$status->fatal( new ApiRawMessage(
$context->msg( 'translate-syntax-error' )->parse(),
'translate-validation-failed',
[
'validation' => [
'errors' => $validationResponse->getDescriptiveErrors( $context ),
'warnings' => $validationResponse->getDescriptiveWarnings( $context )
]
]
) );
return false;
}
}
return true;
}
/** @inheritDoc */
public function onRevisionRecordInserted( $revisionRecord ): void {
$parentId = $revisionRecord->getParentId();
if ( $parentId === 0 || $parentId === null ) {
// No parent, bail out.
return;
}
$prevRev = $this->revisionLookup->getRevisionById( $parentId );
if ( !$prevRev || !$revisionRecord->hasSameContent( $prevRev ) ) {
// Not a null revision, bail out.
return;
}
// List of tags that should be copied over when updating
// tp:tag and tp:mark handling is in Hooks::updateTranstagOnNullRevisions.
$tagsToCopy = [ RevTagStore::FUZZY_TAG, RevTagStore::TRANSVER_PROP ];
$db = $this->loadBalancer->getConnection( DB_PRIMARY );
$db->insertSelect(
'revtag',
'revtag',
[
'rt_type' => 'rt_type',
'rt_page' => 'rt_page',
'rt_revision' => $revisionRecord->getId(),
'rt_value' => 'rt_value',
],
[
'rt_type' => $tagsToCopy,
'rt_revision' => $parentId,
],
__METHOD__
);
}
/** @inheritDoc */
public function onListDefinedTags( &$tags ): void {
$tags[] = 'translate-translation-pages';
}
/** @inheritDoc */
public function onChangeTagsListActive( &$tags ): void {
if ( $this->config->get( 'EnablePageTranslation' ) ) {
$tags[] = 'translate-translation-pages';
}
}
}