"Varchar(255)", "Description" => "Text", "Code" => "Varchar(255)", "Locked" => "Boolean", "Sort" => "Int", "HtmlEditorConfig" => "Text" ); private static $has_one = array( "Parent" => "SilverStripe\\Security\\Group", ); private static $has_many = array( "Permissions" => "SilverStripe\\Security\\Permission", "Groups" => "SilverStripe\\Security\\Group" ); private static $many_many = array( "Members" => "SilverStripe\\Security\\Member", "Roles" => "SilverStripe\\Security\\PermissionRole", ); private static $extensions = array( "SilverStripe\\ORM\\Hierarchy\\Hierarchy", ); private static $table_name = "Group"; public function populateDefaults() { parent::populateDefaults(); if (!$this->Title) { $this->Title = _t(__CLASS__.'.NEWGROUP', "New Group"); } } public function getAllChildren() { $doSet = new ArrayList(); $children = Group::get()->filter("ParentID", $this->ID); foreach ($children as $child) { $doSet->push($child); $doSet->merge($child->getAllChildren()); } return $doSet; } /** * Caution: Only call on instances, not through a singleton. * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}. * * @return FieldList */ public function getCMSFields() { $fields = new FieldList( new TabSet( "Root", new Tab( 'Members', _t(__CLASS__.'.MEMBERS', 'Members'), new TextField("Title", $this->fieldLabel('Title')), $parentidfield = DropdownField::create( 'ParentID', $this->fieldLabel('Parent'), Group::get()->exclude('ID', $this->ID)->map('ID', 'Breadcrumbs') )->setEmptyString(' '), new TextareaField('Description', $this->fieldLabel('Description')) ), $permissionsTab = new Tab( 'Permissions', _t(__CLASS__.'.PERMISSIONS', 'Permissions'), $permissionsField = new PermissionCheckboxSetField( 'Permissions', false, 'SilverStripe\\Security\\Permission', 'GroupID', $this ) ) ) ); $parentidfield->setDescription( _t('SilverStripe\\Security\\Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles') ); if ($this->ID) { $group = $this; $config = GridFieldConfig_RelationEditor::create(); $config->addComponent(new GridFieldButtonRow('after')); $config->addComponents(new GridFieldExportButton('buttons-after-left')); $config->addComponents(new GridFieldPrintButton('buttons-after-left')); /** @var GridFieldAddExistingAutocompleter $autocompleter */ $autocompleter = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldAddExistingAutocompleter'); /** @skipUpgrade */ $autocompleter ->setResultsFormat('$Title ($Email)') ->setSearchFields(array('FirstName', 'Surname', 'Email')); /** @var GridFieldDetailForm $detailForm */ $detailForm = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDetailForm'); $detailForm ->setValidator(Member_Validator::create()) ->setItemEditFormCallback(function ($form, $component) use ($group) { /** @var Form $form */ $record = $form->getRecord(); $groupsField = $form->Fields()->dataFieldByName('DirectGroups'); if ($groupsField) { // If new records are created in a group context, // set this group by default. if ($record && !$record->ID) { $groupsField->setValue($group->ID); } elseif ($record && $record->ID) { // TODO Mark disabled once chosen.js supports it // $groupsField->setDisabledItems(array($group->ID)); $form->Fields()->replaceField( 'DirectGroups', $groupsField->performReadonlyTransformation() ); } } }); $memberList = GridField::create('Members', false, $this->DirectMembers(), $config) ->addExtraClass('members_grid'); // @todo Implement permission checking on GridField //$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); $fields->addFieldToTab('Root.Members', $memberList); } // Only add a dropdown for HTML editor configurations if more than one is available. // Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration. $editorConfigMap = HTMLEditorConfig::get_available_configs_map(); if (count($editorConfigMap) > 1) { $fields->addFieldToTab( 'Root.Permissions', new DropdownField( 'HtmlEditorConfig', 'HTML Editor Configuration', $editorConfigMap ), 'Permissions' ); } if (!Permission::check('EDIT_PERMISSIONS')) { $fields->removeFieldFromTab('Root', 'Permissions'); } // Only show the "Roles" tab if permissions are granted to edit them, // and at least one role exists if (Permission::check('APPLY_ROLES') && PermissionRole::get()->count() && class_exists(SecurityAdmin::class) ) { $fields->findOrMakeTab('Root.Roles', _t(__CLASS__.'.ROLES', 'Roles')); $fields->addFieldToTab( 'Root.Roles', new LiteralField( "", "

" . _t( __CLASS__.'.ROLESDESCRIPTION', "Roles are predefined sets of permissions, and can be assigned to groups.
" . "They are inherited from parent groups if required." ) . '
' . sprintf( '%s', SecurityAdmin::singleton()->Link('show/root#Root_Roles'), // TODO This should include #Root_Roles to switch directly to the tab, // but tabstrip.js doesn't display tabs when directly adressed through a URL pragma _t('SilverStripe\\Security\\Group.RolesAddEditLink', 'Manage roles') ) . "

" ) ); // Add roles (and disable all checkboxes for inherited roles) $allRoles = PermissionRole::get(); if (!Permission::check('ADMIN')) { $allRoles = $allRoles->filter("OnlyAdminCanApply", 0); } if ($this->ID) { $groupRoles = $this->Roles(); $inheritedRoles = new ArrayList(); $ancestors = $this->getAncestors(); foreach ($ancestors as $ancestor) { $ancestorRoles = $ancestor->Roles(); if ($ancestorRoles) { $inheritedRoles->merge($ancestorRoles); } } $groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID'); $inheritedRoleIDs = $inheritedRoles->column('ID'); } else { $groupRoleIDs = array(); $inheritedRoleIDs = array(); } $rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray()) ->setDefaultItems($groupRoleIDs) ->setAttribute('data-placeholder', _t('SilverStripe\\Security\\Group.AddRole', 'Add a role for this group')) ->setDisabledItems($inheritedRoleIDs); if (!$allRoles->count()) { $rolesField->setAttribute('data-placeholder', _t('SilverStripe\\Security\\Group.NoRoles', 'No roles found')); } $fields->addFieldToTab('Root.Roles', $rolesField); } $fields->push($idField = new HiddenField("ID")); $this->extend('updateCMSFields', $fields); return $fields; } /** * @param bool $includerelations Indicate if the labels returned include relation fields * @return array */ public function fieldLabels($includerelations = true) { $labels = parent::fieldLabels($includerelations); $labels['Title'] = _t(__CLASS__.'.GROUPNAME', 'Group name'); $labels['Description'] = _t('SilverStripe\\Security\\Group.Description', 'Description'); $labels['Code'] = _t('SilverStripe\\Security\\Group.Code', 'Group Code', 'Programmatical code identifying a group'); $labels['Locked'] = _t('SilverStripe\\Security\\Group.Locked', 'Locked?', 'Group is locked in the security administration area'); $labels['Sort'] = _t('SilverStripe\\Security\\Group.Sort', 'Sort Order'); if ($includerelations) { $labels['Parent'] = _t('SilverStripe\\Security\\Group.Parent', 'Parent Group', 'One group has one parent group'); $labels['Permissions'] = _t('SilverStripe\\Security\\Group.has_many_Permissions', 'Permissions', 'One group has many permissions'); $labels['Members'] = _t('SilverStripe\\Security\\Group.many_many_Members', 'Members', 'One group has many members'); } return $labels; } /** * Get many-many relation to {@link Member}, * including all members which are "inherited" from children groups of this record. * See {@link DirectMembers()} for retrieving members without any inheritance. * * @param String $filter * @return ManyManyList */ public function Members($filter = '') { // First get direct members as a base result $result = $this->DirectMembers(); // Unsaved group cannot have child groups because its ID is still 0. if (!$this->exists()) { return $result; } // Remove the default foreign key filter in prep for re-applying a filter containing all children groups. // Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific // ones. if (!($result instanceof UnsavedRelationList)) { $result = $result->alterDataQuery(function ($query) { /** @var DataQuery $query */ $query->removeFilterOn('Group_Members'); }); } // Now set all children groups as a new foreign key $groups = Group::get()->byIDs($this->collateFamilyIDs()); $result = $result->forForeignID($groups->column('ID'))->where($filter); return $result; } /** * Return only the members directly added to this group */ public function DirectMembers() { return $this->getManyManyComponents('Members'); } /** * Return a set of this record's "family" of IDs - the IDs of * this record and all its descendants. * * @return array */ public function collateFamilyIDs() { if (!$this->exists()) { throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group."); } $familyIDs = array(); $chunkToAdd = array($this->ID); while ($chunkToAdd) { $familyIDs = array_merge($familyIDs, $chunkToAdd); // Get the children of *all* the groups identified in the previous chunk. // This minimises the number of SQL queries necessary $chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID'); } return $familyIDs; } /** * Returns an array of the IDs of this group and all its parents * * @return array */ public function collateAncestorIDs() { $parent = $this; $items = []; while (isset($parent) && $parent instanceof Group) { $items[] = $parent->ID; $parent = $parent->Parent; } return $items; } /** * This isn't a decendant of SiteTree, but needs this in case * the group is "reorganised"; */ public function cmsCleanup_parentChanged() { } /** * Override this so groups are ordered in the CMS */ public function stageChildren() { return Group::get() ->filter("ParentID", $this->ID) ->exclude("ID", $this->ID) ->sort('"Sort"'); } public function getTreeTitle() { if ($this->hasMethod('alternateTreeTitle')) { return $this->alternateTreeTitle(); } return htmlspecialchars($this->Title, ENT_QUOTES); } /** * Overloaded to ensure the code is always descent. * * @param string */ public function setCode($val) { $this->setField("Code", Convert::raw2url($val)); } public function validate() { $result = parent::validate(); // Check if the new group hierarchy would add certain "privileged permissions", // and require an admin to perform this change in case it does. // This prevents "sub-admin" users with group editing permissions to increase their privileges. if ($this->Parent()->exists() && !Permission::check('ADMIN')) { $inheritedCodes = Permission::get() ->filter('GroupID', $this->Parent()->collateAncestorIDs()) ->column('Code'); $privilegedCodes = Config::inst()->get('SilverStripe\\Security\\Permission', 'privileged_permissions'); if (array_intersect($inheritedCodes, $privilegedCodes)) { $result->addError(sprintf( _t( 'SilverStripe\\Security\\Group.HierarchyPermsError', 'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)' ), $this->Parent()->Title )); } } return $result; } public function onBeforeWrite() { parent::onBeforeWrite(); // Only set code property when the group has a custom title, and no code exists. // The "Code" attribute is usually treated as a more permanent identifier than database IDs // in custom application logic, so can't be changed after its first set. if (!$this->Code && $this->Title != _t(__CLASS__.'.NEWGROUP', "New Group")) { $this->setCode($this->Title); } } public function onBeforeDelete() { parent::onBeforeDelete(); // if deleting this group, delete it's children as well foreach ($this->Groups() as $group) { $group->delete(); } // Delete associated permissions foreach ($this->Permissions() as $permission) { $permission->delete(); } } /** * Checks for permission-code CMS_ACCESS_SecurityAdmin. * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well. * * @param $member Member * @return boolean */ public function canEdit($member = null) { if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) { $member = Member::currentUser(); } // extended access checks $results = $this->extend('canEdit', $member); if ($results && is_array($results)) { if (!min($results)) { return false; } } if (// either we have an ADMIN (bool)Permission::checkMember($member, "ADMIN") || ( // or a privileged CMS user and a group without ADMIN permissions. // without this check, a user would be able to add himself to an administrators group // with just access to the "Security" admin interface Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") && !Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists() ) ) { return true; } return false; } /** * Checks for permission-code CMS_ACCESS_SecurityAdmin. * * @param $member Member * @return boolean */ public function canView($member = null) { if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) { $member = Member::currentUser(); } // extended access checks $results = $this->extend('canView', $member); if ($results && is_array($results)) { if (!min($results)) { return false; } } // user needs access to CMS_ACCESS_SecurityAdmin if (Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) { return true; } return false; } public function canDelete($member = null) { if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) { $member = Member::currentUser(); } // extended access checks $results = $this->extend('canDelete', $member); if ($results && is_array($results)) { if (!min($results)) { return false; } } return $this->canEdit($member); } /** * Returns all of the children for the CMS Tree. * Filters to only those groups that the current user can edit */ public function AllChildrenIncludingDeleted() { /** @var Hierarchy $extInstance */ $extInstance = $this->getExtensionInstance('SilverStripe\\ORM\\Hierarchy\\Hierarchy'); $extInstance->setOwner($this); $children = $extInstance->AllChildrenIncludingDeleted(); $extInstance->clearOwner(); $filteredChildren = new ArrayList(); if ($children) { foreach ($children as $child) { if ($child->canView()) { $filteredChildren->push($child); } } } return $filteredChildren; } /** * Add default records to database. * * This function is called whenever the database is built, after the * database tables have all been created. */ public function requireDefaultRecords() { parent::requireDefaultRecords(); // Add default author group if no other group exists $allGroups = DataObject::get('SilverStripe\\Security\\Group'); if (!$allGroups->count()) { $authorGroup = new Group(); $authorGroup->Code = 'content-authors'; $authorGroup->Title = _t('SilverStripe\\Security\\Group.DefaultGroupTitleContentAuthors', 'Content Authors'); $authorGroup->Sort = 1; $authorGroup->write(); Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain'); Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin'); Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin'); Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE'); } // Add default admin group if none with permission code ADMIN exists $adminGroups = Permission::get_groups_by_permission('ADMIN'); if (!$adminGroups->count()) { $adminGroup = new Group(); $adminGroup->Code = 'administrators'; $adminGroup->Title = _t('SilverStripe\\Security\\Group.DefaultGroupTitleAdministrators', 'Administrators'); $adminGroup->Sort = 0; $adminGroup->write(); Permission::grant($adminGroup->ID, 'ADMIN'); } // Members are populated through Member->requireDefaultRecords() } }