<definition>
<name>com.woltlab.wcf.modifiableContent</name>
</definition>
+
+ <definition>
+ <name>com.woltlab.wcf.versionableObject</name>
+ </definition>
</import>
</data>
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\data;
+use wcf\util\StringUtil;
+
+/**
+ * Abstract class for all versionable data classes.
+ *
+ * @author Jeffrey Reichardt
+ * @copyright 2001-2012 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data
+ * @category Community Framework
+ */
+abstract class VersionableDatabaseObject extends DatabaseObject {
+ /**
+ * name of the object type
+ * @var string
+ */
+ public $objectTypeName = '';
+
+ /**
+ * Returns suffix of database tables.
+ *
+ * @return string
+ */
+ protected static function getDatabaseVersionTableName() {
+ return static::getDatabaseTableName().'_version';
+ }
+
+ /**
+ * Returns name of index in version table.
+ */
+ protected static function getDatabaseVersionTableIndexName() {
+ return 'version'.ucfirst(static::getDatabaseIndexTableName());
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data;
+
+/**
+ * Abstract class for all versionable data actions.
+ *
+ * @author Jeffrey Reichardt
+ * @copyright 2001-2012 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data
+ * @category Community Framework
+ */
+abstract class VersionableDatabaseObjectAction extends AbstractDatabaseObjectAction {
+
+ /**
+ * Validates restoring an version
+ */
+ public function validateRestore() {
+ parent::validateUpdate();
+ }
+
+ /**
+ * Deletes database object and returns the number of deleted objects.
+ *
+ * @return integer
+ */
+ public function delete() {
+ if (!count($this->objects)) {
+ $this->readObjects();
+ }
+
+ // get index name
+ $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+
+ // get ids
+ $objectIDs = array();
+ foreach ($this->objects as $object) {
+ $objectIDs[] = $object->__get($indexName);
+ }
+
+ // execute action
+ return call_user_func(array($this->className, 'deleteAll'), $objectIDs);
+ }
+
+ /**
+ * Updates data.
+ */
+ public function update() {
+ if (!count($this->objects)) {
+ $this->readObjects();
+ }
+
+ //get index name
+ $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+
+ if (isset($this->parameters['data'])) {
+ foreach ($this->objects as $object) {
+ $this->update($this->parameters['data']);
+ //create revision retroactively
+ $this->createRevision();
+ }
+ }
+ }
+
+ /**
+ * Creates a new revision.
+ */
+ protected function createRevision() {
+ $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+
+ foreach($this->objects as $object) {
+ call_user_func(array($this->className, 'createRevision'), array_merge($object->getData(), array($indexName =>$object->__get($indexName))));
+ }
+ }
+
+ /**
+ * Deletes revision.
+ */
+ protected function deleteRevision() {
+ if (!count($this->objects)) {
+ $this->readObjects();
+ }
+
+ // get index name
+ $indexName = call_user_func(array($this->className, 'getDatabaseTableIndexName'));
+
+ // get ids
+ $objectIDs = array();
+ foreach ($this->objects as $object) {
+ $objectIDs[] = $object->__get($indexName);
+ }
+
+ // execute action
+ return call_user_func(array($this->className, 'deleteRevision'), $objectIDs);
+ }
+
+ /**
+ * Restores an revision.
+ */
+ public function restore() {
+ if (!count($this->objects)) {
+ $this->readObjects();
+ }
+
+ //currently we only support restoring one version
+ foreach($this->objects as $object) {
+ $objectType = VersionHandler::getInstance()->getObjectTypeByName($object->objectTypeName);
+ $restoreObject = VersionHandler::getInstance()->getVersionByID($objectType->objectTypeID, $this->parameters['restoreObjectID']);
+
+ $this->parameters['data'] = $restoreObject->getData();
+ }
+
+ $this->update();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data;
+use wcf\system\WCF;
+
+/**
+ * Abstract class for all versionable editor classes.
+ *
+ * @author Jeffrey Reichardt
+ * @copyright 2001-2012 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data
+ * @category Community Framework
+ */
+abstract class VersionableDatabaseObjectEditor extends DatabaseObjectEditor {
+ /**
+ * @see wcf\data\IEditableObject::create()
+ */
+ public static function createRevision(array $parameters = array()) {
+ $keys = $values = '';
+ $statementParameters = array();
+ foreach ($parameters as $key => $value) {
+ if (!empty($keys)) {
+ $keys .= ',';
+ $values .= ',';
+ }
+
+ $keys .= $key;
+ $values .= '?';
+ $statementParameters[] = $value;
+ }
+
+ // save object
+ $sql = "INSERT INTO ".static::getDatabaseVersionTableName()."
+ (".$keys.")
+ VALUES (".$values.")";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($statementParameters);
+
+ // return new object
+ $id = WCF::getDB()->getInsertID(static::getDatabaseVersionTableName(), static::getDatabaseVersionTableIndexName());
+
+ return new static::$baseClass($id);
+ }
+
+ /**
+ * @see wcf\data\IEditableObject::delete()
+ */
+ public function deleteRevision(array $objectIDs = array()) {
+ static::deleteAll(array($this->__get(static::getDatabaseVersionTableIndexName())));
+ }
+
+ /**
+ * @see wcf\data\IEditableObject::deleteAll()
+ */
+ public static function deleteAll(array $objectIDs = array()) {
+ $affectedCount = static::deleteAll($objectIDs);
+
+ //delete versions
+ $sql = "DELETE FROM ".static::getDatabaseVersionTableName()."
+ WHERE ".static::getDatabaseTableIndexName()." = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ WCF::getDB()->beginTransaction();
+ foreach ($objectIDs as $objectID) {
+ $statement->executeUnbuffered(array($objectID));
+ }
+ WCF::getDB()->commitTransaction();
+
+ return $affectedCount;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\package\PackageDependencyHandler;
+
+/**
+ * Caches the versions for a certain package and object type.
+ *
+ * @author Jeffrey Reichardt
+ * @copyright 2001-2012 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.cache.builder
+ * @category Community Framework
+ */
+class VersionCacheBuilder implements ICacheBuilder {
+ /**
+ * @see wcf\system\cache\ICacheBuilder::getData()
+ */
+ public function getData(array $cacheResource) {
+ //get object types
+ $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.versionableObject');
+
+ $data = array(
+ 'versions' => array(),
+ 'versionIDs' => array()
+ );
+
+ foreach ($objectTypes as $objectTypeID => $objectType) {
+ $processorObject = $objectType->getProcessor();
+
+ $sql = "SELECT
+ *
+ FROM ".$processorObject::getDatabaseVersionTableName();
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array());
+
+ while ($row = $statement->fetchArray()) {
+ $object = new $objectType->className(null, $row);
+ $data['versions'][$objectTypeID][$object->{$processorObject::getDatabaseIndexName()}] = $object;
+ $data['versionIDs'][$objectTypeID][$object->{$processorObject::getDatabaseIndexName()}][] = $object->{$processorObject::getDatabaseVersionTableIndexName()};
+ }
+
+ }
+
+ return $data;
+ }
+}
<?php
namespace wcf\system\database\editor;
use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\Regex;
/**
* Database editor implementation for MySQL4.1 or higher.
$statement = $this->dbObj->prepareStatement($sql);
$statement->execute();
while ($row = $statement->fetchArray()) {
- $columns[] = $row['Field'];
- }
+ $typeMatches = Regex::compile('([a-z]+)\(([0-9]+)\)', Regex::CASE_INSENSITIVE)->match($row['Type']);
+
+ $columns[] = array('name' => $row['Field'],
+ 'data' => array(
+ 'type' => $typeMatches[1],
+ 'length' => $typeMatches[2],
+ 'notNull' => (($row['Null'] == 'YES') ? true : false),
+ 'key' => (($row['Key'] == 'PRI') ? 'PRIMARY' : (($row['Key'] == 'UNI') ? 'UNIQUE' : '')),
+ 'default' => $row['Default'],
+ 'autoIncrement' => ($row['Extra'] == 'auto_increment' ? true : false)
+ )
+ );
+ }
return $columns;
}
*/
const CONFIG_FILE = 'config.inc.php';
+ /**
+ * holds state of structuring version tables
+ * @var boolean
+ */
+ protected $requireRestructureVersionTables = false;
+
/**
* Creates a new instance of PackageInstallationDispatcher.
*
// reset stylesheets
StyleHandler::resetStylesheets();
- }
+ }
+
+ if ($this->requireRestructureVersionTables) {
+ $this->restructureVersionTables();
+ }
return $step;
}
throw new SystemException("'".$className."' does not implement 'wcf\system\package\plugin\IPackageInstallationPlugin'");
}
+ if ($plugin instanceof \wcf\system\package\plugin\SQLPackageInstallationPlugin || $plugin instanceof \wcf\system\package\plugin\ObjectTypePackageInstallationPlugin) {
+ $this->requireRestructureVersionTables = true;
+ }
+
// execute PIP
try {
$document = $plugin->{$this->action}();
break;
}
}
+
+ /*
+ * Restructure version tables.
+ */
+ protected function restructureVersionTables() {
+ $objectTypes = \wcf\system\version\VersionHandler::getInstance()->getObjectTypes();
+
+ if (empty($objectTypes)) {
+ return;
+ }
+
+ //base structure of version tables
+ $versionTableBaseColumns = array();
+ $versionTableBaseColumns[] = array('name' => 'versionID', 'data' => array('type' => 'INT', 'key' => 'PRIMARY', 'autoIncrement' => 'AUTO_INCREMENT'));
+ $versionTableBaseColumns[] = array('name' => 'versionUserID', 'data' => array('type' => 'INT'));
+ $versionTableBaseColumns[] = array('name' => 'versionUsername', 'data' => array('type' => 'VARCHAR', 'length' => 255));
+ $versionTableBaseColumns[] = array('name' => 'versionTime', 'data' => array('type' => 'INT'));
+
+ foreach ($objectTypes as $objectTypeID => $objectType) {
+ //get structure of base table
+ $baseTableColumns = WCF::getDB()->getEditor()->getColumns($objectType::getDatabaseTableName());
+ //get structure of version table
+ $versionTableColumns = WCF::getDB()->getEditor()->getColumns($objectType::getDatabaseVersionTableName());
+
+ if (empty($versionTableColumns)){
+ $columns = array_merge($versionTableBaseColumns, $baseTableColumns);
+
+ WCF::getDB()->getEditor()->createTable($objectType::getDatabaseVersionTableName(), $columns);
+ }
+ else {
+ //check garbage columns in versioned table
+ foreach ($versionTableColumns as $columnData) {
+ if (!array_search($columnData['name'], $baseTableColumns, true)) {
+ //delete column
+ WCF::getDB()->getEditor()->dropColumn($objectType::getDatabaseVersionTableName(), $columnData['name']);
+ }
+ }
+
+ //check new columns for versioned table
+ foreach ($baseTableColumns as $columnData) {
+ if (!array_search($columnData['name'], $versionTableColumns, true)) {
+ //add colum
+ WCF::getDB()->getEditor()->addColumn($objectType::getDatabaseVersionTableName(), $columnData['name'], $columnData['data']);
+ }
+ }
+ }
+ }
+ }
}
ApplicationHandler::rebuild();
}
+ if ($this->requireRestructureVersionTables) {
+ $this->restructureVersionTables();
+ }
+
// return next node
return $node;
}
/**
* @see wcf\system\package\PackageInstallationDispatcher::executePIP()
*/
- protected function executePIP(array $nodeData) {
+ protected function executePIP(array $nodeData) {
$pip = new $nodeData['className']($this);
+
+ if ($pip instanceof \wcf\system\package\plugin\SQLPackageInstallationPlugin || $pip instanceof \wcf\system\package\plugin\ObjectTypePackageInstallationPlugin) {
+ $this->requireRestructureVersionTables = true;
+ }
+
$pip->uninstall();
}
--- /dev/null
+<?php
+namespace wcf\system\version;
+use wcf\data\VersionableDatabaseObject;
+use wcf\system\SingletonFactory;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\cache\CacheHandler;
+
+/**
+* Handles versions of DatabaseObjects.
+*
+* @author Jeffrey Reichardt
+* @copyright 2001-2012 WoltLab GmbH
+* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+* @package com.woltlab.wcf
+* @subpackage system.version
+* @category Community Framework
+*/
+class VersionHandler extends SingletonFactory {
+ /**
+ * cached categories
+ * @var array<wcf\data\VersionableDatabaseObject>
+ */
+ protected $versions = array();
+
+ /**
+ * maps each version id to its object type id and object type version id
+ * @var array<array>
+ */
+ protected $versionIDs = array();
+
+ /**
+ * mapes the names of the version object types to the object type ids
+ * @var array<integer>
+ */
+ protected $objectTypeIDs = array();
+
+ /**
+ * list of version object types
+ * @var array<wcf\data\object\type>
+ */
+ protected $objectTypes = array();
+
+ /**
+ * Returns all version of object with the given object type id and object id.
+ *
+ * @param integer $objectTypeID
+ * @param integer $objectID
+ *
+ * @return array<wcf\data\VersionableDatabaseObject>
+ */
+ public function getVersions($objectTypeID, $objectID) {
+ if (isset($this->versions[$objectTypeID][$objectID])) {
+ return $this->versions[$objectTypeID][$objectID];
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns the category object with the given category id.
+ *
+ * @param integer $objectTypeID
+ * @param integer $versionID
+ *
+ * @return wcf\data\VersionableDatabaseObject
+ */
+ public function getVersionByID($objectTypeID, $versionID) {
+ if (isset($this->versionIDs[$objectTypeID][$versionID])) {
+ return $this->versionIDs[$objectTypeID][$versionID];
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the object type with the given id.
+ *
+ * @param integer $objectTypeID
+ *
+ * @return wcf\data\object\type\ObjectType
+ */
+ public function getObjectType($objectTypeID) {
+ if (isset($this->objectTypeIDs[$objectTypeID])) {
+ return $this->getObjectTypeByName($this->objectTypeIDs[$objectTypeID]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the object type with the given name.
+ *
+ * @param string $objectTypeName
+ *
+ * @return wcf\data\object\type\ObjectType
+ */
+ public function getObjectTypeByName($objectTypeName) {
+ if (isset($this->objectTypes[$objectTypeName])) {
+ return $this->objectTypes[$objectTypeName];
+ }
+
+ return null;
+ }
+
+ /**
+ * @see wcf\system\SingletonFactory::init()
+ */
+ protected function init() {
+ $this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.versionableObject');
+
+ foreach ($this->objectTypes as $objectType) {
+ $this->objectTypeIDs[$objectType->objectTypeID] = $objectType->objectType;
+ }
+
+ $cacheName = 'version';
+ CacheHandler::getInstance()->addResource($cacheName, WCF_DIR.'cache/cache.'.$cacheName.'.php', 'wcf\system\cache\builder\VersionCacheBuilder');
+ $this->versions = CacheHandler::getInstance()->get($cacheName, 'versions');
+ $this->versionIDs = CacheHandler::getInstance()->get($cacheName, 'versionIDs');
+ }
+
+ /**
+ * Reloads the version cache.
+ */
+ public function reloadCache() {
+ CacheHandler::getInstance()->clearResource('version');
+
+ $this->init();
+ }
+
+ /**
+ * Returns a list of object types
+ *
+ * @return array<wcf\data\object\type\ObjectType>
+ */
+ public function getObjectTypes() {
+ return $this->objectTypes;
+ }
+}
\ No newline at end of file