<p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
{/if}
-{if $action == 'edit'}<p class="info jsArticleNoticeTrash"{if !$article->isDeleted} style="display: none;"{/if}>{lang}wcf.acp.article.trash.notice{/lang}</p>{/if}
+{if $action == 'edit'}
+ <p class="info jsArticleNoticeTrash"{if !$article->isDeleted} style="display: none;"{/if}>{lang}wcf.acp.article.trash.notice{/lang}</p>
+
+ {if $lastVersion}<p class="info">{lang}wcf.acp.article.lastVersion{/lang}</p>{/if}
+{/if}
<form method="post" action="{if $action == 'add'}{link controller='ArticleAdd'}{/link}{else}{link controller='ArticleEdit' id=$articleID}{/link}{/if}">
<div class="section">
--- /dev/null
+{capture assign='pageTitle'}{$object->getTitle()} - {lang}wcf.edit.versions{/lang}{/capture}
+
+{include file='header'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.edit.versions{/lang}: {$object->getTitle()}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{$object->getLink()}" class="button"><span class="icon icon16 fa-arrow-right"></span> <span>{lang}wcf.edit.button.goToContent{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{if !$diffs|empty}
+{if !$diffs[0]|isset}
+<div class="section tabMenuContainer">
+ <nav class="tabMenu">
+ <ul>
+ {foreach from=$languages item=language}
+ <li data-name="language{@$language->languageID}"><a href="#">{$language}</a></li>
+ {/foreach}
+ </ul>
+ </nav>
+{/if}
+{foreach from=$diffs key=languageID item=properties}
+{if $languageID}<div class="tabMenuContent" data-name="language{@$languageID}">{/if}
+{foreach from=$properties key=property item=diff}
+<section class="section editHistoryDiff">
+ <h2 class="sectionTitle">{lang}wcf.edit.headline.comparison{/lang}: {$objectTypeProcessor->getPropertyLabel($property)}</h2>
+
+ <div class="sideBySide">
+ <div class="containerHeadline">
+ <h3>{lang}wcf.edit.headline.old{/lang}</h3>
+ </div>
+ <div class="containerHeadline">
+ <h3>{lang}wcf.edit.headline.newOrCurrent{/lang}</h3>
+ </div>
+ </div>
+
+<div><div>
+{assign var='prevType' value=''}
+{foreach from=$diff item='line'}
+{if $line[0] !== $prevType}
+ </div>
+
+ {* unmodified, after deletion needs a "fake" insertion *}
+ {if $line[0] === ' ' && $prevType === '-'}<div></div>{/if}
+
+ {* unmodified and deleted start a new container *}
+ {if $line[0] === ' ' || $line[0] === '-'}</div>{/if}
+
+ {* adding, without deleting needs a "fake" deletion *}
+ {if $line[0] === '+' && $prevType !== '-'}
+ </div>
+ <div class="sideBySide">
+ <div></div>
+ {/if}
+
+ {if $line[0] === ' '}
+ <div>
+ {/if}
+ {if $line[0] === '-'}
+ <div class="sideBySide">
+ {/if}
+ <div{if $line[0] === '+'} style="color: green;"{elseif $line[0] === '-'} style="color: red;"{/if}>
+{/if}
+{if $line[0] === ' '}{@$line[1]}<br>{/if}
+{if $line[0] === '-'}{@$line[1]}<br>{/if}
+{if $line[0] === '+'}{@$line[1]}<br>{/if}
+{assign var='prevType' value=$line[0]}
+{/foreach}
+</div></div>
+</section>
+{/foreach}
+{if $languageID}</div>{/if}
+{/foreach}
+{if !$diffs[0]|isset}</div>{/if}
+{/if}
+
+<form action="{link controller='VersionTrackerList'}{/link}" method="post">
+ <section class="section tabularBox editHistoryVersionList">
+ {assign var='versionCount' value=$versions|count}
+ <h2 class="sectionTitle">
+ {lang}wcf.edit.versions{/lang} <span class="badge">{#$versionCount+1}</span>
+ </h2>
+
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnEditID" colspan="2">{lang}wcf.edit.version{/lang}</th>
+ <th class="columnText columnUser">{lang}wcf.user.username{/lang}</th>
+ <th class="columnDate columnTime">{lang}wcf.edit.time{/lang}</th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr>
+ <td class="columnIcon">
+ <span class="icon icon16 fa-undo disabled"></span>
+ <input type="radio" name="oldID" value="current"{if $oldID === 'current'} checked{/if}> <input type="radio" name="newID" value="current"{if $newID === 'current'} checked{/if}>
+ {event name='rowButtons'}
+ </td>
+ <td class="columnID"><strong>{lang}wcf.edit.currentVersion{/lang}</strong></td>
+ <td class="columnText columnUser"><a href="{link controller='UserEdit' id=$object->getUserID()}{/link}">{$object->getUsername()}</a></td>
+ <td class="columnDate columnTime">{@$object->getTime()|time}</td>
+
+ {event name='columns'}
+ </tr>
+ {foreach from=$versions item=edit name=edit}
+ <tr class="jsEditRow">
+ <td class="columnIcon">
+ <span class="icon icon16 fa-undo pointer jsRevertButton jsTooltip" title="{lang}wcf.edit.revert{/lang}" data-object-id="{@$edit->versionID}" data-confirm-message="{lang __encode=true}wcf.edit.revert.sure{/lang}"></span>
+ <input type="radio" name="oldID" value="{@$edit->versionID}"{if $oldID == $edit->versionID} checked{/if}> <input type="radio" name="newID" value="{@$edit->versionID}"{if $newID == $edit->versionID} checked{/if}>
+ {event name='rowButtons'}
+ </td>
+ <td class="columnID">{#($tpl[foreach][edit][total] - $tpl[foreach][edit][iteration] + 1)}</td>
+ <td class="columnText columnUser"><a href="{link controller='User' id=$edit->userID title=$edit->username}{/link}">{$edit->username}</a></td>
+ <td class="columnDate columnTime">{@$edit->time|time}</td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ <script data-relocate="true">
+ $(function () {
+ /*
+ TODO: this needs to be adjusted
+ */
+ new WCF.Message.EditHistory($('input[name=oldID]'), $('input[name=newID]'), '.jsEditRow');
+ });
+ </script>
+ </table>
+ </section>
+
+ <div class="formSubmit">
+ <input type="hidden" name="objectID" value="{$objectID}">
+ <input type="hidden" name="objectType" value="{$objectType}">
+ <button class="button buttonPrimary" data-type="submit">{lang}wcf.edit.button.compare{/lang}</button>
+ </div>
+</form>
+
+{include file='footer'}
use wcf\system\exception\PermissionDeniedException;
use wcf\system\language\LanguageFactory;
use wcf\system\tagging\TagEngine;
+use wcf\system\version\VersionTracker;
use wcf\system\WCF;
use wcf\util\DateUtil;
WCF::getTPL()->assign([
'action' => 'edit',
'articleID' => $this->articleID,
- 'article' => $this->article
+ 'article' => $this->article,
+ 'lastVersion' => VersionTracker::getInstance()->getLastVersion('com.woltlab.wcf.article', $this->articleID)
]);
}
}
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\data\IVersionTrackerObject;
+use wcf\data\language\Language;
+use wcf\page\AbstractPage;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\request\LinkHandler;
+use wcf\system\version\IVersionTrackerProvider;
+use wcf\system\version\VersionTracker;
+use wcf\system\version\VersionTrackerEntry;
+use wcf\system\WCF;
+use wcf\util\Diff;
+use wcf\util\HeaderUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows a list of tracked versions for provided object type and id.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Page
+ */
+class VersionTrackerListPage extends AbstractPage {
+ /**
+ * object id
+ * @var integer
+ */
+ public $objectID = 0;
+
+ /**
+ * object type name
+ * @var string
+ */
+ public $objectType = '';
+
+ /**
+ * @var IVersionTrackerProvider
+ */
+ public $objectTypeProcessor;
+
+ /**
+ * @var VersionTrackerEntry[]
+ */
+ public $versions = [];
+
+ /**
+ * left / old version id
+ * @var integer
+ */
+ public $oldID = 0;
+
+ /**
+ * left / old version
+ * @var VersionTrackerEntry
+ */
+ public $old;
+
+ /**
+ * right / new version id
+ * @var integer
+ */
+ public $newID = 0;
+
+ /**
+ * right / new version
+ * @var VersionTrackerEntry
+ */
+ public $new;
+
+ /**
+ * differences between both versions
+ * @var Diff[]
+ */
+ public $diffs = [];
+
+ /**
+ * requested object
+ * @var IVersionTrackerObject
+ */
+ public $object;
+
+ /**
+ * list of available languages for comparison
+ * @var Language[]
+ */
+ public $languages = [];
+
+ /**
+ * property used for comparison
+ * @var string
+ */
+ public $property = '';
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['objectID'])) $this->objectID = intval($_REQUEST['objectID']);
+ if (isset($_REQUEST['objectType'])) $this->objectType = $_REQUEST['objectType'];
+
+ try {
+ $objectType = VersionTracker::getInstance()->getObjectType($this->objectType);
+ }
+ catch (\InvalidArgumentException $e) {
+ throw new IllegalLinkException();
+ }
+
+ $this->objectTypeProcessor = $objectType->getProcessor();
+ if (!$this->objectTypeProcessor->canAccess()) {
+ throw new PermissionDeniedException();
+ }
+
+ $this->activeMenuItem = $this->objectTypeProcessor->getActiveMenuItem();
+
+ $this->object = $this->objectTypeProcessor->getObjectByID($this->objectID);
+ if (!$this->object->getObjectID()) {
+ throw new IllegalLinkException();
+ }
+
+ $this->versions = VersionTracker::getInstance()->getVersions($this->objectType, $this->objectID);
+
+ if (isset($_REQUEST['oldID'])) {
+ $this->oldID = intval($_REQUEST['oldID']);
+ $this->old = VersionTracker::getInstance()->getVersion($this->objectType, $this->oldID);
+ if (!$this->old->versionID) throw new IllegalLinkException();
+
+ if (isset($_REQUEST['newID']) && $_REQUEST['newID'] !== 'current') {
+ $this->newID = intval($_REQUEST['newID']);
+ $this->new = VersionTracker::getInstance()->getVersion($this->objectType, $this->newID);
+ if (!$this->new->versionID) throw new IllegalLinkException();
+ }
+ }
+
+ if (isset($_REQUEST['newID']) && !$this->new) {
+ $this->new = $this->objectTypeProcessor->getCurrentVersion($this->object);
+ $this->newID = 'current';
+ }
+
+ if (!empty($_POST)) {
+ HeaderUtil::redirect(LinkHandler::getInstance()->getLink('VersionTrackerList', [
+ 'objectID' => $this->objectID,
+ 'objectType' => $this->objectType,
+ 'newID' => $this->newID,
+ 'oldID' => $this->oldID
+ ]));
+ exit;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ // valid IDs were given, calculate diff
+ if ($this->old && $this->new) {
+ $languageIDs = $this->new->getLanguageIDs();
+
+ if (count($languageIDs) > 1 || $languageIDs[0] != 0) {
+ foreach ($languageIDs as $i => $languageID) {
+ $language = LanguageFactory::getInstance()->getLanguage($languageID);
+ if ($language === null) {
+ unset($languageIDs[$i]);
+ }
+ else {
+ $this->languages[$languageID] = $language;
+ }
+ }
+
+ $languageIDs = array_unique($languageIDs);
+ }
+
+ $properties = $this->objectTypeProcessor->getTrackedProperties();
+ foreach ($languageIDs as $languageID) {
+ $this->diffs[$languageID] = [];
+
+ foreach ($properties as $property) {
+ $a = explode("\n", StringUtil::unifyNewlines($this->old->getPayload($property, $languageID)));
+ $b = explode("\n", StringUtil::unifyNewlines($this->new->getPayload($property, $languageID)));
+
+ $diff = new Diff($a, $b);
+ $rawDiff = $diff->getRawDiff();
+
+ // create word diff for small changes (only one consecutive paragraph modified)
+ for ($i = 0, $max = count($rawDiff); $i < $max;) {
+ $previousIsNotRemoved = !isset($rawDiff[$i - 1][0]) || $rawDiff[$i - 1][0] !== Diff::REMOVED;
+ $currentIsRemoved = $rawDiff[$i][0] === Diff::REMOVED;
+ $nextIsAdded = isset($rawDiff[$i + 1][0]) && $rawDiff[$i + 1][0] === Diff::ADDED;
+ $afterNextIsNotAdded = !isset($rawDiff[$i + 2][0]) || $rawDiff[$i + 2][0] !== Diff::ADDED;
+
+ if ($previousIsNotRemoved && $currentIsRemoved && $nextIsAdded && $afterNextIsNotAdded) {
+ $a = preg_split('/(\\W)/u', $rawDiff[$i][1], -1, PREG_SPLIT_DELIM_CAPTURE);
+ $b = preg_split('/(\\W)/u', $rawDiff[$i + 1][1], -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ $diff = new Diff($a, $b);
+ $rawDiff[$i][1] = '';
+ $rawDiff[$i + 1][1] = '';
+ foreach ($diff->getRawDiff() as $entry) {
+ $entry[1] = StringUtil::encodeHTML($entry[1]);
+
+ if ($entry[0] === Diff::SAME) {
+ $rawDiff[$i][1] .= $entry[1];
+ $rawDiff[$i + 1][1] .= $entry[1];
+ }
+ else {
+ if ($entry[0] === Diff::REMOVED) {
+ $rawDiff[$i][1] .= '<strong>' . $entry[1] . '</strong>';
+ }
+ else {
+ if ($entry[0] === Diff::ADDED) {
+ $rawDiff[$i + 1][1] .= '<strong>' . $entry[1] . '</strong>';
+ }
+ }
+ }
+ }
+ $i += 2;
+ }
+ else {
+ $rawDiff[$i][1] = StringUtil::encodeHTML($rawDiff[$i][1]);
+ $i++;
+ }
+ }
+
+ $this->diffs[$languageID][$property] = $rawDiff;
+ }
+ }
+
+ // simply template logic by treating diffs with only one language as "no i18n"
+ if (count($this->diffs) == 1 && !isset($this->diffs[0])) {
+ $this->diffs = [reset($this->diffs)];
+ }
+ }
+
+ // set default values
+ if (!isset($_REQUEST['oldID']) && !isset($_REQUEST['newID'])) {
+ foreach ($this->versions as $version) {
+ $this->oldID = $version->versionID;
+ break;
+ }
+ $this->newID = 'current';
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'oldID' => $this->oldID,
+ 'old' => $this->old,
+ 'newID' => $this->newID,
+ 'new' => $this->new,
+ 'diffs' => $this->diffs,
+ 'objectID' => $this->objectID,
+ 'objectType' => $this->objectType,
+ 'objectTypeProcessor' => $this->objectTypeProcessor,
+ 'object' => $this->object,
+ 'languages' => $this->languages,
+ 'versions' => $this->versions
+ ]);
+ }
+}
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Data
*/
-interface IVersionTrackerObject {
+interface IVersionTrackerObject extends IUserContent {
/**
* Returns the object's unique id.
*
public function getContent() {
return $this->content;
}
-}
\ No newline at end of file
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ return $this->getDecoratedObject()->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getUsername() {
+ return $this->getDecoratedObject()->username;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getUserID() {
+ return $this->getDecoratedObject()->userID;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTime() {
+ return $this->getDecoratedObject()->time;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->getDecoratedObject()->getTitle();
+ }
+}
<?php
namespace wcf\system\version;
+use wcf\data\IVersionTrackerObject;
use wcf\data\object\type\AbstractObjectTypeProvider;
+use wcf\system\WCF;
/**
* Abstract implementation of an version tracker object type provider.
* @package WoltLabSuite\Core\System\Version
*/
abstract class AbstractVersionTrackerProvider extends AbstractObjectTypeProvider implements IVersionTrackerProvider {
+ /**
+ * the default property that should be used when initiating a diff
+ * @var string
+ */
+ public static $defaultProperty = '';
+
+ /**
+ * list of property names to their phrase
+ * @var string[]
+ */
+ public static $propertyLabels = [];
+
/**
* list of properties that should be tracked
* @var string[]
*/
public static $trackedProperties = [];
+
+ /**
+ * internal identifier of the menu item that should be marked as active
+ * @var string
+ */
+ public $activeMenuItem = '';
+
+ /**
+ * true if content supports i18n
+ * @var boolean
+ */
+ public $isI18n = false;
+
+ /**
+ * permission name to access stored versions
+ * @var string
+ */
+ public $permissionCanAccess = '';
+
+ /**
+ * @inheritDoc
+ */
+ public function canAccess() {
+ return WCF::getSession()->getPermission($this->permissionCanAccess);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getActiveMenuItem() {
+ return $this->activeMenuItem;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDefaultProperty() {
+ return static::$defaultProperty;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPropertyLabel($property) {
+ if (isset(static::$propertyLabels[$property])) {
+ return WCF::getLanguage()->get(static::$propertyLabels[$property]);
+ }
+
+ return '(void)';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTrackedProperties() {
+ return static::$trackedProperties;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isI18n(IVersionTrackerObject $object) {
+ return $this->isI18n;
+ }
}
* @package WoltLabSuite\Core\System\Version
*/
class ArticleVersionTrackerProvider extends AbstractVersionTrackerProvider {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.article.list';
+
/**
* @inheritDoc
*/
public $className = Article::class;
+ /**
+ * @inheritDoc
+ */
+ public $decoratorClassName = ArticleVersionTracker::class;
+
/**
* @inheritDoc
*/
/**
* @inheritDoc
*/
- public static $trackedProperties = ['content', 'teaser', 'title'];
+ public $permissionCanAccess = 'admin.content.article.canManageArticle';
+
+ /**
+ * @inheritDoc
+ */
+ public static $defaultProperty = 'content';
+
+ /**
+ * @inheritDoc
+ */
+ public static $propertyLabels = [
+ 'content' => 'wcf.acp.article.content',
+ 'teaser' => 'wcf.acp.article.teaser',
+ 'title' => 'wcf.global.title'
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public static $trackedProperties = ['title', 'teaser', 'content'];
+
+ /**
+ * @inheritDoc
+ */
+ public function getCurrentVersion(IVersionTrackerObject $object) {
+ $properties = $this->getTrackedProperties();
+
+ /** @var Article $object */
+ $payload = [];
+ foreach ($object->getArticleContents() as $languageID => $articleContent) {
+ $payload[$languageID] = [];
+ foreach ($properties as $property) {
+ $payload[$languageID][$property] = $articleContent->{$property};
+ }
+ }
+
+ return new VersionTrackerEntry(null, [
+ 'versionID' => 'current',
+ 'userID' => $object->userID,
+ 'username' => $object->username,
+ 'data' => $payload
+ ]);
+ }
/**
* @inheritDoc
return $data;
}
-}
\ No newline at end of file
+
+ /**
+ * @inheritDoc
+ */
+ public function isI18n(IVersionTrackerObject $object) {
+ /** @var Article $object */
+ return $object->isMultilingual == 1;
+ }
+}
<?php
namespace wcf\system\version;
-use wcf\data\IVersionTrackerObject;
use wcf\data\object\type\IObjectTypeProvider;
+use wcf\data\IVersionTrackerObject;
/**
* Represents objects that support some of their properties to be saved.
* @package WoltLabSuite\Core\System\Version
*/
interface IVersionTrackerProvider extends IObjectTypeProvider {
+ /**
+ * Returns true if current user can view the stored versions of this type.
+ *
+ * @return boolean
+ */
+ public function canAccess();
+
+ /**
+ * Returns the internal identifier for the ACP menu item that should be
+ * marked as active when navigating through the relevant versions.
+ *
+ * @return string active menu item identifier
+ */
+ public function getActiveMenuItem();
+
+ /**
+ * Returns an arbitrary version entry that represents the current version
+ *
+ * @param IVersionTrackerObject $object target object
+ * @return VersionTrackerEntry
+ */
+ public function getCurrentVersion(IVersionTrackerObject $object);
+
+ /**
+ * Returns the name of the default property used when initiating a diff.
+ *
+ * @return string default property name
+ */
+ public function getDefaultProperty();
+
+ /**
+ * Returns the label for provided property.
+ *
+ * @param string $property property name
+ * @return string property label
+ */
+ public function getPropertyLabel($property);
+
/**
* Returns an array containing the values that should be stored in the database.
*
* @return mixed[] property to value mapping
*/
public function getTrackedData(IVersionTrackerObject $object);
+
+ /**
+ * Returns the list of tracked properties.
+ *
+ * @return string[] list of tracked properties
+ */
+ public function getTrackedProperties();
+
+ /**
+ * Indicates that the payload is provided for each language and that the
+ * payload's array indices represent language ids rather than property values.
+ *
+ * @param IVersionTrackerObject $object target object
+ * @return boolean
+ */
+ public function isI18n(IVersionTrackerObject $object);
}
<?php
namespace wcf\system\version;
+use wcf\data\DatabaseObject;
use wcf\data\object\type\ObjectType;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\object\type\ObjectTypeList;
/**
* Adds a new entry to the version history.
*
- * @param string $objectTypeName object typename
+ * @param string $objectTypeName object type name
* @param IVersionTrackerObject $object target object
*/
public function add($objectTypeName, IVersionTrackerObject $object) {
$data = $processor->getTrackedData($object);
$sql = "INSERT INTO ".$this->getTableName($objectType)."_version
- (objectID, data)
- VALUES (?, ?)";
+ (objectID, userID, username, time, data)
+ VALUES (?, ?, ?, ?, ?)";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute([
$object->getObjectID(),
+ WCF::getUser()->userID,
+ WCF::getUser()->username,
+ TIME_NOW,
serialize($data)
]);
}
+ /**
+ * Returns the number of stored versions for provided object type and object id.
+ *
+ * @param string $objectTypeName object type name
+ * @param integer $objectID target object id
+ * @return integer number of stored versions
+ */
+ public function countVersions($objectTypeName, $objectID) {
+ $objectType = $this->getObjectType($objectTypeName);
+
+ $sql = "SELECT COUNT(*) as count
+ FROM ".$this->getTableName($objectType)."_version
+ WHERE objectID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$objectID]);
+
+ return $statement->fetchColumn();
+ }
+
+ /**
+ * Returns the last stored version.
+ *
+ * @param string $objectTypeName object type name
+ * @param integer $objectID target object id
+ * @return VersionTrackerEntry|null|DatabaseObject
+ */
+ public function getLastVersion($objectTypeName, $objectID) {
+ $objectType = $this->getObjectType($objectTypeName);
+
+ $sql = "SELECT *, '' as data
+ FROM ".$this->getTableName($objectType)."_version
+ WHERE objectID = ?
+ ORDER BY versionID DESC";
+ $statement = WCF::getDB()->prepareStatement($sql, 1);
+ $statement->execute([$objectID]);
+
+ return $statement->fetchObject(VersionTrackerEntry::class);
+ }
+
+ /**
+ * Returns the list of stored versions.
+ *
+ * @param string $objectTypeName object type name
+ * @param integer $objectID target object id
+ * @return VersionTrackerEntry[]
+ */
+ public function getVersions($objectTypeName, $objectID) {
+ $objectType = $this->getObjectType($objectTypeName);
+
+ $sql = "SELECT *, '' as data
+ FROM ".$this->getTableName($objectType)."_version
+ WHERE objectID = ?
+ ORDER BY versionID DESC";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$objectID]);
+ $versions = [];
+ while ($version = $statement->fetchObject(VersionTrackerEntry::class)) {
+ $versions[] = $version;
+ }
+
+ return $versions;
+ }
+
+ /**
+ * Returns a version including the contents of the data column.
+ *
+ * @param string $objectTypeName object type name
+ * @param integer $versionID version id
+ * @return VersionTrackerEntry|null|DatabaseObject
+ */
+ public function getVersion($objectTypeName, $versionID) {
+ $objectType = $this->getObjectType($objectTypeName);
+
+ $sql = "SELECT *
+ FROM ".$this->getTableName($objectType)."_version
+ WHERE versionID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql, 1);
+ $statement->execute([$versionID]);
+
+ return $statement->fetchObject(VersionTrackerEntry::class);
+ }
+
/**
* Creates the database tables to store each version.
*/
}
}
+ /**
+ * Retrieves the object type object by its name.
+ *
+ * @param string $name object type name
+ * @return ObjectType target object
+ * @throws SystemException
+ */
+ public function getObjectType($name) {
+ foreach ($this->availableObjectTypes as $objectType) {
+ if ($objectType->objectType === $name) {
+ return $objectType;
+ }
+ }
+
+ throw new \InvalidArgumentException("Unknown object type '".$name."' for definition 'com.woltlab.wcf.versionTracker.objectType'.");
+ }
+
/**
* Creates a database table for an object type unless it exists already.
*
}
$columns = [
+ ['name' => 'versionID', 'data' => ['length' => 10, 'notNull' => true, 'type' => 'int', 'key' => 'PRIMARY', 'autoIncrement' => true]],
['name' => 'objectID', 'data' => ['length' => 10, 'notNull' => true, 'type' => 'int']],
+ ['name' => 'userID', 'data' => ['length' => 10, 'type' => 'int']],
+ ['name' => 'username', 'data' => ['length' => 100, 'notNull' => true, 'type' => 'varchar']],
+ ['name' => 'time', 'data' => ['length' => 10, 'notNull' => true, 'type' => 'int']],
['name' => 'data', 'data' => ['type' => 'longblob']]
];
'ON DELETE' => 'CASCADE'
]
);
+ WCF::getDB()->getEditor()->addForeignKey(
+ $tableName,
+ md5($tableName . '_userID') . '_fk',
+ [
+ 'columns' => 'userID',
+ 'referencedTable' => 'wcf'.WCF_N.'_user',
+ 'referencedColumns' => 'userID',
+ 'ON DELETE' => 'SET NULL'
+ ]
+ );
// add comment
$sql = "ALTER TABLE ".$tableName."
return $tableName;
}
-
- /**
- * Retrieves the object type object by its name.
- *
- * @param string $name object type name
- * @return ObjectType target object
- * @throws SystemException
- */
- protected function getObjectType($name) {
- foreach ($this->availableObjectTypes as $objectType) {
- if ($objectType->objectType === $name) {
- return $objectType;
- }
- }
-
- throw new SystemException("Unknown object type '".$name."' for definition 'com.woltlab.wcf.versionTracker.objectType'.");
- }
}
--- /dev/null
+<?php
+namespace wcf\system\version;
+
+/**
+ * Generic data holder for version tracker entries.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Version
+ *
+ * @property-read integer $versionID unique id of the tracked version entry
+ * @property-read integer $objectID id of the edited object
+ * @property-read integer|null $userID id of the user who has created the previous version of the object or `null` if the user does not exist anymore or if the previous version has been created by a guest
+ * @property-read string $username name of the user who has created the previous version of the object
+ * @property-read integer $time timestamp at which the original version has been created
+ */
+class VersionTrackerEntry {
+ /**
+ * object data
+ * @var array
+ */
+ protected $data = [];
+
+ /**
+ * list of stored properties and their values
+ * @var array
+ */
+ protected $payload = [];
+
+ /**
+ * VersionTrackerEntry constructor.
+ *
+ * @param integer $id id
+ * @param array $data version data
+ */
+ public function __construct($id, array $data) {
+ if ($id !== null) {
+ throw new \InvalidArgumentException("Accessing tracked versions by id is not supported.");
+ }
+
+ if (isset($data['data'])) {
+ $payload = (is_array($data['data'])) ? $data['data'] : @unserialize($data['data']);
+ if ($payload !== false && is_array($payload)) {
+ $this->payload = $payload;
+ }
+
+ unset($data['data']);
+ }
+
+ $this->data = $data;
+ }
+
+ /**
+ * Returns the value of a object data variable with the given name or `null` if no
+ * such data variable exists.
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function __get($name) {
+ if (isset($this->data[$name])) {
+ return $this->data[$name];
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Determines if the object data variable with the given name is set and
+ * is not NULL.
+ *
+ * @param string $name
+ * @return boolean
+ */
+ public function __isset($name) {
+ return isset($this->data[$name]);
+ }
+
+ /**
+ * Returns the stored value of a property or null if unknown.
+ *
+ * @param string $property property name
+ * @param integer $languageID language id
+ * @return string
+ */
+ public function getPayload($property, $languageID) {
+ if (isset($this->payload[$languageID])) {
+ return (isset($this->payload[$languageID][$property])) ? $this->payload[$languageID][$property] : '';
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns the list of language ids.
+ *
+ * @return integer[]
+ */
+ public function getLanguageIDs() {
+ return array_keys($this->payload);
+ }
+}
\ No newline at end of file
<item name="wcf.acp.article.trash.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {if $isArticleEdit|empty}den Artikel <span class="confirmationObject">{$article->getTitle()}</span>{else}diesen Artikel{/if} wirklich in den Papierkorb verschieben?]]></item>
<item name="wcf.acp.article.trash.notice"><![CDATA[Dieser Artikel befindet sich im Papierkorb und wird gegenwärtig nicht angezeigt.]]></item>
<item name="wcf.acp.article.views"><![CDATA[Zugriffe]]></item>
+ <item name="wcf.acp.article.lastVersion"><![CDATA[Es gibt <a href="{link controller='VersionTrackerList' objectType='com.woltlab.wcf.article' objectID=$article->articleID}{/link}">vorherige Versionen</a> dieses Artikels, die <a href="{link controller='VersionTrackerCompare' id=$lastVersion->versionID objectType='com.woltlab.wcf.article'}{/link}">letzte Änderung</a> erfolgte durch <a href="{link controller='UserEdit' id=$lastVersion->userID}{/link}">{$lastVersion->username}</a> ({@$lastVersion->time|time}).]]></item>
</category>
<category name="wcf.acp.attachment">
<item name="wcf.edit.reverted"><![CDATA[Wiederhergestellt auf Version von {$edit->username} vom {$edit->time|plainTime}]]></item>
<item name="wcf.edit.button.compare"><![CDATA[Vergleichen]]></item>
<item name="wcf.edit.button.goToContent"><![CDATA[Zum Inhalt gehen]]></item>
+ <item name="wcf.edit.headline.comparison"><![CDATA[Vergleich]]></item>
<item name="wcf.edit.headline.old"><![CDATA[{if $oldID == 'current'}Aktuelle {/if}Version vom {@$old->time|plainTime} ({$old->username})]]></item>
<item name="wcf.edit.headline.new"><![CDATA[{if $newID == 'current'}Aktuelle {/if}Version vom {@$new->time|plainTime} ({$new->username})]]></item>
+ <item name="wcf.edit.headline.newOrCurrent"><![CDATA[{if $newID == 'current'}Aktuelle Version{else}Version vom {@$new->time|plainTime}{/if} ({$new->username})]]></item>
</category>
<category name="wcf.editor">
<item name="wcf.edit.reverted"><![CDATA[Reverted to the version at {$edit->time|plainTime}, created by {$edit->username}]]></item>
<item name="wcf.edit.button.compare"><![CDATA[Compare]]></item>
<item name="wcf.edit.button.goToContent"><![CDATA[Go to Related Content]]></item>
+ <item name="wcf.edit.headline.comparison"><![CDATA[Comparison]]></item>
<item name="wcf.edit.headline.old"><![CDATA[{if $oldID == 'current'}Current version{else}Version{/if} as of {@$old->time|plainTime} ({$old->username})]]></item>
<item name="wcf.edit.headline.new"><![CDATA[{if $newID == 'current'}Current version{else}Version{/if} as of {@$new->time|plainTime} ({$new->username})]]></item>
+ <item name="wcf.edit.headline.newOrCurrent"><![CDATA[{if $newID == 'current'}Current version{else}Version as of {@$new->time|plainTime}{/if} ({$new->username})]]></item>
</category>
<category name="wcf.editor">