<?php
namespace XF\Service\User;
use function count, is_array;
class DeleteCleanUp extends \XF\Service\AbstractService
{
use \XF\MultiPartRunnerTrait;
protected $userId;
protected $userName;
protected $steps = [
'stepDeleteContent',
'stepDeleteProfilePosts',
'stepDeleteContentVotes',
'stepDeleteBookmarks',
'stepChangeOwner',
'stepMiscCleanUp'
];
protected $deletes = [
'xf_admin' => 'user_id = ?',
'xf_admin_permission_entry' => 'user_id = ?',
'xf_approval_queue' => "content_type = 'user' AND content_id = ?",
'xf_change_log' => "content_type = 'user' AND content_id = ?",
'xf_conversation_user' => 'owner_user_id = ?', // leave recipient record for others
'xf_draft' => 'user_id = ?',
'xf_email_bounce_soft' => 'user_id = ?',
'xf_find_new_default' => 'user_id = ?',
'xf_flood_check' => 'user_id = ?',
'xf_forum_read' => 'user_id = ?',
'xf_forum_watch' => 'user_id = ?',
'xf_ip' => "content_type = 'user' AND content_id = ?", // leave content IPs
'xf_moderator' => 'user_id = ?',
'xf_moderator_content' => 'user_id = ?',
'xf_notice_dismissed' => 'user_id = ? ',
'xf_permission_combination' => 'user_id = ?',
'xf_permission_entry' => 'user_id = ?',
'xf_permission_entry_content' => 'user_id = ?',
'xf_poll_vote' => 'user_id = ?',
'xf_tfa_attempt' => 'user_id = ?',
'xf_thread_read' => 'user_id = ?',
'xf_thread_reply_ban' => 'user_id = ?',
'xf_thread_user_post' => 'user_id = ?',
'xf_thread_watch' => 'user_id = ?',
'xf_user_alert' => 'alerted_user_id = ?',
'xf_user_alert_optout' => 'user_id = ?',
'xf_user_ban' => 'user_id = ?',
'xf_user_change_temp' => 'user_id = ?',
'xf_user_confirmation' => 'user_id = ?',
'xf_user_connected_account' => 'user_id = ?',
'xf_user_field_value' => 'user_id = ?',
'xf_user_follow' => [
'user_id = ?',
'follow_user_id = ?'
],
'xf_user_group_change' => 'user_id = ?',
'xf_user_group_promotion_log' => 'user_id = ?',
'xf_user_group_relation' => 'user_id = ?',
'xf_user_ignored' => [
'user_id = ?',
'ignored_user_id = ?'
],
'xf_user_reject' => 'user_id = ?',
'xf_user_remember' => 'user_id = ?',
'xf_user_tfa' => 'user_id = ?',
'xf_user_tfa_trusted' => 'user_id = ?',
'xf_user_trophy' => 'user_id = ?',
'xf_user_upgrade_active' => 'user_id = ?',
'xf_user_upgrade_expired' => 'user_id = ?',
'xf_warning' => 'user_id = ?',
'xf_warning_action_trigger' => 'user_id = ?',
];
// let some areas be cleaned up by cron: find_new, search, session_activity, tag_result_cache
public function __construct(\XF\App $app, $userId, $username)
{
parent::__construct($app);
$this->userId = $userId;
$this->userName = $username;
$app->fire('user_delete_clean_init', [$this, &$this->deletes]);
}
protected function getSteps()
{
return $this->steps;
}
public function cleanUp($maxRunTime = 0)
{
$result = $this->runLoop($maxRunTime);
return $result;
}
protected function stepDeleteContent($lastOffset, $maxRunTime)
{
$db = $this->db();
// we shouldn't get an array here but has been seen
if (is_array($lastOffset))
{
$lastOffset = null;
}
$lastOffset = $lastOffset === null ? -1 : $lastOffset;
$thisOffset = -1;
$start = microtime(true);
foreach ($this->deletes AS $table => $actions)
{
$thisOffset++;
if ($thisOffset <= $lastOffset)
{
continue;
}
if (!is_array($actions))
{
$actions = [$actions];
}
foreach ($actions AS $action)
{
$db->delete($table, $action, $this->userId);
}
$lastOffset = $thisOffset;
if ($maxRunTime && microtime(true) - $start > $maxRunTime)
{
return $lastOffset; // continue at this position
}
}
return null;
}
protected function stepDeleteProfilePosts($lastOffset, $maxRunTime)
{
$start = microtime(true);
/** @var \XF\Entity\ProfilePost[] $profilePosts */
$finder = $this->finder('XF:ProfilePost')
->where('profile_user_id', $this->userId)
->order('profile_post_id');
// we shouldn't get an array here but has been seen
if (is_array($lastOffset))
{
$lastOffset = null;
}
if ($lastOffset !== null)
{
$finder->where('profile_post_id', '>', $lastOffset);
}
$maxFetch = 1000;
$profilePosts = $finder->fetch($maxFetch);
$fetchedProfilePosts = count($profilePosts);
if (!$fetchedProfilePosts)
{
return null; // done or nothing to do
}
foreach ($profilePosts AS $profilePost)
{
$lastOffset = $profilePost->profile_post_id;
$profilePost->setOption('log_moderator', false);
$profilePost->delete();
if ($maxRunTime && microtime(true) - $start > $maxRunTime)
{
return $lastOffset; // continue at this position
}
}
if ($fetchedProfilePosts == $maxFetch)
{
return $lastOffset; // more to do
}
else
{
return null;
}
}
protected function stepDeleteContentVotes($lastOffset, $maxRunTime)
{
$start = microtime(true);
$finder = $this->finder('XF:ContentVote')
->where('vote_user_id', $this->userId)
->order('vote_id');
// we shouldn't get an array here but has been seen
if (is_array($lastOffset))
{
$lastOffset = null;
}
if ($lastOffset !== null)
{
$finder->where('vote_id', '>', $lastOffset);
}
$maxFetch = 1000;
/** @var \XF\Entity\ContentVote[] $votes */
$votes = $finder->fetch($maxFetch);
$fetchedVotes = count($votes);
if (!$votes)
{
return null; // done or nothing to do
}
foreach ($votes AS $vote)
{
$lastOffset = $vote->vote_id;
$vote->delete();
if ($maxRunTime && microtime(true) - $start > $maxRunTime)
{
return $lastOffset; // continue at this position
}
}
if ($fetchedVotes == $maxFetch)
{
return $lastOffset; // more to do
}
else
{
return null;
}
}
protected function stepDeleteBookmarks($lastOffset, $maxRunTime)
{
$start = microtime(true);
/** @var \XF\Entity\BookmarkItem[] $bookmarks */
$finder = $this->finder('XF:BookmarkItem')
->where('user_id', $this->userId)
->order('bookmark_id');
// we shouldn't get an array here but has been seen
if (is_array($lastOffset))
{
$lastOffset = null;
}
if ($lastOffset !== null)
{
$finder->where('bookmark_id', '>', $lastOffset);
}
$maxFetch = 1000;
$bookmarks = $finder->fetch($maxFetch);
$fetchedBookmarks = count($bookmarks);
if (!$bookmarks)
{
return null; // done or nothing to do
}
foreach ($bookmarks AS $bookmark)
{
$lastOffset = $bookmark->bookmark_id;
$bookmark->delete();
if ($maxRunTime && microtime(true) - $start > $maxRunTime)
{
return $lastOffset; // continue at this position
}
}
if ($fetchedBookmarks == $maxFetch)
{
return $lastOffset; // more to do
}
else
{
return null;
}
}
protected function stepChangeOwner($lastOffset, $maxRunTime)
{
/** @var \XF\Service\User\ContentChange $contentChanger */
$contentChanger = $this->service('XF:User\ContentChange', $this->userId, $this->userName);
$contentChanger->setupForDelete();
if (is_array($lastOffset))
{
list($changeStep, $changeLastOffset) = $lastOffset;
$contentChanger->restoreState($changeStep, $changeLastOffset);
}
$result = $contentChanger->apply($maxRunTime);
if ($result->isCompleted())
{
return null;
}
else
{
$continueData = $result->getContinueData();
return [$continueData['currentStep'], $continueData['lastOffset']];
}
}
protected function stepMiscCleanUp()
{
if ($this->userId)
{
// note: there's no reason for this to be 0, but if it were, this would delete entries it shouldn't
$keys = $this->em()->getFinder('XF:ApiKey')->where('user_id', $this->userId)->fetch();
/** @var \XF\Entity\ApiKey $key */
foreach ($keys AS $key)
{
$key->delete();
}
}
// it's worth keeping these records, but disassociate them
$this->db()->update('xf_email_bounce_log', ['user_id' => 0], 'user_id = ?', $this->userId);
// disassociate these, direct db update is fine as we do not need to adjust solution count as record deleted
$this->db()->update('xf_thread_question', ['solution_user_id' => 0], 'solution_user_id = ?', $this->userId);
// determine if there are any pending username changes for this user and delete them via entity
// this is to ensure caches are rebuilt and the approval queue record is removed.
/** @var \XF\Repository\UsernameChange $usernameChangeRepo */
$usernameChangeRepo = $this->repository('XF:UsernameChange');
$pendingChanges = $usernameChangeRepo->findPendingUsernameChanges()
->where('user_id', $this->userId)
->fetch();
foreach ($pendingChanges AS $change)
{
/** @var \XF\Entity\UsernameChange $change */
$change->delete();
}
// now delete any other non-pending username change records for this user
// TODO: they could be useful to keep but then we don't currently keep normal change logs either
$this->db()->delete('xf_username_change', 'user_id = ?', $this->userId);
/** @var \XF\Repository\ApprovalQueue $approvalRepo */
$approvalRepo = $this->repository('XF:ApprovalQueue');
$approvalRepo->rebuildUnapprovedCounts();
}
}