Completed
Push — 3.2 ( 55e1b6...66b3a6 )
by Damian
12:25
created

Group::Members()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 33
rs 6.7273
cc 7
eloc 16
nc 8
nop 4
1
<?php
2
/**
3
 * A security group.
4
 *
5
 * @package framework
6
 * @subpackage security
7
 *
8
 * @property string Title Name of the group
9
 * @property string Description Description of the group
10
 * @property string Code Group code
11
 * @property string Locked Boolean indicating whether group is locked in security panel
12
 * @property int Sort
13
 * @property string HtmlEditorConfig
14
 *
15
 * @property int ParentID ID of parent group
16
 *
17
 * @method Group Parent() Return parent group
18
 * @method HasManyList Permissions() List of group permissions
19
 * @method HasManyList Groups() List of child groups
20
 * @method ManyManyList Roles() List of PermissionRoles
21
 */
22
class Group extends DataObject {
23
24
	private static $db = array(
25
		"Title" => "Varchar(255)",
26
		"Description" => "Text",
27
		"Code" => "Varchar(255)",
28
		"Locked" => "Boolean",
29
		"Sort" => "Int",
30
		"HtmlEditorConfig" => "Text"
31
	);
32
33
	private static $has_one = array(
34
		"Parent" => "Group",
35
	);
36
37
	private static $has_many = array(
38
		"Permissions" => "Permission",
39
		"Groups" => "Group"
40
	);
41
42
	private static $many_many = array(
43
		"Members" => "Member",
44
		"Roles" => "PermissionRole",
45
	);
46
47
	private static $extensions = array(
48
		"Hierarchy",
49
	);
50
51
	public function populateDefaults() {
52
		parent::populateDefaults();
53
54
		if(!$this->Title) $this->Title = _t('SecurityAdmin.NEWGROUP',"New Group");
55
	}
56
57
	public function getAllChildren() {
58
		$doSet = new ArrayList();
59
60
		$children = DataObject::get('Group')->filter("ParentID", $this->ID);
61
		foreach($children as $child) {
62
			$doSet->push($child);
63
			$doSet->merge($child->getAllChildren());
64
		}
65
66
		return $doSet;
67
	}
68
69
	/**
70
	 * Caution: Only call on instances, not through a singleton.
71
	 * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
72
	 *
73
	 * @return FieldList
74
	 */
75
	public function getCMSFields() {
76
		Requirements::javascript(FRAMEWORK_DIR . '/javascript/PermissionCheckboxSetField.js');
77
78
		$fields = new FieldList(
79
			new TabSet("Root",
80
				new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'),
81
					new TextField("Title", $this->fieldLabel('Title')),
82
					$parentidfield = DropdownField::create(						'ParentID',
83
						$this->fieldLabel('Parent'),
84
						Group::get()->exclude('ID', $this->ID)->map('ID', 'Breadcrumbs')
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
85
					)->setEmptyString(' '),
86
					new TextareaField('Description', $this->fieldLabel('Description'))
87
				),
88
89
				$permissionsTab = new Tab('Permissions', _t('SecurityAdmin.PERMISSIONS', 'Permissions'),
90
					$permissionsField = new PermissionCheckboxSetField(
91
						'Permissions',
92
						false,
93
						'Permission',
94
						'GroupID',
95
						$this
96
					)
97
				)
98
			)
99
		);
100
101
		$parentidfield->setDescription(
102
			_t('Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles')
103
		);
104
105
		// Filter permissions
106
		// TODO SecurityAdmin coupling, not easy to get to the form fields through GridFieldDetailForm
107
		$permissionsField->setHiddenPermissions((array)Config::inst()->get('SecurityAdmin', 'hidden_permissions'));
108
109
		if($this->ID) {
110
			$group = $this;
111
			$config = new GridFieldConfig_RelationEditor();
112
			$config->addComponent(new GridFieldButtonRow('after'));
113
			$config->addComponents(new GridFieldExportButton('buttons-after-left'));
114
			$config->addComponents(new GridFieldPrintButton('buttons-after-left'));
115
			$config->getComponentByType('GridFieldAddExistingAutocompleter')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setResultsFormat() does only exist in the following implementations of said interface: GridFieldAddExistingAutocompleter.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
116
				->setResultsFormat('$Title ($Email)')->setSearchFields(array('FirstName', 'Surname', 'Email'));
117
			$config->getComponentByType('GridFieldDetailForm')
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setValidator() does only exist in the following implementations of said interface: GridFieldDetailForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
118
				->setValidator(new Member_Validator())
119
				->setItemEditFormCallback(function($form, $component) use($group) {
0 ignored issues
show
Unused Code introduced by
The parameter $component is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
120
					$record = $form->getRecord();
121
					$groupsField = $form->Fields()->dataFieldByName('DirectGroups');
122
					if($groupsField) {
123
						// If new records are created in a group context,
124
						// set this group by default.
125
						if($record && !$record->ID) {
126
							$groupsField->setValue($group->ID);
127
						} elseif($record && $record->ID) {
128
							// TODO Mark disabled once chosen.js supports it
129
							// $groupsField->setDisabledItems(array($group->ID));
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
130
							$form->Fields()->replaceField('DirectGroups',
131
								$groupsField->performReadonlyTransformation());
132
						}
133
					}
134
				});
135
			$memberList = GridField::create('Members',false, $this->DirectMembers(), $config)
136
				->addExtraClass('members_grid');
137
			// @todo Implement permission checking on GridField
138
			//$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
78% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
139
			$fields->addFieldToTab('Root.Members', $memberList);
140
		}
141
142
		// Only add a dropdown for HTML editor configurations if more than one is available.
143
		// Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
144
		$editorConfigMap = HtmlEditorConfig::get_available_configs_map();
145
		if(count($editorConfigMap) > 1) {
146
			$fields->addFieldToTab('Root.Permissions',
147
				new DropdownField(
148
					'HtmlEditorConfig',
149
					'HTML Editor Configuration',
150
					$editorConfigMap
151
				),
152
				'Permissions'
153
			);
154
		}
155
156
		if(!Permission::check('EDIT_PERMISSIONS')) {
157
			$fields->removeFieldFromTab('Root', 'Permissions');
158
		}
159
160
		// Only show the "Roles" tab if permissions are granted to edit them,
161
		// and at least one role exists
162
		if(Permission::check('APPLY_ROLES') && DataObject::get('PermissionRole')) {
163
			$fields->findOrMakeTab('Root.Roles', _t('SecurityAdmin.ROLES', 'Roles'));
164
			$fields->addFieldToTab('Root.Roles',
165
				new LiteralField(
166
					"",
167
					"<p>" .
168
					_t(
169
						'SecurityAdmin.ROLESDESCRIPTION',
170
						"Roles are predefined sets of permissions, and can be assigned to groups.<br />"
171
						. "They are inherited from parent groups if required."
172
					) . '<br />' .
173
					sprintf(
174
						'<a href="%s" class="add-role">%s</a>',
175
						singleton('SecurityAdmin')->Link('show/root#Root_Roles'),
176
						// TODO This should include #Root_Roles to switch directly to the tab,
177
						// but tabstrip.js doesn't display tabs when directly adressed through a URL pragma
178
						_t('Group.RolesAddEditLink', 'Manage roles')
179
					) .
180
					"</p>"
181
				)
182
			);
183
184
			// Add roles (and disable all checkboxes for inherited roles)
185
			$allRoles = PermissionRole::get();
186
			if(Permission::check('ADMIN')) {
187
				$allRoles = $allRoles->filter("OnlyAdminCanApply", 0);
188
			}
189
			if($this->ID) {
190
				$groupRoles = $this->Roles();
191
				$inheritedRoles = new ArrayList();
192
				$ancestors = $this->getAncestors();
193
				foreach($ancestors as $ancestor) {
194
					$ancestorRoles = $ancestor->Roles();
195
					if($ancestorRoles) $inheritedRoles->merge($ancestorRoles);
196
				}
197
				$groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID');
198
				$inheritedRoleIDs = $inheritedRoles->column('ID');
199
			} else {
200
				$groupRoleIDs = array();
201
				$inheritedRoleIDs = array();
202
			}
203
204
			$rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray())
205
					->setMultiple(true)
206
					->setDefaultItems($groupRoleIDs)
207
					->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group'))
208
					->setDisabledItems($inheritedRoleIDs);
209
			if(!$allRoles->Count()) {
210
				$rolesField->setAttribute('data-placeholder', _t('Group.NoRoles', 'No roles found'));
211
			}
212
			$fields->addFieldToTab('Root.Roles', $rolesField);
213
		}
214
215
		$fields->push($idField = new HiddenField("ID"));
216
217
		$this->extend('updateCMSFields', $fields);
218
219
		return $fields;
220
	}
221
222
	/**
223
	 *
224
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
225
	 *
226
	 */
227
	public function fieldLabels($includerelations = true) {
228
		$labels = parent::fieldLabels($includerelations);
229
		$labels['Title'] = _t('SecurityAdmin.GROUPNAME', 'Group name');
230
		$labels['Description'] = _t('Group.Description', 'Description');
231
		$labels['Code'] = _t('Group.Code', 'Group Code', 'Programmatical code identifying a group');
232
		$labels['Locked'] = _t('Group.Locked', 'Locked?', 'Group is locked in the security administration area');
233
		$labels['Sort'] = _t('Group.Sort', 'Sort Order');
234
		if($includerelations){
235
			$labels['Parent'] = _t('Group.Parent', 'Parent Group', 'One group has one parent group');
236
			$labels['Permissions'] = _t('Group.has_many_Permissions', 'Permissions', 'One group has many permissions');
237
			$labels['Members'] = _t('Group.many_many_Members', 'Members', 'One group has many members');
238
		}
239
240
		return $labels;
241
	}
242
243
	/**
244
	 * Get many-many relation to {@link Member},
245
	 * including all members which are "inherited" from children groups of this record.
246
	 * See {@link DirectMembers()} for retrieving members without any inheritance.
247
	 *
248
	 * @param string $filter
249
	 * @return ManyManyList
250
	 */
251
	public function Members($filter = "", $sort = "", $join = "", $limit = "") {
252
		if($sort || $join || $limit) {
253
			Deprecation::notice('4.0',
254
				"The sort, join, and limit arguments are deprecated, use sort(), join() and limit() on the resulting"
255
				. " DataList instead.");
256
		}
257
258
		if($join) {
259
			throw new \InvalidArgumentException(
260
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
261
			);
262
		}
263
264
		// First get direct members as a base result
265
		$result = $this->DirectMembers();
266
267
		// Unsaved group cannot have child groups because its ID is still 0.
268
		if(!$this->exists()) return $result;
269
270
		// Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
271
		// Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
272
		// ones.
273
		if(!($result instanceof UnsavedRelationList)) {
274
			$result = $result->alterDataQuery(function($query){
275
				$query->removeFilterOn('Group_Members');
276
			});
277
		}
278
		// Now set all children groups as a new foreign key
279
		$groups = Group::get()->byIDs($this->collateFamilyIDs());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
280
		$result = $result->forForeignID($groups->column('ID'))->where($filter)->sort($sort)->limit($limit);
281
282
		return $result;
283
	}
284
285
	/**
286
	 * Return only the members directly added to this group
287
	 */
288
	public function DirectMembers() {
289
		return $this->getManyManyComponents('Members');
290
	}
291
292
	/**
293
	 * Return a set of this record's "family" of IDs - the IDs of
294
	 * this record and all its descendants.
295
	 *
296
	 * @return array
297
	 */
298
	public function collateFamilyIDs() {
299
		if (!$this->exists()) {
300
			throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group.");
301
		}
302
303
		$familyIDs = array();
304
		$chunkToAdd = array($this->ID);
305
306
		while($chunkToAdd) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chunkToAdd of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
307
			$familyIDs = array_merge($familyIDs,$chunkToAdd);
308
309
			// Get the children of *all* the groups identified in the previous chunk.
310
			// This minimises the number of SQL queries necessary
311
			$chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
312
		}
313
314
		return $familyIDs;
315
	}
316
317
	/**
318
	 * Returns an array of the IDs of this group and all its parents
319
	 */
320
	public function collateAncestorIDs() {
321
		$parent = $this;
322
		while(isset($parent) && $parent instanceof Group) {
323
			$items[] = $parent->ID;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$items was never initialized. Although not strictly required by PHP, it is generally a good practice to add $items = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
324
			$parent = $parent->Parent;
0 ignored issues
show
Bug introduced by
The property Parent does not seem to exist. Did you mean ParentID?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
325
		}
326
		return $items;
0 ignored issues
show
Bug introduced by
The variable $items does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
327
	}
328
329
	/**
330
	 * This isn't a decendant of SiteTree, but needs this in case
331
	 * the group is "reorganised";
332
	 */
333
	public function cmsCleanup_parentChanged() {
334
	}
335
336
	/**
337
	 * Override this so groups are ordered in the CMS
338
	 */
339
	public function stageChildren() {
340
		return Group::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
341
			->filter("ParentID", $this->ID)
342
			->exclude("ID", $this->ID)
343
			->sort('"Sort"');
344
	}
345
346
	public function getTreeTitle() {
347
		if($this->hasMethod('alternateTreeTitle')) return $this->alternateTreeTitle();
348
		else return htmlspecialchars($this->Title, ENT_QUOTES);
349
	}
350
351
	/**
352
	 * Overloaded to ensure the code is always descent.
353
	 *
354
	 * @param string
355
	 */
356
	public function setCode($val){
357
		$this->setField("Code", Convert::raw2url($val));
358
	}
359
360
	protected function validate() {
361
		$result = parent::validate();
362
363
		// Check if the new group hierarchy would add certain "privileged permissions",
364
		// and require an admin to perform this change in case it does.
365
		// This prevents "sub-admin" users with group editing permissions to increase their privileges.
366
		if($this->Parent()->exists() && !Permission::check('ADMIN')) {
367
			$inheritedCodes = Permission::get()
368
				->filter('GroupID', $this->Parent()->collateAncestorIDs())
369
				->column('Code');
370
			$privilegedCodes = Config::inst()->get('Permission', 'privileged_permissions');
371
			if(array_intersect($inheritedCodes, $privilegedCodes)) {
372
				$result->error(sprintf(
373
					_t(
374
						'Group.HierarchyPermsError',
375
						'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)'
376
					),
377
					$this->Parent()->Title
378
				));
379
			}
380
		}
381
382
		return $result;
383
	}
384
385
	public function onBeforeWrite() {
386
		parent::onBeforeWrite();
387
388
		// Only set code property when the group has a custom title, and no code exists.
389
		// The "Code" attribute is usually treated as a more permanent identifier than database IDs
390
		// in custom application logic, so can't be changed after its first set.
391
		if(!$this->Code && $this->Title != _t('SecurityAdmin.NEWGROUP',"New Group")) {
392
			if(!$this->Code) $this->setCode($this->Title);
393
		}
394
	}
395
396
	public function onBeforeDelete() {
397
		parent::onBeforeDelete();
398
399
		// if deleting this group, delete it's children as well
400
		foreach($this->Groups() as $group) {
401
			$group->delete();
402
		}
403
404
		// Delete associated permissions
405
		foreach($this->Permissions() as $permission) {
406
			$permission->delete();
407
		}
408
	}
409
410
	/**
411
	 * Checks for permission-code CMS_ACCESS_SecurityAdmin.
412
	 * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
413
	 *
414
	 * @param $member Member
415
	 * @return boolean
416
	 */
417
	public function canEdit($member = null) {
418
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
419
420
		// extended access checks
421
		$results = $this->extend('canEdit', $member);
422
		if($results && is_array($results)) if(!min($results)) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
423
424
		if(
425
			// either we have an ADMIN
426
			(bool)Permission::checkMember($member, "ADMIN")
427
			|| (
428
				// or a privileged CMS user and a group without ADMIN permissions.
429
				// without this check, a user would be able to add himself to an administrators group
430
				// with just access to the "Security" admin interface
431
				Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
432
				!Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists()
433
			)
434
		) {
435
			return true;
436
		}
437
438
		return false;
439
	}
440
441
	/**
442
	 * Checks for permission-code CMS_ACCESS_SecurityAdmin.
443
	 *
444
	 * @param $member Member
445
	 * @return boolean
446
	 */
447
	public function canView($member = null) {
448
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
449
450
		// extended access checks
451
		$results = $this->extend('canView', $member);
452
		if($results && is_array($results)) if(!min($results)) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
453
454
		// user needs access to CMS_ACCESS_SecurityAdmin
455
		if(Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) return true;
456
457
		return false;
458
	}
459
460
	public function canDelete($member = null) {
461
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
462
463
		// extended access checks
464
		$results = $this->extend('canDelete', $member);
465
		if($results && is_array($results)) if(!min($results)) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
466
467
		return $this->canEdit($member);
468
	}
469
470
	/**
471
	 * Returns all of the children for the CMS Tree.
472
	 * Filters to only those groups that the current user can edit
473
	 */
474
	public function AllChildrenIncludingDeleted() {
475
		$extInstance = $this->getExtensionInstance('Hierarchy');
476
		$extInstance->setOwner($this);
477
		$children = $extInstance->AllChildrenIncludingDeleted();
478
		$extInstance->clearOwner();
479
480
		$filteredChildren = new ArrayList();
481
482
		if($children) foreach($children as $child) {
483
			if($child->canView()) $filteredChildren->push($child);
484
		}
485
486
		return $filteredChildren;
487
	}
488
489
	/**
490
	 * Add default records to database.
491
	 *
492
	 * This function is called whenever the database is built, after the
493
	 * database tables have all been created.
494
	 */
495
	public function requireDefaultRecords() {
496
		parent::requireDefaultRecords();
497
498
		// Add default author group if no other group exists
499
		$allGroups = DataObject::get('Group');
500
		if(!$allGroups->count()) {
501
			$authorGroup = new Group();
502
			$authorGroup->Code = 'content-authors';
503
			$authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors');
504
			$authorGroup->Sort = 1;
505
			$authorGroup->write();
506
			Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
507
			Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
508
			Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
509
			Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
510
		}
511
512
		// Add default admin group if none with permission code ADMIN exists
513
		$adminGroups = Permission::get_groups_by_permission('ADMIN');
514
		if(!$adminGroups->count()) {
515
			$adminGroup = new Group();
516
			$adminGroup->Code = 'administrators';
517
			$adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators');
518
			$adminGroup->Sort = 0;
519
			$adminGroup->write();
520
			Permission::grant($adminGroup->ID, 'ADMIN');
521
		}
522
523
		// Members are populated through Member->requireDefaultRecords()
524
	}
525
526
}
527