| <?php | |
| /** | |
| * General hook definitions | |
| * | |
| * This file is part of the CentralNotice Extension to MediaWiki | |
| * https://www.mediawiki.org/wiki/Extension:CentralNotice | |
| * | |
| * @file | |
| * @ingroup Extensions | |
| * | |
| * @section LICENSE | |
| * This program is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License as published by | |
| * the Free Software Foundation; either version 2 of the License, or | |
| * (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU General Public License along | |
| * with this program; if not, write to the Free Software Foundation, Inc., | |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
| * http://www.gnu.org/copyleft/gpl.html | |
| */ | |
| class CentralNoticeHooks { | |
| /** | |
| * Conditional configuration | |
| */ | |
| public static function onRegistration() { | |
| global $wgHooks, $wgNoticeInfrastructure, $wgSpecialPages, | |
| $wgCentralNoticeLoader, $wgNoticeUseTranslateExtension, | |
| $wgAvailableRights, $wgGroupPermissions, $wgCentralDBname, $wgDBname; | |
| // Default for a standalone wiki is that the CN tables are in the main database. | |
| if ( !$wgCentralDBname ) { | |
| $wgCentralDBname = $wgDBname; | |
| } | |
| // If CentralNotice banners should be shown on this wiki, load the components we need for | |
| // showing banners. For discussion of banner loading strategies, see | |
| // http://wikitech.wikimedia.org/view/CentralNotice/Optimizing_banner_loading | |
| if ( $wgCentralNoticeLoader ) { | |
| $wgHooks['MakeGlobalVariablesScript'][] = | |
| 'CentralNoticeHooks::onMakeGlobalVariablesScript'; | |
| $wgHooks['BeforePageDisplay'][] = 'CentralNoticeHooks::onBeforePageDisplay'; | |
| $wgHooks['SiteNoticeAfter'][] = 'CentralNoticeHooks::onSiteNoticeAfter'; | |
| $wgHooks['ResourceLoaderGetConfigVars'][] = | |
| 'CentralNoticeHooks::onResourceLoaderGetConfigVars'; | |
| } | |
| // If this is the wiki that hosts the management interface, load further components | |
| if ( $wgNoticeInfrastructure ) { | |
| if ( $wgNoticeUseTranslateExtension ) { | |
| $wgHooks['TranslatePostInitGroups'][] = 'BannerMessageGroup::registerGroupHook'; | |
| $wgHooks['TranslateEventMessageGroupStateChange'][] = | |
| 'BannerMessageGroup::updateBannerGroupStateHook'; | |
| } | |
| $wgSpecialPages['CentralNotice'] = 'CentralNotice'; | |
| $wgSpecialPages['NoticeTemplate'] = 'SpecialNoticeTemplate'; | |
| $wgSpecialPages['BannerAllocation'] = 'SpecialBannerAllocation'; | |
| $wgSpecialPages['CentralNoticeLogs'] = 'SpecialCentralNoticeLogs'; | |
| $wgSpecialPages['CentralNoticeBanners'] = 'SpecialCentralNoticeBanners'; | |
| // Register user rights for editing | |
| $wgAvailableRights[] = 'centralnotice-admin'; | |
| // Only sysops can make change | |
| $wgGroupPermissions['sysop']['centralnotice-admin'] = true; | |
| } | |
| } | |
| /** | |
| * Initialization: set default values for some config globals. Invoked via | |
| * $wgExtensionFunctions. | |
| */ | |
| public static function initCentralNotice() { | |
| global $wgCentralBannerRecorder, $wgCentralSelectedBannerDispatcher, | |
| $wgCentralSelectedMobileBannerDispatcher; | |
| // Defaults for infrastructure wiki URLs | |
| if ( !$wgCentralBannerRecorder ) { | |
| $wgCentralBannerRecorder = | |
| SpecialPage::getTitleFor( 'RecordImpression' )->getLocalUrl(); | |
| } | |
| if ( !$wgCentralSelectedBannerDispatcher ) { | |
| $wgCentralSelectedBannerDispatcher = | |
| SpecialPage::getTitleFor( 'BannerLoader' )->getLocalUrl(); | |
| } | |
| if ( !$wgCentralSelectedMobileBannerDispatcher && class_exists( 'MobileContext' ) ) { | |
| $wgCentralSelectedMobileBannerDispatcher = $wgCentralSelectedBannerDispatcher; | |
| } | |
| } | |
| /** | |
| * Tell the UserMerge extension where we store user ids | |
| * @param array[] &$updateFields | |
| * @return true | |
| */ | |
| public static function onUserMergeAccountFields( &$updateFields ) { | |
| global $wgNoticeInfrastructure; | |
| if ( $wgNoticeInfrastructure ) { | |
| // array( tableName, idField, textField ) | |
| $updateFields[] = [ 'cn_notice_log', 'notlog_user_id' ]; | |
| $updateFields[] = [ 'cn_template_log', 'tmplog_user_id' ]; | |
| } | |
| return true; | |
| } | |
| /** | |
| * CanonicalNamespaces hook; adds the CentralNotice namespaces if this is an infrastructure | |
| * wiki, and if CentralNotice is configured to use the Translate extension. | |
| * | |
| * We do this here because there are initialization problems wrt Translate and MW core if | |
| * the language object is initialized before all namespaces are registered -- which would | |
| * be the case if we just used the wgExtensionFunctions hook system. | |
| * | |
| * @param array &$namespaces Modifiable list of namespaces -- similar to $wgExtraNamespaces | |
| * | |
| * @return bool True if the hook completed successfully. | |
| */ | |
| public static function onCanonicalNamespaces( &$namespaces ) { | |
| global $wgExtraNamespaces, $wgNamespacesWithSubpages, $wgTranslateMessageNamespaces; | |
| global $wgNoticeUseTranslateExtension, $wgNoticeInfrastructure; | |
| // TODO XXX Old doc copied from legacy follows, verify accuracy! | |
| // When using the group review feature of translate; this | |
| // will be the namespace ID for the banner staging area -- ie: banners | |
| // here are world editable and will not be moved to the MW namespace | |
| // until they are in $wgNoticeTranslateDeployStates | |
| // TODO This may be unnecessary. Must coordinate with extension.json | |
| if ( !defined( 'NS_CN_BANNER' ) ) { | |
| define( 'NS_CN_BANNER', 866 ); | |
| define( 'NS_CN_BANNER_TALK', 867 ); | |
| } | |
| if ( $wgNoticeInfrastructure && $wgNoticeUseTranslateExtension ) { | |
| $wgExtraNamespaces[NS_CN_BANNER] = 'CNBanner'; | |
| $wgTranslateMessageNamespaces[] = NS_CN_BANNER; | |
| $wgExtraNamespaces[NS_CN_BANNER_TALK] = 'CNBanner_talk'; | |
| $wgNamespacesWithSubpages[NS_CN_BANNER_TALK] = true; | |
| $namespaces[NS_CN_BANNER] = 'CNBanner'; | |
| $namespaces[NS_CN_BANNER_TALK] = 'CNBanner_talk'; | |
| } | |
| return true; | |
| } | |
| /** | |
| * BeforePageDisplay hook handler | |
| * This function adds the startUp and geoIP modules to the page as needed, | |
| * and if there is a forced banner preview, add CSP headers and violation | |
| * reporting javascript. | |
| * | |
| * @param OutputPage $out | |
| * @param Skin $skin | |
| * @return bool | |
| */ | |
| public static function onBeforePageDisplay( $out, $skin ) { | |
| global $wgCentralHost, $wgServer, $wgRequest, $wgCentralNoticeContentSecurityPolicy; | |
| // Always add geoIP | |
| // TODO Separate geoIP from CentralNotice | |
| $out->addModules( 'ext.centralNotice.geoIP' ); | |
| // If we're on a special page, editing, viewing history or a diff, bow out now | |
| // This is to reduce chance of bad misclicks from delayed banner loading | |
| if ( $out->getTitle()->inNamespace( NS_SPECIAL ) || | |
| ( $wgRequest->getText( 'action' ) === 'edit' ) || | |
| ( $wgRequest->getText( 'action' ) === 'history' ) || | |
| $wgRequest->getCheck( 'diff' ) | |
| ) { | |
| return true; | |
| } | |
| // Insert DNS prefetch for banner loading | |
| if ( $wgCentralHost && $wgCentralHost !== $wgServer ) { | |
| $out->addHeadItem( | |
| 'cn-dns-prefetch', | |
| '<link rel="dns-prefetch" href="' . htmlspecialchars( $wgCentralHost ) . '" />' | |
| ); | |
| } | |
| // Insert the startup module | |
| $out->addModules( 'ext.centralNotice.startUp' ); | |
| // FIXME: as soon as I80f6f469ba4c0b60 is available in core, get rid | |
| // of $wgCentralNoticeContentSecurityPolicy and use their stuff. | |
| if ( | |
| $wgCentralNoticeContentSecurityPolicy && | |
| $wgRequest->getVal( 'banner' ) | |
| ) { | |
| $wgRequest->response()->header( | |
| "content-security-policy: $wgCentralNoticeContentSecurityPolicy" | |
| ); | |
| $out->addModules( 'ext.centralNotice.cspViolationAlert' ); | |
| } | |
| return true; | |
| } | |
| /** | |
| * MakeGlobalVariablesScript hook handler | |
| * This function sets the pseudo-global JavaScript variables that are used by CentralNotice | |
| * | |
| * @param array &$vars | |
| * @return bool | |
| */ | |
| public static function onMakeGlobalVariablesScript( &$vars ) { | |
| // Using global $wgUser for compatibility with 1.18 | |
| global $wgNoticeProject, $wgCentralNoticeCookiesToDelete, | |
| $wgCentralNoticeCategoriesUsingLegacy, | |
| $wgCentralNoticeGeoIPBackgroundLookupModule, | |
| $wgUser, $wgMemc; | |
| // FIXME Is this no longer used anywhere in JS following the switch to | |
| // client-side banner selection? If so, remove it. | |
| $vars[ 'wgNoticeProject' ] = $wgNoticeProject; | |
| $vars[ 'wgCentralNoticeCookiesToDelete' ] = $wgCentralNoticeCookiesToDelete; | |
| $vars[ 'wgCentralNoticeCategoriesUsingLegacy' ] = | |
| $wgCentralNoticeCategoriesUsingLegacy; | |
| // No need to provide this variable if it's null, because mw.config.get() | |
| // will return null if it's not there. | |
| if ( $wgCentralNoticeGeoIPBackgroundLookupModule ) { | |
| $vars[ 'wgCentralNoticeGeoIPBackgroundLookupModule' ] = | |
| $wgCentralNoticeGeoIPBackgroundLookupModule; | |
| } | |
| // Output the user's registration date, total edit count, and past year's edit count. | |
| // This is useful for banners that need to be targeted to specific types of users. | |
| // Only do this for logged-in users, keeping anonymous user output equal (for Squid-cache). | |
| if ( $wgUser->isLoggedIn() ) { | |
| $cacheKey = wfMemcKey( 'CentralNotice', 'UserData', $wgUser->getId() ); | |
| $userData = $wgMemc->get( $cacheKey ); | |
| // Cached? | |
| if ( !$userData ) { | |
| // Exclude bots | |
| if ( $wgUser->isAllowed( 'bot' ) ) { | |
| $userData = false; | |
| } else { | |
| $userData = []; | |
| // Add the user's registration date (MediaWiki timestamp) | |
| $registrationDate = $wgUser->getRegistration() ? $wgUser->getRegistration() : 0; | |
| $userData[ 'registration' ] = $registrationDate; | |
| } | |
| // Cache the data for 7 days | |
| $wgMemc->set( $cacheKey, $userData, 7 * 86400 ); | |
| } | |
| // Set the variable that will be output to the page | |
| $vars[ 'wgNoticeUserData' ] = $userData; | |
| } | |
| return true; | |
| } | |
| /** | |
| * SiteNoticeAfter hook handler | |
| * This function outputs the siteNotice div that the banners are loaded into. | |
| * | |
| * @param string &$notice | |
| * @return bool | |
| */ | |
| public static function onSiteNoticeAfter( &$notice ) { | |
| // Ensure that the div including #siteNotice is actually included! | |
| $notice = "<!-- CentralNotice -->$notice"; | |
| return true; | |
| } | |
| /** | |
| * ResourceLoaderGetConfigVars hook handler | |
| * Send php config vars to js via ResourceLoader | |
| * | |
| * @param array &$vars variables to be added to the output of the startup module | |
| * @return bool | |
| */ | |
| public static function onResourceLoaderGetConfigVars( &$vars ) { | |
| global $wgNoticeXXCountries, | |
| $wgNoticeInfrastructure, $wgCentralBannerRecorder, | |
| $wgNoticeNumberOfBuckets, $wgNoticeBucketExpiry, | |
| $wgNoticeNumberOfControllerBuckets, $wgNoticeCookieDurations, | |
| $wgNoticeHideUrls, $wgCentralNoticeSampleRate, | |
| $wgCentralNoticeImpressionEventSampleRate, | |
| $wgCentralSelectedBannerDispatcher, $wgCentralSelectedMobileBannerDispatcher, | |
| $wgCentralNoticePerCampaignBucketExtension, $wgCentralNoticeCampaignMixins; | |
| // TODO Check if the following comment still applies | |
| // Making these calls too soon will causes issues with the namespace localisation cache. | |
| // This seems to be just right. We require them at all because MW will 302 page requests | |
| // made to non localised namespaces which results in wasteful extra calls. | |
| // Set infrastructure URL variables, which change between mobile/desktop | |
| if ( class_exists( 'MobileContext' ) ) { | |
| $mc = MobileContext::singleton(); | |
| $displayMobile = $mc->shouldDisplayMobileView(); | |
| } else { | |
| $displayMobile = false; | |
| } | |
| if ( $displayMobile ) { | |
| $wgCentralBannerRecorder = $mc->getMobileUrl( $wgCentralBannerRecorder ); | |
| $bannerDispatcher = $wgCentralSelectedMobileBannerDispatcher; | |
| } else { | |
| $bannerDispatcher = $wgCentralSelectedBannerDispatcher; | |
| } | |
| $vars[ 'wgCentralNoticeActiveBannerDispatcher' ] = $bannerDispatcher; | |
| // TODO Temporary setting to support cached javascript following deploy; remove. | |
| $vars[ 'wgCentralSelectedBannerDispatcher' ] = $bannerDispatcher; | |
| $vars[ 'wgCentralBannerRecorder' ] = $wgCentralBannerRecorder; | |
| $vars[ 'wgCentralNoticeSampleRate' ] = $wgCentralNoticeSampleRate; | |
| $vars[ 'wgCentralNoticeImpressionEventSampleRate' ] = | |
| $wgCentralNoticeImpressionEventSampleRate; | |
| // TODO Remove, no longer used | |
| $vars[ 'wgNoticeXXCountries' ] = $wgNoticeXXCountries; | |
| $vars[ 'wgNoticeNumberOfBuckets' ] = $wgNoticeNumberOfBuckets; | |
| $vars[ 'wgNoticeBucketExpiry' ] = $wgNoticeBucketExpiry; | |
| $vars[ 'wgNoticeNumberOfControllerBuckets' ] = $wgNoticeNumberOfControllerBuckets; | |
| $vars[ 'wgNoticeCookieDurations' ] = $wgNoticeCookieDurations; | |
| $vars[ 'wgNoticeHideUrls' ] = $wgNoticeHideUrls; | |
| $vars[ 'wgCentralNoticePerCampaignBucketExtension' ] = | |
| $wgCentralNoticePerCampaignBucketExtension; | |
| if ( $wgNoticeInfrastructure ) { | |
| // Add campaign mixin defs for use in admin interface | |
| $vars[ 'wgCentralNoticeCampaignMixins' ] = $wgCentralNoticeCampaignMixins; | |
| } | |
| return true; | |
| } | |
| /** | |
| * ResourceLoaderTestModules hook handler | |
| * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderTestModules | |
| * | |
| * @param array &$testModules | |
| * @param ResourceLoader $resourceLoader | |
| * @return bool | |
| */ | |
| public static function onResourceLoaderTestModules( array &$testModules, | |
| ResourceLoader $resourceLoader | |
| ) { | |
| global $wgResourceModules, $wgAutoloadClasses; | |
| // Set up test fixtures module, which is added as a dependency for all QUnit | |
| // tests. | |
| $testModules['qunit']['ext.centralNotice.testFixtures'] = [ | |
| 'class' => 'CNTestFixturesResourceLoaderModule' | |
| ]; | |
| // These classes are only used here or in phpunit tests | |
| $wgAutoloadClasses['CNTestFixturesResourceLoaderModule'] = | |
| __DIR__ . '/tests/phpunit/CNTestFixturesResourceLoaderModule.php'; | |
| // Note: the following setting is repeated in efCentralNoticeUnitTests() | |
| $wgAutoloadClasses['CentralNoticeTestFixtures'] = | |
| __DIR__ . '/tests/phpunit/CentralNoticeTestFixtures.php'; | |
| $testModuleBoilerplate = [ | |
| 'localBasePath' => __DIR__, | |
| 'remoteExtPath' => 'CentralNotice', | |
| ]; | |
| // find test files for every RL module | |
| $prefix = 'ext.centralNotice'; | |
| foreach ( $wgResourceModules as $key => $module ) { | |
| if ( substr( $key, 0, strlen( $prefix ) ) === | |
| $prefix && isset( $module['scripts'] ) | |
| ) { | |
| $testFiles = []; | |
| foreach ( ( (array)$module['scripts'] ) as $script ) { | |
| $testFile = 'tests/qunit/' . $script; | |
| $testFile = preg_replace( '/.js$/', '.tests.js', $testFile ); | |
| // if a test file exists for a given JS file, add it | |
| if ( file_exists( __DIR__ . '/' . $testFile ) ) { | |
| $testFiles[] = $testFile; | |
| } | |
| } | |
| // if test files exist for given module, create a corresponding test | |
| // module | |
| if ( count( $testFiles ) > 0 ) { | |
| $testModules['qunit']["$key.tests"] = $testModuleBoilerplate + | |
| [ | |
| 'dependencies' => | |
| [ $key, 'ext.centralNotice.testFixtures' ], | |
| 'scripts' => $testFiles, | |
| ]; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| } |
US