Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Service/Post/Merger.php
<?php

namespace XF\Service\Post;

use
XF\Entity\Post;

use function
array_key_exists, is_array;

class
Merger extends \XF\Service\AbstractService
{
   
/**
     * @var Post
     */
   
protected $target;

    protected
$originalTargetMessage;

   
/**
     * @var \XF\Service\Post\Preparer
     */
   
protected $postPreparer;

    protected
$alert = false;
    protected
$alertReason = '';

    protected
$log = true;

   
/**
     * @var \XF\Entity\Thread[]
     */
   
protected $sourceThreads = [];

   
/**
     * @var \XF\Entity\Post[]
     */
   
protected $sourcePosts = [];

   
/**
     * @var array
     */
   
protected $movedReactions = [];

    public function
__construct(\XF\App $app, Post $target)
    {
       
parent::__construct($app);

       
$this->target = $target;
       
$this->originalTargetMessage = $target->message;
       
$this->postPreparer = $this->service('XF:Post\Preparer', $this->target);
    }

    public function
getTarget()
    {
        return
$this->target;
    }

    public function
setSendAlert($alert, $reason = null)
    {
       
$this->alert = (bool)$alert;
        if (
$reason !== null)
        {
           
$this->alertReason = $reason;
        }
    }

    public function
setLog($log)
    {
       
$this->log = (bool)$log;
    }

    public function
setMessage($message, $format = true)
    {
        return
$this->postPreparer->setMessage($message, $format);
    }

    public function
merge($sourcePostsRaw)
    {
        if (
$sourcePostsRaw instanceof \XF\Mvc\Entity\AbstractCollection)
        {
           
$sourcePostsRaw = $sourcePostsRaw->toArray();
        }
        else if (
$sourcePostsRaw instanceof Post)
        {
           
$sourcePostsRaw = [$sourcePostsRaw];
        }
        else if (!
is_array($sourcePostsRaw))
        {
            throw new \
InvalidArgumentException('Posts must be provided as collection, array or entity');
        }

        if (!
$sourcePostsRaw)
        {
            return
false;
        }

       
$db = $this->db();

       
/** @var Post[] $sourcePosts */
       
$sourcePosts = [];

       
/** @var \XF\Entity\Thread[] $sourceThreads */
       
$sourceThreads = [];

        foreach (
$sourcePostsRaw AS $sourcePost)
        {
           
$sourcePost->setOption('log_moderator', false);
           
$sourcePosts[$sourcePost->post_id] = $sourcePost;

           
/** @var \XF\Entity\Thread $sourceThread */
           
$sourceThread = $sourcePost->Thread;
            if (!isset(
$sourceThreads[$sourceThread->thread_id]))
            {
               
$sourceThread->setOption('log_moderator', false);
               
$sourceThreads[$sourceThread->thread_id] = $sourceThread;
            }
        }

       
$this->sourceThreads = $sourceThreads;
       
$this->sourcePosts = $sourcePosts;

       
$target = $this->target;
       
$target->setOption('log_moderator', false);

       
$db->beginTransaction();

       
$this->moveDataToTarget();
       
$this->updateTargetData();
       
$this->updateSourceData();
       
$this->updateUserCounters();

        if (
$this->alert)
        {
           
$this->sendAlert();
        }

       
$this->finalActions();

       
$target->save();

       
$this->cleanupActions();

       
$db->commit();

        return
true;
    }

    protected function
moveDataToTarget()
    {
       
$db = $this->db();
       
$target = $this->target;

       
$sourcePosts = $this->sourcePosts;
       
$sourcePostIds = array_keys($sourcePosts);
       
$sourceIdsQuoted = $db->quote($sourcePostIds);

       
$this->repository('XF:ContentVote')->moveVotesBetweenContent($target, $sourcePosts);

       
$sourceReactions = $db->fetchAllKeyed(
           
"SELECT *
                FROM xf_reaction_content AS reaction_content
                INNER JOIN xf_reaction AS reaction
                    ON (reaction.reaction_id = reaction_content.reaction_id)
                WHERE content_type = 'post'
                AND content_id IN (
{$sourceIdsQuoted})",
           
'reaction_content_id'
       
);
        if (
$sourceReactions)
        {
           
$updateReactions = [];
           
$targetReactions = $db->fetchAllKeyed(
               
"SELECT *
                    FROM xf_reaction_content
                    WHERE content_type = 'post'
                    AND content_id = ?"
,
               
'reaction_user_id',
               
$target->post_id
           
);

            foreach (
$sourceReactions AS $reactionContentId => $reactionContent)
            {
               
$reactionUserId = $reactionContent['reaction_user_id'];
                if (
$reactionUserId && (
                   
$reactionUserId == $target->user_id ||
                    isset(
$targetReactions[$reactionUserId])
                ))
                {
                   
// reaction is from the content user
                    // or the user has already reacted to the content
                    // these will be cleaned up when the source content is deleted
                   
unset($sourceReactions[$reactionContentId]);
                    continue;
                }

               
$updateReactions[] = $reactionContentId;
               
$targetReactions[$reactionUserId] = $reactionContent;
            }

           
$this->movedReactions = $sourceReactions;

            if (
$updateReactions)
            {
               
$updateReactionsQuoted = $db->quote($updateReactions);
               
$targetReactionsCount = (
                   
$target->message_state == 'visible' &&
                   
$target->Thread->discussion_state == 'visible'
               
);

               
$db->update(
                   
'xf_reaction_content',
                    [
                       
'content_id' => $target->post_id,
                       
'content_user_id' => $target->user_id,
                       
'is_counted' => $targetReactionsCount ? 1 : 0,
                    ],
                   
"reaction_content_id IN ({$updateReactionsQuoted})"
               
);
            }
        }

       
$rows = $db->update('xf_attachment',
            [
'content_id' => $target->post_id],
           
"content_id IN ($sourceIdsQuoted) AND content_type = 'post'"
       
);

       
$target->attach_count += $rows;

       
$db->update(
           
'xf_bookmark_item',
            [
               
'content_type' => 'post',
               
'content_id' => $target->post_id
           
],
           
"content_id IN ($sourceIdsQuoted) AND content_type = 'post'",
            [],
'IGNORE'
       
);

        foreach (
$sourcePosts AS $sourcePost)
        {
           
$sourcePost->delete();
        }
    }

    protected function
updateTargetData()
    {
       
/** @var \XF\Entity\Thread $targetThread */
       
$targetThread = $this->target->Thread;

       
$targetThread->rebuildCounters();
       
$targetThread->save();

        if (
$this->target->post_id == $targetThread->first_post_id && $this->target->message_state != 'visible')
        {
           
// first post of the thread must always be visible
           
$this->target->message_state = 'visible';
        }
    }

    protected function
updateSourceData()
    {
       
/** @var \XF\Repository\Thread $threadRepo */
       
$threadRepo = $this->repository('XF:Thread');

        foreach (
$this->sourceThreads AS $sourceThread)
        {
           
$sourceThread->rebuildCounters();

           
$sourceThread->save(); // has to be saved for the delete to work (if needed).

           
if (array_key_exists($sourceThread->first_post_id, $this->sourcePosts) && $sourceThread->reply_count == 0)
            {
               
$sourceThread->delete(); // first post has been moved out, no other replies, thread now empty
           
}
            else
            {
               
$threadRepo->rebuildThreadPostPositions($sourceThread->thread_id);
               
$threadRepo->rebuildThreadUserPostCounters($sourceThread->thread_id);
            }

           
$sourceThread->Forum->rebuildCounters();
           
$sourceThread->Forum->save();
        }
    }

    protected function
updateUserCounters()
    {
       
$target = $this->target;

       
$userReactionCountAdjust = [];
       
$targetReactionsCount = (
           
$target->message_state == 'visible' &&
           
$target->Thread->discussion_state == 'visible'
       
);

        foreach (
$this->movedReactions AS $reactionContentId => $reactionContent)
        {
           
$sourceReactionsCount = $reactionContent['is_counted'];
           
$sourceUserId = $reactionContent['content_user_id'];
           
$targetUserId = $target->user_id;
           
$reactionScore = $reactionContent['reaction_score'];

            if (
$sourceReactionsCount && $sourceUserId)
            {
                if (!isset(
$userReactionCountAdjust[$sourceUserId]))
                {
                   
$userReactionCountAdjust[$sourceUserId] = 0;
                }

               
$userReactionCountAdjust[$sourceUserId] -= $reactionScore;
            }

            if (
$targetReactionsCount && $targetUserId)
            {
                if (!isset(
$userReactionCountAdjust[$targetUserId]))
                {
                   
$userReactionCountAdjust[$targetUserId] = 0;
                }

               
$userReactionCountAdjust[$targetUserId] += $reactionScore;
            }
        }

        foreach (
$userReactionCountAdjust AS $userId => $adjust)
        {
            if (!
$adjust)
            {
                continue;
            }

           
$this->db()->query(
               
'UPDATE xf_user
                    SET reaction_score = reaction_score + ?
                    WHERE user_id = ?'
,
                [
$adjust, $userId]
            );
        }
    }

    protected function
sendAlert()
    {
       
/** @var \XF\Repository\Post $postRepo */
       
$postRepo = $this->repository('XF:Post');

       
$alerted = [];
        foreach (
$this->sourcePosts AS $sourcePost)
        {
            if (isset(
$alerted[$sourcePost->user_id]))
            {
                continue;
            }

            if (
$sourcePost->message_state == 'visible' && $sourcePost->user_id != \XF::visitor()->user_id)
            {
               
$postRepo->sendModeratorActionAlert($sourcePost, 'merge', $this->alertReason);
               
$alerted[$sourcePost->user_id] = true;
            }
        }
    }

    protected function
finalActions()
    {
       
$target = $this->target;


       
$preEditMergeMessage = $this->originalTargetMessage;
        foreach (
$this->sourcePosts AS $s)
        {
           
$preEditMergeMessage .= "\n\n" . $s->message;
        }
       
$preEditMergeMessage = trim($preEditMergeMessage);

       
$options = $this->app->options();
        if (
$options->editLogDisplay['enabled'] && $this->log && $target->message != $preEditMergeMessage)
        {
           
$target->last_edit_date = \XF::$time;
           
$target->last_edit_user_id = \XF::visitor()->user_id;
        }

        if (
$options->editHistory['enabled'])
        {
           
$visitor = \XF::visitor();
           
$ip = $this->app->request()->getIp();

           
/** @var \XF\Repository\EditHistory $editHistoryRepo */
           
$editHistoryRepo = $this->app->repository('XF:EditHistory');

           
// Log an edit history record for the target post's original message then log a further record
            // for the pre-merge result of all the source and target messages. These two entries should ensure
            // there is no context loss as a result of merging a series of posts.
           
$editHistoryRepo->insertEditHistory('post', $target, $visitor, $this->originalTargetMessage, $ip);
           
$target->edit_count++;

            if (
$target->message != $preEditMergeMessage)
            {
               
$editHistoryRepo->insertEditHistory('post', $target, $visitor, $preEditMergeMessage, $ip);
               
$target->edit_count++;
            }
        }
    }

    protected function
cleanupActions()
    {
       
$target = $this->target;
       
$targetThread = $this->target->Thread;
       
$postIds = array_keys($this->sourcePosts);

        if (
$postIds)
        {
           
$this->app->jobManager()->enqueue('XF:SearchIndex', [
               
'content_type' => 'post',
               
'content_ids' => $postIds
           
]);
        }

        if (
$this->log)
        {
           
$this->app->logger()->logModeratorAction('post', $target, 'merge_target',
                [
'ids' => implode(', ', $postIds)]
            );
        }

       
/** @var \XF\Repository\Thread $threadRepo */
       
$threadRepo = $this->repository('XF:Thread');
       
$threadRepo->rebuildThreadPostPositions($targetThread->thread_id);
       
$threadRepo->rebuildThreadUserPostCounters($targetThread->thread_id);

       
/** @var \XF\Repository\Reaction $reactionRepo */
       
$reactionRepo = $this->repository('XF:Reaction');
       
$reactionRepo->rebuildContentReactionCache('post', $target->post_id);
    }
}