Merged com.woltlab.wcf.acl into WCF
authorAlexander Ebert <ebert@woltlab.com>
Mon, 31 Dec 2012 02:33:22 +0000 (03:33 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 31 Dec 2012 02:33:22 +0000 (03:33 +0100)
32 files changed:
com.woltlab.wcf/objectTypeDefinition.xml
com.woltlab.wcf/packageInstallationPlugin.xml
com.woltlab.wcf/template/aclPermissionJavaScript.tpl [new file with mode: 0644]
com.woltlab.wcf/template/aclPermissions.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/aclPermissionJavaScript.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/aclPermissions.tpl [new file with mode: 0644]
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/js/WCF.ACL.js [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/acl/option/ACLOption.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/ACLOptionAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/ACLOptionEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/ACLOptionList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategory.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/ViewableCategory.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/ViewableCategoryNode.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/category/ViewableCategoryNodeList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/acl/ACLHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/ACLOptionCategoryCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/CategoryACLOptionCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/category/CategoryPermissionHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/package/plugin/ACLOptionPackageInstallationPlugin.class.php [new file with mode: 0644]
wcfsetup/install/files/style/acl.less [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index dac95324854d507176e35565d45c69d40d360c6d..ed67be93987fa0880c25fb58a641fe229b74fc02 100644 (file)
@@ -1,6 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/objectTypeDefinition.xsd">
        <import>
+               <definition>
+                       <name>com.woltlab.wcf.acl</name>
+               </definition>
+               
                <definition>
                        <name>com.woltlab.wcf.collapsibleContent</name>
                </definition>
index 8c576bec14bafe144e2930dbb30656eb4f87069b..140fd6b14e4306744bb1f911f495b470c81faa14 100644 (file)
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/packageInstallationPlugin.xsd">
        <import>
+               <pip name="aclOption">wcf\system\package\plugin\ACLOptionPackageInstallationPlugin</pip>
                <pip name="acpMenu">wcf\system\package\plugin\ACPMenuPackageInstallationPlugin</pip>
                <pip name="acpTemplate">wcf\system\package\plugin\ACPTemplatePackageInstallationPlugin</pip>
                <pip name="clipboardAction">wcf\system\package\plugin\ClipboardActionPackageInstallationPlugin</pip>
diff --git a/com.woltlab.wcf/template/aclPermissionJavaScript.tpl b/com.woltlab.wcf/template/aclPermissionJavaScript.tpl
new file mode 100644 (file)
index 0000000..897fd83
--- /dev/null
@@ -0,0 +1,63 @@
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               {if $aclValues[$objectTypeID]|isset}
+                       var initialPermissions = { 
+                               returnValues: {
+                                       options: {
+                                               {implode from=$aclValues[$objectTypeID][options] key='__optionID' item='__optionData'}
+                                                       {@$__optionID}: {
+                                                               categoryName: '{@$__optionData[categoryName]|encodeJS}',
+                                                               label: '{@$__optionData[label]|encodeJS}',
+                                                               optionName: '{@$__optionData[optionName]|encodeJS}'
+                                                       }
+                                               {/implode}
+                                       },
+                                       categories: {
+                                               {implode from=$aclValues[$objectTypeID][categories] key='__category' item='__categoryName'}
+                                                       '{@$__category|encodeJS}': '{@$__categoryName|encodeJS}'
+                                               {/implode}
+                                       },
+                                       user: {
+                                               {if $aclValues[$objectTypeID][user]|isset}
+                                                       option: {
+                                                               {implode from=$aclValues[$objectTypeID][user][option] key='__userID' item='__optionData'}
+                                                                       {@$__userID}: {
+                                                                               {implode from=$__optionData key='__optionID' item='__optionValue'}
+                                                                                       {@$__optionID}: {@$__optionValue}
+                                                                               {/implode}
+                                                                       }
+                                                               {/implode}
+                                                       },
+                                                       label: {
+                                                               {implode from=$aclValues[$objectTypeID][user][label] key='__userID' item='__label'}
+                                                                       {@$__userID}: '{@$__label|encodeJS}'
+                                                               {/implode}
+                                                       }
+                                               {/if}
+                                       },
+                                       group: {
+                                               {if $aclValues[$objectTypeID][group]|isset}
+                                                       option: {
+                                                               {implode from=$aclValues[$objectTypeID][group][option] key='__groupID' item='__optionData'}
+                                                                       {@$__groupID}: {
+                                                                               {implode from=$__optionData key='__optionID' item='__optionValue'}
+                                                                                       {@$__optionID}: {@$__optionValue}
+                                                                               {/implode}
+                                                                       }
+                                                               {/implode}
+                                                       },
+                                                       label: {
+                                                               {implode from=$aclValues[$objectTypeID][group][label] key='__groupID' item='__label'}
+                                                                       {@$__groupID}: '{@$__label|encodeJS}'
+                                                               {/implode}
+                                                       }
+                                               {/if}
+                                       }
+                               }
+                       };
+               {/if}
+               new {if $aclListClassName|isset}{@$aclListClassName}{else}WCF.ACL.List{/if}($('#{@$containerID}'), {@$objectTypeID}, {if $categoryName|isset}'{@$categoryName}'{else}null{/if}, {if $objectID|isset}{@$objectID}{else}0{/if}, {if !$includeUserGroups|isset || $includeUserGroups}true{else}false{/if}{if $aclValues[$objectTypeID]|isset}, initialPermissions{/if});
+       });
+       //]]>
+</script>
\ No newline at end of file
diff --git a/com.woltlab.wcf/template/aclPermissions.tpl b/com.woltlab.wcf/template/aclPermissions.tpl
new file mode 100644 (file)
index 0000000..3e44ddd
--- /dev/null
@@ -0,0 +1,19 @@
+<script type="text/javascript" src="{@$__wcf->getPath()}js/WCF.ACL.js"></script>
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               WCF.Icon.addObject({
+                       'wcf.icon.delete': '{@$__wcf->getPath()}icon/delete1.svg',
+                       'wcf.icon.user': '{@$__wcf->getPath()}icon/user1.svg',
+                       'wcf.icon.users': '{@$__wcf->getPath()}icon/users1.svg'
+               });
+               
+               WCF.Language.addObject({
+                       'wcf.acl.option.deny': '{lang}wcf.acl.option.deny{/lang}',
+                       'wcf.acl.option.fullAccess': '{lang}wcf.acl.option.fullAccess{/lang}',
+                       'wcf.acl.option.grant': '{lang}wcf.acl.option.grant{/lang}',
+                       'wcf.global.button.delete': '{lang}wcf.global.button.delete{/lang}'
+               });
+       });
+       //]]>
+</script>
\ No newline at end of file
diff --git a/wcfsetup/install/files/acp/templates/aclPermissionJavaScript.tpl b/wcfsetup/install/files/acp/templates/aclPermissionJavaScript.tpl
new file mode 100644 (file)
index 0000000..897fd83
--- /dev/null
@@ -0,0 +1,63 @@
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               {if $aclValues[$objectTypeID]|isset}
+                       var initialPermissions = { 
+                               returnValues: {
+                                       options: {
+                                               {implode from=$aclValues[$objectTypeID][options] key='__optionID' item='__optionData'}
+                                                       {@$__optionID}: {
+                                                               categoryName: '{@$__optionData[categoryName]|encodeJS}',
+                                                               label: '{@$__optionData[label]|encodeJS}',
+                                                               optionName: '{@$__optionData[optionName]|encodeJS}'
+                                                       }
+                                               {/implode}
+                                       },
+                                       categories: {
+                                               {implode from=$aclValues[$objectTypeID][categories] key='__category' item='__categoryName'}
+                                                       '{@$__category|encodeJS}': '{@$__categoryName|encodeJS}'
+                                               {/implode}
+                                       },
+                                       user: {
+                                               {if $aclValues[$objectTypeID][user]|isset}
+                                                       option: {
+                                                               {implode from=$aclValues[$objectTypeID][user][option] key='__userID' item='__optionData'}
+                                                                       {@$__userID}: {
+                                                                               {implode from=$__optionData key='__optionID' item='__optionValue'}
+                                                                                       {@$__optionID}: {@$__optionValue}
+                                                                               {/implode}
+                                                                       }
+                                                               {/implode}
+                                                       },
+                                                       label: {
+                                                               {implode from=$aclValues[$objectTypeID][user][label] key='__userID' item='__label'}
+                                                                       {@$__userID}: '{@$__label|encodeJS}'
+                                                               {/implode}
+                                                       }
+                                               {/if}
+                                       },
+                                       group: {
+                                               {if $aclValues[$objectTypeID][group]|isset}
+                                                       option: {
+                                                               {implode from=$aclValues[$objectTypeID][group][option] key='__groupID' item='__optionData'}
+                                                                       {@$__groupID}: {
+                                                                               {implode from=$__optionData key='__optionID' item='__optionValue'}
+                                                                                       {@$__optionID}: {@$__optionValue}
+                                                                               {/implode}
+                                                                       }
+                                                               {/implode}
+                                                       },
+                                                       label: {
+                                                               {implode from=$aclValues[$objectTypeID][group][label] key='__groupID' item='__label'}
+                                                                       {@$__groupID}: '{@$__label|encodeJS}'
+                                                               {/implode}
+                                                       }
+                                               {/if}
+                                       }
+                               }
+                       };
+               {/if}
+               new {if $aclListClassName|isset}{@$aclListClassName}{else}WCF.ACL.List{/if}($('#{@$containerID}'), {@$objectTypeID}, {if $categoryName|isset}'{@$categoryName}'{else}null{/if}, {if $objectID|isset}{@$objectID}{else}0{/if}, {if !$includeUserGroups|isset || $includeUserGroups}true{else}false{/if}{if $aclValues[$objectTypeID]|isset}, initialPermissions{/if});
+       });
+       //]]>
+</script>
\ No newline at end of file
diff --git a/wcfsetup/install/files/acp/templates/aclPermissions.tpl b/wcfsetup/install/files/acp/templates/aclPermissions.tpl
new file mode 100644 (file)
index 0000000..f0db0ed
--- /dev/null
@@ -0,0 +1,19 @@
+<script type="text/javascript" src="{@$__wcf->getPath()}js/WCF.ACL.js"></script>
+<script type="text/javascript">
+       //<![CDATA[
+       $(function() {
+               WCF.Icon.addObject({
+                       'wcf.icon.delete': '{@$__wcf->getPath()}icon/delete.svg',
+                       'wcf.icon.user': '{@$__wcf->getPath()}icon/user.svg',
+                       'wcf.icon.users': '{@$__wcf->getPath()}icon/users.svg'
+               });
+               
+               WCF.Language.addObject({
+                       'wcf.acl.option.deny': '{lang}wcf.acl.option.deny{/lang}',
+                       'wcf.acl.option.fullAccess': '{lang}wcf.acl.option.fullAccess{/lang}',
+                       'wcf.acl.option.grant': '{lang}wcf.acl.option.grant{/lang}',
+                       'wcf.global.button.delete': '{lang}wcf.global.button.delete{/lang}'
+               });
+       });
+       //]]>
+</script>
\ No newline at end of file
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..0f499be
--- /dev/null
@@ -0,0 +1,157 @@
+{include file='header'}
+
+{if $aclObjectTypeID}
+       {include file='aclPermissions'}
+       
+       {if !$category|isset}
+               {include file='aclPermissionJavaScript' containerID='groupPermissions' objectTypeID=$aclObjectTypeID}
+       {else}
+               {include file='aclPermissionJavaScript' containerID='groupPermissions' objectTypeID=$aclObjectTypeID objectID=$category->categoryID}
+       {/if}
+{/if}
+
+{include file='multipleLanguageInputJavascript' elementIdentifier='title' forceSelection=false}
+{if $objectType->getProcessor()->hasDescription()}
+       {include file='multipleLanguageInputJavascript' elementIdentifier='description' forceSelection=false}
+{/if}
+
+<header class="boxHeadline">
+       <hgroup>
+               <h1>{@$objectType->getProcessor()->getLanguageVariable($action)}</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}
+
+{hascontent}
+       <div class="contentNavigation">
+               <nav>
+                       <ul>
+                               {content}
+                                       {if $objectType->getProcessor()->canDeleteCategory() || $objectType->getProcessor()->canEditCategory()}
+                                               <li><a href="{link controller=$listController}{/link}" title="{$objectType->getProcessor()->getLanguageVariable('button.list')}" class="button"><img src="{@$__wcf->getPath()}icon/list.svg" alt="" class="icon24" /> <span>{@$objectType->getProcessor()->getLanguageVariable('button.list')}</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 object=$category}{/link}{/if}">
+       <div class="container containerPadding marginTop">
+               <fieldset>
+                       <legend>{lang}wcf.global.form.data{/lang}</legend>
+                       
+                       {if $objectType->getProcessor()->getMaximumNestingLevel() && $categoryNodeList|count}
+                               <dl{if $errorField == 'parentCategoryID'} class="formError"{/if}>
+                                       <dt><label for="parentCategoryID">{@$objectType->getProcessor()->getLanguageVariable('parentCategoryID')}</label></dt>
+                                       <dd>
+                                               <select id="parentCategoryID" name="parentCategoryID">
+                                                       <option value="0"></option>
+                                                       {include file='categoryOptionList' categoryID=$parentCategoryID maximumNestingLevel=$objectType->getProcessor()->getMaximumNestingLevel() - 1}
+                                               </select>
+                                               {if $errorField == 'parentCategoryID'}
+                                                       <small class="innerError">
+                                                               {assign var=__languageVariable value='parentCategoryID.error.'|concat:$errorType}
+                                                               {@$objectType->getProcessor()->getLanguageVariable($__languageVariable)}
+                                                       </small>
+                                               {/if}
+                                               {hascontent}<small>{content}{@$objectType->getProcessor()->getLanguageVariable('parentCategoryID.description', true)}{/content}</small>{/hascontent}
+                                       </dd>
+                               </dl>
+                       {/if}
+                       
+                       <dl{if $errorField == 'title'} class="formError"{/if}>
+                               <dt><label for="title">{@$objectType->getProcessor()->getLanguageVariable('title')}</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}
+                                                               {assign var=__languageVariable value='title.error.'|concat:$errorType}
+                                                               {@$objectType->getProcessor()->getLanguageVariable($__languageVariable)}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                                       {hascontent}<small>{content}{@$objectType->getProcessor()->getLanguageVariable('title.description', true)}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       {if $objectType->getProcessor()->hasDescription()}
+                               <dl{if $errorField == 'description'} class="formError"{/if}>
+                                       <dt><label for="description">{@$objectType->getProcessor()->getLanguageVariable('description')}</label></dt>
+                                       <dd>
+                                               <textarea cols="40" rows="10" id="description" name="description">{$i18nPlainValues['description']}</textarea>
+                                               {if $errorField == 'description'}
+                                                       <small class="innerError">
+                                                               {if $errorType == 'empty'}
+                                                                       {lang}wcf.global.form.error.empty{/lang}
+                                                               {else}
+                                                                       {assign var=__languageVariable value='description.error.'|concat:$errorType}
+                                                                       {@$objectType->getProcessor()->getLanguageVariable($__languageVariable)}
+                                                               {/if}
+                                                       </small>
+                                               {/if}
+                                               {hascontent}<small>{content}{@$objectType->getProcessor()->getLanguageVariable('description.description', true)}{/content}</small>{/hascontent}
+                                       </dd>
+                               </dl>
+                       {/if}
+                       
+                       <dl{if $errorField == 'isDisabled'} class="formError"{/if}>
+                               <dt class="reversed"><label for="isDisabled">{@$objectType->getProcessor()->getLanguageVariable('isDisabled')}</label></dt>
+                               <dd>
+                                       <input type="checkbox" id="isDisabled" name="isDisabled"{if $isDisabled} checked="checked"{/if} />
+                                       {hascontent}<small>{content}{@$objectType->getProcessor()->getLanguageVariable('isDisabled.description', true)}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       <dl{if $errorField == 'showOrder'} class="formError"{/if}>
+                               <dt><label for="showOrder">{@$objectType->getProcessor()->getLanguageVariable('showOrder')}</label></dt>
+                               <dd>
+                                       <input type="text" id="showOrder" name="showOrder" value="{$showOrder}" class="short" />
+                                       {if $errorField == 'showOrder'}
+                                               <small class="innerError">
+                                                       {assign var=__languageVariable value='showOrder.error.'|concat:$errorType}
+                                                       {@$objectType->getProcessor()->getLanguageVariable($__languageVariable)}
+                                               </small>
+                                       {/if}
+                                       {hascontent}<small>{content}{@$objectType->getProcessor()->getLanguageVariable('showOrder.description', true)}{/content}</small>{/hascontent}
+                               </dd>
+                       </dl>
+                       
+                       {event name='dataFields'}
+               </fieldset>
+               
+               {if $aclObjectTypeID}
+                       <fieldset>
+                               <legend>{lang}wcf.acp.acl.permissions{/lang}</legend>
+
+                               <dl id="groupPermissions" class="wide">
+                                       <dt>{lang}wcf.acp.acl.permissions{/lang}</dt>
+                                       <dd></dd>
+                               </dl>
+                               
+                               {event name='permissionFields'}
+                       </fieldset>
+               {/if}
+               
+               {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..42d6876
--- /dev/null
@@ -0,0 +1,164 @@
+{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}
+                                       var sortableNodes = $('.sortableNode');
+                                       sortableNodes.each(function(index, node) {
+                                               $(node).wcfIdentify();
+                                       });
+                                       
+                                       new WCF.Sortable.List('categoryList', 'wcf\\data\\category\\CategoryAction', 0{if $objectType->getProcessor()->getMaximumNestingLevel() != -1}, {
+                                               /**
+                                                * Updates the sortable nodes after a sorting is started with
+                                                * regard to their possibility to have child the currently sorted
+                                                * category as a child category.
+                                                */
+                                               start: function(event, ui) {
+                                                       var sortedListItem = $(ui.item);
+                                                       var itemNestingLevel = sortedListItem.find('.sortableList:has(.sortableNode)').length;
+                                                       
+                                                       sortableNodes.each(function(index, node) {
+                                                               node = $(node);
+                                                               
+                                                               if (node.attr('id') != sortedListItem.attr('id')) {
+                                                                       if (node.parents('.sortableList').length + itemNestingLevel >= {@$objectType->getProcessor()->getMaximumNestingLevel() + 1}) {
+                                                                               node.addClass('sortableNoNesting');
+                                                                       }
+                                                                       else if (node.hasClass('sortableNoNesting')) {
+                                                                               node.removeClass('sortableNoNesting');
+                                                                       }
+                                                               }
+                                                       });
+                                               },
+                                               /**
+                                                * Updates the sortable nodes after a sorting is completed with
+                                                * regard to their possibility to have child categories.
+                                                */
+                                               stop: function(event, ui) {
+                                                       sortableNodes.each(function(index, node) {
+                                                               node = $(node);
+                                                               
+                                                               if (node.parents('.sortableList').length == {@$objectType->getProcessor()->getMaximumNestingLevel() + 1}) {
+                                                                       node.addClass('sortableNoNesting');
+                                                               }
+                                                               else if (node.hasClass('sortableNoNesting')) {
+                                                                       node.removeClass('sortableNoNesting');
+                                                               }
+                                                       });
+                                               }
+                                       }{/if});
+                               {/if}
+                       {/if}
+               });
+               //]]>
+       </script>
+{/if}
+
+<header class="box48 boxHeadline">
+       <hgroup>
+               <h1>{@$objectType->getProcessor()->getLanguageVariable('list')}</h1>
+       </hgroup>
+</header>
+
+{hascontent}
+       <div class="contentNavigation">
+               <nav>
+                       <ul>
+                               {content}
+                                       {if $objectType->getProcessor()->canAddCategory()}
+                                               <li><a href="{link controller=$addController}{/link}" title="{$objectType->getProcessor()->getLanguageVariable('add')}" class="button"><img src="{@$__wcf->getPath()}icon/add.svg" alt="" class="icon24" /> <span>{@$objectType->getProcessor()->getLanguageVariable('add')}</span></a></li>
+                                       {/if}
+                                       
+                                       {event name='contentNavigationButtons'}
+                               {/content}
+                       </ul>
+               </nav>
+       </div>
+{/hascontent}
+
+{if $categoryNodeList|count}
+       <section id="categoryList" class="container containerPadding marginTop{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}
+                               {section name=i loop=$oldDepth-$categoryNodeList->getDepth()}</ol></li>{/section}
+                               
+                               <li class="{if $objectType->getProcessor()->canEditCategory() && $categoryNodeList|count > 1}sortableNode {if $categoryNodeList->getDepth() == $objectType->getProcessor()->getMaximumNestingLevel()}sortableNoNesting {/if}{/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="sortableNodeLabel">
+                                               <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="{@$objectType->getProcessor()->getLanguageVariable('delete.sure')}" />
+                                                       {else}
+                                                               <img src="{@$__wcf->getPath()}icon/delete.svg" alt="" title="{lang}wcf.global.button.delete{/lang}" class="icon16 disabled" />
+                                                       {/if}
+
+                                                       {if $objectType->getProcessor()->canEditCategory()}
+                                                               {* todo: toggle icons aren't clickable *}
+                                                               <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>
+                                       </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="{$objectType->getProcessor()->getLanguageVariable('add')}" class="button"><img src="{@$__wcf->getPath()}icon/add.svg" alt="" class="icon24" /> <span>{@$objectType->getProcessor()->getLanguageVariable('add')}</span></a></li>
+                                               {/if}
+
+                                               {event name='contentNavigationButtons'}
+                                       {/content}
+                               </ul>
+                       </nav>
+               </div>
+       {/hascontent}
+{else}
+       <p class="info">{@$objectType->getProcessor()->getLanguageVariable('noneAvailable')}</p>
+{/if}
+
+{include file='footer'}
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WCF.ACL.js b/wcfsetup/install/files/js/WCF.ACL.js
new file mode 100644 (file)
index 0000000..078ae0a
--- /dev/null
@@ -0,0 +1,574 @@
+/**
+ * Namespace for ACL
+ */
+WCF.ACL = {};
+
+/**
+ * ACL support for WCF
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+WCF.ACL.List = Class.extend({
+       /**
+        * name of the category the acl options belong to
+        * @var string
+        */
+       _categoryName: '',
+       
+       /**
+        * ACL container
+        * @var jQuery
+        */
+       _container: null,
+       
+       /**
+        * list of ACL container elements
+        * @var object
+        */
+       _containerElements: { },
+       
+       /**
+        * object id
+        * @var integer
+        */
+       _objectID: 0,
+       
+       /**
+        * object type id
+        * @var integer
+        */
+       _objectTypeID: null,
+       
+       /**
+        * list of available ACL options
+        * @var object
+        */
+       _options: { },
+       
+       /**
+        * action proxy
+        * @var WCF.Action.Proxy
+        */
+       _proxy: null,
+       
+       /**
+        * user search handler
+        * @var WCF.Search.User
+        */
+       _search: null,
+       
+       /**
+        * list of ACL settings
+        * @var object
+        */
+       _values: {
+               group: { },
+               user: { }
+       },
+       
+       /**
+        * Initializes the ACL configuration.
+        * 
+        * @param       string          containerSelector
+        * @param       integer         objectTypeID
+        * @param       string          categoryName
+        * @param       integer         objectID
+        * @param       boolean         includeUserGroups
+        */
+       init: function(containerSelector, objectTypeID, categoryName, objectID, includeUserGroups, initialPermissions) {
+               this._objectID = objectID;
+               this._objectTypeID = objectTypeID;
+               this._categoryName = categoryName;
+               if (includeUserGroups === undefined) {
+                       includeUserGroups = true;
+               }
+               
+               this._proxy = new WCF.Action.Proxy({
+                       showLoadingOverlay: false,
+                       success: $.proxy(this._success, this)
+               });
+               
+               // bind hidden container
+               this._container = $(containerSelector).hide().addClass('aclContainer');
+               
+               // insert container elements
+               var $elementContainer = this._container.children('dd');
+               var $aclList = $('<ul class="aclList container" />').appendTo($elementContainer);
+               var $searchInput = $('<input type="text" class="long" />').appendTo($elementContainer);
+               var $permissionList = $('<ul class="aclPermissionList container" />').hide().appendTo($elementContainer);
+               
+               // set elements
+               this._containerElements = {
+                       aclList: $aclList,
+                       denyAll: null,
+                       grantAll: null,
+                       permissionList: $permissionList,
+                       searchInput: $searchInput
+               };
+               
+               // prepare search input
+               this._search = new WCF.Search.User($searchInput, $.proxy(this.addObject, this), includeUserGroups);
+               
+               // bind event listener for submit
+               var $form = this._container.parents('form:eq(0)');
+               $form.submit($.proxy(this.submit, this));
+               
+               // reset ACL on click
+               var $resetButton = $form.find('input[type=reset]:eq(0)');
+               if ($resetButton.length) {
+                       $resetButton.click($.proxy(this._reset, this));
+               }
+               
+               if (initialPermissions) {
+                       this._success(initialPermissions);
+               }
+               else {
+                       this._loadACL();
+               }
+       },
+       
+       /**
+        * Restores the original ACL state.
+        */
+       _reset: function() {
+               // reset stored values
+               this._values = {
+                       group: { },
+                       user: { }
+               };
+               
+               // remove entries
+               this._containerElements.aclList.empty();
+               this._containerElements.searchInput.val('');
+               
+               // deselect all input elements
+               this._containerElements.permissionList.hide().find('input[type=checkbox]').removeAttr('checked');
+       },
+       
+       /**
+        * Loads current ACL configuration.
+        */
+       _loadACL: function() {
+               this._proxy.setOption('data',  {
+                       actionName: 'loadAll',
+                       className: 'wcf\\data\\acl\\option\\ACLOptionAction',
+                       parameters: {
+                               data: {
+                                       categoryName: this._categoryName,
+                                       objectID: this._objectID,
+                                       objectTypeID: this._objectTypeID
+                               }
+                       }
+               });
+               this._proxy.sendRequest();
+       },
+       
+       /**
+        * Adds a new object to acl list.
+        * 
+        * @param       object          data
+        */
+       addObject: function(data) {
+               var $listItem = $('<li><img src="' + WCF.Icon.get('wcf.icon.user' + ((data.type == 'group') ? 's' : '')) + '" alt="" class="icon16" /> <span>' + data.label + '</span></li>').appendTo(this._containerElements.aclList);
+               $listItem.data('objectID', data.objectID).data('type', data.type).click($.proxy(this._click, this));
+               $('<img src="' + WCF.Icon.get('wcf.icon.delete') + '" alt="" title="' + WCF.Language.get('wcf.global.button.delete') + '" class="icon16 jsTooltip" />').click($.proxy(this._removeItem, this)).appendTo($listItem);
+               
+               // toggle element
+               this._savePermissions();
+               this._containerElements.aclList.children('li').removeClass('active');
+               $listItem.addClass('active');
+               
+               this._search.addExcludedSearchValue(data.label);
+               
+               // uncheck all option values
+               this._containerElements.permissionList.find('input[type=checkbox]').removeAttr('checked');
+               
+               // clear search input
+               this._containerElements.searchInput.val('');
+               
+               // show permissions
+               this._containerElements.permissionList.show();
+       },
+       
+       /**
+        * Removes an item from list.
+        * 
+        * @param       object          event
+        */
+       _removeItem: function(event) {
+               var $listItem = $(event.currentTarget).parent();
+               var $type = $listItem.data('type');
+               var $objectID = $listItem.data('objectID');
+               
+               this._search.removeExcludedSearchValue($listItem.children('span:eq(0)').text());
+               $listItem.remove();
+               
+               // remove stored data
+               if (this._values[$type][$objectID]) {
+                       delete this._values[$type][$objectID];
+               }
+               
+               // try to select something else
+               this._selectFirstEntry();
+       },
+       
+       /**
+        * Selects the first available entry.
+        */
+       _selectFirstEntry: function() {
+               var $listItem = this._containerElements.aclList.children('li:eq(0)');
+               if ($listItem.length) {
+                       this._select($listItem, false);
+               }
+               else {
+                       this._reset();
+               }
+       },
+       
+       /**
+        * Parses current ACL configuration.
+        * 
+        * @param       object          data
+        * @param       string          textStatus
+        * @param       jQuery          jqXHR
+        */
+       _success: function(data, textStatus, jqXHR) {
+               if (!$.getLength(data.returnValues.options)) {
+                       return;
+               }
+               
+               // prepare options
+               var $count = 0;
+               var $structure = { };
+               for (var $optionID in data.returnValues.options) {
+                       var $option = data.returnValues.options[$optionID];
+                       
+                       var $listItem = $('<li><span>' + $option.label +  '</span></li>').data('optionID', $optionID).data('optionName', $option.optionName);
+                       var $grantPermission = $('<input type="checkbox" id="grant' + $optionID + '" />').appendTo($listItem).wrap('<label for="grant' + $optionID + '" />');
+                       var $denyPermission = $('<input type="checkbox" id="deny' + $optionID + '" />').appendTo($listItem).wrap('<label for="deny' + $optionID + '" />');
+                       
+                       $grantPermission.data('type', 'grant').data('optionID', $optionID).change($.proxy(this._change, this));
+                       $denyPermission.data('type', 'deny').data('optionID', $optionID).change($.proxy(this._change, this));
+                       
+                       if (!$structure[$option.categoryName]) {
+                               $structure[$option.categoryName] = [ ];
+                       }
+                       
+                       if ($option.categoryName === '') {
+                               $listItem.appendTo(this._containerElements.permissionList);
+                       }
+                       else {
+                               $structure[$option.categoryName].push($listItem);
+                       }
+                       
+                       $count++;
+               }
+               
+               // add a "full access" permission if there are more than one option
+               if ($count > 1) {
+                       var $listItem = $('<li class="aclFullAccess"><span>' + WCF.Language.get('wcf.acl.option.fullAccess') + '</span></li>').prependTo(this._containerElements.permissionList);
+                       this._containerElements.grantAll = $('<input type="checkbox" id="grantAll" />').appendTo($listItem).wrap('<label for="grantAll" />');
+                       this._containerElements.denyAll = $('<input type="checkbox" id="denyAll" />').appendTo($listItem).wrap('<label for="denyAll" />');
+                       
+                       // bind events
+                       this._containerElements.grantAll.data('type', 'grant').change($.proxy(this._changeAll, this));
+                       this._containerElements.denyAll.data('type', 'deny').change($.proxy(this._changeAll, this));
+               }
+               
+               if ($.getLength($structure)) {
+                       for (var $categoryName in $structure) {
+                               var $listItems = $structure[$categoryName];
+                               
+                               if (data.returnValues.categories[$categoryName]) {
+                                       $('<li class="aclCategory">' + data.returnValues.categories[$categoryName] + '</li>').appendTo(this._containerElements.permissionList);
+                               }
+                               
+                               for (var $i = 0, $length = $listItems.length; $i < $length; $i++) {
+                                       $listItems[$i].appendTo(this._containerElements.permissionList);
+                               }
+                       }
+               }
+               
+               // set data
+               this._parseData(data, 'group');
+               this._parseData(data, 'user');
+               
+               // show container
+               this._container.show();
+               
+               // pre-select an entry
+               this._selectFirstEntry();
+       },
+       
+       /**
+        * Parses user and group data.
+        * 
+        * @param       object          data
+        * @param       string          type
+        */
+       _parseData: function(data, type) {
+               if (!$.getLength(data.returnValues[type].option)) {
+                       return;
+               }
+               
+               // add list items
+               for (var $typeID in data.returnValues[type].label) {
+                       this.addObject({
+                               label: data.returnValues[type].label[$typeID],
+                               objectID: $typeID,
+                               type: type
+                       });
+                       
+                       this._search.addExcludedSearchValue(data.returnValues[type].label[$typeID]);
+               }
+               
+               // add options
+               this._values[type] = data.returnValues[type].option;
+       },
+       
+       /**
+        * Prepares permission list for a specific object.
+        * 
+        * @param       object          event
+        */
+       _click: function(event) {
+               var $listItem  = $(event.currentTarget);
+               if ($listItem.hasClass('active')) {
+                       return;
+               }
+               
+               this._select($listItem, true);
+       },
+       
+       /**
+        * Selects the given item and marks it as active.
+        * 
+        * @param       jQuery          listItem
+        * @param       boolean         savePermissions
+        */
+       _select: function(listItem, savePermissions) {
+               // save previous permissions
+               if (savePermissions) {
+                       this._savePermissions();
+               }
+               
+               // switch active item
+               this._containerElements.aclList.children('li').removeClass('active');
+               listItem.addClass('active');
+               
+               // apply permissions for current item
+               this._setupPermissions(listItem.data('type'), listItem.data('objectID'));
+       },
+       
+       /**
+        * Toggles between deny and grant.
+        * 
+        * @param       object          event
+        */
+       _change: function(event) {
+               var $checkbox = $(event.currentTarget);
+               var $optionID = $checkbox.data('optionID');
+               var $type = $checkbox.data('type');
+               
+               if ($checkbox.is(':checked')) {
+                       if ($type === 'deny') {
+                               $('#grant' + $optionID).removeAttr('checked');
+                               
+                               if (this._containerElements.grantAll !== null) {
+                                       this._containerElements.grantAll.removeAttr('checked');
+                               }
+                       }
+                       else {
+                               $('#deny' + $optionID).removeAttr('checked');
+                               
+                               if (this._containerElements.denyAll !== null) {
+                                       this._containerElements.denyAll.removeAttr('checked');
+                               }
+                       }
+               }
+               else {
+                       if ($type === 'deny' && this._containerElements.denyAll !== null) {
+                               this._containerElements.denyAll.removeAttr('checked');
+                       }
+                       else if ($type === 'grant' && this._containerElements.grantAll !== null) {
+                               this._containerElements.grantAll.removeAttr('checked');
+                       }
+               }
+               
+               var $allChecked = true;
+               this._containerElements.permissionList.find('input[type=checkbox]').each(function(index, item) {
+                       var $item = $(item);
+                       
+                       if ($item.data('type') === $type && $item.attr('id') !== $type + 'All') {
+                               if (!$item.is(':checked')) {
+                                       $allChecked = false;
+                                       return false;
+                               }
+                       }
+               });
+               if ($type == 'deny') {
+                       if (this._containerElements.denyAll !== null) {
+                               if ($allChecked) this._containerElements.denyAll.prop('checked', 'checked')
+                               else this._containerElements.denyAll.removeAttr('checked');
+                       }
+               }
+               else {
+                       if (this._containerElements.grantAll !== null) {
+                               if ($allChecked) this._containerElements.grantAll.prop('checked', 'checked')
+                               else this._containerElements.grantAll.removeAttr('checked');
+                       }
+               }
+       },
+       
+       /**
+        * Toggles all options between deny and grant.
+        * 
+        * @param       object          event
+        */
+       _changeAll: function(event) {
+               var $checkbox = $(event.currentTarget);
+               var $type = $checkbox.data('type');
+               
+               if ($checkbox.is(':checked')) {
+                       if ($type === 'deny') {
+                               this._containerElements.grantAll.removeAttr('checked');
+                               
+                               this._containerElements.permissionList.find('input[type=checkbox]').each(function(index, item) {
+                                       var $item = $(item);
+                                       
+                                       if ($item.data('type') === 'deny' && $item.attr('id') !== 'denyAll') {
+                                               $item.prop('checked', 'checked').trigger('change');
+                                       }
+                               });
+                       }
+                       else {
+                               this._containerElements.denyAll.removeAttr('checked');
+                               
+                               this._containerElements.permissionList.find('input[type=checkbox]').each(function(index, item) {
+                                       var $item = $(item);
+                                       
+                                       if ($item.data('type') === 'grant' && $item.attr('id') !== 'grantAll') {
+                                               $item.prop('checked', 'checked').trigger('change');
+                                       }
+                               });
+                       }
+               }
+               else {
+                       if ($type === 'deny') {
+                               this._containerElements.grantAll.removeAttr('checked');
+                               
+                               this._containerElements.permissionList.find('input[type=checkbox]').each(function(index, item) {
+                                       var $item = $(item);
+                                       
+                                       if ($item.data('type') === 'deny' && $item.attr('id') !== 'denyAll') {
+                                               $item.removeAttr('checked').trigger('change');
+                                       }
+                               });
+                       }
+                       else {
+                               this._containerElements.denyAll.removeAttr('checked');
+                               
+                               this._containerElements.permissionList.find('input[type=checkbox]').each(function(index, item) {
+                                       var $item = $(item);
+                                       
+                                       if ($item.data('type') === 'grant' && $item.attr('id') !== 'grantAll') {
+                                               $item.removeAttr('checked').trigger('change');
+                                       }
+                               });
+                       }
+               }
+       },
+       
+       /**
+        * Setups permission input for given object.
+        * 
+        * @param       string          type
+        * @param       integer         objectID
+        */
+       _setupPermissions: function(type, objectID) {
+               // reset all checkboxes to unchecked
+               this._containerElements.permissionList.find("input[type='checkbox']").removeAttr('checked');
+               
+               // use stored permissions if applicable
+               if (this._values[type] && this._values[type][objectID]) {
+                       for (var $optionID in this._values[type][objectID]) {
+                               if (this._values[type][objectID][$optionID] == 1) {
+                                       $('#grant' + $optionID).attr('checked', 'checked').trigger('change');
+                               }
+                               else {
+                                       $('#deny' + $optionID).attr('checked', 'checked').trigger('change');
+                               }
+                       }
+               }
+               
+               // show permissions
+               this._containerElements.permissionList.show();
+       },
+       
+       /**
+        * Saves currently set permissions.
+        */
+       _savePermissions: function() {
+               // get active object
+               var $activeObject = this._containerElements.aclList.find('li.active');
+               var $objectID = $activeObject.data('objectID');
+               var $type = $activeObject.data('type');
+               
+               var self = this;
+               this._containerElements.permissionList.find("input[type='checkbox']").each(function(index, checkbox) {
+                       var $checkbox = $(checkbox);
+                       if ($checkbox.attr('id') != 'grantAll' && $checkbox.attr('id') != 'denyAll') {
+                               var $optionValue = ($checkbox.data('type') === 'deny') ? 0 : 1;
+                               var $optionID = $checkbox.data('optionID');
+
+                               if ($checkbox.is(':checked')) {
+                                       if (!self._values[$type][$objectID]) {
+                                               self._values[$type][$objectID] = { };
+                                       }
+
+                                       // store value
+                                       self._values[$type][$objectID][$optionID] = $optionValue;
+
+                                       // reset value afterwards
+                                       $checkbox.removeAttr('checked');
+                               }
+                               else if (self._values[$type] && self._values[$type][$objectID] && self._values[$type][$objectID][$optionID] && self._values[$type][$objectID][$optionID] == $optionValue) {
+                                       delete self._values[$type][$objectID][$optionID];
+                               }
+                       }
+               });
+       },
+       
+       /**
+        * Prepares ACL values on submit.
+        * 
+        * @param       object          event
+        */
+       submit: function(event) {
+               this._savePermissions();
+               
+               this._save('group');
+               this._save('user');
+       },
+       
+       /**
+        * Inserts hidden form elements for each value.
+        *
+        * @param       string          $type
+        */
+       _save: function($type) {
+               if ($.getLength(this._values[$type])) {
+                       var $form = this._container.parents('form:eq(0)');
+                       
+                       for (var $objectID in this._values[$type]) {
+                               var $object = this._values[$type][$objectID];
+
+                               for (var $optionID in $object) {
+                                       $('<input type="hidden" name="aclValues[' + $type + '][' + $objectID + '][' + $optionID + ']" value="' + $object[$optionID] + '" />').appendTo($form);
+                               }
+                       }
+               }
+       }
+});
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..903221c
--- /dev/null
@@ -0,0 +1,348 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\category\CategoryAction;
+use wcf\data\category\CategoryEditor;
+use wcf\data\category\CategoryNodeList;
+use wcf\form\AbstractForm;
+use wcf\system\acl\ACLHandler;
+use wcf\system\category\CategoryHandler;
+use wcf\system\category\CategoryPermissionHandler;
+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\ArrayUtil;
+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 AbstractForm {
+       /**
+        * 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 = '';
+       
+       /**
+        * additional category data
+        * @var array
+        */
+       public $additionalData = array();
+       
+       /**
+        * 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::__run()
+        */
+       public function __run() {
+               $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::__run();
+       }
+       
+       /**
+        * @see wcf\page\IPage::assignVariables()
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               I18nHandler::getInstance()->assignVariables();
+               if ($this->aclObjectTypeID) {
+                       ACLHandler::getInstance()->assignVariables($this->aclObjectTypeID);
+               }
+               
+               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
+               ));
+       }
+       
+       /**
+        * 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();
+               }
+       }
+       
+       /**
+        * Reads the categories.
+        */
+       protected function readCategories() {
+               $this->categoryNodeList = new CategoryNodeList($this->objectType->objectType, 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();
+               
+               // get acl object type id
+               $aclObjectTypeName = $this->objectType->getProcessor()->getObjectTypeName('com.woltlab.wcf.acl');
+               if ($aclObjectTypeName) {
+                       $this->aclObjectTypeID = ACLHandler::getInstance()->getObjectTypeID($aclObjectTypeName);
+               }
+               
+               // autoset package id
+               if (!$this->packageID) {
+                       $this->packageID = $this->objectType->packageID;
+               }
+               
+               if ($this->objectType->getProcessor()->hasDescription()) {
+                       I18nHandler::getInstance()->register('description');
+               }
+               I18nHandler::getInstance()->register('title');
+               
+               $this->readCategories();
+               
+               parent::readData();
+       }
+       
+       /**
+        * @see wcf\page\IForm::readFormParameters()
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               I18nHandler::getInstance()->readValues();
+               
+               if (isset($_POST['additionalData'])) {
+                       $this->additionalData = ArrayUtil::trim($_POST['additionalData']);
+               }
+               if ($this->objectType->getProcessor()->hasDescription() && 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\IForm::save()
+        */
+       public function save() {
+               parent::save();
+               
+               $this->objectAction = new CategoryAction(array(), 'create', array(
+                       'data' => array(
+                               'additionalData' => serialize($this->additionalData),
+                               'description' => $this->description,
+                               'isDisabled' => $this->isDisabled,
+                               'objectTypeID' => $this->objectType->objectTypeID,
+                               'parentCategoryID' => $this->parentCategoryID,
+                               'showOrder' => $this->showOrder > 0 ? $this->showOrder : null,
+                               'title' => $this->title
+                       )
+               ));
+               $this->objectAction->executeAction();
+               $returnValues = $this->objectAction->getReturnValues();
+               
+               if (($this->objectType->getProcessor()->hasDescription() && !I18nHandler::getInstance()->isPlainValue('description')) || !I18nHandler::getInstance()->isPlainValue('title')) {
+                       $categoryID = $returnValues['returnValues']->categoryID;
+                       
+                       $updateData = array();
+                       if ($this->objectType->getProcessor()->hasDescription() && !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);
+                       ACLHandler::getInstance()->disableAssignVariables();
+                       CategoryPermissionHandler::getInstance()->resetCache();
+               }
+               
+               // 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();
+               
+               if (!I18nHandler::getInstance()->validateValue('title')) {
+                       throw new UserInputException('title');
+               }
+               
+               if ($this->objectType->getProcessor()->hasDescription() && !I18nHandler::getInstance()->validateValue('description', false, !$this->objectType->getProcessor()->forceDescription())) {
+                       throw new UserInputException('description');
+               }
+       }
+       
+       /**
+        * Validates the parent category.
+        */
+       protected function validateParentCategory() {
+               if ($this->parentCategoryID) {
+                       if (!$this->objectType->getProcessor()->getMaximumNestingLevel()) {
+                               $this->parentCategoryID = 0;
+                               return;
+                       }
+                       
+                       if (CategoryHandler::getInstance()->getCategory($this->parentCategoryID) === null) {
+                               throw new UserInputException('parentCategoryID', 'invalid');
+                       }
+                       
+                       if ($this->objectType->getProcessor()->getMaximumNestingLevel() != -1) {
+                               foreach ($this->categoryNodeList as $category) {
+                                       if ($category->categoryID == $this->parentCategoryID) {
+                                               if ($this->categoryNodeList->getDepth() > $this->objectType->getProcessor()->getMaximumNestingLevel() - 1) {
+                                                       throw new UserInputException('parentCategoryID', 'invalid');
+                                               }
+
+                                               break;
+                                       }
+                               }
+                       }
+               }
+       }
+}
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..37f5652
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\category\Category;
+use wcf\data\category\CategoryAction;
+use wcf\data\category\CategoryNodeList;
+use wcf\form\AbstractForm;
+use wcf\system\acl\ACLHandler;
+use wcf\system\category\CategoryHandler;
+use wcf\system\category\CategoryPermissionHandler;
+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->objectType, 0, true, array($this->category->categoryID));
+       }
+       
+       /**
+        * @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)) {
+                       if ($this->objectType->getProcessor()->hasDescription()) {
+                               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->additionalData = $this->category->additionalData;
+                       $this->isDisabled = $this->category->isDisabled;
+                       $this->parentCategoryID = $this->category->parentCategoryID;
+                       $this->showOrder = $this->category->showOrder;
+               }
+       }
+       
+       /**
+        * @see wcf\form\IForm::save()
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               // handle description
+               if ($this->objectType->getProcessor()->hasDescription()) {
+                       $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(
+                               'additionalData' => serialize($this->additionalData),
+                               '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);
+                       CategoryPermissionHandler::getInstance()->resetCache();
+               }
+               
+               // 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..08ff917
--- /dev/null
@@ -0,0 +1,155 @@
+<?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 {
+       /**
+        * 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::__run()
+        */
+       public function __run() {
+               $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::__run();
+       }
+       
+       /**
+        * @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->objectType, 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();
+               
+               // note that the implementation of wcf\system\category\ICategoryType
+               // needs to support a object type of the pseudo definition
+               // 'com.woltlab.wcf.collapsibleContent.acp' which has to be registered
+               // during package installation as a 'com.woltlab.wcf.collapsibleContent'
+               // object type if you want to support collapsible categories in the
+               // acp; the pseudo object type is used to distinguish between
+               // collapsible categories in the frontend and the acp
+               $collapsibleObjectTypeName = $this->objectType->getProcessor()->getObjectTypeName('com.woltlab.wcf.collapsibleContent.acp');
+               if ($collapsibleObjectTypeName) {
+                       $this->collapsibleObjectTypeID = UserCollapsibleContentHandler::getInstance()->getObjectTypeID($collapsibleObjectTypeName);
+                       // get ids of collapsed category
+                       if ($this->collapsibleObjectTypeID !== null) {
+                               $this->collapsedCategoryIDs = UserCollapsibleContentHandler::getInstance()->getCollapsedContent($this->collapsibleObjectTypeID);
+                               $this->collapsedCategoryIDs = array_flip($this->collapsedCategoryIDs);
+                       }
+               }
+               
+               parent::readData();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/ACLOption.class.php b/wcfsetup/install/files/lib/data/acl/option/ACLOption.class.php
new file mode 100644 (file)
index 0000000..5e18526
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+namespace wcf\data\acl\option;
+use wcf\data\DatabaseObject;
+
+/**
+ * Represents an acl option.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option
+ * @category   Community Framework
+ */
+class ACLOption extends DatabaseObject {
+       /**
+        * @see wcf\data\DatabaseObject::$databaseTableName
+        */
+       protected static $databaseTableName = 'acl_option';
+       
+       /**
+        * @see wcf\data\DatabaseObject::$databaseTableIndexName
+        */
+       protected static $databaseTableIndexName = 'optionID';
+       
+       /**
+        * Returns a list of options by object type id.
+        * 
+        * @param       integer         $objectTypeID
+        * @return      wcf\data\acl\option\ACLOptionList
+        */
+       public static function getOptions($objectTypeID) {
+               $optionList = new ACLOptionList();
+               $optionList->getConditionBuilder()->add("acl_option.objectTypeID = ?", array($objectTypeID));
+               $optionList->sqlLimit = 0;
+               $optionList->readObjects();
+               
+               return $optionList;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/ACLOptionAction.class.php b/wcfsetup/install/files/lib/data/acl/option/ACLOptionAction.class.php
new file mode 100644 (file)
index 0000000..b771e65
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace wcf\data\acl\option;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\exception\UserInputException;
+use wcf\system\acl\ACLHandler;
+
+/**
+ * Executes acl option-related actions.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option
+ * @category   Community Framework
+ */
+class ACLOptionAction extends AbstractDatabaseObjectAction {
+       /**
+        * @see wcf\data\AbstractDatabaseObjectAction::$className
+        */
+       protected $className = 'wcf\data\acl\option\ACLOptionEditor';
+       
+       /**
+        * Validates parameters for ACL options.
+        */
+       public function validateLoadAll() {
+               if (!isset($this->parameters['data']['objectTypeID'])) {
+                       throw new UserInputException('objectTypeID');
+               }
+       }
+       
+       /**
+        * Returns a set of permissions and their values if applicable.
+        * 
+        * @return      array
+        */
+       public function loadAll() {
+               $objectIDs = (isset($this->parameters['data']['objectID'])) ? array($this->parameters['data']['objectID']) : array();
+               $permissions = ACLHandler::getInstance()->getPermissions($this->parameters['data']['objectTypeID'], $objectIDs, null, true, true);
+               
+               return $permissions;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/ACLOptionEditor.class.php b/wcfsetup/install/files/lib/data/acl/option/ACLOptionEditor.class.php
new file mode 100644 (file)
index 0000000..f973663
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\acl\option;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit acl options.
+ *
+ * @author     Marcel Werk
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option
+ * @category   Community Framework
+ */
+class ACLOptionEditor extends DatabaseObjectEditor {
+       /**
+        * @see wcf\data\DatabaseObjectDecorator::$baseClass
+        */
+       public static $baseClass = 'wcf\data\acl\option\ACLOption';
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/ACLOptionList.class.php b/wcfsetup/install/files/lib/data/acl/option/ACLOptionList.class.php
new file mode 100644 (file)
index 0000000..7973413
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\acl\option;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of acl options.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option
+ * @category   Community Framework
+ */
+class ACLOptionList extends DatabaseObjectList {
+       /**
+        * @see wcf\data\DatabaseObjectList::$className
+        */
+       public $className = 'wcf\data\acl\option\ACLOption';
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategory.class.php b/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategory.class.php
new file mode 100644 (file)
index 0000000..061b57e
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+namespace wcf\data\acl\option\category;
+use wcf\data\DatabaseObject;
+
+/**
+ * Represents an acl option category.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option.category
+ * @category   Community Framework
+ */
+class ACLOptionCategory extends DatabaseObject {
+       /**
+        * @see wcf\data\DatabaseObject::$databaseTableName
+        */
+       protected static $databaseTableName = 'acl_option_category';
+       
+       /**
+        * @see wcf\data\DatabaseObject::$databaseTableIndexName
+        */
+       protected static $databaseTableIndexName = 'categoryID';
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryAction.class.php b/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryAction.class.php
new file mode 100644 (file)
index 0000000..d3a2713
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\acl\option\category;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes acl option category-related actions.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option.category
+ * @category   Community Framework
+ */
+class ACLOptionCategoryAction extends AbstractDatabaseObjectAction {
+       /**
+        * @see wcf\data\AbstractDatabaseObjectAction::$className
+        */
+       protected $className = 'wcf\data\acl\option\category\ACLOptionCategoryEditor';
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryEditor.class.php b/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryEditor.class.php
new file mode 100644 (file)
index 0000000..e9f9533
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\acl\option\category;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit acl option categories.
+ *
+ * @author     Alexander Ebert
+ * @copyright  2001-2012 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option.category
+ * @category   Community Framework
+ */
+class ACLOptionCategoryEditor extends DatabaseObjectEditor {
+       /**
+        * @see wcf\data\DatabaseObjectDecorator::$baseClass
+        */
+       public static $baseClass = 'wcf\data\acl\option\category\ACLOptionCategory';
+}
diff --git a/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryList.class.php b/wcfsetup/install/files/lib/data/acl/option/category/ACLOptionCategoryList.class.php
new file mode 100644 (file)
index 0000000..f6e7c40
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\acl\option\category;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of acl option categories.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.acl.option.category
+ * @category   Community Framework
+ */
+class ACLOptionCategoryList extends DatabaseObjectList {
+       /**
+        * @see wcf\data\DatabaseObjectList::$className
+        */
+       public $className = 'wcf\data\acl\option\category\ACLOptionCategory';
+}
diff --git a/wcfsetup/install/files/lib/data/category/ViewableCategory.class.php b/wcfsetup/install/files/lib/data/category/ViewableCategory.class.php
new file mode 100644 (file)
index 0000000..c0aad1b
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+namespace wcf\data\category;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\system\category\CategoryPermissionHandler;
+use wcf\system\exception\PermissionDeniedException;
+
+/**
+ * Represents a viewable 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 ViewableCategory extends DatabaseObjectDecorator {
+       /**
+        * @see wcf\data\DatabaseObjectDecorator::$baseClass
+        */
+       protected static $baseClass = 'wcf\data\category\Category';
+       
+       /**
+        * acl permissions for the active user of this category
+        * @var array<boolean>
+        */
+       protected $permissions = null;
+       
+       /**
+        * Checks if the active user has all given permissions and throws a 
+        * PermissionDeniedException if that isn't the case.
+        * 
+        * @param       array<string>           $permissions
+        */
+       public function checkPermissions(array $permissions) {
+               foreach ($permissions as $permission) {
+                       if (!$this->getPermission($permission)) {
+                               throw new PermissionDeniedException();
+                       }
+               }
+       }
+       
+       /**
+        * Returns the acl permission value of the given permission for the active
+        * user and of this category.
+        * 
+        * @param       string          $permission
+        * @return      boolean
+        */
+       public function getPermission($permission) {
+               if ($this->permissions === null) {
+                       $this->permissions = CategoryPermissionHandler::getInstance()->getPermissions($this->getDecoratedObject());
+               }
+               
+               if (isset($this->permissions[$permission])) {
+                       return $this->permissions[$permission];
+               }
+               
+               return false;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/category/ViewableCategoryNode.class.php b/wcfsetup/install/files/lib/data/category/ViewableCategoryNode.class.php
new file mode 100644 (file)
index 0000000..a4fbbc9
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+namespace wcf\data\category;
+
+/**
+ * Represents a viewable 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 ViewableCategoryNode extends CategoryNode {
+       /**
+        * @see wcf\data\DatabaseObjectDecorator::__construct()
+        */
+       public function __construct(DatabaseObject $object, $includeDisabledCategories = false, array $excludedCategoryIDs = array()) {
+               parent::__construct(new ViewableCategory($object), $includeDisabledCategories, $excludedCategoryIDs);
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/category/ViewableCategoryNodeList.class.php b/wcfsetup/install/files/lib/data/category/ViewableCategoryNodeList.class.php
new file mode 100644 (file)
index 0000000..9c389ac
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+namespace wcf\data\category;
+
+/**
+ * Represents a list of viewable category nodes.
+ *
+ * @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 ViewableCategoryNodeList extends CategoryNodeList {
+       /**
+        * @see wcf\data\category\CategoryNodeList::$nodeClassName
+        */
+       protected $nodeClassName = 'wcf\data\category\ViewableCategoryNode';
+}
diff --git a/wcfsetup/install/files/lib/system/acl/ACLHandler.class.php b/wcfsetup/install/files/lib/system/acl/ACLHandler.class.php
new file mode 100644 (file)
index 0000000..729cc43
--- /dev/null
@@ -0,0 +1,413 @@
+<?php
+namespace wcf\system\acl;
+use wcf\data\acl\option\category\ACLOptionCategory;
+use wcf\data\acl\option\category\ACLOptionCategoryList;
+use wcf\data\acl\option\ACLOption;
+use wcf\data\acl\option\ACLOptionList;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\user\User;
+use wcf\system\cache\CacheHandler;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\SystemException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Handles ACL permissions.
+ *
+ * @author     Alexander Ebert
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.acl
+ * @category   Community Framework
+ */
+class ACLHandler extends SingletonFactory {
+       /**
+        * indicates if assignment of variables is disabled
+        * @var integer
+        */
+       protected $assignVariablesDisabled = false;
+       
+       /**
+        * list of available object types
+        * @var array
+        */
+       protected $availableObjectTypes = array();
+       
+       /**
+        * list of acl option categories sorted by their object type id and name
+        * @var array<array>
+        */
+       protected $categories = array();
+       
+       /**
+        * Assignes the acl values to the template.
+        * 
+        * @param       integer         $objectTypeID
+        */
+       public function assignVariables($objectTypeID) {
+               if (WCF::getTPL()->get('aclValues') === null) {
+                       WCF::getTPL()->assign('aclValues', array());
+               }
+               
+               if (!$this->assignVariablesDisabled && isset($_POST['aclValues'])) {
+                       $values = $_POST['aclValues'];
+                       
+                       $data = $this->getPermissions($objectTypeID, array(), null, true, true);
+                       
+                       foreach ($values as $type => $optionData) {
+                               if ($type === 'user') {
+                                       $users = User::getUsers(array_keys($optionData));
+                               }
+                               
+                               $values[$type] = array(
+                                       'label' => array(),
+                                       'option' => array()
+                               );
+                               
+                               foreach ($optionData as $typeID => $optionValues) {
+                                       foreach ($optionValues as $optionID => $optionValue) {
+                                               if (!isset($data['options'][$optionID])) {
+                                                       unset($optionValues[$optionID]);
+                                               }
+                                       }
+                                       
+                                       if (empty($optionValues)) {
+                                               continue;
+                                       }
+                                       
+                                       $values[$type]['option'][$typeID] = $optionValues;
+                                       
+                                       if ($type === 'group') {
+                                               $values[$type]['label'][$typeID] = WCF::getLanguage()->get('wcf.acp.group.group'.$typeID);
+                                       }
+                                       else {
+                                               $values[$type]['label'][$typeID] = $users[$typeID]->username;
+                                       }
+                               }
+                       }
+                       
+                       $values['options'] = $data['options'];
+                       $values['categories'] = $data['categories'];
+                       
+                       WCF::getTPL()->append('aclValues', array(
+                               $objectTypeID => $values
+                       ));
+               }
+       }
+       
+       /**
+        * Disables assignment of variables to template.
+        */
+       public function disableAssignVariables() {
+               $this->assignVariablesDisabled = true;
+       }
+       
+       /**
+        * Enables assignment of variables to template.
+        */
+       public function enableAssignVariables() {
+               $this->assignVariablesDisabled = false;
+       }
+       
+       /**
+        * @see wcf\system\SingletonFactory::init()
+        */
+       protected function init() {
+               $this->availableObjectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.acl');
+               
+               CacheHandler::getInstance()->addResource(
+                       'aclOptionCategory',
+                       WCF_DIR.'cache/cache.aclOptionCategory.php',
+                       'wcf\system\cache\builder\ACLOptionCategoryCacheBuilder'
+               );              
+               $this->categories = CacheHandler::getInstance()->get('aclOptionCategory');
+       }
+       
+       /**
+        * Gets the object type id.
+        * 
+        * @param       string          $objectType
+        * @return      integer
+        */
+       public function getObjectTypeID($objectType) {
+               if (!isset($this->availableObjectTypes[$objectType])) {
+                       throw new SystemException("unknown object type '".$objectType."'");
+               }
+               
+               return $this->availableObjectTypes[$objectType]->objectTypeID;
+       }
+       
+       /**
+        * Returns the acl option category with the given object type id and name.
+        * 
+        * @param       integer         $objectTypeID
+        * @param       string          $categoryName
+        * @return      wcf\data\acl\option\category\ACLOptionCategory
+        */
+       public function getCategory($objectTypeID, $categoryName) {
+               if (isset($this->categories[$objectTypeID][$categoryName])) {
+                       return $this->categories[$objectTypeID][$categoryName];
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Saves acl for a given object.
+        * 
+        * @param       integer         $objectID
+        * @param       integer         $objectTypeID
+        */
+       public function save($objectID, $objectTypeID) {
+               // get options
+               $optionList = ACLOption::getOptions($objectTypeID);
+               
+               $this->replaceValues($optionList, 'group', $objectID);
+               $this->replaceValues($optionList, 'user', $objectID);
+       }
+       
+       /**
+        * Replaces values for given type and object.
+        * 
+        * @param       wcf\data\acl\option\ACLOptionList       $optionList
+        * @param       string                                  $type
+        * @param       integer                                 $objectID
+        */
+       protected function replaceValues(ACLOptionList $optionList, $type, $objectID) {
+               $options = $optionList->getObjects();
+               
+               // remove previous values
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("optionID IN (?)", array(array_keys($options)));
+               $conditions->add("objectID = ?", array($objectID));
+               
+               $sql = "DELETE FROM     wcf".WCF_N."_acl_option_to_".$type."
+                       ".$conditions;
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($conditions->getParameters());
+               
+               // add new values if given
+               if (!isset($_POST['aclValues']) || !isset($_POST['aclValues'][$type])) {
+                       return;
+               }
+               
+               $sql = "INSERT INTO     wcf".WCF_N."_acl_option_to_".$type."
+                                       (optionID, objectID, ".$type."ID, optionValue)
+                       VALUES          (?, ?, ?, ?)";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $values =& $_POST['aclValues'][$type];
+               
+               WCF::getDB()->beginTransaction();
+               foreach ($values as $typeID => $optionData) {
+                       foreach ($optionData as $optionID => $optionValue) {
+                               // ignore invalid option ids
+                               if (!isset($options[$optionID])) {
+                                       continue;
+                               }
+                               
+                               $statement->execute(array(
+                                       $optionID,
+                                       $objectID,
+                                       $typeID,
+                                       $optionValue
+                               ));
+                       }
+               }
+               WCF::getDB()->commitTransaction();
+       }
+       
+       /**
+        * Returns a list of permissions by object type id.
+        * 
+        * @param       integer                                         $objectTypeID
+        * @param       array                                           $objectIDs
+        * @param       wcf\data\acl\option\category\ACLOptionCategory  $category
+        * @param       boolean                                         $usePackageDependencies
+        * @param       boolean                                         $settingsView
+        * @return      array
+        */
+       public function getPermissions($objectTypeID, array $objectIDs, ACLOptionCategory $category = null, $usePackageDependencies = true, $settingsView = false) {
+               $optionList = $this->getOptions($objectTypeID, $category, $usePackageDependencies, $settingsView);
+               
+               $data = array(
+                       'options' => $optionList,
+                       'group' => array(),
+                       'user' => array()
+               );
+               
+               if (!empty($objectIDs)) {
+                       $this->getValues($optionList, 'group', $objectIDs, $data, $settingsView);
+                       $this->getValues($optionList, 'user', $objectIDs, $data, $settingsView);
+               }
+               
+               // use alternative data structure for settings
+               if ($settingsView) {
+                       $data['options'] = array();
+                       $data['categories'] = array();
+                       
+                       $categoryNames = array();
+                       foreach ($optionList as $option) {
+                               $data['options'][$option->optionID] = array(
+                                       'categoryName' => $option->categoryName,
+                                       'label' => WCF::getLanguage()->get('wcf.acl.option.' . $option->package . '.' . $option->optionName),
+                                       'optionName' => $option->optionName
+                               );
+                               
+                               if (!in_array($option->categoryName, $categoryNames)) {
+                                       $categoryNames[] = $option->categoryName;
+                               }
+                       }
+                       
+                       // load categories
+                       $categoryList = new ACLOptionCategoryList();
+                       $categoryList->sqlLimit = 0;
+                       $categoryList->sqlSelects = "package.package";
+                       $categoryList->sqlJoins = "LEFT JOIN wcf".WCF_N."_package package ON (package.packageID = acl_option_category.packageID)";
+                       $categoryList->getConditionBuilder()->add("acl_option_category.categoryName IN (?)", array($categoryNames));
+                       $categoryList->getConditionBuilder()->add("acl_option_category.objectTypeID = ?", array($objectTypeID));
+                       $categoryList->readObjects();
+                       
+                       foreach ($categoryList as $category) {
+                               $data['categories'][$category->categoryName] = WCF::getLanguage()->get('wcf.acl.option.category.'.$category->package.'.'.$category->categoryName);
+                       }
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * Fetches ACL option values by type.
+        * 
+        * @param       wcf\data\acl\option\ACLOptionList       $optionList
+        * @param       string                                  $type
+        * @param       array                                   $objectIDs
+        * @param       array                                   $data
+        * @param       boolean                                 $settingsView
+        */
+       protected function getValues(ACLOptionList $optionList, $type, array $objectIDs, array &$data, $settingsView) {
+               $data[$type] = array();
+               $optionsIDs = array();
+               foreach ($optionList as $option) {
+                       $optionsIDs[] = $option->optionID;
+               }
+               
+               $columnID = $type.'ID';
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("optionID IN (?)", array($optionsIDs));
+               $conditions->add("objectID IN (?)", array($objectIDs));
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_acl_option_to_".$type."
+                       ".$conditions;
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($conditions->getParameters());
+               while ($row = $statement->fetchArray()) {
+                       if (!isset($data[$type][$row['objectID']])) {
+                               $data[$type][$row['objectID']] = array();
+                       }
+                       
+                       if (!isset($data[$type][$row['objectID']][$row[$columnID]])) {
+                               $data[$type][$row['objectID']][$row[$columnID]] = array();
+                       }
+                       
+                       $data[$type][$row['objectID']][$row[$columnID]][$row['optionID']] = $row['optionValue'];
+               }
+               
+               // use alternative data structure for settings
+               if ($settingsView) {
+                       $objectID = current($objectIDs);
+                       if (!isset($data[$type][$objectID])) {
+                               $data[$type][$objectID] = array();
+                       }
+                       
+                       // build JS-compilant structure
+                       $data[$type] = array(
+                               'label' => array(),
+                               'option' => $data[$type][$objectID]
+                       );
+                       
+                       // load labels
+                       if (!empty($data[$type]['option'])) {
+                               $conditions = new PreparedStatementConditionBuilder();
+                               
+                               if ($type == 'group') {
+                                       $conditions->add("groupID IN (?)", array(array_keys($data[$type]['option'])));
+                                       $sql = "SELECT  groupID, groupName
+                                               FROM    wcf".WCF_N."_user_group
+                                               ".$conditions;
+                                       $statement = WCF::getDB()->prepareStatement($sql);
+                                       $statement->execute($conditions->getParameters());
+                                       
+                                       while ($row = $statement->fetchArray()) {
+                                               $data['group']['label'][$row['groupID']] = WCF::getLanguage()->get($row['groupName']);
+                                       }
+                               }
+                               else {
+                                       $conditions->add("userID IN (?)", array(array_keys($data[$type]['option'])));
+                                       $sql = "SELECT  userID, username
+                                               FROM    wcf".WCF_N."_user
+                                               ".$conditions;
+                                       $statement = WCF::getDB()->prepareStatement($sql);
+                                       $statement->execute($conditions->getParameters());
+                                       
+                                       while ($row = $statement->fetchArray()) {
+                                               $data['user']['label'][$row['userID']] = $row['username'];
+                                       }
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * Returns a list of options by object type id.
+        * 
+        * @param       integer                                         $objectTypeID
+        * @param       wcf\data\acl\option\category\ACLOptionCategory  $category
+        * @param       boolean                                         $usePackageDependencies
+        * @param       boolean                                         $settingsView
+        * @param       boolean                                         $settingsView
+        * @return      wcf\data\acl\option\ACLOptionList
+        */
+       public function getOptions($objectTypeID, ACLOptionCategory $category = null, $usePackageDependencies = true, $settingsView = false) {
+               $optionList = new ACLOptionList();
+               $optionList->sqlLimit = 0;
+               if ($category !== null) {
+                       $optionList->getConditionBuilder()->add("acl_option.categoryName = ?", array($category->categoryName));
+               }
+               if ($settingsView) {
+                       $optionList->sqlSelects = "package.package";
+                       $optionList->sqlJoins = "LEFT JOIN wcf".WCF_N."_package package ON (package.packageID = acl_option.packageID)";
+               }
+               $optionList->getConditionBuilder()->add("acl_option.objectTypeID = ?", array($objectTypeID));
+               $optionList->readObjects();
+               
+               return $optionList;
+       }
+       
+       /**
+        * Removes ACL values from database.
+        * 
+        * @param       integer                                         $objectTypeID
+        * @param       array<integer>                                  $objectIDs
+        * @param       wcf\data\acl\option\category\ACLOptionCategory  $category
+        * @param       boolean                                         $usePackageDependencies
+        */
+       public function removeValues($objectTypeID, array $objectIDs, ACLOptionCategory $category = null, $usePackageDependencies = true) {
+               $optionList = $this->getOptions($objectTypeID, $category, $usePackageDependencies);
+               $options = $optionList->getObjects();
+               
+               $conditions = new PreparedStatementConditionBuilder();
+               $conditions->add("optionID IN (?)", array(array_keys($options)));
+               $conditions->add("objectID IN (?)", array($objectIDs));
+               
+               WCF::getDB()->beginTransaction();
+               foreach (array('group', 'user') as $type) {
+                       $sql = "DELETE FROM     wcf".WCF_N."_acl_option_to_".$type."
+                               ".$conditions;
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute($conditions->getParameters());
+               }
+               WCF::getDB()->commitTransaction();
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/ACLOptionCategoryCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/ACLOptionCategoryCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..18144ea
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\acl\option\category\ACLOptionCategoryList;
+
+/**
+ * Caches the acl 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 ACLOptionCategoryCacheBuilder implements ICacheBuilder {
+       /**
+        * @see wcf\system\cache\ICacheBuilder::getData()
+        */
+       public function getData(array $cacheResource) {
+               $list = new ACLOptionCategoryList();
+               $list->sqlLimit = 0;
+               $list->readObjects();
+               
+               $data = array();
+               foreach ($list as $aclOptionCategory) {
+                       if (!isset($data[$aclOptionCategory->objectTypeID])) {
+                               $data[$aclOptionCategory->objectTypeID] = array();
+                       }
+                       
+                       $data[$aclOptionCategory->objectTypeID][$aclOptionCategory->categoryName] = $aclOptionCategory;
+               }
+               
+               return $data;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/CategoryACLOptionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/CategoryACLOptionCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..a65f2d6
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+namespace wcf\system\cache\builder;
+use wcf\system\category\CategoryHandler;
+use wcf\system\acl\ACLHandler;
+
+/**
+ * Caches the acl options 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 system.cache.builder
+ * @category   Community Framework
+ */
+class CategoryACLOptionCacheBuilder implements ICacheBuilder {
+       /**
+        * @see wcf\system\cache\ICacheBuilder::getData()
+        */
+       public function getData(array $cacheResource) {
+               $data = array();
+               foreach (CategoryHandler::getInstance()->getCategories() as $objectTypeName => $categories) {
+                       $objectType = CategoryHandler::getInstance()->getObjectTypeByName($objectTypeName);
+                       $aclObjectType = $objectType->getProcessor()->getObjectTypeName('com.woltlab.wcf.acl');
+                       if (!$aclObjectType) {
+                               continue;
+                       }
+                       
+                       $aclOptions = ACLHandler::getInstance()->getPermissions(ACLHandler::getInstance()->getObjectTypeID($aclObjectType), array_keys($categories));
+                       $options = $aclOptions['options']->getObjects();
+                       
+                       $data = array();
+                       foreach (array('group', 'user') as $type) {
+                               foreach ($aclOptions[$type] as $categoryID => $optionData) {
+                                       if (!isset($aclValues[$categoryID])) {
+                                               $data[$categoryID] = array(
+                                                       'group' => array(),
+                                                       'user' => array()
+                                               );
+                                       }
+
+                                       foreach ($optionData as $typeID => $optionValues) {
+                                               $data[$categoryID][$type][$typeID] = array();
+                                               
+                                               foreach ($optionValues as $optionID => $optionValue) {
+                                                       $data[$categoryID][$type][$typeID][$options[$optionID]->optionName] = $optionValue;
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
+               return $data;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/category/CategoryPermissionHandler.class.php b/wcfsetup/install/files/lib/system/category/CategoryPermissionHandler.class.php
new file mode 100644 (file)
index 0000000..61e3b49
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+namespace wcf\system\category;
+use wcf\data\category\Category;
+use wcf\data\user\User;
+use wcf\system\cache\CacheHandler;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Handles the category permissions.
+ *
+ * @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 CategoryPermissionHandler extends SingletonFactory {
+       /**
+        * cached category acl options
+        * @var array
+        */
+       protected $categoryPermissions = array();
+       
+       /**
+        * Returns the acl options for the given category and for the given user.
+        * If no user is given, the active user is used.
+        * 
+        * @param       wcf\data\category\Category      $category
+        * @param       wcf\data\user\User              $user
+        */
+       public function getPermissions(Category $category, User $user = null) {
+               if ($user === null) {
+                       $user = WCF::getUser();
+               }
+               
+               $permissions = array();
+               if (isset($this->categoryPermissions[$category->categoryID])) {
+                       if (isset($this->categoryPermissions[$category->categoryID]['group'])) {
+                               foreach ($user->getGroupIDs() as $groupID) {
+                                       if (isset($this->categoryPermissions[$category->categoryID]['group'][$groupID])) {
+                                               foreach ($this->categoryPermissions[$category->categoryID]['group'][$groupID] as $optionName => $optionValue) {
+                                                       if (isset($permissions[$optionName])) {
+                                                               $permissions[$optionName] = $permissions[$optionName] || $optionValue;
+                                                       }
+                                                       else {
+                                                               $permissions[$optionName] = $optionValue;
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       if (isset($this->categoryPermissions[$category->categoryID]['user']) && isset($this->categoryPermissions[$category->categoryID]['user'][$user->userID])) {
+                               foreach ($this->categoryPermissions[$category->categoryID]['user'][$user->userID] as $optionName => $optionValue) {
+                                       $permissions[$optionName] = $optionValue;
+                               }
+                       }
+               }
+               
+               return $permissions;
+       }
+       
+       /**
+        * @see wcf\system\SingletonFactory::init()
+        */
+       protected function init() {
+               CacheHandler::getInstance()->addResource(
+                       'categoryACLOption',
+                       WCF_DIR.'cache/cache.categoryACLOption.php',
+                       'wcf\system\cache\builder\CategoryACLOptionCacheBuilder'
+               );
+               $this->categoryPermissions = CacheHandler::getInstance()->get('categoryACLOption');
+       }
+       
+       /**
+        * Resets the category permission cache.
+        */
+       public function resetCache() {
+               CacheHandler::getInstance()->clearResource('categoryACLOption');
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/package/plugin/ACLOptionPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/ACLOptionPackageInstallationPlugin.class.php
new file mode 100644 (file)
index 0000000..34ac8bd
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+namespace wcf\system\package\plugin;
+use wcf\system\exception\SystemException;
+use wcf\system\WCF;
+
+/**
+ * This PIP installs, updates or deletes acl options.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2011 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.package.plugin
+ * @category   Community Framework
+ */
+class ACLOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin {
+       /**
+        * @see wcf\system\package\plugin\AbstractXMLPackageInstallationPlugin::$className
+        */
+       public $className = 'wcf\data\acl\option\ACLOptionEditor';
+       
+       /**
+        * list of loaded acl object type ids sorted by their option type name
+        * @var array<integer>
+        */
+       protected $optionTypeIDs = array();
+       
+       /**
+        * @see wcf\system\package\plugin\AbstractPackageInstallationPlugin::$tableName
+        */
+       public $tableName = 'acl_option';
+       
+       /**
+        * @see wcf\system\package\plugin\AbstractXMLPackageInstallationPlugin::$tagName
+        */     
+       public $tagName = 'option';
+       
+       /**
+        * @see wcf\system\package\plugin\AbstractOptionPackageInstallationPlugin::importCategories()
+        */
+       protected function importCategories(\DOMXPath $xpath) {
+               $elements = $xpath->query('/ns:data/ns:import/ns:categories/ns:category');
+               foreach ($elements as $element) {
+                       $data = array('categoryName' => $element->getAttribute('name'));
+                               
+                       // get child elements
+                       $children = $xpath->query('child::*', $element);
+                       foreach ($children as $child) {
+                               $data[$child->tagName] = $child->nodeValue;
+                       }
+
+                       $this->saveCategory($data);
+               }
+       }
+       
+       /**
+        * @see wcf\system\package\plugin\AbstractOptionPackageInstallationPlugin::saveCategory()
+        */
+       protected function saveCategory($category) {
+               $objectTypeID = $this->getObjectTypeID($category['objecttype']);
+               
+               // search existing category
+               $sql = "SELECT  categoryID
+                       FROM    wcf".WCF_N."_".$this->tableName."_category
+                       WHERE   categoryName = ?
+                               AND objectTypeID = ?
+                               AND packageID = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute(array(
+                       $category['categoryName'],
+                       $objectTypeID,
+                       $this->installation->getPackageID()
+               ));
+               $row = $statement->fetchArray();
+               if (!$row) {
+                       // insert new category
+                       $sql = "INSERT INTO     wcf".WCF_N."_".$this->tableName."_category
+                                               (packageID, objectTypeID, categoryName)
+                               VALUES          (?, ?, ?)";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute(array(
+                               $this->installation->getPackageID(),
+                               $objectTypeID,
+                               $category['categoryName']
+                       ));
+               }
+       }
+       
+       /**
+        * Imports options.
+        *
+        * @param       \DOMXPath       $xpath
+        */
+       protected function importOptions(\DOMXPath $xpath) {
+               $elements = $xpath->query('/ns:data/ns:import/ns:options/ns:option');
+               foreach ($elements as $element) {
+                       $data = array();
+                       $children = $xpath->query('child::*', $element);
+                       foreach ($children as $child) {
+                               $data[$child->tagName] = $child->nodeValue;
+                       }
+                       
+                       $objectTypeID = $this->getObjectTypeID($data['objecttype']);
+                       
+                       // validate category name
+                       if (isset($data['categoryname'])) {
+                               $sql = "SELECT  COUNT(categoryID) AS count
+                                       FROM    wcf".WCF_N."_".$this->tableName."_category
+                                       WHERE   categoryName = ?
+                                               AND objectTypeID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute(array(
+                                       $data['categoryname'],
+                                       $objectTypeID
+                               ));
+                               $row = $statement->fetchArray();
+                               if (!$row) {
+                                       throw new SystemException("unknown category '".$data['categoryname']."' for acl object type '".$data['objecttype']."' given");
+                               }
+                       }
+                       
+                       $data = array(
+                               'categoryName' => (isset($data['categoryname'])) ? $data['categoryname'] : '',
+                               'optionName' => $element->getAttribute('name'),
+                               'objectTypeID' => $objectTypeID
+                       );
+                       
+                       // check for option existence
+                       $sql = "SELECT  optionID
+                               FROM    wcf".WCF_N."_".$this->tableName."
+                               WHERE   optionName = ?
+                                       AND objectTypeID = ?
+                                       AND packageID = ?";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute(array(
+                               $data['optionName'],
+                               $data['objectTypeID'],
+                               $this->installation->getPackageID()
+                       ));
+                       $row = $statement->fetchArray();
+                       if (!$row) {
+                               $sql = "INSERT INTO     wcf".WCF_N."_".$this->tableName."
+                                                       (packageID, objectTypeID, optionName, categoryName)
+                                       VALUES          (?, ?, ?, ?)";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute(array(
+                                       $this->installation->getPackageID(),
+                                       $data['objectTypeID'],
+                                       $data['optionName'],
+                                       $data['categoryName']
+                               ));
+                       }
+                       else {
+                               $sql = "UPDATE  wcf".WCF_N."_".$this->tableName."
+                                       SET     categoryName = ?
+                                       WHERE   optionID = ?";
+                               $statement = WCF::getDB()->prepareStatement($sql);
+                               $statement->execute(array(
+                                       $data['categoryName'],
+                                       $row['optionID']
+                               ));
+                       }
+               }
+       }
+       
+       /**
+        * @see wcf\system\package\plugin\AbstractOptionPackageInstallationPlugin::saveOption()
+        */
+       protected function saveOption($option, $categoryName, $existingOptionID = 0) {
+               /* Does nothing */
+       }
+       
+       /**
+        * Returns the object type id of the acl option type with the given name
+        * or throws a SystemException if no such option type exists.
+        * 
+        * @param       string          $optionType
+        * @return      integer
+        */
+       protected function getObjectTypeID($optionType) {
+               if (!isset($this->optionTypeIDs[$optionType])) {
+                       $sql = "SELECT  objectTypeID
+                               FROM    wcf".WCF_N."_object_type
+                               WHERE   objectType = ?
+                                       AND definitionID IN (
+                                               SELECT  definitionID
+                                               FROM    wcf".WCF_N."_object_type_definition
+                                               WHERE   definitionName = 'com.woltlab.wcf.acl'
+                                       )";
+                       $statement = WCF::getDB()->prepareStatement($sql, 1);
+                       $statement->execute(array($optionType));
+                       $row = $statement->fetchArray();
+                       if (!$row) {
+                               throw new SystemException("unknown object type '".$optionType."' given");
+                       }
+
+                       $this->optionTypeIDs[$optionType] = $row['objectTypeID'];
+               }
+               
+               return $this->optionTypeIDs[$optionType];
+       }
+}
diff --git a/wcfsetup/install/files/style/acl.less b/wcfsetup/install/files/style/acl.less
new file mode 100644 (file)
index 0000000..6efa9df
--- /dev/null
@@ -0,0 +1,80 @@
+/* todo */
+.aclContainer > dd > span {
+       position: relative;
+}
+
+.aclList,
+.aclPermissionList {
+       margin-top: 0;
+       min-height: 100px;
+
+       > li {
+               display: block;
+               padding: @wcfGapTiny;
+
+               &:first-child {
+                       border-top-left-radius: @wcfContainerBorderRadius;
+                       border-top-right-radius: @wcfContainerBorderRadius;
+               }
+               
+               &:last-child:not(:first-child) {
+                       border-bottom-left-radius: @wcfContainerBorderRadius;
+                       border-bottom-right-radius: @wcfContainerBorderRadius;
+               }
+       }
+}
+
+.aclList {
+       margin-bottom: @wcfGapSmall;
+       
+       > li {
+               cursor: pointer;
+
+               &:hover,
+               &.active:hover {
+                       background-color: @wcfButtonHoverBackgroundColor;
+               }
+               
+               &.active {
+                       background-color: @wcfContainerAccentBackgroundColor;
+               }
+               
+               > img:last-child {
+                       float: right;
+                       margin-right: @wcfGapSmall;
+               }
+       }
+}
+
+.aclPermissionList {
+       margin-top: @wcfGapSmall;
+       min-height: 200px;
+       text-align: right;
+
+       > li {
+               &:hover {
+                       background-color: @wcfButtonHoverBackgroundColor;
+               }
+               
+               &.aclCategory {
+                       background-color: @wcfContainerHoverBackgroundColor;
+                       padding: @wcfGapSmall;
+                       text-align: left;
+               }
+               
+               &.aclFullAccess {
+                       background-color: @wcfContainerAccentBackgroundColor;
+               }
+               
+               > span {
+                       float: left;
+                       padding-left: @wcfGapSmall;
+               }
+               
+               > label {
+                       cursor: pointer;
+                       margin: 0 @wcfGapSmall;
+                       padding: 0 @wcfGapLarge;
+               }
+       }
+}
\ No newline at end of file
index 697991bd641ea25d5fac2fe3bb97b1293edf4ec2..0085592b5e6553d39417f83906dd998aa9635faf 100644 (file)
@@ -1,5 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/language.xsd" languagecode="de" languagename="Deutsch" countrycode="de">
+       <category name="wcf.acl">
+               <item name="wcf.acl.option.deny"><![CDATA[Verweigern]]></item>
+               <item name="wcf.acl.option.fullAccess"><![CDATA[Vollzugriff]]></item>
+               <item name="wcf.acl.option.grant"><![CDATA[Erlauben]]></item>
+               <item name="wcf.acl.permissions"><![CDATA[Benutzer-/Benutzergruppenrechte]]></item>
+       </category>
+       
        <category name="wcf.acp">
                <item name="wcf.acp"><![CDATA[Administrationsoberfläche]]></item>
        </category>
index f10dea3670ea53fa0230261768080ddbb44f90bc..befea72c40c1375d0ea65934d2a0fa2cbd48c06f 100644 (file)
@@ -1,5 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/language.xsd" languagecode="en" languagename="English" countrycode="gb">
+       <category name="wcf.acl">
+               <item name="wcf.acl.option.deny"><![CDATA[Deny]]></item>
+               <item name="wcf.acl.option.fullAccess"><![CDATA[Full access]]></item>
+               <item name="wcf.acl.option.grant"><![CDATA[Grant]]></item>
+               <item name="wcf.acl.permissions"><![CDATA[User/User group permissions]]></item>
+       </category>
+       
        <category name="wcf.acp">
                <item name="wcf.acp"><![CDATA[Administration]]></item>
        </category>
index a84db208c1eee052566d503634271cc9ed20fe92..6c26a1f323a40e691e69dbbcfcbb9f8d0b614302 100644 (file)
@@ -1,4 +1,41 @@
 /* tables */
+DROP TABLE IF EXISTS wcf1_acl_option;
+CREATE TABLE wcf1_acl_option (
+       optionID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       packageID INT(10) NOT NULL,
+       objectTypeID INT(10) NOT NULL,
+       optionName VARCHAR(255) NOT NULL,
+       categoryName VARCHAR(255) NOT NULL,
+       UNIQUE KEY (packageID, objectTypeID, optionName)
+);
+
+DROP TABLE IF EXISTS wcf1_acl_option_category;
+CREATE TABLE wcf1_acl_option_category (
+       categoryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       packageID INT(10) NOT NULL,
+       objectTypeID INT(10) NOT NULL,
+       categoryName VARCHAR(255) NOT NULL,
+       UNIQUE KEY (packageID, objectTypeID, categoryName)
+);
+
+DROP TABLE IF EXISTS wcf1_acl_option_to_user;
+CREATE TABLE wcf1_acl_option_to_user (
+       optionID INT(10) NOT NULL,
+       objectID INT(10) NOT NULL,
+       userID INT(10) NOT NULL,
+       optionValue TINYINT(1) NOT NULL DEFAULT 0,
+       UNIQUE KEY userID (userID, objectID, optionID)
+);
+
+DROP TABLE IF EXISTS wcf1_acl_option_to_group;
+CREATE TABLE wcf1_acl_option_to_group (
+       optionID INT(10) NOT NULL,
+       objectID INT(10) NOT NULL,
+       groupID INT(10) NOT NULL,
+       optionValue TINYINT(1) NOT NULL DEFAULT 0,
+       UNIQUE KEY groupID (groupID, objectID, optionID)
+);
+
 DROP TABLE IF EXISTS wcf1_acp_menu_item;
 CREATE TABLE wcf1_acp_menu_item (
        menuItemID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@@ -726,6 +763,18 @@ CREATE TABLE wcf1_user_to_language (
 );
 
 /* foreign keys */
+ALTER TABLE wcf1_acl_option ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_acl_option ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
+
+ALTER TABLE wcf1_acl_option_category ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_acl_option_category ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
+
+ALTER TABLE wcf1_acl_option_to_user ADD FOREIGN KEY (optionID) REFERENCES wcf1_acl_option (optionID) ON DELETE CASCADE;
+ALTER TABLE wcf1_acl_option_to_user ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE;
+
+ALTER TABLE wcf1_acl_option_to_group ADD FOREIGN KEY (optionID) REFERENCES wcf1_acl_option (optionID) ON DELETE CASCADE;
+ALTER TABLE wcf1_acl_option_to_group ADD FOREIGN KEY (groupID) REFERENCES wcf1_user_group (groupID) ON DELETE CASCADE;
+
 ALTER TABLE wcf1_acp_menu_item ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
 
 ALTER TABLE wcf1_acp_search_provider ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;