XF 1.2 Post Edit History and Logging

Post edit history and logging has been one of the most requested features and it will be a core feature in XenForo 1.2.

Post Edit Logging
Post edit logging is the simpler of the two features. It adds an indication to the post when it has been edited. We have intentionally kept this feature simple for the average user. They have no reason that they have to enter; if they're inclined, they can always include the reason in the message.

We do track the last person to edit the message, but this is not displayed. It could be easily added with an add-on. In most cases, the last edit will be by the owner and the average user likely doesn't care about who edited it. If a moderator needs to know, the history gives much more detailed information.

The edit log can be set to not be displayed if a post is edited in the first X minutes.

So what does this look like on a post?

ss-2013-03-22_11-36-01.webp


Moderators also get a few extra options to control the public log of the edit. Note that these options do not affect the history. This only allows them to suppress the public notice.

ss-2013-03-22_11-36-36.webp


Post Edit History
Post edit history actually keeps all previous versions of a message rather than just an indication that it was edited. This can be used for many things, including handling the "rage-delete" situation, where a user edits all of their content, potentially destroying the flow of many threads.

For developers, it's worth noting that the history system has mostly been developed to be content agnostic. Add-ons can make use of the system and automatically get access to the history and comparison interface.

Whenever a post is edited--even with a "silent" moderator edit--the history is logged. You may have noticed it in the screenshot above, but a history link will appear whenever a post has been edited.

When you click that link, you will have an opportunity to pick two versions to compare and see the actual changes made

Note that the UI I'm demonstrating below is still a work in progress and improvements will be made.

ss-2013-03-22_11-43-27.webp


You can also view the previous version in its raw form with a button to the right (which I didn't include in the screenshot). Options to automatically revert to a previous version may be included.

History data can be set to only be maintained for a specific number of days.
 
Now that I've started looking at the 1.2 underlying changes, I'm surprised that the edit history doesn't log the IP of the user making the edit. Hopefully just an oversight and maybe added before final version?

I also wish that it did have an optional "reason" for users (I know that @Mike said it was intentionally left out, but not super excited about that).
 
Quick question: Is this a moderator only feature, or can it be enabled across all usergroups. (The ability to view post edit history).

Thanks,
oman
 
I don't have the link handy, but Mike had explained it as a moderator feature due to the fact that it shows exact dates, times, and who did the edits. It also allows a user to see content that was deemed unsuitable by a moderator.
 
Hmmm... I guess lots of things I need to change with it... IP logging, optional reason and permission-based.

Not sure the argument about mods editing inappropriate content really happens that often. On my site, we delete inappropriate posts, not edit them... User's ability to see edit history is more about being able to see posts that said one thing and then were changed (bait and switch).
 
I don't know if it was already asked, but I would be interested if the vB3->XF Importer will import edit history from vB3 too...
Not sure if @Mike or @Kier have been working on it already, but this is the code snippet I've come up with to successfully import the edit history from my test vBulletin install. Maybe it's of some help.

(I was too lazy to write an addon extending the importer since my vB importer files are already heavily modified to suit my specific purposes).

In /library/XenForo/Importer/vBulletin.php:

PHP:
    public function getSteps()
    {
        return array(
...
            'edithistory' => array(
                'title' => 'MobileRead Import Edit History',
                'depends' => array('threads', 'users', 'forums')
          ),
...
        );

PHP:
    public function stepedithistory($start, array $options)
    {
        $options = array_merge(array(
            'limit' => 10,
            'max' => false
        ), $options);

        $sDb = $this->_sourceDb;
        $prefix = $this->_prefix;

        /* @var $model XenForo_Model_Import */
        $model = $this->_importModel;

        if ($options['max'] === false)
        {
            $options['max'] = $sDb->fetchOne('
                SELECT MAX(postid)
                FROM ' . $prefix . 'postedithistory
            ');
        }

        $postIds = $sDb->fetchCol($sDb->limit(
            '
                SELECT DISTINCT postid
                FROM ' . $prefix . 'postedithistory
                WHERE postid > ' . $sDb->quote($start) . '
                ORDER BY postid
            ', $options['limit']
        ));

        if (!$postIds)
        {
            return true;
        }

        $next = 0;
        $total = 0;

        XenForo_Db::beginTransaction();

        foreach ($postIds as $postId)
        {
            $next = $postId;

            // order by dateline!
            $oldEdits = $sDb->fetchAll(
                '
                    SELECT postedithistoryid AS edit_history_id, original, \'post\' AS content_type, postid AS content_id, userid AS edit_user_id, dateline AS edit_date, pagetext AS old_text
                    FROM ' . $prefix . 'postedithistory
                    WHERE postid =  ' . $sDb->quote($postId) . '
                    ORDER BY dateline
                '
            );

            $userIdMap = $model->getUserIdsMapFromArray($oldEdits, 'edit_user_id');
            $postIdMap = $model->getPostIdsMapFromArray($oldEdits, 'content_id');

            $newEdits = array();
            foreach ($oldEdits as $i => $oldEdit)
            {
                $newUserId = $this->_mapLookUp($userIdMap, $oldEdit['edit_user_id']);
                $newContentId = $this->_mapLookUp($postIdMap, $oldEdit['content_id']);

                if (!$newUserId)
                {
                    continue;
                }

                $oldEdit['old_text'] = $this->_convertToUtf8($oldEdit['old_text']);

                // rewrite quotes into xF 1.2 syntax
                if (stripos($oldEdit['old_text'], '[quote=') !== false)
                {
                    if (preg_match_all('/\[quote=("|\'|)(?P<username>[^;]*);\s*(?P<postid>\d+)\s*\1\]/siU', $oldEdit['old_text'], $quotes, PREG_SET_ORDER))
                    {
                        $post['quotes'] = array();

                        foreach ($quotes AS $quote)
                        {
                            $quotedPostId = intval($quote['postid']);

                            $quotedPostIds[] = $quotedPostId;

                            $post['quotes'][$quote[0]] = array($quote['username'], $quotedPostId);
                        }
                    }
                }

                $quotedPostIdMap = (empty($quotedPostIds) ? array() : $model->getImportContentMap('post', $quotedPostIds));

                if (!empty($post['quotes']))
                {
                    $oldEdit['old_text'] = $this->_rewriteQuotes($oldEdit['old_text'], $post['quotes'], $quotedPostIdMap);
                }
                // quote rewrite end

                $newEdits[$i]['old_text'] = $oldEdit['old_text'];

                if (!$oldEdit['original'] == 1)
                {
                    $newEdits[$i-1]['content_type'] = $oldEdit['content_type'];
                    $newEdits[$i-1]['content_id'] = $newContentId;
                    $newEdits[$i-1]['edit_user_id'] = $newUserId;
                    $newEdits[$i-1]['edit_date'] = $oldEdit['edit_date'];
                }

                $lastUser = $newUserId;
                $lastDate = $oldEdit['edit_date'];
            }
           
            // remove last
            array_pop($newEdits);

            $model->importEditHistory($newEdits, $postId, sizeof($newEdits), $lastUser, $lastDate);

            $total++;
   
        }

        XenForo_Db::commit();

        $this->_session->incrementStepImportTotal($total);

        return array($next, $options, $this->_getProgressOutput($next, $options['max']));
    }

In /library/XenForo/Model/Import.php:

PHP:
    public function importEditHistory($newEdits, $contentId, $count, $lastUser, $lastDate)
    {
        foreach ($newEdits as $newEdit)
        {
            $historyDw = XenForo_DataWriter::create('XenForo_DataWriter_EditHistory', XenForo_DataWriter::ERROR_SILENT);
            $historyDw->bulkSet($newEdit);
            $historyDw->save();
        }

        $dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post', XenForo_DataWriter::ERROR_SILENT);
        $dw->setExistingData($contentId);
        $dw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_LOG_EDIT, false);
        $dw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_EDIT_DATE_DELAY, -1);
        $dw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_IS_AUTOMATED, true);
        $dw->set('edit_count', $count);
        $dw->set('last_edit_date', $lastDate);
        $dw->set('last_edit_user_id', $lastUser);
        return $dw->save();
    }
 
Can we import post edit history from vb 3.8 to xf 1.2 ?
hope you guys will upgrade the vb3.8 importers for xf 1.2 new features.
 
Not sure if @Mike or @Kier have been working on it already, but this is the code snippet I've come up with to successfully import the edit history from my test vBulletin install. Maybe it's of some help.

(I was too lazy to write an addon extending the importer since my vB importer files are already heavily modified to suit my specific purposes).

In /library/XenForo/Importer/vBulletin.php:

PHP:
    public function getSteps()
    {
        return array(
...
            'edithistory' => array(
                'title' => 'MobileRead Import Edit History',
                'depends' => array('threads', 'users', 'forums')
          ),
...
        );

PHP:
    public function stepedithistory($start, array $options)
    {
        $options = array_merge(array(
            'limit' => 10,
            'max' => false
        ), $options);

        $sDb = $this->_sourceDb;
        $prefix = $this->_prefix;

        /* @var $model XenForo_Model_Import */
        $model = $this->_importModel;

        if ($options['max'] === false)
        {
            $options['max'] = $sDb->fetchOne('
                SELECT MAX(postid)
                FROM ' . $prefix . 'postedithistory
            ');
        }

        $postIds = $sDb->fetchCol($sDb->limit(
            '
                SELECT DISTINCT postid
                FROM ' . $prefix . 'postedithistory
                WHERE postid > ' . $sDb->quote($start) . '
                ORDER BY postid
            ', $options['limit']
        ));

        if (!$postIds)
        {
            return true;
        }

        $next = 0;
        $total = 0;

        XenForo_Db::beginTransaction();

        foreach ($postIds as $postId)
        {
            $next = $postId;

            // order by dateline!
            $oldEdits = $sDb->fetchAll(
                '
                    SELECT postedithistoryid AS edit_history_id, original, \'post\' AS content_type, postid AS content_id, userid AS edit_user_id, dateline AS edit_date, pagetext AS old_text
                    FROM ' . $prefix . 'postedithistory
                    WHERE postid =  ' . $sDb->quote($postId) . '
                    ORDER BY dateline
                '
            );

            $userIdMap = $model->getUserIdsMapFromArray($oldEdits, 'edit_user_id');
            $postIdMap = $model->getPostIdsMapFromArray($oldEdits, 'content_id');

            $newEdits = array();
            foreach ($oldEdits as $i => $oldEdit)
            {
                $newUserId = $this->_mapLookUp($userIdMap, $oldEdit['edit_user_id']);
                $newContentId = $this->_mapLookUp($postIdMap, $oldEdit['content_id']);

                if (!$newUserId)
                {
                    continue;
                }

                $oldEdit['old_text'] = $this->_convertToUtf8($oldEdit['old_text']);

                // rewrite quotes into xF 1.2 syntax
                if (stripos($oldEdit['old_text'], '[quote=') !== false)
                {
                    if (preg_match_all('/\[quote=("|\'|)(?P<username>[^;]*);\s*(?P<postid>\d+)\s*\1\]/siU', $oldEdit['old_text'], $quotes, PREG_SET_ORDER))
                    {
                        $post['quotes'] = array();

                        foreach ($quotes AS $quote)
                        {
                            $quotedPostId = intval($quote['postid']);

                            $quotedPostIds[] = $quotedPostId;

                            $post['quotes'][$quote[0]] = array($quote['username'], $quotedPostId);
                        }
                    }
                }

                $quotedPostIdMap = (empty($quotedPostIds) ? array() : $model->getImportContentMap('post', $quotedPostIds));

                if (!empty($post['quotes']))
                {
                    $oldEdit['old_text'] = $this->_rewriteQuotes($oldEdit['old_text'], $post['quotes'], $quotedPostIdMap);
                }
                // quote rewrite end

                $newEdits[$i]['old_text'] = $oldEdit['old_text'];

                if (!$oldEdit['original'] == 1)
                {
                    $newEdits[$i-1]['content_type'] = $oldEdit['content_type'];
                    $newEdits[$i-1]['content_id'] = $newContentId;
                    $newEdits[$i-1]['edit_user_id'] = $newUserId;
                    $newEdits[$i-1]['edit_date'] = $oldEdit['edit_date'];
                }

                $lastUser = $newUserId;
                $lastDate = $oldEdit['edit_date'];
            }
          
            // remove last
            array_pop($newEdits);

            $model->importEditHistory($newEdits, $postId, sizeof($newEdits), $lastUser, $lastDate);

            $total++;
  
        }

        XenForo_Db::commit();

        $this->_session->incrementStepImportTotal($total);

        return array($next, $options, $this->_getProgressOutput($next, $options['max']));
    }

In /library/XenForo/Model/Import.php:

PHP:
    public function importEditHistory($newEdits, $contentId, $count, $lastUser, $lastDate)
    {
        foreach ($newEdits as $newEdit)
        {
            $historyDw = XenForo_DataWriter::create('XenForo_DataWriter_EditHistory', XenForo_DataWriter::ERROR_SILENT);
            $historyDw->bulkSet($newEdit);
            $historyDw->save();
        }

        $dw = XenForo_DataWriter::create('XenForo_DataWriter_DiscussionMessage_Post', XenForo_DataWriter::ERROR_SILENT);
        $dw->setExistingData($contentId);
        $dw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_LOG_EDIT, false);
        $dw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_EDIT_DATE_DELAY, -1);
        $dw->setOption(XenForo_DataWriter_DiscussionMessage::OPTION_IS_AUTOMATED, true);
        $dw->set('edit_count', $count);
        $dw->set('last_edit_date', $lastDate);
        $dw->set('last_edit_user_id', $lastUser);
        return $dw->save();
    }

I try this night to import the vb edit history from my old forum, with your code, thanks a lot for share.
 
This can be used for many things, including handling the "rage-delete" situation, where a user edits all of their content, potentially destroying the flow of many threads.

Being a forum mod, I signed up here to say thank you, THANK YOU, THANK YOU for this feature. You can't imagine how THANKFUL I am for this.
 
Top Bottom