diff --git a/extension.json b/extension.json index 90920e8..580334d 100644 --- a/extension.json +++ b/extension.json @@ -46,12 +46,6 @@ "AddNewAccount": [ "LoginNotify\\Hooks::onAddNewAccount" ], - "UserLoadOptions": [ - "LoginNotify\\Hooks::onUserLoadOptions" - ], - "UserSaveOptions": [ - "LoginNotify\\Hooks::onUserSaveOptions" - ], "LocalUserCreated": [ "LoginNotify\\Hooks::onLocalUserCreated" ] @@ -72,8 +66,6 @@ "LoginNotifyCheckKnownIPs": true, "@docLoginNotifyEnableOnSuccess": "Whether to trigger a notification after successful logins from unknown IPs.", "LoginNotifyEnableOnSuccess": true, - "@docLoginNotifyEnableForPriv": "Set different default notification preferences for different user groups. For user groups that have any of the user rights listed in this array, the preferences specified in Hooks:getOverriddenOptions() are on by default.", - "LoginNotifyEnableForPriv": [ "editinterface", "userrights" ], "@docLoginNotifySecretKey": "Override this to use a different secret than $wgSecretKey", "LoginNotifySecretKey": null, "@docLoginNotifyCookieExpire": "Expiry in seconds. Default is 180 days", diff --git a/includes/Hooks.php b/includes/Hooks.php index 7fe5b72..88fb06e 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -15,10 +15,6 @@ use MediaWiki\Auth\AuthenticationResponse; use User; class Hooks { - - const OPTIONS_FAKE_TRUTH = 2; - const OPTIONS_FAKE_FALSE = 'fake-false'; - /** * Add LoginNotify events to Echo * @@ -199,112 +195,4 @@ class Hooks { $loginNotify->setCurrentAddressAsKnown( $user ); } } - - /** - * Hook for loading options. - * - * This is a bit hacky. Used to be able to set a different - * default for admins than other users - * - * @param User $user The user in question. - * @param mixed[] &$options The options. - * @return bool - */ - public static function onUserLoadOptions( User $user, array &$options ) { - global $wgLoginNotifyEnableForPriv; - if ( !is_array( $wgLoginNotifyEnableForPriv ) ) { - return true; - } - - if ( !self::isUserOptionOverridden( $user ) ) { - return true; - } - - $defaultOpts = User::getDefaultOptions(); - $optionsToCheck = self::getOverriddenOptions(); - - foreach ( $optionsToCheck as $opt ) { - if ( $options[$opt] === self::OPTIONS_FAKE_FALSE ) { - $options[$opt] = '0'; - } - if ( $defaultOpts[$opt] !== false ) { - continue; - } - if ( $options[$opt] === false ) { - $options[$opt] = self::OPTIONS_FAKE_TRUTH; - } - } - return true; - } - - /** - * Hook for saving options. - * - * This is a bit hacky. Used to be able to set a different - * default for admins than other users. Since admins are higher value - * targets, it may make sense to have notices enabled by default for - * them, but disabled for normal users. - * - * @todo This is a bit icky. Need to decide if we really want to do this. - * @todo If someone explicitly enables, gets admin rights, gets de-admined, - * this will then disable the preference, which is definitely non-ideal. - * @param User $user The user that is being saved. - * @param mixed[] &$options The options. - * @return bool - */ - public static function onUserSaveOptions( User $user, array &$options ) { - $optionsToCheck = self::getOverriddenOptions(); - $defaultOpts = User::getDefaultOptions(); - if ( !self::isUserOptionOverridden( $user ) ) { - return true; - } - foreach ( $optionsToCheck as $opt ) { - if ( $defaultOpts[$opt] !== false ) { - continue; - } - - if ( $options[$opt] === self::OPTIONS_FAKE_TRUTH ) { - $options[$opt] = false; - } - if ( $options[$opt] !== self::OPTIONS_FAKE_TRUTH - && $options[$opt] - ) { - // Its checked on the form. Keep at default - } - - if ( !$options[$opt] ) { - // Somehow this means it got unchecked on form - $options[$opt] = self::OPTIONS_FAKE_FALSE; - } - } - return true; - } - - /** - * Helper for onUser(Load|Save)Options - * - * @return array Which option keys to check - */ - private static function getOverriddenOptions() { - // For login-success, it makes most sense to email - // people about it, but auto-subscribing people to email - // is a bit icky as nobody likes to be spammed. - return [ - 'echo-subscriptions-web-login-fail', - 'echo-subscriptions-web-login-success' - ]; - } - - private static function isUserOptionOverridden( User $user ) { - global $wgLoginNotifyEnableForPriv; - // Note: isAllowedAny calls into session for per-session restrictions, - // which we do not want to take into account, and more importantly - // causes an infinite loop. - $rights = User::getGroupPermissions( $user->getEffectiveGroups() ); - if ( !array_intersect( $rights, $wgLoginNotifyEnableForPriv ) ) { - // Not a user we care about. - return false; - } - return true; - } } diff --git a/maintenance/migratePreferences.php b/maintenance/migratePreferences.php new file mode 100644 index 0000000..a7c0453 --- /dev/null +++ b/maintenance/migratePreferences.php @@ -0,0 +1,130 @@ + false, + self::OPTIONS_FAKE_TRUTH => true, + ]; + + public function __construct() { + parent::__construct(); + $this->addDescription( 'Cleans up old-style preferences used by LoginNotify' ); + $this->setBatchSize( 500 ); + } + + /** + * Do the actual work. All child classes will need to implement this. + * Return true to log the update as done or false (usually on failure). + * @return bool + */ + protected function doDBUpdates() { + $dbr = $this->getDB( DB_REPLICA, 'vslow' ); + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + + $iterator = new BatchRowIterator( $dbr, + [ 'user_properties', 'user' ], + [ 'up_user', 'up_property' ], + $this->mBatchSize + ); + $iterator->addConditions( [ + 'user_id=up_user', + 'up_property' => [ + 'echo-subscriptions-web-login-fail', + 'echo-subscriptions-web-login-success', + 'echo-subscriptions-email-login-fail', + 'echo-subscriptions-email-login-success', + ], + 'up_value' => [ + self::OPTIONS_FAKE_TRUTH, + self::OPTIONS_FAKE_FALSE, + ], + ] ); + $iterator->setFetchColumns( [ '*' ] ); + + $lastRow = (object)[ 'user_id' => 0 ]; + $optionsToUpdate = []; + $rows = 0; + $total = 0; + $iterator = new RecursiveIteratorIterator( $iterator ); + foreach ( $iterator as $row ) { + $userId = $row->user_id; + $option = $row->up_property; + $value = $row->up_value; + + if ( $userId != $lastRow->user_id ) { + $rows += $this->updateUser( $lastRow, $optionsToUpdate ); + if ( $rows >= $this->mBatchSize ) { + $this->output( " Updated {$rows} rows up to user ID {$lastRow->user_id}\n" ); + $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] ); + $total += $rows; + $rows = 0; + } + } + if ( isset( self::$mapping[ $value ] ) ) { + $optionsToUpdate[$option] = self::$mapping[ $value ]; + } + $lastRow = $row; + } + + $total += $this->updateUser( $lastRow, $optionsToUpdate ); + + $this->output( "{$total} rows updated.\n" ); + + return true; + } + + /** + * Update one user's preferences + * + * @param object $userRow Row from the user table + * @param array $options Associative array of preference => value + * @return int Number of options updated + */ + private function updateUser( $userRow, array &$options ) { + if ( $userRow->user_id && $options ) { + $user = User::newFromRow( $userRow ); + foreach ( $options as $option => $value ) { + $user->setOption( $option, $value ); + } + $user->saveSettings(); + } + $count = count( $options ); + $options = []; + return $count; + } + + /** + * Get the update key name to go in the update log table + * @return string + */ + protected function getUpdateKey() { + return 'LoginNotify::migratePreferences'; + } +} + +$maintClass = MigratePreferences::class; +require_once RUN_MAINTENANCE_IF_MAIN;