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

namespace XF\Service\Post;

use
XF\Entity\Post;
use
XF\Entity\Thread;

use function
array_key_exists, intval, is_array;

class
Mover extends \XF\Service\AbstractService
{
   
/**
     * @var Thread
     */
   
protected $target;

    protected
$existingTarget = false;

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

    protected
$prefixId = null;

    protected
$log = true;

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

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

    public function
__construct(\XF\App $app, Thread $target)
    {
       
parent::__construct($app);
       
$this->target = $target;
    }

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

    public function
setExistingTarget($existing)
    {
       
$this->existingTarget = (bool)$existing;
    }

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

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

    public function
setPrefix($prefixId)
    {
       
$this->prefixId = ($prefixId === null ? $prefixId : intval($prefixId));
    }

    public function
move($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 */
        /** @var Thread[] $sourceThreads */
       
$sourcePosts = [];
       
$sourceThreads = [];

       
$target = $this->target;

        foreach (
$sourcePostsRaw AS $sourcePost)
        {
            if (
$sourcePost->thread_id == $target->thread_id)
            {
                continue;
            }

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

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

        if (!
$sourcePosts)
        {
            return
false; // nothing to do
       
}

       
$sourcePosts = \XF\Util\Arr::columnSort($sourcePosts, 'post_date');

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

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

        if (!
$target->thread_id)
        {
           
$firstPost = reset($sourcePosts);

           
$target->user_id = $firstPost->user_id;
           
$target->username = $firstPost->username;
           
$target->post_date = $firstPost->post_date;
        }

       
$db->beginTransaction();

       
$target->save();

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

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

       
$this->finalActions();

       
$db->commit();

        return
true;
    }

    protected function
isTargetFirstPost(\XF\Entity\Post $post)
    {
        return (!
$this->existingTarget || $post->post_date <= $this->target->post_date);
    }

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

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

       
// we need to keep the original thread/state references locally, but we want the global/cached
        // versions of the entities to reflect their new status, so we'll update the local versions
        // to be a clone and then we'll update $sourcePosts (which will share references with the global ones)
       
$this->sourcePosts = \XF\Util\Arr::cloneArray($sourcePosts);

       
$db->update('xf_post',
            [
'thread_id' => $target->thread_id],
           
"post_id IN ($sourceIdsQuoted)"
       
);

        foreach (
$sourcePosts AS $sourcePost)
        {
           
$sourcePost->setAsSaved('thread_id', $target->thread_id);
        }

       
$firstPost = reset($sourcePosts);
        if (
           
$firstPost->message_state != 'visible'
           
&& $this->isTargetFirstPost($firstPost)
        )
        {
           
$firstPost->message_state = 'visible';
           
$firstPost->save();
           
// this used to clone, but we've already done that above
       
}

        if (!
$this->existingTarget)
        {
           
$db->update('xf_bookmark_item', [
               
'content_type' => 'thread',
               
'content_id' => $target->thread_id
           
], 'content_type = ? AND content_id = ?', ['post', $firstPost->post_id]);
        }
    }

    protected function
updateTargetData()
    {
       
$target = $this->target;
       
$firstPost = reset($this->sourcePosts);

        if (
$this->prefixId !== null)
        {
           
$target->prefix_id = $this->prefixId;
        }

        if (
$this->isTargetFirstPost($firstPost))
        {
           
$target->discussion_state = $firstPost->message_state;
        }

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

       
$target->Forum->rebuildCounters();
       
$target->Forum->save();

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

    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
            {
                if (!
$sourceThread->FirstPost->isVisible())
                {
                   
$sourceThread->discussion_state = $sourceThread->FirstPost->message_state;
                   
$sourceThread->save();

                   
$sourceThread->FirstPost->message_state = 'visible';
                   
$sourceThread->FirstPost->save();
                }

               
$threadRepo->rebuildThreadPostPositions($sourceThread->thread_id);
               
$threadRepo->rebuildThreadUserPostCounters($sourceThread->thread_id);
            }

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

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

       
$targetMessagesCount = (
           
$target->Forum && $target->Forum->count_messages
           
&& $target->discussion_state == 'visible'
       
);
       
$targetReactionsCount = ($target->discussion_state == 'visible');

       
$sourcesMessagesCount = [];
       
$sourcesReactionsCount = [];
        foreach (
$this->sourceThreads AS $id => $sourceThread)
        {
           
$sourcesMessagesCount[$id] = (
               
$sourceThread->Forum && $sourceThread->Forum->count_messages
               
&& $sourceThread->discussion_state == 'visible'
           
);
           
$sourcesReactionsCount[$id] = ($sourceThread->discussion_state == 'visible');
        }

       
$reactionsEnable = [];
       
$reactionsDisable = [];
       
$userMessageCountAdjust = [];

       
/** @var \XF\Repository\NewsFeed $newsFeedRepo */
       
$newsFeedRepo = $this->repository('XF:NewsFeed');

        foreach (
$this->sourcePosts AS $id => $post)
        {
            if (
$post->message_state != 'visible')
            {
                continue;
// everything will stay the same in the new thread
           
}

           
$sourceMessagesCount = !empty($sourcesMessagesCount[$post->thread_id]);
           
$sourceReactionsCount = !empty($sourcesReactionsCount[$post->thread_id]);

            if (
$post->reactions)
            {
                if (
$sourceReactionsCount && !$targetReactionsCount)
                {
                   
$reactionsDisable[] = $id;
                }
                else if (!
$sourceReactionsCount && $targetReactionsCount)
                {
                   
$reactionsEnable[] = $id;
                }
            }

           
$userId = $post->user_id;
            if (
$userId)
            {
                if (
$sourceMessagesCount && !$targetMessagesCount)
                {
                    if (!isset(
$userMessageCountAdjust[$userId]))
                    {
                       
$userMessageCountAdjust[$userId] = 0;
                    }
                   
$userMessageCountAdjust[$userId]--;
                }
                else if (!
$sourceMessagesCount && $targetMessagesCount)
                {
                    if (!isset(
$userMessageCountAdjust[$userId]))
                    {
                       
$userMessageCountAdjust[$userId] = 0;
                    }
                   
$userMessageCountAdjust[$userId]++;
                }
            }

            if (!
$this->existingTarget)
            {
               
// if moving to a new thread (which will publish a new feed entry)
                // unpublish the original reply to prevent a duplicate entry.
               
$newsFeedRepo->unpublish('post', $post->post_id, $post->user_id, 'insert');
            }
        }

        if (
$reactionsDisable)
        {
           
/** @var \XF\Repository\Reaction $reactionRepo */
           
$reactionRepo = $this->repository('XF:Reaction');
           
$reactionRepo->fastUpdateReactionIsCounted('post', $reactionsDisable, false);
        }
        if (
$reactionsEnable)
        {
           
/** @var \XF\Repository\Reaction $reactionRepo */
           
$reactionRepo = $this->repository('XF:Reaction');
           
$reactionRepo->fastUpdateReactionIsCounted('post', $reactionsEnable, true);
        }
        foreach (
$userMessageCountAdjust AS $userId => $adjust)
        {
            if (
$adjust)
            {
               
$this->db()->query("
                    UPDATE xf_user
                    SET message_count = GREATEST(0, message_count + ?)
                    WHERE user_id = ?
                "
, [$adjust, $userId]);
            }
        }
    }

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

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

        foreach (
$this->sourcePosts AS $sourcePost)
        {
            if (
$sourcePost->Thread->discussion_state == 'visible'
               
&& $sourcePost->message_state == 'visible'
               
&& $sourcePost->user_id != \XF::visitor()->user_id
           
)
            {
               
$targetPost = clone $sourcePost;
               
$targetPost->setAsSaved('thread_id', $target->thread_id);

               
$alertExtras = [
                   
'sourceTitle' => $sourcePost->Thread->title,
                   
'targetLink' => $this->app->router('public')->buildLink('nopath:posts', $sourcePost)
                ];

               
$postRepo->sendModeratorActionAlert($targetPost, 'move', $this->alertReason, $alertExtras);
            }
        }
    }

    protected function
finalActions()
    {
       
$target = $this->target;
       
$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('thread', $target, 'post_move_target' . ($this->existingTarget ? '_existing' : ''),
                [
'ids' => implode(', ', $postIds)]
            );

            foreach (
$this->sourceThreads AS $sourceThread)
            {
               
$this->app->logger()->logModeratorAction('thread', $sourceThread, 'post_move_source', [
                   
'url' => $this->app->router('public')->buildLink('nopath:threads', $target),
                   
'title' => $target->title
               
]);
            }
        }
    }
}