New group type: Owner (5)
authorAlexander Ebert <ebert@woltlab.com>
Mon, 8 Apr 2019 08:39:20 +0000 (10:39 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 8 Apr 2019 08:39:20 +0000 (10:39 +0200)
wcfsetup/install/files/acp/templates/optionFieldList.tpl
wcfsetup/install/files/acp/templates/userGroupAdd.tpl
wcfsetup/install/files/acp/templates/userGroupList.tpl
wcfsetup/install/files/lib/acp/form/UserGroupEditForm.class.php
wcfsetup/install/files/lib/data/user/User.class.php
wcfsetup/install/files/lib/data/user/group/UserGroup.class.php
wcfsetup/install/files/lib/system/cache/builder/UserGroupPermissionCacheBuilder.class.php
wcfsetup/install/files/lib/system/option/user/group/UserGroupOptionHandler.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 740eed8e1fd247263a518f82a5bf675c25a0dbb2..4ed68fe6ba2488874e03465db15e6914eba39606 100644 (file)
@@ -1,4 +1,5 @@
 {if !$isGuestGroup|isset}{assign var=isGuestGroup value=false}{/if}
+{if !$groupIsOwner|isset}{assign var=groupIsOwner value=false}{/if}
 {foreach from=$options item=optionData}
        {assign var=option value=$optionData[object]}
        {if $errorType|is_array && $errorType[$option->optionName]|isset}
@@ -7,7 +8,16 @@
                {assign var=error value=''}
        {/if}
        <dl class="{$option->optionName}Input{if $error} formError{/if}">
-               <dt{if $optionData[cssClassName]} class="{$optionData[cssClassName]}"{/if}>{if $isSearchMode|empty || !$optionData[hideLabelInSearch]}<label for="{$option->optionName}">{if VISITOR_USE_TINY_BUILD && $isGuestGroup && $option->excludedInTinyBuild}<span class="icon icon16 fa-bolt red jsTooltip" title="{lang}wcf.acp.group.excludedInTinyBuild{/lang}"></span> {/if}{lang}{@$langPrefix}{$option->optionName}{/lang}</label>{/if}</dt>
+               <dt{if $optionData[cssClassName]} class="{$optionData[cssClassName]}"{/if}>
+                       {if $isSearchMode|empty || !$optionData[hideLabelInSearch]}
+                               <label for="{$option->optionName}">
+                                       {if VISITOR_USE_TINY_BUILD && $isGuestGroup && $option->excludedInTinyBuild}<span class="icon icon16 fa-bolt red jsTooltip" title="{lang}wcf.acp.group.excludedInTinyBuild{/lang}"></span> {/if}
+                                       {if $groupIsOwner && $option->optionName|in_array:$ownerGroupPermissions}<span class="icon icon16 fa-shield jsTooltip" title="{lang}wcf.acp.group.ownerGroupPermission{/lang}"></span> {/if}
+                                       
+                                       {lang}{@$langPrefix}{$option->optionName}{/lang}
+                               </label>
+                       {/if}
+               </dt>
                <dd>{@$optionData[html]}
                        {if $error}
                                <small class="innerError">
@@ -21,4 +31,4 @@
                        <small>{lang __optional=true}{@$langPrefix}{$option->optionName}.description{/lang}</small>
                </dd>
        </dl>
-{/foreach}
\ No newline at end of file
+{/foreach}
index bd03a7537cb97023fc167e85704480d1f212a690..94e8fd5e73c242b6914527e6834c7f0350f1996e 100644 (file)
        <p class="warning">{lang}wcf.acp.group.excludedInTinyBuild.notice{/lang}</p>
 {/if}
 
+{if $action == 'edit' && $group->isOwner()}
+       <p class="info"><span class="icon icon16 fa-shield"></span> {lang}wcf.acp.group.type.owner.description{/lang}</p>
+{/if}
+
 {if $warningSelfEdit|isset}
        <p class="warning">{lang}wcf.acp.group.edit.warning.selfIsMember{/lang}</p>
 {/if}
        </div>
 </form>
 
+{if $action === 'edit'}
+       <script>
+               (function () {
+                       {if $groupIsOwner}
+                               elBySelAll('input[name="values[admin.user.accessibleGroups][]"]', undefined, function(input) {
+                                       var shadow = elCreate('input');
+                                       shadow.type = 'hidden';
+                                       shadow.name = input.name;
+                                       shadow.value = input.value;
+                                       
+                                       input.parentNode.appendChild(shadow);
+                                       
+                                       input.disabled = true;
+                               });
+                               
+                               var permissions = [{implode from=$ownerGroupPermissions item=$_ownerPermission}'{$_ownerPermission|encodeJS}'{/implode}];
+                               permissions.forEach(function(permission) {
+                                       elBySelAll('input[name="values[' + permission + ']"]', undefined, function (input) {
+                                               if (input.value === '1') {
+                                                       input.checked = true;
+                                               }
+                                               else {
+                                                       input.disabled = true;
+                                               }
+                                       });
+                               });
+                       {elseif $ownerGroupID}
+                               var input = elBySel('input[name="values[admin.user.accessibleGroups][]"][value="{$ownerGroupID}"]');
+                               if (input) {
+                                       elRemove(input.closest('label'));
+                               }
+                       {/if}
+               })();
+       </script>
+{/if}
+
 {include file='footer'}
index 809dd14764918f030c8469efb4e4fef356d16d5d..d525eb1438a545c7ed1e42bc57e7d9d11d01eb3b 100644 (file)
@@ -69,6 +69,9 @@
                                                {else}
                                                        {lang}{$group->groupName}{/lang}
                                                {/if}
+                                               {if $group->isOwner()}
+                                                       <span class="icon icon16 fa-shield jsTooltip" title="{lang}wcf.acp.group.type.owner{/lang}"></span>
+                                               {/if}
                                        </td>
                                        <td class="columnDigits columnMembers">
                                                {if $group->groupType == 1 ||$group->groupType == 2}
index 8faefff4b69ddba20e62109a8bd2b81d99096eb2..d3cdf40c2f12e5f92ac0b3aabdbe6eb0946abd0f 100755 (executable)
@@ -116,6 +116,14 @@ class UserGroupEditForm extends UserGroupAddForm {
                
                I18nHandler::getInstance()->assignVariables(!empty($_POST));
                
+               $ownerGroupPermissions = [];
+               if ($this->group->isOwner()) {
+                       $ownerGroupPermissions = UserGroup::getOwnerPermissions();
+                       $ownerGroupPermissions[] = 'admin.user.accessibleGroups';
+               }
+               
+               $ownerGroup = UserGroup::getGroupByType(UserGroup::OWNER);
+               
                WCF::getTPL()->assign([
                        'groupID' => $this->group->groupID,
                        'group' => $this->group,
@@ -124,7 +132,10 @@ class UserGroupEditForm extends UserGroupAddForm {
                        'groupIsEveryone' => $this->group->groupType == UserGroup::EVERYONE,
                        'groupIsGuest' => $this->group->groupType == UserGroup::GUESTS,
                        'groupIsUsers' => $this->group->groupType == UserGroup::USERS,
+                       'groupIsOwner' => $this->group->isOwner(),
                        'isUnmentionableGroup' => $this->isUnmentionableGroup ? 1 : 0,
+                       'ownerGroupPermissions' => $ownerGroupPermissions,
+                       'ownerGroupID' => $ownerGroup ? $ownerGroup->groupID : null,
                ]);
                
                // add warning when the initiator is in the group
index b34fa26781371a494c4834d7b3b9de5b0a62784a..8b14b3cd518657c48c781ac9e98c794fee17c14e 100644 (file)
@@ -478,8 +478,7 @@ final class User extends DatabaseObject implements IRouteController, IUserConten
                        $this->hasAdministrativePermissions = false;
                        
                        if ($this->userID) {
-                               foreach ($this->getGroupIDs() as $groupID) {
-                                       $group = UserGroup::getGroupByID($groupID);
+                               foreach (UserGroup::getGroupsByIDs($this->getGroupIDs()) as $group) {
                                        if ($group->isAdminGroup()) {
                                                $this->hasAdministrativePermissions = true;
                                                break;
@@ -491,6 +490,29 @@ final class User extends DatabaseObject implements IRouteController, IUserConten
                return $this->hasAdministrativePermissions;
        }
        
+       /**
+        * Returns true, if this user is a member of the owner group.
+        * 
+        * @return bool
+        * @since 5.2
+        */
+       public function hasOwnerAccess() {
+               static $isOwner;
+               
+               if ($isOwner === null) {
+                       $isOwner = false;
+                       
+                       foreach (UserGroup::getGroupsByIDs($this->getGroupIDs()) as $group) {
+                               if ($group->isOwner()) {
+                                       $isOwner = true;
+                                       break;
+                               }
+                       }
+               }
+               
+               return $isOwner;
+       }
+       
        /**
         * @inheritDoc
         */
index abf5f314c6644a45b693921c01a226fead8740dc..6da1c1ba728dc9b8ae503cc14e9a06ae02cbe449 100644 (file)
@@ -56,6 +56,12 @@ class UserGroup extends DatabaseObject implements ITitledObject {
         */
        const OTHER = 4;
        
+       /**
+        * the owner group is always an administrator group
+        * @var int
+        */
+       const OWNER = 5;
+       
        /**
         * group cache
         * @var UserGroup[]
@@ -122,7 +128,7 @@ class UserGroup extends DatabaseObject implements ITitledObject {
         * @throws      SystemException
         */
        public static function getGroupByType($type) {
-               if ($type != self::EVERYONE && $type != self::GUESTS && $type != self::USERS) {
+               if ($type != self::EVERYONE && $type != self::GUESTS && $type != self::USERS && $type != self::OWNER) {
                        throw new SystemException('invalid value for type argument');
                }
                
@@ -197,6 +203,16 @@ class UserGroup extends DatabaseObject implements ITitledObject {
                return $this->groupType == self::USERS;
        }
        
+       /**
+        * Returns true if this is the 'Owner' group.
+        * 
+        * @return bool
+        * @since 5.2
+        */
+       public function isOwner() {
+               return $this->groupType == self::OWNER;
+       }
+       
        /**
         * Returns true if the given groups are accessible for the active user.
         * 
@@ -239,16 +255,25 @@ class UserGroup extends DatabaseObject implements ITitledObject {
        }
        
        /**
-        * Returns true if the current group is an admin-group.
-        * Every group that may access EVERY group is an admin-group.
+        * Returns true if the current group is an admin-group, which requires it to fulfill
+        * one of these conditions:
+        *  a) The WCFSetup is running and the group id is 4.
+        *  b) This is the 'Owner' group.
+        *  c) The group can access all groups (the 'Owner' group does not count).
         * 
         * @return      boolean
         */
        public function isAdminGroup() {
-               // workaround for WCF-Setup
-               if (!PACKAGE_ID && $this->groupID == 4) return true;
+               // WCFSetup
+               if (!PACKAGE_ID && $this->groupID == 4) {
+                       return true;
+               }
                
-               $groupIDs = array_keys(self::getGroupsByType());
+               if ($this->groupType === self::OWNER) {
+                       return true;
+               }
+               
+               $groupIDs = array_keys(self::getGroupsByType([], [self::OWNER]));
                $accessibleGroupIDs = explode(',', (string) $this->getGroupOption('admin.user.accessibleGroups'));
                
                // no differences -> all groups are included
@@ -329,7 +354,7 @@ class UserGroup extends DatabaseObject implements ITitledObject {
                if (!$this->isAccessible()) return false;
                
                // cannot delete static groups
-               if ($this->groupType == self::EVERYONE || $this->groupType == self::GUESTS || $this->groupType == self::USERS) return false;
+               if ($this->groupType == self::EVERYONE || $this->groupType == self::GUESTS || $this->groupType == self::USERS || $this->groupType == self::OWNER) return false;
                
                return true;
        }
@@ -452,4 +477,25 @@ class UserGroup extends DatabaseObject implements ITitledObject {
                
                return self::$cache['groups'];
        }
+       
+       /**
+        * Returns the list of irrevocable permissions of the owner group.
+        * 
+        * @return string[]
+        * @since 5.2
+        */
+       public static function getOwnerPermissions() {
+               return [
+                       'admin.configuration.canEditOption',
+                       'admin.configuration.canManageApplication',
+                       'admin.configuration.package.canInstallPackage',
+                       'admin.configuration.package.canUninstallPackage',
+                       'admin.configuration.package.canUpdatePackage',
+                       'admin.general.canUseAcp',
+                       'admin.general.canViewPageDuringOfflineMode',
+                       'admin.user.canEditGroup',
+                       'admin.user.canEditUser',
+                       'admin.user.canSearchUser',
+               ];
+       }
 }
index c79991a969b0e1fbe9f39f56b002da1c581340ba..d61aa563ca2d8264f4a03991947bbdc8b746c560 100644 (file)
@@ -68,6 +68,17 @@ class UserGroupPermissionCacheBuilder extends AbstractCacheBuilder {
                        $data[$row['optionName']]['values'][] = $row['optionValue'];
                }
                
+               $includesOwnerGroup = false;
+               $ownerGroup = UserGroup::getGroupByType(UserGroup::OWNER);
+               if ($ownerGroup && in_array($ownerGroup->groupID, $parameters)) {
+                       $includesOwnerGroup = true;
+               }
+               
+               $forceGrantPermission = [];
+               if ($includesOwnerGroup) {
+                       $forceGrantPermission = UserGroup::getOwnerPermissions();
+               }
+               
                // merge values
                $neverValues = [];
                foreach ($data as $optionName => $option) {
@@ -89,6 +100,23 @@ class UserGroupPermissionCacheBuilder extends AbstractCacheBuilder {
                                }
                        }
                        
+                       if ($ownerGroup && $optionName === 'admin.user.accessibleGroups') {
+                               $accessibleGroupIDs = explode(',', $result);
+                               if ($includesOwnerGroup) {
+                                       // Regardless of the actual permissions, the owner group has access to all groups.
+                                       
+                                       $accessibleGroupIDs[] = $ownerGroup->groupID;
+                               }
+                               else if (!$includesOwnerGroup && in_array($ownerGroup->groupID, $accessibleGroupIDs)) {
+                                       $accessibleGroupIDs = array_diff($accessibleGroupIDs, [$ownerGroup->groupID]);
+                               }
+                               
+                               $result = implode(',', $accessibleGroupIDs);
+                       }
+                       else if ($includesOwnerGroup && in_array($optionName, $forceGrantPermission)) {
+                               $result = 1;
+                       }
+                       
                        // handle special value 'Never' for boolean options
                        if ($option['type'] === 'boolean' && $result == -1) {
                                $neverValues[$optionName] = $optionName;
index 0c1fefcc419806cb8183f038f262fffa48c22132..de1fcd01b11415edfab9ed7503c930c3bd71cca7 100644 (file)
@@ -26,7 +26,7 @@ class UserGroupOptionHandler extends OptionHandler {
         * user group object
         * @var UserGroup
         */
-       protected $group = null;
+       protected $group;
        
        /**
         * true if current user can edit every user group
@@ -34,6 +34,13 @@ class UserGroupOptionHandler extends OptionHandler {
         */
        protected $isAdmin = null;
        
+       /**
+        * true if the user is part of the owner group
+        * @var bool
+        * @since 5.2
+        */
+       protected $isOwner = null;
+       
        /**
         * Sets current user group.
         * 
@@ -114,19 +121,26 @@ class UserGroupOptionHandler extends OptionHandler {
         */
        protected function isAdmin() {
                if ($this->isAdmin === null) {
-                       $this->isAdmin = false;
-                       
-                       foreach (WCF::getUser()->getGroupIDs() as $groupID) {
-                               if (UserGroup::getGroupByID($groupID)->isAdminGroup()) {
-                                       $this->isAdmin = true;
-                                       break;
-                               }
-                       }
+                       $this->isAdmin = WCF::getUser()->hasAdministrativeAccess();
                }
                
                return $this->isAdmin;
        }
        
+       /**
+        * Returns true, if the current user is a member of the owner group.
+        * 
+        * @return bool
+        * @since 5.2
+        */
+       protected function isOwner() {
+               if ($this->isOwner === null) {
+                       $this->isOwner = WCF::getUser()->hasOwnerAccess();
+               }
+               
+               return $this->isOwner;
+       }
+       
        /**
         * @inheritDoc
         */
@@ -141,7 +155,7 @@ class UserGroupOptionHandler extends OptionHandler {
                                throw new UserInputException($option->optionName, 'exceedsOwnPermission');
                        }
                }
-               else if ($option->optionName == 'admin.user.accessibleGroups' && $this->group !== null && $this->group->isAdminGroup()) {
+               else if (!$this->isOwner() && $option->optionName == 'admin.user.accessibleGroups' && $this->group !== null && $this->group->isAdminGroup()) {
                        $hasOtherAdminGroup = false;
                        foreach (UserGroup::getGroupsByType() as $userGroup) {
                                if ($userGroup->groupID != $this->group->groupID && $userGroup->isAdminGroup()) {
index d69c0fb50596a5fd627a13ce361bba40f400ef26..c144dadd23ddfe14952f904fddff8cf4028713cf 100644 (file)
@@ -880,6 +880,9 @@ Das Fehlerprotokoll enthält {$data[count]} neue Einträge. Die ersten drei, in
                <item name="wcf.acp.group.option.user.contactForm.attachment.maxCount"><![CDATA[Maximale Dateianhänge pro Nachricht]]></item>
                <item name="wcf.acp.group.option.user.profile.canEditUserProfile"><![CDATA[Kann eigenes Profil bearbeiten]]></item>
                <item name="wcf.acp.group.allowMention"><![CDATA[Benutzergruppe kann erwähnt werden]]></item>
+               <item name="wcf.acp.group.type.owner"><![CDATA[Besitzer]]></item>
+               <item name="wcf.acp.group.type.owner.description"><![CDATA[Die Besitzer-Gruppe verfügt über nicht entziehbare Berechtigungen und kann von anderen Gruppen nicht bearbeitet werden, diese Gruppe kann nur durch die Besitzer-Gruppe selbst bearbeitet werden. Mitglieder dieser Benutzer können andere Benutzer zu dieser Gruppe hinzufügen, sich aber selbst nicht daraus entfernen.]]></item>
+               <item name="wcf.acp.group.ownerGroupPermission"><![CDATA[Die Besitzer-Gruppe verfügt immer über diese Berechtigung, sie kann nicht entzogen werden.]]></item>
        </category>
        <category name="wcf.acp.index">
                <item name="wcf.acp.index.credits"><![CDATA[Über WoltLab Suite&trade;]]></item>
index 477dd695b21f33c4d19dd5ff15960091d7a93472..2e154db23aa4b5165e72e4b448ec34c681f74e50 100644 (file)
@@ -857,6 +857,9 @@ This protocol file contains {$data[count]} new entries. The first three error me
                <item name="wcf.acp.group.option.user.contactForm.attachment.maxCount"><![CDATA[Maximum Attachments per Message]]></item>
                <item name="wcf.acp.group.option.user.profile.canEditUserProfile"><![CDATA[Can edit their profile]]></item>
                <item name="wcf.acp.group.allowMention"><![CDATA[User group can be mentioned]]></item>
+               <item name="wcf.acp.group.type.owner"><![CDATA[Owner]]></item>
+               <item name="wcf.acp.group.type.owner.description"><![CDATA[The owner group features a few irrevocable permissions and is protected from edits by other groups, only the owner group can edit itself. Members of this group can add other users to this group, but cannot remove themselves.]]></item>
+               <item name="wcf.acp.group.ownerGroupPermission"><![CDATA[The owner group always has this permission, it cannot be revoked.]]></item>
        </category>
        <category name="wcf.acp.index">
                <item name="wcf.acp.index.credits"><![CDATA[About WoltLab Suite&trade;]]></item>
index 9ed46d7ccbc1307cea051f620403bbed829a81bf..1937a06c4c8a2c67cca0c223ca78a63783bd1ca7 100644 (file)
@@ -2224,7 +2224,7 @@ ALTER TABLE wcf1_notice_dismissed ADD FOREIGN KEY (userID) REFERENCES wcf1_user
 INSERT INTO wcf1_user_group (groupID, groupName, groupType) VALUES (1, 'wcf.acp.group.group1', 1); -- Everyone
 INSERT INTO wcf1_user_group (groupID, groupName, groupType) VALUES (2, 'wcf.acp.group.group2', 2); -- Guests
 INSERT INTO wcf1_user_group (groupID, groupName, groupType) VALUES (3, 'wcf.acp.group.group3', 3); -- Registered Users
-INSERT INTO wcf1_user_group (groupID, groupName, groupType) VALUES (4, 'wcf.acp.group.group4', 4); -- Administrators
+INSERT INTO wcf1_user_group (groupID, groupName, groupType) VALUES (4, 'wcf.acp.group.group4', 5); -- Administrators
 INSERT INTO wcf1_user_group (groupID, groupName, groupType) VALUES (5, 'wcf.acp.group.group5', 4); -- Moderators
 
 -- default user group options