Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Service/User/ContentChange.php
<?php

namespace XF\Service\User;

use
XF\Entity\User;

use function
is_null, is_string;

class
ContentChange extends \XF\Service\AbstractService
{
    use \
XF\MultiPartRunnerTrait;

    protected
$originalUserId = 0;
    protected
$originalUserName = '';

    protected
$newUserId = null;
    protected
$newUserName = null;

    protected
$steps = [
       
'stepReassignContent',
       
'stepMergeThreadUserPost',
       
'stepRebuildLikes',
       
'stepRebuildReactions',
       
'stepConversationRecipientCache',
       
'stepApplyWarningGroupChanges',
       
'stepReassignSearchIndex',
       
'stepRebuildFinalCaches'
   
];

    protected
$updates = [
       
'xf_admin_log' => ['user_id', 'emptyable' => false],
       
'xf_attachment_data' => ['user_id'],
       
'xf_ban_email' => ['create_user_id'],
       
'xf_bookmark_item' => ['user_id'],
       
'xf_bookmark_label' => ['user_id'],
       
'xf_content_vote' => [
            [
'vote_user_id'],
            [
'content_user_id']
        ],
       
'xf_conversation_master' => [
            [
'user_id', 'username'],
            [
'last_message_user_id', 'last_message_username']
        ],
       
'xf_conversation_message' => ['user_id', 'username'],
       
'xf_conversation_recipient' => ['user_id', 'emptyable' => false],
       
'xf_conversation_user' => [
            [
'owner_user_id', 'emptyable' => false],
            [
'last_message_user_id', 'last_message_username']
        ],
       
'xf_deletion_log' => ['delete_user_id', 'delete_username'],
       
'xf_edit_history' => ['edit_user_id'],
       
'xf_error_log' => ['user_id'],
       
'xf_feed' => ['user_id'],
       
'xf_forum' => ['last_post_user_id', 'last_post_username'],
       
'xf_forum_watch' => ['user_id', 'emptyable' => false],
       
'xf_ip' => ['user_id', 'emptyable' => false],
       
'xf_ip_match' => ['create_user_id'],
       
'xf_moderator_log' => [
            [
'user_id'],
            [
'content_user_id', 'content_username']
        ],
       
'xf_news_feed' => ['user_id', 'username'],
       
'xf_poll_vote' => ['user_id', 'emptyable' => false],
       
'xf_post' => ['user_id', 'username'], // skip last edit user ID (performance reasons, minor benefit)
       
'xf_profile_post' => [
            [
'profile_user_id', 'emptyable' => false],
            [
'user_id', 'username']
        ],
       
'xf_profile_post_comment' => ['user_id', 'username'],
       
'xf_reaction_content' => [
            [
'reaction_user_id'],
            [
'content_user_id']
        ],
       
'xf_report' => [
            [
'content_user_id'],
            [
'assigned_user_id'],
            [
'last_modified_user_id', 'last_modified_username']
        ],
       
'xf_report_comment' => ['user_id', 'username'],
       
'xf_spam_cleaner_log' => [
            [
'user_id', 'username'],
            [
'applying_user_id', 'applying_username']
        ],
       
'xf_spam_trigger_log' => ['user_id'],
       
'xf_tag_content' => ['add_user_id'],
       
'xf_thread' => [
            [
'user_id', 'username'],
            [
'last_post_user_id', 'last_post_username']
        ],
       
'xf_thread_reply_ban' => [
            [
'user_id', 'emptyable' => false],
            [
'ban_user_id']
        ],
       
'xf_thread_question' => ['solution_user_id'],
       
'xf_thread_user_post' => ['user_id'], // this is combined with queries below to do a "merge"
       
'xf_thread_watch' => ['user_id', 'emptyable' => false],
       
'xf_user_alert' => ['user_id', 'username'],
       
'xf_user_ban' => ['ban_user_id'],
       
'xf_user_follow' => [
            [
'user_id', 'emptyable' => false],
            [
'follow_user_id', 'emptyable' => false]
        ],
       
'xf_user_ignored' => [
            [
'user_id', 'emptyable' => false],
            [
'ignored_user_id', 'emptyable' => false]
        ],
       
'xf_user_trophy' => ['user_id', 'emptyable' => false],
       
'xf_user_upgrade_active' => ['user_id', 'emptyable' => false], // TODO: not merging change records, so can't really do this
       
'xf_user_upgrade_expired' => ['user_id', 'emptyable' => false],
       
'xf_username_change' => ['user_id', 'emptyable' => false],
       
'xf_warning' => [
            [
'user_id'],
            [
'warning_user_id']
        ],
    ];

    public function
__construct(\XF\App $app, $originalUserId, $originalUsername = null)
    {
       
parent::__construct($app);

        if (
$originalUserId instanceof User)
        {
            if (
$originalUsername === null)
            {
               
$originalUsername = $originalUserId->username;
            }
           
$originalUserId = $originalUserId->user_id;
        }

        if (
$originalUsername === '' || is_null($originalUsername))
        {
            throw new \
LogicException("Must provide an original username explicitly or a User entity");
        }

       
$this->originalUserId = $originalUserId;
       
$this->originalUserName = $originalUsername;

       
$app->fire('user_content_change_init', [$this, &$this->updates]);
    }

    public function
getOriginalUserId()
    {
        return
$this->originalUserId;
    }

    public function
getOriginalUserName()
    {
        return
$this->originalUserName;
    }

    public function
getNewUserId()
    {
        return
$this->newUserId;
    }

    public function
getNewUserName()
    {
        return
$this->newUserName;
    }

    public function
setupForDelete()
    {
       
$this->newUserId = 0;
       
$this->newUserName = $this->originalUserName; // ensure the values are correct

       
return $this;
    }

    public function
setupForNameChange($newUsername)
    {
       
$this->newUserId = null;
       
$this->newUserName = $newUsername;

        return
$this;
    }

    public function
setupForMerge(User $newUser)
    {
       
$this->newUserId = $newUser->user_id;
       
$this->newUserName = $newUser->username;

        return
$this;
    }

    public function
setupRaw($newUserId, $newUserName)
    {
       
$this->newUserId = $newUserId;
       
$this->newUserName = $newUserName;

        return
$this;
    }

    protected function
getSteps()
    {
        return
$this->steps;
    }

    public function
apply($maxRunTime = 0)
    {
        if (
$this->newUserId === null && $this->newUserName === null)
        {
           
// no work to do
           
return \XF\ContinuationResult::completed();
        }

       
$result = $this->runLoop($maxRunTime);

        return
$result;
    }

    protected function
stepReassignContent($lastOffset, $maxRunTime)
    {
       
$db = $this->db();

       
$originalUserId = $this->originalUserId;
       
$newUserId = $this->newUserId;
       
$newUsername = $this->newUserName;

       
$lastOffset = $lastOffset === null ? -1 : $lastOffset;
       
$thisOffset = -1;
       
$start = microtime(true);

        foreach (
$this->updates AS $table => $changes)
        {
           
$thisOffset++;
            if (
$thisOffset <= $lastOffset)
            {
                continue;
            }

            if (
is_string($changes[0]))
            {
               
// changes the simple ['user_id'] format to a consistent [['user_id']] format
               
$changes = [$changes];
            }

            foreach (
$changes AS $change)
            {
               
$userIdColumn = $change[0];
               
$usernameColumn = !empty($change[1]) ? $change[1] : null;

               
$sqlUpdates = [];
                if (
$newUserId !== null)
                {
                    if (!
$newUserId && isset($change['emptyable']) && !$change['emptyable'])
                    {
                       
// trying to update user ID to 0 and this is set to be ignored
                       
continue;
                    }

                   
$sqlUpdates[] = "`{$userIdColumn}` = " . $db->quote($newUserId);
                }
                if (
$newUsername !== null && $usernameColumn)
                {
                   
$sqlUpdates[] = "`{$usernameColumn}` = " . $db->quote($newUsername);
                }
                if (
$sqlUpdates)
                {
                   
$db->query("
                        UPDATE IGNORE `
{$table}` SET
                            "
. implode(', ', $sqlUpdates) . "
                        WHERE `
{$userIdColumn}` = ?
                    "
, $originalUserId);
                }
            }

           
$lastOffset = $thisOffset;
            if (
$maxRunTime && microtime(true) - $start > $maxRunTime)
            {
                return
$lastOffset; // continue at this position
           
}
        }

        return
null; // finished
   
}

    protected function
stepMergeThreadUserPost()
    {
        if (
$this->newUserId === null)
        {
            return;
        }

       
// merge the post counts here for accurate values
       
$this->db()->beginTransaction();
       
$this->db()->query("
            UPDATE xf_thread_user_post AS source, xf_thread_user_post AS target
            SET target.post_count = target.post_count + source.post_count
            WHERE source.user_id = ?
                AND source.thread_id = target.thread_id
                AND target.user_id = ?
        "
, [$this->originalUserId, $this->newUserId]);
       
$this->db()->delete('xf_thread_user_post', 'user_id = ?', $this->originalUserId);
       
$this->db()->commit();
    }

    protected function
stepRebuildLikes($lastOffset, $maxRunTime)
    {
       
$newLikeUserId = $this->newUserId !== null ? $this->newUserId : $this->originalUserId;
       
$newLikeUsername = $this->newUserName !== null ? $this->newUserName : $this->originalUserName;

       
$lastOffset = $lastOffset === null ? -1 : $lastOffset;
       
$thisOffset = -1;
       
$start = microtime(true);

       
/** @var \XF\Repository\LikedContent $likeRepo */
       
$likeRepo = $this->repository('XF:LikedContent');
        foreach (
$likeRepo->getLikeHandlers() AS $contentType => $likeHandler)
        {
           
$thisOffset++;
            if (
$thisOffset <= $lastOffset)
            {
                continue;
            }

           
$likeHandler->updateRecentCacheForUserChange(
               
$this->originalUserId, $newLikeUserId,
               
$this->originalUserName, $newLikeUsername
           
);

           
$lastOffset = $thisOffset;
            if (
$maxRunTime && microtime(true) - $start > $maxRunTime)
            {
                return
$lastOffset; // continue at this position
           
}
        }

        return
null;
    }

    protected function
stepRebuildReactions($lastOffset, $maxRunTime)
    {
       
$newReactionUserId = $this->newUserId !== null ? $this->newUserId : $this->originalUserId;
       
$newReactionUsername = $this->newUserName !== null ? $this->newUserName : $this->originalUserName;

       
$lastOffset = $lastOffset === null ? -1 : $lastOffset;
       
$thisOffset = -1;
       
$start = microtime(true);

       
/** @var \XF\Repository\Reaction $reactionRepo */
       
$reactionRepo = $this->repository('XF:Reaction');
        foreach (
$reactionRepo->getReactionHandlers() AS $contentType => $reactionHandler)
        {
           
$thisOffset++;
            if (
$thisOffset <= $lastOffset)
            {
                continue;
            }

           
$reactionHandler->updateRecentCacheForUserChange(
               
$this->originalUserId, $newReactionUserId,
               
$this->originalUserName, $newReactionUsername
           
);

           
$lastOffset = $thisOffset;
            if (
$maxRunTime && microtime(true) - $start > $maxRunTime)
            {
                return
$lastOffset; // continue at this position
           
}
        }

        return
null;
    }

    protected function
stepConversationRecipientCache()
    {
       
$newUserId = $this->newUserId !== null ? $this->newUserId : $this->originalUserId;
       
$newUserName = $this->newUserName !== null ? $this->newUserName : $this->originalUserName;

       
/** @var \XF\Repository\Conversation $convRepo */
       
$convRepo = $this->repository('XF:Conversation');
       
$convRepo->updateRecipientCacheForUserChange(
           
$this->originalUserId, $newUserId,
           
$this->originalUserName, $newUserName
       
);
    }

    protected function
stepApplyWarningGroupChanges()
    {
       
$newUserId = $this->getNewUserId();
        if (!
$newUserId)
        {
           
// user has been deleted or it's just a name change, so no action needed
           
return;
        }

       
$userGroupChanges = $this->db()->fetchPairs("
            SELECT change_key, group_ids
            FROM xf_user_group_change
            WHERE user_id = ?
                AND change_key REGEXP BINARY '^warning_[0-9]+$'
        "
, $this->getOriginalUserId());

        foreach (
$userGroupChanges AS $changeKey => $groupIds)
        {
           
$userGroupChangeService = \XF::service('XF:User\UserGroupChange');
           
$userGroupChangeService->addUserGroupChange($newUserId, $changeKey, $groupIds);
        }
    }

    protected function
stepReassignSearchIndex()
    {
        if (
$this->newUserId === null)
        {
            return;
        }

       
$this->app->search()->reassignContent($this->originalUserId, $this->newUserId);
    }

    protected function
stepRebuildFinalCaches()
    {
        if (
$this->newUserId === null)
        {
            return;
        }

       
$this->repository('XF:UserFollow')->rebuildFollowingCache($this->newUserId);
       
$this->repository('XF:UserIgnored')->rebuildIgnoredCache($this->newUserId);
    }
}