From 82a7de7f6432f4b106a5c5cceac6d1f962f9d94c Mon Sep 17 00:00:00 2001 From: Kiv4h Date: Thu, 13 Dec 2012 15:21:11 +0100 Subject: [PATCH] add versioning support --- com.woltlab.wcf/objectTypeDefinition.xml | 4 + .../data/VersionableDatabaseObject.class.php | 37 +++++ .../VersionableDatabaseObjectAction.class.php | 116 +++++++++++++++ .../VersionableDatabaseObjectEditor.class.php | 72 +++++++++ .../builder/VersionCacheBuilder.class.php | 48 ++++++ .../editor/MySQLDatabaseEditor.class.php | 16 +- .../PackageInstallationDispatcher.class.php | 64 +++++++- .../PackageUninstallationDispatcher.class.php | 11 +- .../system/version/VersionHandler.class.php | 138 ++++++++++++++++++ 9 files changed, 502 insertions(+), 4 deletions(-) create mode 100644 wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php create mode 100644 wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php create mode 100644 wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php create mode 100644 wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php create mode 100644 wcfsetup/install/files/lib/system/version/VersionHandler.class.php diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 18a21176e1..dac9532485 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -17,5 +17,9 @@ com.woltlab.wcf.modifiableContent + + + com.woltlab.wcf.versionableObject + \ No newline at end of file diff --git a/wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php b/wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php new file mode 100644 index 0000000000..8094c98e9d --- /dev/null +++ b/wcfsetup/install/files/lib/data/VersionableDatabaseObject.class.php @@ -0,0 +1,37 @@ + + * @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()); + } +} diff --git a/wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php b/wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php new file mode 100644 index 0000000000..83d52436cb --- /dev/null +++ b/wcfsetup/install/files/lib/data/VersionableDatabaseObjectAction.class.php @@ -0,0 +1,116 @@ + + * @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(); + } +} diff --git a/wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php b/wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php new file mode 100644 index 0000000000..2cbd931fc9 --- /dev/null +++ b/wcfsetup/install/files/lib/data/VersionableDatabaseObjectEditor.class.php @@ -0,0 +1,72 @@ + + * @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; + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php new file mode 100644 index 0000000000..e51b8dce0d --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/VersionCacheBuilder.class.php @@ -0,0 +1,48 @@ + + * @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; + } +} diff --git a/wcfsetup/install/files/lib/system/database/editor/MySQLDatabaseEditor.class.php b/wcfsetup/install/files/lib/system/database/editor/MySQLDatabaseEditor.class.php index 6b34b1ffc0..e5fe929582 100644 --- a/wcfsetup/install/files/lib/system/database/editor/MySQLDatabaseEditor.class.php +++ b/wcfsetup/install/files/lib/system/database/editor/MySQLDatabaseEditor.class.php @@ -1,6 +1,7 @@ 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; } diff --git a/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php b/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php index b221662b13..7f6485dab9 100644 --- a/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php +++ b/wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php @@ -76,6 +76,12 @@ class PackageInstallationDispatcher { */ const CONFIG_FILE = 'config.inc.php'; + /** + * holds state of structuring version tables + * @var boolean + */ + protected $requireRestructureVersionTables = false; + /** * Creates a new instance of PackageInstallationDispatcher. * @@ -160,7 +166,11 @@ class PackageInstallationDispatcher { // reset stylesheets StyleHandler::resetStylesheets(); - } + } + + if ($this->requireRestructureVersionTables) { + $this->restructureVersionTables(); + } return $step; } @@ -444,6 +454,10 @@ class PackageInstallationDispatcher { 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}(); @@ -931,4 +945,52 @@ class PackageInstallationDispatcher { 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']); + } + } + } + } + } } diff --git a/wcfsetup/install/files/lib/system/package/PackageUninstallationDispatcher.class.php b/wcfsetup/install/files/lib/system/package/PackageUninstallationDispatcher.class.php index ae6f96ed1e..e26427ea87 100644 --- a/wcfsetup/install/files/lib/system/package/PackageUninstallationDispatcher.class.php +++ b/wcfsetup/install/files/lib/system/package/PackageUninstallationDispatcher.class.php @@ -96,6 +96,10 @@ class PackageUninstallationDispatcher extends PackageInstallationDispatcher { ApplicationHandler::rebuild(); } + if ($this->requireRestructureVersionTables) { + $this->restructureVersionTables(); + } + // return next node return $node; } @@ -103,8 +107,13 @@ class PackageUninstallationDispatcher extends PackageInstallationDispatcher { /** * @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(); } diff --git a/wcfsetup/install/files/lib/system/version/VersionHandler.class.php b/wcfsetup/install/files/lib/system/version/VersionHandler.class.php new file mode 100644 index 0000000000..f1cf53152e --- /dev/null +++ b/wcfsetup/install/files/lib/system/version/VersionHandler.class.php @@ -0,0 +1,138 @@ + +* @package com.woltlab.wcf +* @subpackage system.version +* @category Community Framework +*/ +class VersionHandler extends SingletonFactory { + /** + * cached categories + * @var array + */ + protected $versions = array(); + + /** + * maps each version id to its object type id and object type version id + * @var array + */ + protected $versionIDs = array(); + + /** + * mapes the names of the version object types to the object type ids + * @var array + */ + protected $objectTypeIDs = array(); + + /** + * list of version object types + * @var array + */ + 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 + */ + 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 + */ + public function getObjectTypes() { + return $this->objectTypes; + } +} \ No newline at end of file -- 2.20.1