Adds abstract nestable category implementation
authorMatthias Schmidt <gravatronics@live.com>
Mon, 21 May 2012 10:38:31 +0000 (12:38 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Mon, 21 May 2012 11:03:50 +0000 (13:03 +0200)
23 files changed:
com.woltlab.wcf/objectTypeDefinition.xml
com.woltlab.wcf/template/categorySelectList.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/js/WCF.ACP.js
wcfsetup/install/files/acp/templates/categoryAdd.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/categoryList.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/categorySelectList.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/AbstractCategoryAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/AbstractCategoryEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/AbstractCategoryListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/ICategorizedObject.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/Category.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/CategoryNodeList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/CategoryCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/category/CategoryHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/category/ICategoryType.class.php [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index d7a7898586a6aeaf0b82afe223a583c752e1fdec..ab88252e9bfc6553788232b0e9ba419040c69877 100644 (file)
@@ -8,5 +8,10 @@
                <definition>
                        <name>com.woltlab.wcf.clipboardItem</name>
                </definition>
+               
+               <definition>
+                       <interfacename>wcf\system\category\ICategoryType</interfacename>
+                       <name>com.woltlab.wcf.category</name>
+               </definition>
        </import>
 </data>
\ 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 (file)
index 0000000..db6f59a
--- /dev/null
@@ -0,0 +1,3 @@
+{foreach from=$categoryNodeList item=category}
+       <option value="{$category->objectTypeCategoryID}"{if $categoryID|isset && $categoryID == $category->objectTypeCategoryID} selected="selected"{/if}>{section name=i loop=$categoryNodeList->getDepth()}&nbsp;&nbsp;&nbsp;&nbsp;{/section}{$category->getTitle()}</option>
+{/foreach}
\ No newline at end of file
index 34bb6ac9e45b79279511d6c9554df34caadcd492..3b7d16aa18e605a12736bafb2daf26a6f375e447 100644 (file)
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
  */
 
@@ -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 (file)
index 0000000..09ec2b4
--- /dev/null
@@ -0,0 +1,156 @@
+{include file='header'}
+
+{if $aclObjectTypeID}
+       {include file='aclPermissions'}
+{/if}
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               {if $aclObjectTypeID}
+                       new WCF.ACL.List($('#groupPermissions'), {@$aclObjectTypeID}{if $category|isset}, '', {@$category->categoryID}{/if});
+               {/if}
+               
+               var $availableLanguages = { {implode from=$availableLanguages key=languageID item=languageName}{@$languageID}: '{$languageName}'{/implode} };
+               
+               var $titleValues = { {implode from=$i18nValues['title'] key=languageID item=value}'{@$languageID}': '{$value}'{/implode} };
+               new WCF.MultipleLanguageInput('title', false, $titleValues, $availableLanguages);
+               
+               var $descriptionValues = { {implode from=$i18nValues['description'] key=languageID item=value}'{@$languageID}': '{$value}'{/implode} };
+               new WCF.MultipleLanguageInput('description', false, $descriptionValues, $availableLanguages);
+       });
+       //]]>
+</script>
+
+<header class="boxHeadline">
+       <hgroup>
+               <h1>{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.{@$action}{/lang}{/content}{hascontentelse}{lang}wcf.category.{@$action}{/lang}{/hascontent}</h1>
+       </hgroup>
+</header>
+
+{if $errorField}
+       <p class="error">{lang}wcf.global.form.error{/lang}</p>
+{/if}
+
+{if $success|isset}
+       <p class="success">{lang}wcf.global.form.{@$action}.success{/lang}</p>  
+{/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}
+       <div class="contentNavigation">
+               <nav>
+                       <ul>
+                               {content}
+                                       {if $objectType->getProcessor()->canDeleteCategory() || $objectType->getProcessor()->canEditCategory()}
+                                               <li><a href="{link controller=$listController}{/link}" title="{$listLangVar}" class="button"><img src="{@$__wcf->getPath()}icon/list.svg" alt="" class="icon24" /> <span>{@$listLangVar}</span></a></li>
+                                       {/if}
+                                       
+                                       {event name='contentNavigationButtons'}
+                               {/content}
+                       </ul>
+               </nav>
+       </div>
+{/hascontent}
+
+<form method="post" action="{if $action == 'add'}{link controller=$addController}{/link}{else}{link controller=$editController id=$category->categoryID title=$category->getTitle()}{/link}{/if}">
+       <div class="container containerPadding marginTop shadow">
+               <fieldset>
+                       <legend>{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.data{/lang}{/content}{hascontentelse}{lang}wcf.category.data{/lang}{/hascontent}</legend>
+                       
+                       {if $categoryNodeList|count}
+                               <dl{if $errorField == 'parentCategoryID'} class="formError"{/if}>
+                                       <dt><label for="parentCategoryID">{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.parentCategoryID{/lang}{/content}{hascontentelse}{lang}wcf.category.parentCategoryID{/lang}{/hascontent}</label></dt>
+                                       <dd>
+                                               <select id="parentCategoryID" name="parentCategoryID">
+                                                       <option value="0"></option>
+                                                       {include file='categorySelectList' categoryID=$parentCategoryID}
+                                               </select>
+                                               {if $errorField == 'parentCategoryID'}
+                                                       <small class="innerError">
+                                                               {hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.parentCategoryID.error.{@$errorType}{/lang}{/content}{hascontentelse}{lang}wcf.category.parentCategoryID.error.{@$errorType}{/lang}{/hascontent}
+                                                       </small>
+                                               {/if}
+                                               {hascontent}<small>{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.parentCategoryID.description{/lang}{/content}</small>{/hascontent}
+                                       </dd>
+                               </dl>
+                       {/if}
+                       
+                       <dl{if $errorField == 'title'} class="formError"{/if}>
+                               <dt><label for="title">{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.title{/lang}{/content}{hascontentelse}{lang}wcf.category.title{/lang}{/hascontent}</label></dt>
+                               <dd>
+                                       <input type="text" id="title" name="title" value="{$i18nPlainValues['title']}" class="long" />
+                                       {if $errorField == 'title'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {else}
+                                                               {lang}{@$objectType->getProcessor()->getLangVarPrefix()}.title.error.{@$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                                       {hascontent}<small>{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.title.description{/lang}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       <dl{if $errorField == 'description'} class="formError"{/if}>
+                               <dt><label for="description">{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.description{/lang}{/content}{hascontentelse}{lang}wcf.category.description{/lang}{/hascontent}</label></dt>
+                               <dd>
+                                       <textarea cols="40" rows="10" id="description" name="description">{$i18nPlainValues['description']}</textarea>
+                                       {if $errorType == 'description'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {else}
+                                                               {lang}{@$objectType->getProcessor()->getLangVarPrefix()}.description.error.{@$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                                       {hascontent}<small>{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.description.description{/lang}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       <dl{if $errorField == 'isDisabled'} class="formError"{/if}>
+                               <dt class="reversed"><label for="isDisabled">{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.isDisabled{/lang}{/content}{hascontentelse}{lang}wcf.category.isDisabled{/lang}{/hascontent}</label></dt>
+                               <dd>
+                                       <input type="checkbox" id="isDisabled" name="isDisabled"{if $isDisabled} checked="checked"{/if} />
+                                       {hascontent}<small>{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.isDisabled.description{/lang}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       <dl{if $errorField == 'showOrder'} class="formError"{/if}>
+                               <dt><label for="showOrder">{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.showOrder{/lang}{/content}{hascontentelse}{lang}wcf.category.showOrder{/lang}{/hascontent}</label></dt>
+                               <dd>
+                                       <input type="text" id="showOrder" name="showOrder" value="{$showOrder}" class="short" />
+                                       {if $errorField == 'title'}
+                                               <small class="innerError">
+                                                       {lang}{@$objectType->getProcessor()->getLangVarPrefix()}.showOrder.error.{@$errorType}{/lang}
+                                               </small>
+                                       {/if}
+                                       {hascontent}<small>{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.showOrder.description{/lang}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       {if $aclObjectTypeID}
+                               <dl id="groupPermissions">
+                                       <dt>{lang}wcf.acp.acl.permissions{/lang}</dt>
+                                       <dd></dd>
+                               </dl>
+                       {/if}
+                       
+                       {event name='fields'}
+               </fieldset>
+               
+               {event name='fieldsets'}
+       </div>
+       
+       <div class="formSubmit">
+               <input type="reset" value="{lang}wcf.global.button.reset{/lang}" accesskey="r" />
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+       </div>
+</form>
+
+{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 (file)
index 0000000..b5b8b5b
--- /dev/null
@@ -0,0 +1,124 @@
+{include file='header'}
+
+{if $categoryNodeList|count}
+       <script type="text/javascript">
+               //<![CDATA[
+               $(function() {
+                       {if $collapsibleObjectTypeID && $categoryNodeList|count > 1}
+                               new WCF.ACP.Category.Collapsible('wcf\\data\\category\\CategoryAction', {@$collapsibleObjectTypeID});
+                       {/if}
+                       
+                       {if $objectType->getProcessor()->canDeleteCategory()}
+                               new WCF.ACP.Category.Delete('wcf\\data\\category\\CategoryAction', $('.jsCategory'));
+                       {/if}
+                       {if $objectType->getProcessor()->canEditCategory()}
+                               new WCF.Action.Toggle('wcf\\data\\category\\CategoryAction', $('.jsCategory'), '> .buttons > .jsToggleButton');
+                               
+                               {if $categoryNodeList|count > 1}
+                                       new WCF.Sortable.List('categoryList', 'wcf\\data\\category\\CategoryAction');
+                               {/if}
+                       {/if}
+               });
+               //]]>
+       </script>
+{/if}
+
+<header class="box48 boxHeadline">
+       <hgroup>
+               <h1>{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.list{/lang}{/content}{hascontentelse}{lang}wcf.category.list{/lang}{/hascontent}</h1>
+       </hgroup>
+</header>
+
+{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}
+       <div class="contentNavigation">
+               <nav>
+                       <ul>
+                               {content}
+                                       {if $objectType->getProcessor()->canAddCategory()}
+                                               <li><a href="{link controller=$addController}{/link}" title="{$addLangVar}" class="button"><img src="{@$__wcf->getPath()}icon/add.svg" alt="" class="icon24" /> <span>{@$addLangVar}</span></a></li>
+                                       {/if}
+                                       
+                                       {event name='contentNavigationButtons'}
+                               {/content}
+                       </ul>
+               </nav>
+       </div>
+{/hascontent}
+
+{if $categoryNodeList|count}
+       <section id="categoryList" class="container containerPadding marginTop shadow{if $objectType->getProcessor()->canEditCategory() && $categoryNodeList|count > 1} sortableListContainer{/if}">
+               <ol class="categoryList sortableList" data-object-id="0">
+                       {assign var=oldDepth value=0}
+                       {foreach from=$categoryNodeList item=category}
+                               {if $categoryNodeList->getDepth() < $oldDepth}
+                                       </ol></li>
+                               {/if}
+                               
+                               <li class="{if $objectType->getProcessor()->canEditCategory() && $categoryNodeList|count > 1}sortableNode {/if}jsCategory" data-object-id="{@$category->categoryID}"{if $collapsedCategoryIDs|is_array} data-is-open="{if $collapsedCategoryIDs[$category->categoryID]|isset}0{else}1{/if}"{/if}>
+                                       <span class="buttons">
+                                               {if $objectType->getProcessor()->canEditCategory()}
+                                                       <a href="{link controller=$editController id=$category->categoryID title=$category->getTitle()}{/link}"><img src="{@$__wcf->getPath()}icon/edit.svg" alt="" title="{lang}wcf.global.button.edit{/lang}" class="icon16 jsTooltip" /></a>
+                                               {else}
+                                                       <img src="{@$__wcf->getPath()}icon/edit.svg" alt="" title="{lang}wcf.global.button.edit{/lang}" class="icon16 disabled" />
+                                               {/if}
+
+                                               {if $objectType->getProcessor()->canDeleteCategory()}
+                                                       <img src="{@$__wcf->getPath()}icon/delete.svg" alt="" title="{lang}wcf.global.button.delete{/lang}" class="icon16 jsDeleteButton jsTooltip" data-object-id="{@$category->categoryID}" data-confirm-message="{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.delete.sure{/lang}{/content}{hascontentelse}{lang}wcf.category.delete.sure{/lang}{/hascontent}" />
+                                               {else}
+                                                       <img src="{@$__wcf->getPath()}icon/delete.svg" alt="" title="{lang}wcf.global.button.delete{/lang}" class="icon16 disabled" />
+                                               {/if}
+
+                                               {if $objectType->getProcessor()->canEditCategory()}
+                                                       <img src="{@$__wcf->getPath()}icon/{if !$category->isDisabled}enabled{else}disabled{/if}.svg" alt="" title="{lang}wcf.global.button.{if !$category->isDisabled}disable{else}enable{/if}{/lang}" class="icon16 jsToggleButton jsTooltip" data-object-id="{@$category->categoryID}" />
+                                               {else}
+                                                       <img src="{@$__wcf->getPath()}icon/{if !$category->isDisabled}enabled{else}disabled{/if}.svg" alt="" title="{lang}wcf.global.button.{if !$category->isDisabled}enable{else}disable{/if}{/lang}" class="icon16 disabled" />
+                                               {/if}
+
+                                               {event name='buttons'}
+                                       </span>
+
+                                       <span class="title">
+                                               {$category->getTitle()}
+                                       </span>
+                               
+                                       <ol class="categoryList sortableList" data-object-id="{@$category->categoryID}">
+                               {if !$categoryNodeList->current()->hasChildren()}
+                                       </ol></li>
+                               {/if}
+                               {assign var=oldDepth value=$categoryNodeList->getDepth()}
+                       {/foreach}
+                       {section name=i loop=$oldDepth}</ol></li>{/section}
+               </ol>
+               
+               {if $objectType->getProcessor()->canEditCategory() && $categoryNodeList|count > 1}
+                       <div class="formSubmit">
+                               <button class="button default" data-type="submit">{lang}wcf.global.button.save{/lang}</button>
+                       </div>
+               {/if}
+       </section>
+               
+       {hascontent}
+               <div class="contentNavigation">
+                       <nav>
+                               <ul>
+                                       {content}
+                                               {if $objectType->getProcessor()->canAddCategory()}
+                                                       <li><a href="{link controller=$addController}{/link}" title="{$addLangVar}" class="button"><img src="{@$__wcf->getPath()}icon/add.svg" alt="" class="icon24" /> <span>{@$addLangVar}</span></a></li>
+                                               {/if}
+
+                                               {event name='contentNavigationButtons'}
+                                       {/content}
+                               </ul>
+                       </nav>
+               </div>
+       {/hascontent}
+{else}
+       <p class="warning">{hascontent}{content}{lang __optional=true}{@$objectType->getProcessor()->getLangVarPrefix()}.list.noneAvailable{/lang}{/content}{hascontentelse}{lang}wcf.category.list.noneAvailable{/lang}{/hascontent}</p>
+{/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 (file)
index 0000000..db6f59a
--- /dev/null
@@ -0,0 +1,3 @@
+{foreach from=$categoryNodeList item=category}
+       <option value="{$category->objectTypeCategoryID}"{if $categoryID|isset && $categoryID == $category->objectTypeCategoryID} selected="selected"{/if}>{section name=i loop=$categoryNodeList->getDepth()}&nbsp;&nbsp;&nbsp;&nbsp;{/section}{$category->getTitle()}</option>
+{/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 (file)
index 0000000..d35768e
--- /dev/null
@@ -0,0 +1,309 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\category\CategoryAction;
+use wcf\data\category\CategoryEditor;
+use wcf\data\category\CategoryNodeList;
+use wcf\system\acl\ACLHandler;
+use wcf\system\category\CategoryHandler;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\SystemException;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\I18nHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Abstract implementation of a form to create categories.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 (file)
index 0000000..6dcad76
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\category\Category;
+use wcf\data\category\CategoryAction;
+use wcf\data\category\CategoryNodeList;
+use wcf\system\acl\ACLHandler;
+use wcf\system\category\CategoryHandler;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\I18nHandler;
+use wcf\system\WCF;
+
+/**
+ * Abstract implementation of a form to edit a category.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 (file)
index 0000000..c3357b6
--- /dev/null
@@ -0,0 +1,162 @@
+<?php
+namespace wcf\acp\page;
+use wcf\data\category\CategoryNodeList;
+use wcf\page\AbstractPage;
+use wcf\system\category\CategoryHandler;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\SystemException;
+use wcf\system\menu\acp\ACPMenu;
+use wcf\system\user\collapsible\content\UserCollapsibleContentHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Abstract implementation of a page with lists all categories of a certain object
+ * type.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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<integer>
+        */
+       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 (file)
index 0000000..225b972
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every categorized object has to implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @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
+ */
+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 (file)
index 0000000..701d70c
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+namespace wcf\data\category;
+use wcf\data\DatabaseObject;
+use wcf\system\category\CategoryHandler;
+use wcf\system\request\IRouteController;
+use wcf\system\WCF;
+
+/**
+ * Represents a category.
+ * 
+ * @author     Matthias Schmidt
+ * @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
+ * @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<wcf\data\category\Category>
+        */
+       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<wcf\data\category\Category>
+        */
+       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 (file)
index 0000000..b771094
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+namespace wcf\data\category;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\category\CategoryHandler;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\ValidateActionException;
+use wcf\system\user\collapsible\content\UserCollapsibleContentHandler;
+use wcf\system\WCF;
+
+/**
+ * Executes category-related actions.
+ *
+ * @author     Matthias Schmidt
+ * @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
+ * @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 (file)
index 0000000..47de399
--- /dev/null
@@ -0,0 +1,253 @@
+<?php
+namespace wcf\data\category;
+use wcf\data\object\type\ObjectTypeEditor;
+use wcf\data\DatabaseObjectEditor;
+use wcf\data\IEditableCachedObject;
+use wcf\system\cache\CacheHandler;
+use wcf\system\category\CategoryHandler;
+use wcf\system\exception\SystemException;
+use wcf\system\WCF;
+
+/**
+ * Provides functions to edit categories.
+ *
+ * @author     Matthias Schmidt
+ * @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
+ * @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 (file)
index 0000000..81191e8
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace wcf\data\category;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of categories.
+ *
+ * @author     Matthias Schmidt
+ * @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
+ * @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 (file)
index 0000000..5128420
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+namespace wcf\data\category;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\data\DatabaseObject;
+use wcf\system\category\CategoryHandler;
+
+/**
+ * Represents a category node.
+ *
+ * @author     Matthias Schmidt
+ * @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
+ * @category   Community Framework
+ */
+class CategoryNode extends DatabaseObjectDecorator implements \RecursiveIterator, \Countable {
+       /**
+        * child category nodes
+        * @var array<wcf\data\category\CategoryNode>
+        */
+       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 (file)
index 0000000..23bf7cc
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+namespace wcf\data\category;
+use wcf\system\exception\SystemException;
+use wcf\system\category\CategoryHandler;
+
+/**
+ * Represents a category node list.
+ *
+ * @author     Matthias Schmidt
+ * @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
+ * @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<integer>  $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 (file)
index 0000000..6b7cd3d
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\category\CategoryList;
+use wcf\system\package\PackageDependencyHandler;
+
+/**
+ * Caches the categories for a certain package.
+ *
+ * @author     Matthias Schmidt
+ * @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 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 (file)
index 0000000..4e45a4b
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+namespace wcf\system\category;
+use wcf\data\category\CategoryEditor;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Abstract implementation of a category type.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 (file)
index 0000000..f8aa26d
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+namespace wcf\system\category;
+use wcf\data\category\Category;
+use wcf\data\category\CategoryEditor;
+use wcf\system\SingletonFactory;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\cache\CacheHandler;
+
+/**
+ * Handles categories.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.category
+ * @category   Community Framework
+ */
+class CategoryHandler extends SingletonFactory {
+       /**
+        * cached categories
+        * @var array<wcf\data\category\Category>
+        */
+       protected $categories = array();
+       
+       /**
+        * maps each category id to its object type id and object type category id
+        * @var array<array>
+        */
+       protected $categoryIDs = array();
+       
+       /**
+        * mapes the names of the category object types to the object type ids
+        * @var array<integer>
+        */
+       protected $objectTypeIDs = array();
+       
+       /**
+        * list of category object types
+        * @var array<wcf\data\object\type>
+        */
+       protected $objectTypes = array();
+       
+       /**
+        * Returns all category objects with the given object type id.
+        * 
+        * @param       integer         $objectTypeID
+        * @return      array<wcf\data\category\Category>
+        */
+       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<wcf\data\category\Category>
+        */
+       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 (file)
index 0000000..f5da181
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+namespace wcf\system\category;
+use wcf\data\category\CategoryEditor;
+
+/**
+ * Every category type has to implement this interface.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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();
+}
index d79ecfa8ba90163e0d4da46016c598e3e76cf88a..d8543d3aa81315949edbc6074df2b19992f81961 100644 (file)
                <item name="wcf.acp.user.sendMail.text"><![CDATA[Nachricht]]></item>
        </category>
        
+       <category name="wcf.category">
+               <item name="wcf.category.add"><![CDATA[Kategorie hinzufügen]]></item>
+               <item name="wcf.category.button.list"><![CDATA[Kategorien auflisten]]></item>
+               <item name="wcf.category.data"><![CDATA[Allgemeine Daten]]></item>
+               <item name="wcf.category.delete.sure"><![CDATA[Sind Sie sicher, dass Sie diese Kategorie löschen möchten? Alle sich in dieser Kategorie befindlichen Unterkategorien werden in die Elternkategorie dieser Kategorie verschoben.]]></item>
+               <item name="wcf.category.description"><![CDATA[Beschreibung]]></item>
+               <item name="wcf.category.edit"><![CDATA[Kategorie bearbeiten]]></item>
+               <item name="wcf.category.isDisabled"><![CDATA[deaktiviert]]></item>
+               <item name="wcf.category.list"><![CDATA[Kategorien]]></item>
+               <item name="wcf.category.list.noneAvailable"><![CDATA[Es wurde noch keine Kategorie hinzugefügt.]]></item>
+               <item name="wcf.category.parentCategoryID"><![CDATA[Übergeordnete Kategorie]]></item>
+               <item name="wcf.category.parentCategoryID.error.invalid"><![CDATA[Die ausgewählte Kategorie existiert nicht.]]></item>
+               <item name="wcf.category.showOrder"><![CDATA[Position]]></item>
+               <item name="wcf.category.title"><![CDATA[Titel]]></item>
+       </category>
+       
        <category name="wcf.clipboard">
                <item name="wcf.clipboard.item.user.assignToGroup"><![CDATA[Benutzergruppe zuweisen]]></item>
                <item name="wcf.clipboard.item.user.delete"><![CDATA[Benutzer löschen]]></item>
index f17a01931dc293caeadc440766486fa0604c3007..ce1c64592894799fb0e0f449b9fbb4ede4347bb9 100644 (file)
                <item name="wcf.acp.user.sendMail.text"><![CDATA[Message]]></item>
        </category>
        
+       <category name="wcf.category">
+               <item name="wcf.category.add"><![CDATA[Add category]]></item>
+               <item name="wcf.category.button.list"><![CDATA[List categories]]></item>
+               <item name="wcf.category.data"><![CDATA[General data]]></item>
+               <item name="wcf.category.delete.sure"><![CDATA[Are you sure that you would like to delete this category? All sub categories of this category will be moved to the parent category of this category.]]></item>
+               <item name="wcf.category.description"><![CDATA[Description]]></item>
+               <item name="wcf.category.edit"><![CDATA[Edit category]]></item>
+               <item name="wcf.category.isDisabled"><![CDATA[disabled]]></item>
+               <item name="wcf.category.list"><![CDATA[Categories]]></item>
+               <item name="wcf.category.list.noneAvailable"><![CDATA[No category has been added yet.]]></item>
+               <item name="wcf.category.parentCategoryID"><![CDATA[Parent category]]></item>
+               <item name="wcf.category.parentCategoryID.error.invalid"><![CDATA[The chosen category doesn't exist.]]></item>
+               <item name="wcf.category.showOrder"><![CDATA[Position]]></item>
+               <item name="wcf.category.title"><![CDATA[Title]]></item>
+       </category>
+       
        <category name="wcf.clipboard">
                <item name="wcf.clipboard.item.user.assignToGroup"><![CDATA[Assign to user group]]></item>
                <item name="wcf.clipboard.item.user.delete"><![CDATA[Delete users]]></item>
index 0850825fde8578f2a4ab7db49444fae4c01d0d5d..771ff418c3c288369087d127848559cee0521900 100644 (file)
@@ -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;