Version: current

      1 <?php
      2 /**
      3  * A security group.
      4  *
      5  * @package sapphire
      6  * @subpackage security
      7  */
      8 class Group extends DataObject {
      9
     10     static $db = array(
     11         "Title" => "Varchar",
     12         "Description" => "Text",
     13         "Code" => "Varchar",
     14         "Locked" => "Boolean",
     15         "Sort" => "Int",
     16         "IPRestrictions" => "Text",
     17         "HtmlEditorConfig" => "Varchar"
     18     );
     19
     20     static $has_one = array(
     21         "Parent" => "Group",
     22     );
     23
     24     static $has_many = array(
     25         "Permissions" => "Permission",
     26         "Groups" => "Group"
     27     );
     28
     29     static $many_many = array(
     30         "Members" => "Member",
     31         "Roles" => "PermissionRole",
     32     );
     33
     34     static $extensions = array(
     35         "Hierarchy",
     36     );
     37
     38     function getAllChildren() {
     39         $doSet = new DataObjectSet();
     40
     41         if ($children = DataObject::get('Group', '"ParentID" = '.$this->ID)) {
     42             foreach($children as $child) {
     43                 $doSet->push($child);
     44                 $doSet->merge($child->getAllChildren());
     45             }
     46         }
     47
     48         return $doSet;
     49     }
     50
     51     /**
     52      * Caution: Only call on instances, not through a singleton.
     53      *
     54      * @return FieldSet
     55      */
     56     public function getCMSFields() {
     57         $fields = new FieldSet(
     58             new TabSet("Root",
     59                 new Tab('Members',_t('SecurityAdmin.MEMBERS','Members'),
     60                     new TextField("Title", $this->fieldLabel('Title')),
     61                     $memberList = new MemberTableField(
     62                         $this,
     63                         "Members",
     64                         $this
     65                     )
     66                 ),
     67
     68                 $permissionsTab = new Tab('Permissions', _t('SecurityAdmin.PERMISSIONS', 'Permissions'),
     69                     new PermissionCheckboxSetField(
     70                         'Permissions',
     71                         false,
     72                         'Permission',
     73                         'GroupID',
     74                         $this
     75                     )
     76                 ),
     77
     78                 new Tab('IP Addresses',_t('Security.IPADDRESSES', 'IP Addresses'),
     79                     new LiteralField("", _t('SecurityAdmin.IPADDRESSESHELP',"<p>You can restrict this group to a particular
     80                         IP address range (one range per line). <br />Ranges can be in any of the following forms: <br />
     81                         203.96.152.12<br />
     82                         203.96.152/24<br />
     83                         203.96/16<br />
     84                         203/8<br /><br />If you enter one or more IP address ranges in this box, then members will only get
     85                         the rights of being in this group if they log on from one of the valid IP addresses.  It won't prevent
     86                         people from logging in.  This is because the same user might have to log in to access parts of the
     87                         system without IP address restrictions.")),
     88                     new TextareaField("IPRestrictions", _t('Security.IP_RANGES', "IP Ranges"), 10)
     89                 )
     90             )
     91         );
     92
     93         // Only add a dropdown for HTML editor configurations if more than one is available.
     94         // Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
     95         $editorConfigMap = HtmlEditorConfig::get_available_configs_map();
     96         if(count($editorConfigMap) > 1) {
     97             $fields->addFieldToTab('Root.Permissions',
     98                 new DropdownField(
     99                     'HtmlEditorConfig',
    100                     'HTML Editor Configuration',
    101                     $editorConfigMap
    102                 ),
    103                 'Permissions'
    104             );
    105         }
    106
    107         if(!Permission::check('EDIT_PERMISSIONS')) {
    108             $fields->removeFieldFromTab('Root', 'Permissions');
    109             $fields->removeFieldFromTab('Root', 'IP Addresses');
    110         }
    111
    112         // Only show the "Roles" tab if permissions are granted to edit them,
    113         // and at least one role exists
    114         if(Permission::check('APPLY_ROLES') && DataObject::get('PermissionRole')) {
    115             $fields->findOrMakeTab('Root.Roles', _t('SecurityAdmin.ROLES', 'Roles'));
    116             $fields->addFieldToTab('Root.Roles',
    117                 new LiteralField(
    118                     "",
    119                     "<p>" .
    120                     _t('SecurityAdmin.ROLESDESCRIPTION',
    121                         "This section allows you to add roles to this group. Roles are logical groupings of permissions, which can be editied in the Roles tab"
    122                     ) .
    123                      "</p>"
    124                 )
    125             );
    126
    127             // Add roles (and disable all checkboxes for inherited roles)
    128             $allRoles = Permission::check('ADMIN') ? DataObject::get('PermissionRole') : DataObject::get('PermissionRole', 'OnlyAdminCanApply = 0');
    129             $groupRoles = $this->Roles();
    130             $inheritedRoles = new DataObjectSet();
    131             $ancestors = $this->getAncestors();
    132             foreach($ancestors as $ancestor) {
    133                 $ancestorRoles = $ancestor->Roles();
    134                 if($ancestorRoles) $inheritedRoles->merge($ancestorRoles);
    135             }
    136             $fields->addFieldToTab(
    137                 'Root.Roles',
    138                 $rolesField = new CheckboxSetField('Roles', _t('Group.Roles', 'Roles'), $allRoles)
    139             );
    140             $rolesField->setDefaultItems($inheritedRoles->column('ID'));
    141             $rolesField->setDisabledItems($inheritedRoles->column('ID'));
    142         }
    143
    144         $memberList->setController($this);
    145         $memberList->setPermissions(array('edit', 'delete', 'export', 'add'));
    146         $memberList->setParentClass('Group');
    147         $memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User'));
    148         $memberList->setRelationAutoSetting(false);
    149
    150         $fields->push($idField = new HiddenField("ID"));
    151
    152         $this->extend('updateCMSFields', $fields);
    153
    154         return $fields;
    155     }
    156
    157     /**
    158      *
    159      * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
    160      *
    161      */
    162     function fieldLabels($includerelations = true) {
    163         $labels = parent::fieldLabels($includerelations);
    164         $labels['Title'] = _t('SecurityAdmin.GROUPNAME', 'Group name');
    165         $labels['Description'] = _t('Group.Description', 'Description');
    166         $labels['Code'] = _t('Group.Code', 'Group Code', PR_MEDIUM, 'Programmatical code identifying a group');
    167         $labels['Locked'] = _t('Group.Locked', 'Locked?', PR_MEDIUM, 'Group is locked in the security administration area');
    168         $labels['Sort'] = _t('Group.Sort', 'Sort Order');
    169         $labels['IPRestrictions'] = _t('Group.IPRestrictions', 'IP Address Restrictions');
    170         if($includerelations){
    171             $labels['Parent'] = _t('Group.Parent', 'Parent Group', PR_MEDIUM, 'One group has one parent group');
    172             $labels['Permissions'] = _t('Group.has_many_Permissions', 'Permissions', PR_MEDIUM, 'One group has many permissions');
    173             $labels['Members'] = _t('Group.many_many_Members', 'Members', PR_MEDIUM, 'One group has many members');
    174         }
    175
    176         return $labels;
    177     }
    178
    179     /**
    180      * Add a member to a group. This will create the group if the given
    181      * group code doesn't work.
    182      *
    183      * @param DataObject $member
    184      * @param string $groupcode
    185      */
    186     static function addToGroupByName($member, $groupcode) {
    187         $group = DataObject::get_one('Group', "\"Code\" = '" . Convert::raw2sql($groupcode). "'");
    188         if($group) {
    189             $member->Groups()->add($group);
    190         }
    191         else {
    192             $group = new Group();
    193             $group->Code = $groupcode;
    194             $group->Title = $groupcode;
    195             $group->write();
    196
    197             $member->Groups()->add($group);
    198         }
    199     }
    200
    201     /**
    202      * Overloaded getter.
    203      *
    204      * @TODO Where is this used, why is this overloaded?
    205      *
    206      * @param $limit string SQL
    207      * @param $offset int
    208      * @param $filter string SQL
    209      * @param $sort string SQL
    210      * @param $join string SQL
    211      * @return ComponentSet
    212      */
    213     public function Members($limit = "", $offset = "", $filter = "", $sort = "", $join = "") {
    214         $table = "Group_Members";
    215         if($filter) $filter = is_array($filter) ? $filter : array($filter);
    216
    217         if( is_numeric( $limit ) ) {
    218             if( is_numeric( $offset ) )
    219                 $limit = "$limit OFFSET $offset";
    220             else
    221                 $limit = "$limit OFFSET 0";
    222         } else {
    223             $limit = "";
    224         }
    225
    226         // Get all of groups that this group contains
    227         $groupFamily = implode(", ", $this->collateFamilyIDs());
    228
    229         $filter[] = "\"$table\".\"GroupID\" IN ($groupFamily)";
    230         $join .= " INNER JOIN \"$table\" ON \"$table\".\"MemberID\" = \"Member\".\"ID\"" . Convert::raw2sql($join);
    231
    232         $result = singleton("Member")->instance_get(
    233             $filter,
    234             $sort,
    235             $join,
    236             $limit,
    237             "ComponentSet" // datatype
    238             );
    239
    240         if(!$result) $result = new ComponentSet();
    241
    242         $result->setComponentInfo("many-to-many", $this, "Group", $table, "Member");
    243         foreach($result as $item) $item->GroupID = $this->ID;
    244         return $result;
    245     }
    246
    247     public function map($filter = "", $sort = "", $blank="") {
    248         $ret = new SQLMap(singleton('Group')->extendedSQL($filter, $sort));
    249         if($blank){
    250             $blankGroup = new Group();
    251             $blankGroup->Title = $blank;
    252             $blankGroup->ID = 0;
    253
    254             $ret->getItems()->shift($blankGroup);
    255         }
    256         return $ret;
    257     }
    258
    259     /**
    260      * Return a set of this record's "family" of IDs - the IDs of
    261      * this record and all its descendants.
    262      * @return array
    263      */
    264     public function collateFamilyIDs() {
    265         $familyIDs = array();
    266         $chunkToAdd = array(array("ID" => $this->ID));
    267
    268         while($chunkToAdd) {
    269             $idList = array();
    270             foreach($chunkToAdd as $item) {
    271                 $idList[] = $item['ID'];
    272                 $familyIDs[] = $item['ID'];
    273             }
    274             $idList = implode(',', $idList);
    275
    276             // Get the children of *all* the groups identified in the previous chunk.
    277             // This minimises the number of SQL queries necessary
    278             $sql = $this->extendedSQL("\"ParentID\" IN ($idList)", "");
    279             $dbResult = $sql->execute();
    280             $chunkToAdd = array();
    281             foreach($dbResult as $item) $chunkToAdd[] = $item;
    282         }
    283
    284         return $familyIDs;
    285     }
    286
    287     /**
    288      * Returns an array of the IDs of this group and all its parents
    289      */
    290     public function collateAncestorIDs() {
    291         $parent = $this;
    292         while(isset($parent)) {
    293             $items[] = $parent->ID;
    294             $parent = $parent->Parent;
    295         }
    296         return $items;
    297     }
    298
    299     /**
    300      * This isn't a decendant of SiteTree, but needs this in case
    301      * the group is "reorganised";
    302      */
    303     function cmsCleanup_parentChanged() {
    304     }
    305
    306     /**
    307      * Override this so groups are ordered in the CMS
    308      */
    309     public function stageChildren() {
    310         return DataObject::get('Group', "\"Group\".\"ParentID\" = " . (int)$this->ID . " AND \"Group\".\"ID\" != " . (int)$this->ID, '"Sort"');
    311     }
    312
    313     public function TreeTitle() {
    314         if($this->hasMethod('alternateTreeTitle')) return $this->alternateTreeTitle();
    315         else return htmlspecialchars($this->Title, ENT_QUOTES);
    316     }
    317
    318     function CMSTreeClasses($controller) {
    319         $classes = $this->class;
    320
    321         if(!$this->canEdit()) $classes .= " disabled";
    322
    323         return $classes;
    324     }
    325
    326     /**
    327      * Overloaded to ensure the code is always descent.
    328      */
    329     public function setCode($val){
    330         $this->setField("Code",SiteTree::generateURLSegment($val));
    331     }
    332
    333     function onBeforeWrite() {
    334         parent::onBeforeWrite();
    335
    336         if(stripos($this->Code, _t('SecurityAdmin.NEWGROUPPREFIX','new-')) === 0) {
    337             $this->setCode($this->Title);
    338         }
    339     }
    340
    341     function onAfterDelete() {
    342         parent::onAfterDelete();
    343
    344         // Delete associated permissions
    345         $permissions = $this->Permissions();
    346         foreach ( $permissions as $permission ) {
    347             $permission->delete();
    348         }
    349     }
    350
    351     /**
    352      * Checks for permission-code CMS_ACCESS_SecurityAdmin.
    353      * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
    354      *
    355      * @param $member Member
    356      * @return boolean
    357      */
    358     public function canEdit($member = null) {
    359         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
    360
    361         // decorated access checks
    362         $results = $this->extend('canEdit', $member);
    363         if($results && is_array($results)) if(!min($results)) return false;
    364
    365         if(
    366             // either we have an ADMIN
    367             (bool)Permission::checkMember($member, "ADMIN")
    368             || (
    369                 // or a privileged CMS user and a group without ADMIN permissions.
    370                 // without this check, a user would be able to add himself to an administrators group
    371                 // with just access to the "Security" admin interface
    372                 Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
    373                 !DataObject::get("Permission", "GroupID = $this->ID AND Code = 'ADMIN'")
    374             )
    375         ) {
    376             return true;
    377         }
    378
    379         return false;
    380     }
    381
    382     /**
    383      * Checks for permission-code CMS_ACCESS_SecurityAdmin.
    384      *
    385      * @param $member Member
    386      * @return boolean
    387      */
    388     public function canView($member = null) {
    389         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
    390
    391         // decorated access checks
    392         $results = $this->extend('canView', $member);
    393         if($results && is_array($results)) if(!min($results)) return false;
    394
    395         // user needs access to CMS_ACCESS_SecurityAdmin
    396         if(Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) return true;
    397
    398         return false;
    399     }
    400
    401     public function canDelete($member = null) {
    402         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
    403
    404         // decorated access checks
    405         $results = $this->extend('canDelete', $member);
    406         if($results && is_array($results)) if(!min($results)) return false;
    407
    408         return $this->canEdit($member);
    409     }
    410
    411     /**
    412      * Returns all of the children for the CMS Tree.
    413      * Filters to only those groups that the current user can edit
    414      */
    415     function AllChildrenIncludingDeleted() {
    416         $extInstance = $this->getExtensionInstance('Hierarchy');
    417         $extInstance->setOwner($this);
    418         $children = $extInstance->AllChildrenIncludingDeleted();
    419         $extInstance->clearOwner();
    420
    421         $filteredChildren = new DataObjectSet();
    422
    423         if($children) foreach($children as $child) {
    424             if($child->canView()) $filteredChildren->push($child);
    425         }
    426
    427         return $filteredChildren;
    428     }
    429
    430     /**
    431      * Returns true if the given IP address is granted access to this group.
    432      * For unrestricted groups, this always returns true.
    433      */
    434     function allowedIPAddress($ip) {
    435         if(!$this->IPRestrictions) return true;
    436         if(!$ip) return false;
    437
    438         $ipPatterns = explode("\n", $this->IPRestrictions);
    439         foreach($ipPatterns as $ipPattern) {
    440             $ipPattern = trim($ipPattern);
    441             if(preg_match('/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/', $ipPattern, $matches)) {
    442                 if($ip == $ipPattern) return true;
    443             } else if(preg_match('/^([0-9]+\.[0-9]+\.[0-9]+)\/24$/', $ipPattern, $matches)
    444                     || preg_match('/^([0-9]+\.[0-9]+)\/16$/', $ipPattern, $matches)
    445                     || preg_match('/^([0-9]+)\/8$/', $ipPattern, $matches)) {
    446                 if(substr($ip, 0, strlen($matches[1])) == $matches[1]) return true;
    447             }
    448         }
    449         return false;
    450     }
    451
    452     /**
    453      * Add default records to database.
    454      *
    455      * This function is called whenever the database is built, after the
    456      * database tables have all been created.
    457      */
    458     public function requireDefaultRecords() {
    459         parent::requireDefaultRecords();
    460
    461         // Add default author group if no other group exists
    462         $allGroups = DataObject::get('Group');
    463         if(!$allGroups) {
    464             $authorGroup = new Group();
    465             $authorGroup->Code = 'content-authors';
    466             $authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors');
    467             $authorGroup->Sort = 1;
    468             $authorGroup->write();
    469             Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
    470             Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
    471             Permission::grant($authorGroup->ID, 'CMS_ACCESS_CommentAdmin');
    472             Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
    473             Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
    474         }
    475
    476         // Add default admin group if none with permission code ADMIN exists
    477         $adminGroups = Permission::get_groups_by_permission('ADMIN');
    478         if(!$adminGroups) {
    479             $adminGroup = new Group();
    480             $adminGroup->Code = 'administrators';
    481             $adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators');
    482             $adminGroup->Sort = 0;
    483             $adminGroup->write();
    484             Permission::grant($adminGroup->ID, 'ADMIN');
    485         }
    486
    487         // Members are populated through Member->requireDefaultRecords()
    488     }
    489 }
    490
    491 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -