From 441e52718e4db4eb45bd5c76c5af446496f56f96 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 15 Mar 2010 15:08:16 -0700 Subject: [PATCH] Background deletion of user accounts. Notices are deleted in chunks, then the user itself when they're all gone. While deletion is in progress, the account is locked with the 'deleted' role, which disables all actions with rights control. Todo: * Pretty up the notice on the profile page about the pending delete. Show status? * Possibly more thorough account disabling, such as disallowing all use for login and access. * Improve error recovery; worst case is that an account gets left locked in 'deleted' state but the queue jobs have gotten dropped out. This would leave the username in use and any undeleted notices in place. --- actions/deleteuser.php | 10 +++- classes/Profile.php | 3 ++ classes/Profile_role.php | 1 + lib/deluserqueuehandler.php | 95 +++++++++++++++++++++++++++++++++++++ lib/queuemanager.php | 3 ++ lib/userprofile.php | 11 +++++ 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 lib/deluserqueuehandler.php diff --git a/actions/deleteuser.php b/actions/deleteuser.php index c4f84fad2d..4e6b273953 100644 --- a/actions/deleteuser.php +++ b/actions/deleteuser.php @@ -162,7 +162,15 @@ class DeleteuserAction extends ProfileFormAction function handlePost() { if (Event::handle('StartDeleteUser', array($this, $this->user))) { - $this->user->delete(); + // Mark the account as deleted and shove low-level deletion tasks + // to background queues. Removing a lot of posts can take a while... + if (!$this->user->hasRole(Profile_role::DELETED)) { + $this->user->grantRole(Profile_role::DELETED); + } + + $qm = QueueManager::get(); + $qm->enqueue($this->user, 'deluser'); + Event::handle('EndDeleteUser', array($this, $this->user)); } } diff --git a/classes/Profile.php b/classes/Profile.php index 91f6e46922..eded1ff71f 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -732,6 +732,9 @@ class Profile extends Memcached_DataObject function hasRight($right) { $result = false; + if ($this->hasRole(Profile_role::DELETED)) { + return false; + } if (Event::handle('UserRightsCheck', array($this, $right, &$result))) { switch ($right) { diff --git a/classes/Profile_role.php b/classes/Profile_role.php index d0a0b31f0f..e7aa1f0f06 100644 --- a/classes/Profile_role.php +++ b/classes/Profile_role.php @@ -53,6 +53,7 @@ class Profile_role extends Memcached_DataObject const ADMINISTRATOR = 'administrator'; const SANDBOXED = 'sandboxed'; const SILENCED = 'silenced'; + const DELETED = 'deleted'; // Pending final deletion of notices... public static function isValid($role) { diff --git a/lib/deluserqueuehandler.php b/lib/deluserqueuehandler.php new file mode 100644 index 0000000000..4a1233a5ef --- /dev/null +++ b/lib/deluserqueuehandler.php @@ -0,0 +1,95 @@ +. + */ + +/** + * Background job to delete prolific users without disrupting front-end too much. + * + * Up to 50 messages are deleted on each run through; when all messages are gone, + * the actual account is deleted. + * + * @package QueueHandler + * @maintainer Brion Vibber + */ + +class DelUserQueueHandler extends QueueHandler +{ + const DELETION_WINDOW = 50; + + public function transport() + { + return 'deluser'; + } + + public function handle($user) + { + if (!($user instanceof User)) { + common_log(LOG_ERR, "Got a bogus user, not deleting"); + return true; + } + + $user = User::staticGet('id', $user->id); + if (!$user) { + common_log(LOG_INFO, "User {$user->nickname} was deleted before we got here."); + return true; + } + + if (!$user->hasRole(Profile_role::DELETED)) { + common_log(LOG_INFO, "User {$user->nickname} is not pending deletion; aborting."); + return true; + } + + $notice = $this->getNextBatch($user); + if ($notice->N) { + common_log(LOG_INFO, "Deleting next {$notice->N} notices by {$user->nickname}"); + while ($notice->fetch()) { + $del = clone($notice); + $del->delete(); + } + + // @todo improve reliability in case we died during the above deletions + // with a fatal error. If the job is lost, we should perform some kind + // of garbage collection later. + + // Queue up the next batch. + $qm = QueueManager::get(); + $qm->enqueue($user, 'deluser'); + } else { + // Out of notices? Let's finish deleting this guy! + $user->delete(); + common_log(LOG_INFO, "User $user->id $user->nickname deleted."); + return true; + } + + return true; + } + + /** + * Fetch the next self::DELETION_WINDOW messages for this user. + * @return Notice + */ + protected function getNextBatch(User $user) + { + $notice = new Notice(); + $notice->profile_id = $user->id; + $notice->limit(self::DELETION_WINDOW); + $notice->find(); + return $notice; + } + +} diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 87bd356aa2..0829c8a8bc 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -264,6 +264,9 @@ abstract class QueueManager extends IoManager $this->connect('sms', 'SmsQueueHandler'); } + // Background user management tasks... + $this->connect('deluser', 'DelUserQueueHandler'); + // Broadcasting profile updates to OMB remote subscribers $this->connect('profile', 'ProfileQueueHandler'); diff --git a/lib/userprofile.php b/lib/userprofile.php index 8464c24464..2c3b1ea453 100644 --- a/lib/userprofile.php +++ b/lib/userprofile.php @@ -228,6 +228,17 @@ class UserProfile extends Widget function showEntityActions() { + if ($this->profile->hasRole(Profile_role::DELETED)) { + $this->out->elementStart('div', 'entity_actions'); + $this->out->element('h2', null, _('User actions')); + $this->out->elementStart('ul'); + $this->out->elementStart('p', array('class' => 'profile_deleted')); + $this->out->text(_('User deletion in progress...')); + $this->out->elementEnd('p'); + $this->out->elementEnd('ul'); + $this->out->elementEnd('div'); + return; + } if (Event::handle('StartProfilePageActionsSection', array(&$this->out, $this->profile))) { $cur = common_current_user(); -- 2.39.2