From: Matthias Schmidt Date: Mon, 21 May 2012 10:38:31 +0000 (+0200) Subject: Adds abstract nestable category implementation X-Git-Tag: 2.0.0_Beta_1~1096^2~3 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=13d8b49b676eecfebbc71aa0fdefe7fe190429e6;p=GitHub%2FWoltLab%2FWCF.git Adds abstract nestable category implementation --- diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index d7a7898586..ab88252e9b 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -8,5 +8,10 @@ com.woltlab.wcf.clipboardItem + + + wcf\system\category\ICategoryType + com.woltlab.wcf.category + \ No newline at end of file diff --git a/com.woltlab.wcf/template/categorySelectList.tpl b/com.woltlab.wcf/template/categorySelectList.tpl new file mode 100644 index 0000000000..db6f59a7d7 --- /dev/null +++ b/com.woltlab.wcf/template/categorySelectList.tpl @@ -0,0 +1,3 @@ +{foreach from=$categoryNodeList item=category} + +{/foreach} \ No newline at end of file diff --git a/wcfsetup/install/files/acp/js/WCF.ACP.js b/wcfsetup/install/files/acp/js/WCF.ACP.js index 34bb6ac9e4..3b7d16aa18 100644 --- a/wcfsetup/install/files/acp/js/WCF.ACP.js +++ b/wcfsetup/install/files/acp/js/WCF.ACP.js @@ -1,8 +1,8 @@ /** * Class and function collection for WCF ACP * - * @author Alexander Ebert - * @copyright 2001-2011 WoltLab GmbH + * @author Alexander Ebert, Matthias Schmidt + * @copyright 2001-2012 WoltLab GmbH * @license GNU Lesser General Public License */ @@ -754,3 +754,120 @@ WCF.ACP.Worker.prototype = { } } }; + +/** + * Namespace for category-related functions. + */ +WCF.ACP.Category = {}; + +/** + * Handles collapsing categories. + * + * @param string className + * @param integer objectTypeID + */ +WCF.ACP.Category.Collapsible = WCF.Collapsible.SimpleRemote.extend({ + /** + * @see WCF.Collapsible.Remote.init() + */ + init: function(className, objectTypeID) { + this._objectTypeID = objectTypeID; + + var sortButton = $('.formSubmit > button[data-type="submit"]'); + if (sortButton) { + sortButton.click($.proxy(this._sort, this)); + } + + this._super(className); + }, + + /** + * @see WCF.Collapsible.Remote._getAdditionalParameters() + */ + _getAdditionalParameters: function(containerID) { + return {objectTypeID : this._objectTypeID}; + }, + + /** + * @see WCF.Collapsible.Remote._getButtonContainer() + */ + _getButtonContainer: function(containerID) { + return $('#' + containerID + ' > .buttons'); + }, + + /** + * @see WCF.Collapsible.Remote._getContainers() + */ + _getContainers: function() { + return $('.jsCategory').has('ol').has('li'); + }, + + /** + * @see WCF.Collapsible.Remote._getTarget() + */ + _getTarget: function(containerID) { + return $('#' + containerID + ' > ol'); + }, + + /** + * Handles a click on the sort button. + */ + _sort: function() { + // remove existing collapsible buttons + $('.collapsibleButton').remove(); + + // reinit containers + this._containers = {}; + this._containerData = {}; + + var $containers = this._getContainers(); + if ($containers.length == 0) { + console.debug('[WCF.Category.Collapsible] Empty container set given, aborting.'); + } + $containers.each($.proxy(function(index, container) { + var $container = $(container); + var $containerID = $container.wcfIdentify(); + this._containers[$containerID] = $container; + + this._initContainer($containerID); + }, this)); + } +}); + +/** + * @see WCF.Action.Delete + */ +WCF.ACP.Category.Delete = WCF.Action.Delete.extend({ + /** + * @see WCF.Action.Delete.triggerEffect() + */ + triggerEffect: function(objectIDs) { + this.containerList.each($.proxy(function(index, container) { + container = $(container); + var $objectID = container.find('.jsDeleteButton').data('objectID'); + if (WCF.inArray($objectID, objectIDs)) { + // move child categories up + if (container.has('ol').has('li')) { + if (container.is(':only-child')) { + container.parent().replaceWith(container.find('> ol')); + } + else { + container.replaceWith(container.find('> ol > li')); + } + } + else { + container.wcfBlindOut('up', function() { + container.empty().remove(); + }, container); + } + + // update badges + if (this.badgeList) { + this.badgeList.each(function(innerIndex, badge) { + $(badge).html($(badge).html() - 1); + }); + } + } + }, this)); + } +}); diff --git a/wcfsetup/install/files/acp/templates/categoryAdd.tpl b/wcfsetup/install/files/acp/templates/categoryAdd.tpl new file mode 100644 index 0000000000..09ec2b4c38 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/categoryAdd.tpl @@ -0,0 +1,156 @@ +{include file='header'} + +{if $aclObjectTypeID} + {include file='aclPermissions'} +{/if} + + +
+
+

{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.{@$action}{/lang}{/content}{hascontentelse}{lang}wcf.category.{@$action}{/lang}{/hascontent}

+
+
+ +{if $errorField} +

{lang}wcf.global.form.error{/lang}

+{/if} + +{if $success|isset} +

{lang}wcf.global.form.{@$action}.success{/lang}

+{/if} + +{capture assign='listLangVar'}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.button.list{/lang}{/capture} +{if !$listLangVar} + {capture assign='listLangVar'}{lang}wcf.category.button.list{/lang}{/capture} +{/if} + +{hascontent} +
+ +
+{/hascontent} + +
+
+
+ {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.data{/lang}{/content}{hascontentelse}{lang}wcf.category.data{/lang}{/hascontent} + + {if $categoryNodeList|count} + +
+
+ + {if $errorField == 'parentCategoryID'} + + {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.parentCategoryID.error.{@$errorType}{/lang}{/content}{hascontentelse}{lang}wcf.category.parentCategoryID.error.{@$errorType}{/lang}{/hascontent} + + {/if} + {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.parentCategoryID.description{/lang}{/content}{/hascontent} +
+ + {/if} + + +
+
+ + {if $errorField == 'title'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}{@$objectType->getProcessor()->getLangVarPrefix()}.title.error.{@$errorType}{/lang} + {/if} + + {/if} + {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.title.description{/lang}{/content}{/hascontent} +
+ + + +
+
+ + {if $errorType == 'description'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}{@$objectType->getProcessor()->getLangVarPrefix()}.description.error.{@$errorType}{/lang} + {/if} + + {/if} + {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.description.description{/lang}{/content}{/hascontent} +
+ + + +
+
+ + {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.isDisabled.description{/lang}{/content}{/hascontent} +
+ + + +
+
+ + {if $errorField == 'title'} + + {lang}{@$objectType->getProcessor()->getLangVarPrefix()}.showOrder.error.{@$errorType}{/lang} + + {/if} + {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.showOrder.description{/lang}{/content}{/hascontent} +
+ + + {if $aclObjectTypeID} +
+
{lang}wcf.acp.acl.permissions{/lang}
+
+
+ {/if} + + {event name='fields'} +
+ + {event name='fieldsets'} +
+ +
+ + +
+
+ +{include file='footer'} \ No newline at end of file diff --git a/wcfsetup/install/files/acp/templates/categoryList.tpl b/wcfsetup/install/files/acp/templates/categoryList.tpl new file mode 100644 index 0000000000..b5b8b5bfc1 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/categoryList.tpl @@ -0,0 +1,124 @@ +{include file='header'} + +{if $categoryNodeList|count} + +{/if} + +
+
+

{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.list{/lang}{/content}{hascontentelse}{lang}wcf.category.list{/lang}{/hascontent}

+
+
+ +{capture assign='addLangVar'}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.add{/lang}{/capture} +{if !$addLangVar} + {capture assign='addLangVar'}{lang}wcf.category.add{/lang}{/capture} +{/if} + +{hascontent} +
+ +
+{/hascontent} + +{if $categoryNodeList|count} +
+
    + {assign var=oldDepth value=0} + {foreach from=$categoryNodeList item=category} + {if $categoryNodeList->getDepth() < $oldDepth} +
+ {/if} + +
  • + + {if $objectType->getProcessor()->canEditCategory()} + + {else} + + {/if} + + {if $objectType->getProcessor()->canDeleteCategory()} + + {else} + + {/if} + + {if $objectType->getProcessor()->canEditCategory()} + + {else} + + {/if} + + {event name='buttons'} + + + + {$category->getTitle()} + + +
      + {if !$categoryNodeList->current()->hasChildren()} +
  • + {/if} + {assign var=oldDepth value=$categoryNodeList->getDepth()} + {/foreach} + {section name=i loop=$oldDepth}{/section} + + + {if $objectType->getProcessor()->canEditCategory() && $categoryNodeList|count > 1} +
    + +
    + {/if} +
    + + {hascontent} +
    + +
    + {/hascontent} +{else} +

    {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.list.noneAvailable{/lang}{/content}{hascontentelse}{lang}wcf.category.list.noneAvailable{/lang}{/hascontent}

    +{/if} + +{include file='footer'} \ No newline at end of file diff --git a/wcfsetup/install/files/acp/templates/categorySelectList.tpl b/wcfsetup/install/files/acp/templates/categorySelectList.tpl new file mode 100644 index 0000000000..db6f59a7d7 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/categorySelectList.tpl @@ -0,0 +1,3 @@ +{foreach from=$categoryNodeList item=category} + +{/foreach} \ No newline at end of file diff --git a/wcfsetup/install/files/lib/acp/form/AbstractCategoryAddForm.class.php b/wcfsetup/install/files/lib/acp/form/AbstractCategoryAddForm.class.php new file mode 100644 index 0000000000..d35768ea51 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/AbstractCategoryAddForm.class.php @@ -0,0 +1,309 @@ + + * @package com.woltlab.wcf + * @subpackage acp.form + * @category Community Framework + */ +abstract class AbstractCategoryAddForm extends ACPForm { + /** + * id of the category acl object type + * @var integer + */ + public $aclObjectTypeID = 0; + + /** + * name of the controller used to add new categories + * @var string + */ + public $addController = ''; + + /** + * list with the category nodes + * @var wcf\data\category\CategoryNodeList + */ + public $categoryNodeList = null; + + /** + * description of the category + * @var string + */ + public $description = ''; + + /** + * indicates if the category is disabled + * @var integer + */ + public $isDisabled = 0; + + /** + * name of the controller used to edit categories + * @var string + */ + public $editController = ''; + + /** + * name of the controller used to list the categories + * @var string + */ + public $listController = ''; + + /** + * category object type object + * @var wcf\data\object\type\ObjectType + */ + public $objectType = null; + + /** + * name of the category object type + * @var string + */ + public $objectTypeName = ''; + + /** + * id of the package the created package belongs to + * @var integer + */ + public $packageID = 0; + + /** + * id of the parent category id + * @var integer + */ + public $parentCategoryID = 0; + + /** + * category show order + * @var integer + */ + public $showOrder = 0; + + /** + * @see wcf\page\AbstractPage::$templateName + */ + public $templateName = 'categoryAdd'; + + /** + * title of the category + * @var string + */ + public $title = ''; + + /** + * @see wcf\page\AbstractPage::assignVariables() + */ + public function __construct() { + $classNameParts = explode('\\', get_called_class()); + $className = array_pop($classNameParts); + + // autoset controllers + if (empty($this->addController)) { + $this->addController = StringUtil::replace(array('AddForm', 'EditForm'), 'Add', $className); + } + if (empty($this->editController)) { + $this->editController = StringUtil::replace(array('AddForm', 'EditForm'), 'Edit', $className); + } + if (empty($this->listController)) { + $this->listController = StringUtil::replace(array('AddForm', 'EditForm'), 'List', $className); + } + + parent::__construct(); + } + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + I18nHandler::getInstance()->assignVariables(); + + WCF::getTPL()->assign(array( + 'aclObjectTypeID' => $this->aclObjectTypeID, + 'action' => 'add', + 'addController' => $this->addController, + 'categoryNodeList' => $this->categoryNodeList, + 'description' => $this->description, + 'editController' => $this->editController, + 'isDisabled' => $this->isDisabled, + 'listController' => $this->listController, + 'objectType' => $this->objectType, + 'parentCategoryID' => $this->parentCategoryID, + 'showOrder' => $this->showOrder, + 'title' => $this->title + )); + } + + /** + * Reads the categories. + */ + protected function readCategories() { + $this->categoryNodeList = new CategoryNodeList($this->objectType->objectTypeID, 0, true); + } + + /** + * Checks if the active user has the needed permissions to add a new category. + */ + protected function checkCategoryPermissions() { + if (!$this->objectType->getProcessor()->canAddCategory()) { + throw new PermissionDeniedException(); + } + } + + /** + * @see wcf\page\IPage::readData() + */ + public function readData() { + $this->objectType = CategoryHandler::getInstance()->getObjectTypeByName($this->objectTypeName); + if ($this->objectType === null) { + throw new SystemException("Unknown category object type with name '".$this->objectTypeName."'"); + } + + // check permissions + $this->checkCategoryPermissions(); + + // get acl object type id + if ($this->objectType->getProcessor()->getACLObjectTypeName()) { + $this->aclObjectTypeID = ACLHandler::getInstance()->getObjectTypeID($this->objectType->getProcessor()->getACLObjectTypeName()); + } + + // autoset package id + if (!$this->packageID) { + $this->packageID = $this->objectType->packageID; + } + + parent::readData(); + + $this->readCategories(); + } + + /** + * @see wcf\page\IForm::readFormParameters() + */ + public function readFormParameters() { + parent::readFormParameters(); + + I18nHandler::getInstance()->readValues(); + + if (isset($_POST['description'])) { + $this->description = StringUtil::trim($_POST['description']); + } + if (isset($_POST['isDisabled'])) { + $this->isDisabled = 1; + } + if (isset($_POST['parentCategoryID'])) { + $this->parentCategoryID = intval($_POST['parentCategoryID']); + } + if (isset($_POST['showOrder'])) { + $this->showOrder = intval($_POST['showOrder']); + } + if (isset($_POST['title'])) { + $this->title = StringUtil::trim($_POST['title']); + } + } + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + I18nHandler::getInstance()->register('description'); + I18nHandler::getInstance()->register('title'); + } + + /** + * @see wcf\page\IForm::save() + */ + public function save() { + parent::save(); + + $this->objectAction = new CategoryAction(array(), 'create', array( + 'data' => array( + 'description' => $this->description, + 'isDisabled' => $this->isDisabled, + 'objectTypeID' => $this->objectType->objectTypeID, + 'parentCategoryID' => $this->parentCategoryID, + 'showOrder' => $this->showOrder, + 'title' => $this->title + ) + )); + $this->objectAction->executeAction(); + $returnValues = $this->objectAction->getReturnValues(); + + if (!I18nHandler::getInstance()->isPlainValue('description') || !I18nHandler::getInstance()->isPlainValue('title')) { + $categoryID = $returnValues['returnValues']->categoryID; + + $updateData = array(); + if (!I18nHandler::getInstance()->isPlainValue('description')) { + $updateData['description'] = $this->objectType->getProcessor()->getI18nLangVarPrefix().'.description.category'.$categoryID; + I18nHandler::getInstance()->save('description', $updateData['description'], $this->objectType->getProcessor()->getDescriptionLangVarCategory(), $this->packageID); + } + if (!I18nHandler::getInstance()->isPlainValue('title')) { + $updateData['title'] = $this->objectType->getProcessor()->getI18nLangVarPrefix().'.title.category'.$categoryID; + I18nHandler::getInstance()->save('title', $updateData['title'], $this->objectType->getProcessor()->getTitleLangVarCategory(), $this->packageID); + } + + // update description/title + $editor = new CategoryEditor($returnValues['returnValues']); + $editor->update($updateData); + } + + // save acl + if ($this->aclObjectTypeID) { + ACLHandler::getInstance()->save($returnValues['returnValues']->categoryID, $this->aclObjectTypeID); + } + + // reload cache + CategoryHandler::getInstance()->reloadCache(); + + // reset values + $this->parentCategoryID = 0; + $this->showOrder = 0; + + $this->saved(); + + // disable assignment of i18n values + I18nHandler::getInstance()->disableAssignValueVariables(); + + // show success message + WCF::getTPL()->assign('success', true); + } + + /** + * @see wcf\page\IForm::validate() + */ + public function validate() { + parent::validate(); + + $this->validateParentCategory(); + } + + /** + * Validates the parent category. + */ + protected function validateParentCategory() { + if ($this->parentCategoryID) { + if (CategoryHandler::getInstance()->getCategory($this->objectType->objectTypeID, $this->parentCategoryID) === null) { + throw new UserInputException('parentCategoryID', 'invalid'); + } + } + } +} diff --git a/wcfsetup/install/files/lib/acp/form/AbstractCategoryEditForm.class.php b/wcfsetup/install/files/lib/acp/form/AbstractCategoryEditForm.class.php new file mode 100644 index 0000000000..6dcad76e2b --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/AbstractCategoryEditForm.class.php @@ -0,0 +1,162 @@ + + * @package com.woltlab.wcf + * @subpackage acp.form + * @category Community Framework + */ +class AbstractCategoryEditForm extends AbstractCategoryAddForm { + /** + * edited category + * @var wcf\data\category\Category + */ + public $category = null; + + /** + * id of the edited category + * @var integer + */ + public $categoryID = 0; + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + I18nHandler::getInstance()->assignVariables(!empty($_POST)); + + WCF::getTPL()->assign(array( + 'action' => 'edit', + 'category' => $this->category + )); + } + + /** + * @see wcf\acp\form\AbstractCategoryAddForm::checkCategoryPermissions() + */ + protected function checkCategoryPermissions() { + if (!$this->objectType->getProcessor()->canEditCategory()) { + throw new PermissionDeniedException(); + } + } + + /** + * @see wcf\acp\form\AbstractCategoryAddForm::readCategories() + */ + protected function readCategories() { + $this->categoryNodeList = new CategoryNodeList($this->objectType->objectTypeID, 0, true, array($this->category->objectTypeCategoryID)); + } + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + if (isset($_REQUEST['id'])) { + $this->categoryID = intval($_REQUEST['id']); + } + $this->category = new Category($this->categoryID); + if (!$this->category->categoryID) { + throw new IllegalLinkException(); + } + } + + /** + * @see wcf\page\IPage::readData() + */ + public function readData() { + parent::readData(); + + if (empty($_POST)) { + I18nHandler::getInstance()->setOptions('description', $this->packageID, $this->category->description, $this->objectType->getProcessor()->getI18nLangVarPrefix().'.description.category\d+'); + I18nHandler::getInstance()->setOptions('title', $this->packageID, $this->category->title, $this->objectType->getProcessor()->getI18nLangVarPrefix().'.title.category\d+'); + + $this->isDisabled = $this->category->isDisabled; + $this->parentCategoryID = $this->category->parentCategoryID; + $this->showOrder = $this->category->showOrder; + } + } + + /** + * @see wcf\form\IForm::save() + */ + public function save() { + ACPForm::save(); + + // handle description + $this->description = $this->objectType->getProcessor()->getI18nLangVarPrefix().'.description.category'.$this->category->categoryID; + if (I18nHandler::getInstance()->isPlainValue('description')) { + I18nHandler::getInstance()->remove($this->description, $this->packageID); + $this->description = I18nHandler::getInstance()->getValue('description'); + } + else { + I18nHandler::getInstance()->save('description', $this->description, $this->objectType->getProcessor()->getDescriptionLangVarCategory(), $this->packageID); + } + + // handle title + $this->title = $this->objectType->getProcessor()->getI18nLangVarPrefix().'.title.category'.$this->category->categoryID; + if (I18nHandler::getInstance()->isPlainValue('title')) { + I18nHandler::getInstance()->remove($this->title, $this->packageID); + $this->title = I18nHandler::getInstance()->getValue('title'); + } + else { + I18nHandler::getInstance()->save('title', $this->title, $this->objectType->getProcessor()->getTitleLangVarCategory(), $this->packageID); + } + + // update category + $this->objectAction = new CategoryAction(array($this->category), 'update', array( + 'data' => array( + 'description' => $this->description, + 'isDisabled' => $this->isDisabled, + 'parentCategoryID' => $this->parentCategoryID, + 'showOrder' => $this->showOrder, + 'title' => $this->title + ) + )); + $this->objectAction->executeAction(); + + // update acl + if ($this->aclObjectTypeID) { + ACLHandler::getInstance()->save($this->category->categoryID, $this->aclObjectTypeID); + } + + // reload cache + CategoryHandler::getInstance()->reloadCache(); + + $this->saved(); + + // show success message + WCF::getTPL()->assign('success', true); + } + + /** + * @see wcf\acp\form\AbstractCategoryAddForm::validateParentCategory() + */ + protected function validateParentCategory() { + parent::validateParentCategory(); + + // check if new parent category is no child category of the category + $childCategories = CategoryHandler::getInstance()->getChildCategories($this->category); + if (isset($childCategories[$this->parentCategoryID])) { + throw new UserInputException('parentCategoryID', 'invalid'); + } + } +} diff --git a/wcfsetup/install/files/lib/acp/page/AbstractCategoryListPage.class.php b/wcfsetup/install/files/lib/acp/page/AbstractCategoryListPage.class.php new file mode 100644 index 0000000000..c3357b647a --- /dev/null +++ b/wcfsetup/install/files/lib/acp/page/AbstractCategoryListPage.class.php @@ -0,0 +1,162 @@ + + * @package com.woltlab.wcf + * @subpackage acp.page + * @category Community Framework + */ +abstract class AbstractCategoryListPage extends AbstractPage { + /** + * active acp menu item + * @var string + */ + public $activeMenuItem = ''; + + /** + * name of the controller used to add new categories + * @var string + */ + public $addController = ''; + + /** + * category node list + * @var wcf\data\category\CategoryNodeList + */ + public $categoryNodeList = null; + + /** + * ids of collapsed categories + * @var array + */ + public $collapsedCategoryIDs = null; + + /** + * id of the collapsible category object type + * @var integer + */ + public $collapsibleObjectTypeID = 0; + + /** + * name of the controller used to edit categories + * @var string + */ + public $editController = ''; + + /** + * category object type object + * @var wcf\data\object\type\ObjectType + */ + public $objectType = null; + + /** + * name of the category object type + * @var string + */ + public $objectTypeName = ''; + + /** + * @see wcf\page\AbstractPage::$templateName + */ + public $templateName = 'categoryList'; + + /** + * @see wcf\page\AbstractPage::assignVariables() + */ + public function __construct() { + $classNameParts = explode('\\', get_called_class()); + $className = array_pop($classNameParts); + + // autoset controllers + if (empty($this->addController)) { + $this->addController = StringUtil::replace('ListPage', 'Add', $className); + } + if (empty($this->editController)) { + $this->editController = StringUtil::replace('ListPage', 'Edit', $className); + } + + parent::__construct(); + } + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign(array( + 'addController' => $this->addController, + 'categoryNodeList' => $this->categoryNodeList, + 'collapsedCategoryIDs' => $this->collapsedCategoryIDs, + 'collapsibleObjectTypeID' => $this->collapsibleObjectTypeID, + 'editController' => $this->editController, + 'objectType' => $this->objectType + )); + } + + /** + * Checks if the active user has the needed permissions to view this list. + */ + protected function checkCategoryPermissions() { + if (!$this->objectType->getProcessor()->canDeleteCategory() && !$this->objectType->getProcessor()->canEditCategory()) { + throw new PermissionDeniedException(); + } + } + + /** + * Reads the categories. + */ + protected function readCategories() { + $this->categoryNodeList = new CategoryNodeList($this->objectType->objectTypeID, 0, true); + } + + /** + * @see wcf\page\IPage::readData() + */ + public function readData() { + $this->objectType = CategoryHandler::getInstance()->getObjectTypeByName($this->objectTypeName); + if ($this->objectType === null) { + throw new SystemException("Unknown category object type with name '".$this->objectTypeName."'"); + } + + // check permissions + $this->checkCategoryPermissions(); + + $this->readCategories(); + + // get ids of collapsed category + $this->collapsibleObjectTypeID = UserCollapsibleContentHandler::getInstance()->getObjectTypeID($this->objectType->getProcessor()->getCollapsibleObjectTypeName()); + if ($this->collapsibleObjectTypeID !== null) { + $this->collapsedCategoryIDs = UserCollapsibleContentHandler::getInstance()->getCollapsedContent($this->collapsibleObjectTypeID); + $this->collapsedCategoryIDs = array_flip($this->collapsedCategoryIDs); + } + + parent::readData(); + } + + /** + * @see wcf\page\IPage::show() + */ + public function show() { + if ($this->activeMenuItem) { + ACPMenu::getInstance()->setActiveMenuItem($this->activeMenuItem); + } + + parent::show(); + } +} diff --git a/wcfsetup/install/files/lib/data/ICategorizedObject.class.php b/wcfsetup/install/files/lib/data/ICategorizedObject.class.php new file mode 100644 index 0000000000..225b972a88 --- /dev/null +++ b/wcfsetup/install/files/lib/data/ICategorizedObject.class.php @@ -0,0 +1,21 @@ + + * @package com.woltlab.wcf + * @subpackage data + * @category Community Framework + */ +interface ICategorizedObject { + /** + * Returns the category this object belongs to. + * + * @return wcf\data\category\Category + */ + public function getCategory(); +} \ No newline at end of file diff --git a/wcfsetup/install/files/lib/data/category/Category.class.php b/wcfsetup/install/files/lib/data/category/Category.class.php new file mode 100644 index 0000000000..701d70c00d --- /dev/null +++ b/wcfsetup/install/files/lib/data/category/Category.class.php @@ -0,0 +1,134 @@ + + * @package com.woltlab.wcf + * @subpackage data.category + * @category Community Framework + */ +class Category extends DatabaseObject implements IRouteController { + /** + * category type of this category + * @var wcf\system\category\ICategoryType + */ + protected $categoryType = null; + + /** + * list of all parent category generations of this category + * @var array + */ + protected $parentCategories = null; + + /** + * parent category of this category + * @var wcf\data\category\Category + */ + protected $parentCategory = null; + + /** + * @see wcf\data\DatabaseObject::$databaseTableIndexName + */ + protected static $databaseTableIndexName = 'categoryID'; + + /** + * @see wcf\data\DatabaseObject::$databaseTableName + */ + protected static $databaseTableName = 'category'; + + /** + * @see wcf\data\IStorableObject::__get() + */ + public function __get($name) { + $value = parent::__get($name); + + // check additional data + if ($value === null) { + if (isset($this->data['additionalData'][$name])) { + $value = $this->data['additionalData'][$name]; + } + } + + return $value; + } + + /** + * Returns the category type of this category. + * + * @return wcf\system\category\ICategoryType + */ + public function getCategoryType() { + if ($this->categoryType === null) { + $this->categoryType = CategoryHandler::getInstance()->getObjectType($this->objectTypeID)->getProcessor(); + } + + return $this->categoryType; + } + + /** + * @see wcf\system\request\IRouteController::getID() + */ + public function getID() { + return $this->objectTypeCategoryID; + } + + /** + * Returns the parent category of this category. + * + * @return wcf\data\category\Category + */ + public function getParentCategory() { + if ($this->parentCategoryID && $this->parentCategory === null) { + $this->parentCategory = CategoryHandler::getInstance()->getCategory($this->objectTypeID, $this->parentCategoryID); + } + + return $this->parentCategory; + } + + /** + * Returns the parent categories of this category. + * + * @return array + */ + public function getParentCategories() { + if ($this->parentCategories === null) { + $this->parentCategories = array(); + $parentCaregory = $this; + while ($parentCaregory = $parentCaregory->getParentCategory()) { + $this->parentCategories[] = $parentCaregory; + } + + $this->parentCategories = array_reverse($this->parentCategories); + } + + return $this->parentCategories; + } + + /** + * @see wcf\system\request\IRouteController::getTitle() + */ + public function getTitle() { + return WCF::getLanguage()->get($this->title); + } + + /** + * @see wcf\data\DatabaseObject::handleData() + */ + protected function handleData($data) { + // handle additional data + $data['additionalData'] = @unserialize($data['additionalData']); + if (!is_array($data['additionalData'])) { + $data['additionalData'] = array(); + } + + parent::handleData($data); + } +} diff --git a/wcfsetup/install/files/lib/data/category/CategoryAction.class.php b/wcfsetup/install/files/lib/data/category/CategoryAction.class.php new file mode 100644 index 0000000000..b771094739 --- /dev/null +++ b/wcfsetup/install/files/lib/data/category/CategoryAction.class.php @@ -0,0 +1,246 @@ + + * @package com.woltlab.wcf + * @subpackage data.category + * @category Community Framework + */ +class CategoryAction extends AbstractDatabaseObjectAction { + /** + * categorized object type + * @var wcf\data\object\type\ObjectType + */ + protected $objectType = null; + + /** + * @see wcf\data\AbstractDatabaseObjectAction::delete() + */ + public function delete() { + $returnValue = parent::delete(); + + // call category types + foreach ($this->objects as $categoryEditor) { + $categoryEditor->getCategoryType()->afterDeletion($categoryEditor); + } + + return $returnValue; + } + + /** + * Toggles the activity status of categories. + */ + public function toggle() { + foreach ($this->objects as $categoryEditor) { + $categoryEditor->update(array( + 'isDisabled' => 1 - $categoryEditor->isDisabled + )); + } + } + + /** + * Toggles the collapse status of categories. + */ + public function toggleContainer() { + $objectTypeID = UserCollapsibleContentHandler::getInstance()->getObjectTypeID($this->objects[0]->getCategoryType()->getCollapsibleObjectTypeName()); + $collapsedCategories = UserCollapsibleContentHandler::getInstance()->getCollapsedContent($objectTypeID); + + $categoryID = $this->objects[0]->categoryID; + if (array_search($categoryID, $collapsedCategories) !== false) { + UserCollapsibleContentHandler::getInstance()->markAsOpened($objectTypeID, $categoryID); + } + else { + UserCollapsibleContentHandler::getInstance()->markAsCollapsed($objectTypeID, $categoryID); + } + } + + /** + * Updates the position of categories. + */ + public function updatePosition() { + $showOrders = array(); + + WCF::getDB()->beginTransaction(); + foreach ($this->parameters['data']['structure'] as $parentCategoryID => $categoryIDs) { + if (!isset($showOrders[$parentCategoryID])) { + $showOrders[$parentCategoryID] = 1; + } + + foreach ($categoryIDs as $categoryID) { + $this->objects[$categoryID]->update(array( + 'parentCategoryID' => $parentCategoryID ? $this->objects[$parentCategoryID]->objectTypeCategoryID : 0, + 'showOrder' => $showOrders[$parentCategoryID]++ + )); + } + } + WCF::getDB()->commitTransaction(); + } + + /** + * @see wcf\data\AbstractDatabaseObjectAction::validateDelete() + */ + public function validateCreate() { + // validate permissions + if (count($this->permissionsCreate)) { + try { + WCF::getSession()->checkPermissions($this->permissionsCreate); + } + catch (PermissionDeniedException $e) { + throw new ValidateActionException('Insufficient permissions'); + } + } + + if (!isset($this->parameters['data']['objectTypeID'])) { + throw new ValidateActionException("Missing 'objectTypeID' data parameter"); + } + + $objectType = CategoryHandler::getInstance()->getObjectType($this->parameters['data']['objectTypeID']); + if ($objectType === null) { + throw new ValidateActionException("Unknown category object type with id '".$this->parameters['data']['objectTypeID']."'"); + } + if (!$objectType->getProcessor()->canAddCategory()) { + throw new ValidateActionException('Insufficient permissions'); + } + } + + /** + * @see wcf\data\AbstractDatabaseObjectAction::validateDelete() + */ + public function validateDelete() { + // validate permissions + if (count($this->permissionsDelete)) { + try { + WCF::getSession()->checkPermissions($this->permissionsDelete); + } + catch (PermissionDeniedException $e) { + throw new ValidateActionException('Insufficient permissions'); + } + } + + // read objects + if (!count($this->objects)) { + $this->readObjects(); + } + + if (!count($this->objects)) { + throw new ValidateActionException('Invalid object id'); + } + + foreach ($this->objects as $categoryEditor) { + if (!$categoryEditor->getCategoryType()->canAddCategory()) { + throw new ValidateActionException('Insufficient permissions'); + } + } + } + + /** + * Validates the 'toggle' action. + */ + public function validateToggle() { + $this->validateUpdate(); + } + + /** + * Validates the 'toggleContainer' action. + */ + public function validateToggleContainer() { + $this->validateUpdate(); + } + + /** + * @see wcf\data\AbstractDatabaseObjectAction::validateUpdate() + */ + public function validateUpdate() { + // validate permissions + if (count($this->permissionsUpdate)) { + try { + WCF::getSession()->checkPermissions($this->permissionsUpdate); + } + catch (PermissionDeniedException $e) { + throw new ValidateActionException('Insufficient permissions'); + } + } + + // read objects + if (!count($this->objects)) { + $this->readObjects(); + } + + if (!count($this->objects)) { + throw new ValidateActionException('Invalid object id'); + } + + foreach ($this->objects as $categoryEditor) { + if (!$categoryEditor->getCategoryType()->canEditCategory()) { + throw new ValidateActionException('Insufficient permissions'); + } + } + } + + /** + * Validates the 'updatePosition' action. + */ + public function validateUpdatePosition() { + // validate permissions + if (count($this->permissionsUpdate)) { + try { + WCF::getSession()->checkPermissions($this->permissionsUpdate); + } + catch (PermissionDeniedException $e) { + throw new ValidateActionException('Insufficient permissions'); + } + } + + // validate 'structure' parameter + if (!isset($this->parameters['data']['structure'])) { + throw new ValidateActionException("Missing 'structure' parameter"); + } + if (!is_array($this->parameters['data']['structure'])) { + throw new ValidateActionException("'structure' parameter is no array"); + } + + // validate given category ids + foreach ($this->parameters['data']['structure'] as $parentCategoryID => $categoryIDs) { + if ($parentCategoryID) { + // validate category + $category = CategoryHandler::getInstance()->getCategoryByID($parentCategoryID); + if ($category === null) { + throw new ValidateActionException("Unknown category with id '".$parentCategoryID."'"); + } + + $this->objects[$category->categoryID] = new $this->className($category); + + // validate permissions + if (!$category->getCategoryType()->canEditCategory()) { + throw new ValidateActionException('Insufficient permissions'); + } + } + + foreach ($categoryIDs as $categoryID) { + // validate category + $category = CategoryHandler::getInstance()->getCategoryByID($categoryID); + if ($category === null) { + throw new ValidateActionException("Unknown category with id '".$categoryID."'"); + } + + $this->objects[$category->categoryID] = new $this->className($category); + + // validate permissions + if (!$category->getCategoryType()->canEditCategory()) { + throw new ValidateActionException('Insufficient permissions'); + } + } + } + } +} diff --git a/wcfsetup/install/files/lib/data/category/CategoryEditor.class.php b/wcfsetup/install/files/lib/data/category/CategoryEditor.class.php new file mode 100644 index 0000000000..47de399d6d --- /dev/null +++ b/wcfsetup/install/files/lib/data/category/CategoryEditor.class.php @@ -0,0 +1,253 @@ + + * @package com.woltlab.wcf + * @subpackage data.category + * @category Community Framework + */ +class CategoryEditor extends DatabaseObjectEditor implements IEditableCachedObject { + /** + * @see wcf\data\DatabaseObjectDecorator::$baseClass + */ + protected static $baseClass = 'wcf\data\category\Category'; + + /** + * @see wcf\data\IEditableObject::update() + */ + public function update(array $parameters = array()) { + // update show order + if (isset($parameters['parentCategoryID']) || isset($parameters['showOrder'])) { + if (!isset($parameters['parentCategoryID'])) { + $parameters['parentCategoryID'] = $this->parentCategoryID; + } + + if (!isset($parameters['showOrder'])) { + $parameters['showOrder'] = $this->showOrder; + } + + $parameters['showOrder'] = $this->updateShowOrder($parameters['parentCategoryID'], $parameters['showOrder']); + } + + parent::update($parameters); + } + + /** + * Prepares the update of the show order of this category and return the + * correct new show order. + * + * @param integer $parentCategoryID + * @param integer $showOrder + * @return integer + */ + protected function updateShowOrder($parentCategoryID, $showOrder) { + // correct invalid values + if ($showOrder <= 0) { + $showOrder = 1; + } + + if ($parentCategoryID != $this->parentCategoryID) { + $sql = "UPDATE ".static::getDatabaseTableName()." + SET showOrder = showOrder - 1 + WHERE showOrder > ? + AND parentCategoryID = ? + AND objectTypeID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $this->showOrder, + $this->parentCategoryID, + $this->objectTypeID + )); + + return static::getShowOrder($this->objectTypeID, $parentCategoryID, $showOrder); + } + else { + if ($showOrder < $this->showOrder) { + $sql = "UPDATE ".static::getDatabaseTableName()." + SET showOrder = showOrder + 1 + WHERE showOrder >= ? + AND showOrder < ? + AND objectTypeID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $showOrder, + $this->showOrder, + $this->objectTypeID + )); + } + else if ($showOrder > $this->showOrder) { + $sql = "SELECT MAX(showOrder) AS showOrder + FROM ".static::getDatabaseTableName()." + WHERE objectTypeID = ? + AND parentCategoryID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $this->objectTypeID, + $this->parentCategoryID + )); + $row = $statement->fetchArray(); + $maxShowOrder = 0; + if (!empty($row)) { + $maxShowOrder = intval($row['showOrder']); + } + + if ($showOrder > $maxShowOrder) { + $showOrder = $maxShowOrder; + } + + $sql = "UPDATE ".static::getDatabaseTableName()." + SET showOrder = showOrder - 1 + WHERE showOrder <= ? + AND showOrder > ? + AND objectTypeID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $showOrder, + $this->showOrder, + $this->objectTypeID + )); + } + + return $showOrder; + } + } + + /** + * @see wcf\data\IEditableObject::create() + */ + public static function create(array $parameters = array()) { + // handle time + if (!isset($parameters['time'])) { + $parameters['time'] = TIME_NOW; + } + + // handle show order + $parameters['showOrder'] = static::getShowOrder($parameters['objectTypeID'], $parameters['parentCategoryID'], $parameters['showOrder']); + $parameters['objectTypeCategoryID'] = static::getNextCategoryID($parameters['objectTypeID']); + + // handle additionalData + if (!isset($parameters['additionalData'])) { + $parameters['additionalData'] = array(); + } + $parameters['additionalData'] = serialize($parameters['additionalData']); + + return parent::create($parameters); + } + + /** + * @see wcf\data\IEditableObject::deleteAll() + */ + public static function deleteAll(array $objectIDs = array()) { + // update positions + $sql = "UPDATE ".static::getDatabaseTableName()." + SET showOrder = showOrder - 1 + WHERE parentCategoryID = ? + AND showOrder > ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + foreach ($objectIDs as $categoryID) { + $category = CategoryHandler::getInstance()->getCategoryByID($categoryID); + $statement->execute(array($category->parentCategoryID, $category->showOrder)); + } + + return parent::deleteAll($objectIDs); + } + + /** + * Returns the next category for the given object type id. + * + * @param integer $objectTypeID + * @return integer + */ + protected static function getNextCategoryID($objectTypeID) { + $objectType = CategoryHandler::getInstance()->getObjectType($objectTypeID); + if ($objectType === null) { + throw new SystemException("Invalid category object type id '".$objectTypeID."'"); + } + + $nextCategoryID = 1; + if ($objectType->nextCategoryID !== null) { + $nextCategoryID = $objectType->nextCategoryID; + } + + // update next category additional data + $objectTypeEditor = new ObjectTypeEditor($objectType); + $objectTypeEditor->update(array( + 'additionalData' => serialize(array_merge($objectType->additionalData, array( + 'nextCategoryID' => $nextCategoryID + 1 + ))) + )); + + // reset object type cache + CacheHandler::getInstance()->clear(WCF_DIR.'cache/', 'cache.objectType-*.php'); + + return $nextCategoryID; + } + + /** + * Returns the show order for a new category. + * + * @param integer $objectTypeID + * @param integer $parentCategoryID + * @param integer $showOrder + * @return integer + */ + protected static function getShowOrder($objectTypeID, $parentCategoryID, $showOrder) { + // correct invalid values + if ($showOrder <= 0) { + $showOrder = 1; + } + + $sql = "SELECT MAX(showOrder) AS showOrder + FROM ".static::getDatabaseTableName()." + WHERE objectTypeID = ? + AND parentCategoryID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $objectTypeID, + $parentCategoryID + )); + $row = $statement->fetchArray(); + $maxShowOrder = 0; + if (!empty($row)) { + $maxShowOrder = intval($row['showOrder']); + } + + if ($maxShowOrder && $showOrder <= $maxShowOrder) { + $sql = "UPDATE ".static::getDatabaseTableName()." + SET showOrder = showOrder + 1 + WHERE objectTypeID = ? + AND showOrder >= ? + AND parentCategoryID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $objectTypeID, + $showOrder, + $parentCategoryID + )); + + return $showOrder; + } + + return $maxShowOrder + 1; + } + + /** + * @see wcf\data\IEditableCachedObject::resetCache() + */ + public static function resetCache() { + CacheHandler::getInstance()->clear(WCF_DIR.'cache/', 'cache.category-'.PACKAGE_ID.'.php'); + } +} diff --git a/wcfsetup/install/files/lib/data/category/CategoryList.class.php b/wcfsetup/install/files/lib/data/category/CategoryList.class.php new file mode 100644 index 0000000000..81191e8a32 --- /dev/null +++ b/wcfsetup/install/files/lib/data/category/CategoryList.class.php @@ -0,0 +1,15 @@ + + * @package com.woltlab.wcf + * @subpackage data.category + * @category Community Framework + */ +class CategoryList extends DatabaseObjectList { } diff --git a/wcfsetup/install/files/lib/data/category/CategoryNode.class.php b/wcfsetup/install/files/lib/data/category/CategoryNode.class.php new file mode 100644 index 0000000000..5128420211 --- /dev/null +++ b/wcfsetup/install/files/lib/data/category/CategoryNode.class.php @@ -0,0 +1,97 @@ + + * @package com.woltlab.wcf + * @subpackage data.category + * @category Community Framework + */ +class CategoryNode extends DatabaseObjectDecorator implements \RecursiveIterator, \Countable { + /** + * child category nodes + * @var array + */ + protected $childCategories = array(); + + /** + * @see wcf\data\DatabaseObjectDecorator::$baseClass + */ + protected static $baseClass = 'wcf\data\category\Category'; + + /** + * @see wcf\data\DatabaseObjectDecorator::__construct() + */ + public function __construct(DatabaseObject $object, $inludeDisabledCategories = false, array $excludedObjectTypeCategoryIDs = array()) { + parent::__construct($object); + + foreach (CategoryHandler::getInstance()->getChildCategories($this->getDecoratedObject()) as $category) { + if (!in_array($category->objectTypeCategoryID, $excludedObjectTypeCategoryIDs) && ($inludeDisabledCategories || !$category->isDisabled)) { + $this->childCategories[] = new CategoryNode($category, $inludeDisabledCategories, $excludedObjectTypeCategoryIDs); + } + } + } + + /** + * @see \Countable::count() + */ + public function count() { + return count($this->childCategories); + } + + /** + * @see \Iterator::current() + */ + public function current() { + return $this->childCategories[$this->index]; + } + + /** + * @see \RecursiveIterator::getChildren() + */ + public function getChildren() { + return $this->childCategories[$this->index]; + } + + /** + * @see \RecursiveIterator::getChildren() + */ + public function hasChildren() { + return !empty($this->childCategories); + } + + /** + * @see \Iterator::key() + */ + public function key() { + return $this->index; + } + + /** + * @see \Iterator::next() + */ + public function next() { + $this->index++; + } + + /** + * @see \Iterator::rewind() + */ + public function rewind() { + $this->index = 0; + } + + /** + * @see \Iterator::valid() + */ + public function valid() { + return isset($this->childCategories[$this->index]); + } +} diff --git a/wcfsetup/install/files/lib/data/category/CategoryNodeList.class.php b/wcfsetup/install/files/lib/data/category/CategoryNodeList.class.php new file mode 100644 index 0000000000..23bf7ccf36 --- /dev/null +++ b/wcfsetup/install/files/lib/data/category/CategoryNodeList.class.php @@ -0,0 +1,72 @@ + + * @package com.woltlab.wcf + * @subpackage data.category + * @category Community Framework + */ +class CategoryNodeList extends \RecursiveIteratorIterator implements \Countable { + /** + * number of (real) category nodes in this list + * @var integer + */ + protected $count = null; + + /** + * id of the parent category + * @var integer + */ + protected $parentCategoryID = 0; + + /** + * Creates a new CategoryNodeList instance. + * + * @param integer $objectTypeID + * @param integer $parentCategoryID + * @param boolean $inludeDisabledCategories + * @param array $excludedCategoryIDs + */ + public function __construct($objectTypeID, $parentCategoryID = 0, $inludeDisabledCategories = false, array $excludedObjectTypeCategoryIDs = array()) { + $this->parentCategoryID = $parentCategoryID; + + // get parent category + if (!$this->parentCategoryID) { + // empty node + $parentCategory = new Category(null, array( + 'categoryID' => $this->parentCategoryID, + 'objectTypeID' => $objectTypeID, + 'objectTypeCategoryID' => $this->parentCategoryID + )); + } + else { + $parentCategory = CategoryHandler::getInstance()->getCategory($objectTypeID, $this->parentCategoryID); + if ($parentCategory === null) { + throw new SystemException("There is no category with id '".$this->parentCategoryID."' and object type id '".$objectTypeID."'"); + } + } + + parent::__construct(new CategoryNode($parentCategory, $inludeDisabledCategories, $excludedObjectTypeCategoryIDs), \RecursiveIteratorIterator::SELF_FIRST); + } + + /** + * @see \Countable::count() + */ + public function count() { + if ($this->count === null) { + $this->count = 0; + foreach ($this as $categoryNode) { + $this->count++; + } + } + + return $this->count; + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/CategoryCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/CategoryCacheBuilder.class.php new file mode 100644 index 0000000000..6b7cd3d572 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/CategoryCacheBuilder.class.php @@ -0,0 +1,51 @@ + + * @package com.woltlab.wcf + * @subpackage system.cache.builder + * @category Community Framework + */ +class CategoryCacheBuilder implements ICacheBuilder { + /** + * @see wcf\system\cache\ICacheBuilder::getData() + */ + public function getData(array $cacheResource) { + list(, $packageID) = explode('-', $cacheResource['cache']); + + $list = new CategoryList(); + $list->sqlLimit = 0; + $list->sqlJoins = " LEFT JOIN wcf".WCF_N."_object_type object_type + ON (object_type.objectTypeID = category.objectTypeID) + LEFT JOIN wcf".WCF_N."_package_dependency package_dependency + ON (package_dependency.dependency = object_type.packageID)"; + $list->getConditionBuilder()->add("package_dependency.packageID = ?", array($packageID)); + $list->sqlOrderBy = "package_dependency.priority ASC, category.showOrder ASC"; + $list->readObjects(); + + $data = array( + 'categories' => array(), + 'categoryIDs' => array() + ); + foreach ($list as $category) { + if (!isset($data['categories'][$category->objectTypeID])) { + $data['categories'][$category->objectTypeID] = array(); + } + + $data['categories'][$category->objectTypeID][$category->objectTypeCategoryID] = $category; + $data['categoryIDs'][$category->categoryID] = array( + 'objectTypeID' => $category->objectTypeID, + 'objectTypeCategoryID' => $category->objectTypeCategoryID + ); + } + + return $data; + } +} diff --git a/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php b/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php new file mode 100644 index 0000000000..4e45a4b8f7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php @@ -0,0 +1,123 @@ + + * @package com.woltlab.wcf + * @subpackage system.category + * @category Community Framework + */ +abstract class AbstractCategoryType extends SingletonFactory implements ICategoryType { + /** + * name of the acl object type + * @var string + */ + protected $aclObjectTypeName = ''; + + /** + * name of the collapsible object type + * @var string + */ + protected $collapsibleObjectTypeName = ''; + + /** + * language category which contains the language variables of i18n values + * @var string + */ + protected $i18nLangVarCategory = ''; + + /** + * prefix used for language variables in templates + * @var string + */ + protected $langVarPrefix = 'wcf.category'; + + /** + * permission prefix for the add/delete/edit permissions + * @var string + */ + protected $permissionPrefix = ''; + + /** + * @see wcf\system\category\ICategoryType::afterDeletion() + */ + public function afterDeletion(CategoryEditor $categoryEditor) { + // move child categories to parent category + foreach (CategoryHandler::getInstance()->getChildCategories($categoryEditor->getDecoratedObject()) as $category) { + $__categoryEditor = new CategoryEditor($category); + $__categoryEditor->update(array( + 'parentCategoryID' => $categoryEditor->parentCategoryID + )); + } + } + + /** + * @see wcf\system\category\ICategoryType::canAddCategory() + */ + public function canAddCategory() { + return WCF::getSession()->getPermission($this->permissionPrefix.'.canAddCategory'); + } + + /** + * @see wcf\system\category\ICategoryType::canDeleteCategory() + */ + public function canDeleteCategory() { + return WCF::getSession()->getPermission($this->permissionPrefix.'.canDeleteCategory'); + } + + /** + * @see wcf\system\category\ICategoryType::canEditCategory() + */ + public function canEditCategory() { + return WCF::getSession()->getPermission($this->permissionPrefix.'.canEditCategory'); + } + + /** + * @see wcf\system\category\ICategoryType::getACLObjectTypeName() + */ + public function getACLObjectTypeName() { + return $this->aclObjectTypeName ?: null; + } + + /** + * @see wcf\system\category\ICategoryType::getCollapsibleObjectTypeName() + */ + public function getCollapsibleObjectTypeName() { + return $this->aclObjectTypeName ?: null; + } + + /** + * @see wcf\system\category\ICategoryType::getDescriptionLangVarCategory() + */ + public function getDescriptionLangVarCategory() { + return $this->i18nLangVarCategory; + } + + /** + * @see wcf\system\category\ICategoryType::getI18nLangVarPrefix() + */ + public function getI18nLangVarPrefix() { + return $this->i18nLangVarCategory.'.category'; + } + + /** + * @see wcf\system\category\ICategoryType::getLangVarPrefix() + */ + public function getLangVarPrefix() { + return $this->langVarPrefix; + } + + /** + * @see wcf\system\category\ICategoryType::getTitleLangVarCategory() + */ + public function getTitleLangVarCategory() { + return $this->i18nLangVarCategory; + } +} diff --git a/wcfsetup/install/files/lib/system/category/CategoryHandler.class.php b/wcfsetup/install/files/lib/system/category/CategoryHandler.class.php new file mode 100644 index 0000000000..f8aa26d954 --- /dev/null +++ b/wcfsetup/install/files/lib/system/category/CategoryHandler.class.php @@ -0,0 +1,163 @@ + + * @package com.woltlab.wcf + * @subpackage system.category + * @category Community Framework + */ +class CategoryHandler extends SingletonFactory { + /** + * cached categories + * @var array + */ + protected $categories = array(); + + /** + * maps each category id to its object type id and object type category id + * @var array + */ + protected $categoryIDs = array(); + + /** + * mapes the names of the category object types to the object type ids + * @var array + */ + protected $objectTypeIDs = array(); + + /** + * list of category object types + * @var array + */ + protected $objectTypes = array(); + + /** + * Returns all category objects with the given object type id. + * + * @param integer $objectTypeID + * @return array + */ + public function getCategories($objectTypeID) { + if (isset($this->categories[$objectTypeID])) { + return $this->categories[$objectTypeID]; + } + + return array(); + } + + /** + * Returns the category object with the given object type id and object + * type category id. + * + * @param integer $objectTypeID + * @param integer $objectTypeCategoryID + * @return wcf\data\category\Category + */ + public function getCategory($objectTypeID, $objectTypeCategoryID) { + if (isset($this->categories[$objectTypeID][$objectTypeCategoryID])) { + return $this->categories[$objectTypeID][$objectTypeCategoryID]; + } + + return null; + } + + /** + * Returns the category object with the given category id. + * + * @param integer $categoryID + * @return wcf\data\category\Category + */ + public function getCategoryByID($categoryID) { + if (isset($this->categoryIDs[$categoryID])) { + return $this->getCategory($this->categoryIDs[$categoryID]['objectTypeID'], $this->categoryIDs[$categoryID]['objectTypeCategoryID']); + } + + return null; + } + + /** + * Returns the child categories of the given category. + * + * @param wcf\data\category\Category $category + * @return array + */ + public function getChildCategories(Category $category) { + $categories = array(); + + if (isset($this->categories[$category->objectTypeID])) { + foreach ($this->categories[$category->objectTypeID] as $__category) { + if ($__category->parentCategoryID == $category->objectTypeCategoryID) { + $categories[$__category->categoryID] = $__category; + } + } + } + + return $categories; + } + + /** + * 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.category'); + foreach ($this->objectTypes as $objectType) { + $this->objectTypeIDs[$objectType->objectTypeID] = $objectType->objectType; + } + + $cacheName = 'category-'.PACKAGE_ID; + CacheHandler::getInstance()->addResource( + $cacheName, + WCF_DIR.'cache/cache.'.$cacheName.'.php', + 'wcf\system\cache\builder\CategoryCacheBuilder' + ); + $this->categories = CacheHandler::getInstance()->get($cacheName, 'categories'); + $this->categoryIDs = CacheHandler::getInstance()->get($cacheName, 'categoryIDs'); + } + + /** + * Reloads the category cache. + */ + public function reloadCache() { + CacheHandler::getInstance()->clearResource('category-'.PACKAGE_ID); + + $this->init(); + } +} diff --git a/wcfsetup/install/files/lib/system/category/ICategoryType.class.php b/wcfsetup/install/files/lib/system/category/ICategoryType.class.php new file mode 100644 index 0000000000..f5da181564 --- /dev/null +++ b/wcfsetup/install/files/lib/system/category/ICategoryType.class.php @@ -0,0 +1,92 @@ + + * @package com.woltlab.wcf + * @subpackage system.category + * @category Community Framework + */ +interface ICategoryType { + /** + * Is called right after the given category is deleted. + * + * @param wcf\data\category\CategoryEditor $categoryEditor + */ + public function afterDeletion(CategoryEditor $categoryEditor); + + /** + * Returns true, if the active user can add a category of this type. + * + * @return boolean + */ + public function canAddCategory(); + + /** + * Returns true, if the active user can delete a category of this type. + * + * @return boolean + */ + public function canDeleteCategory(); + + /** + * Returns true, if the active user can edit a category of this type. + * + * @return boolean + */ + public function canEditCategory(); + + /** + * Returns the name of the acl object type for categories of this type. + * Returns null if categories of this type don't support acl. + * + * @return string + */ + public function getACLObjectTypeName(); + + /** + * Returns the name of the collapsible object type for categories of this + * type. Returns null if categories of this type don't support collapsing. + * + * @return string + */ + public function getCollapsibleObjectTypeName(); + + /** + * Returns the language variable category for the description language + * variables of categories of this type. + * + * @return string + */ + public function getDescriptionLangVarCategory(); + + /** + * Returns the prefix used for language variables of i18n values. + * + * @return string + */ + public function getI18nLangVarPrefix(); + + /** + * Returns the prefix used for language variables in templates. If a custom + * prefix is used (not 'wcf.category'), a fallback to the default prefix + * ('wcf.category') is used if the relevant language variable doesn't exist + * in the custom category. + * + * @return string + */ + public function getLangVarPrefix(); + + /** + * Returns the language variable category for the title language variables + * of categories of this type. + * + * @return string + */ + public function getTitleLangVarCategory(); +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index d79ecfa8ba..d8543d3aa8 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -405,6 +405,22 @@ + + + + + + + + + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index f17a01931d..ce1c645928 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -405,6 +405,22 @@ + + + + + + + + + + + + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 0850825fde..771ff418c3 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -81,6 +81,21 @@ CREATE TABLE wcf1_cache_resource ( cacheResource VARCHAR(255) NOT NULL PRIMARY KEY ); +DROP TABLE IF EXISTS wcf1_category; +CREATE TABLE wcf1_category ( + categoryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectTypeID INT(10) NOT NULL, + objectTypeCategoryID INT(10) NOT NULL, + parentCategoryID INT(10) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + showOrder INT(10) NOT NULL, + time INT(10) NOT NULL, + isDisabled TINYINT(1) NOT NULL DEFAULT 0, + additionalData TEXT, + UNIQUE KEY (objectTypeID, objectTypeCategoryID) +); + DROP TABLE IF EXISTS wcf1_cleanup_listener; CREATE TABLE wcf1_cleanup_listener ( listenerID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -740,6 +755,8 @@ ALTER TABLE wcf1_acp_template ADD FOREIGN KEY (packageID) REFERENCES wcf1_packag ALTER TABLE wcf1_application ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; ALTER TABLE wcf1_application ADD FOREIGN KEY (groupID) REFERENCES wcf1_application_group (groupID) ON DELETE SET NULL; +ALTER TABLE wcf1_category ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; + ALTER TABLE wcf1_cleanup_listener ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; ALTER TABLE wcf1_cleanup_log ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;