Completed
Push — master ( 20efb0...a2cc06 )
by Hamish
29s
created

Group   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 493
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 493
rs 1.3043
wmc 80
lcom 1
cbo 30

20 Methods

Rating   Name   Duplication   Size   Complexity  
A populateDefaults() 0 5 2
A getAllChildren() 0 11 2
D getCMSFields() 0 145 16
A fieldLabels() 0 15 2
A Members() 0 21 3
A DirectMembers() 0 3 1
A collateFamilyIDs() 0 18 3
A collateAncestorIDs() 0 8 3
A cmsCleanup_parentChanged() 0 2 1
A stageChildren() 0 6 1
A getTreeTitle() 0 4 2
A setCode() 0 3 1
B validate() 0 24 4
A onBeforeWrite() 0 10 4
A onBeforeDelete() 0 13 3
C canEdit() 0 23 10
B canView() 0 12 8
B canDelete() 0 9 7
A AllChildrenIncludingDeleted() 0 14 4
B requireDefaultRecords() 0 30 3

How to fix   Complexity   

Complex Class

Complex classes like Group often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Group, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Security;
4
5
6
use SilverStripe\ORM\ArrayList;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\HasManyList;
9
use SilverStripe\ORM\ManyManyList;
10
use SilverStripe\ORM\UnsavedRelationList;
11
use Requirements;
12
use FieldList;
13
use TabSet;
14
use Tab;
15
use TextField;
16
use DropdownField;
17
use TextareaField;
18
use Config;
19
use GridFieldConfig_RelationEditor;
20
use GridFieldButtonRow;
21
use GridFieldExportButton;
22
use GridFieldPrintButton;
23
use GridField;
24
use HTMLEditorConfig;
25
use LiteralField;
26
use ListboxField;
27
use HiddenField;
28
use InvalidArgumentException;
29
use Convert;
30
31
/**
32
 * A security group.
33
 *
34
 * @package framework
35
 * @subpackage security
36
 *
37
 * @property string Title Name of the group
38
 * @property string Description Description of the group
39
 * @property string Code Group code
40
 * @property string Locked Boolean indicating whether group is locked in security panel
41
 * @property int Sort
42
 * @property string HtmlEditorConfig
43
 *
44
 * @property int ParentID ID of parent group
45
 *
46
 * @method Group Parent() Return parent group
47
 * @method HasManyList Permissions() List of group permissions
48
 * @method HasManyList Groups() List of child groups
49
 * @method ManyManyList Roles() List of PermissionRoles
50
 */
51
class Group extends DataObject {
52
53
	private static $db = array(
54
		"Title" => "Varchar(255)",
55
		"Description" => "Text",
56
		"Code" => "Varchar(255)",
57
		"Locked" => "Boolean",
58
		"Sort" => "Int",
59
		"HtmlEditorConfig" => "Text"
60
	);
61
62
	private static $has_one = array(
63
		"Parent" => "SilverStripe\\Security\\Group",
64
	);
65
66
	private static $has_many = array(
67
		"Permissions" => "SilverStripe\\Security\\Permission",
68
		"Groups" => "SilverStripe\\Security\\Group"
69
	);
70
71
	private static $many_many = array(
72
		"Members" => "SilverStripe\\Security\\Member",
73
		"Roles" => "SilverStripe\\Security\\PermissionRole",
74
	);
75
76
	private static $extensions = array(
77
		"SilverStripe\\ORM\\Hierarchy\\Hierarchy",
78
	);
79
80
	private static $table_name = "Group";
81
82
	public function populateDefaults() {
83
		parent::populateDefaults();
84
85
		if(!$this->Title) $this->Title = _t('SecurityAdmin.NEWGROUP',"New Group");
86
	}
87
88
	public function getAllChildren() {
89
		$doSet = new ArrayList();
90
91
		$children = Group::get()->filter("ParentID", $this->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...
92
		foreach($children as $child) {
93
			$doSet->push($child);
94
			$doSet->merge($child->getAllChildren());
95
		}
96
97
		return $doSet;
98
	}
99
100
	/**
101
	 * Caution: Only call on instances, not through a singleton.
102
	 * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
103
	 *
104
	 * @return FieldList
105
	 */
106
	public function getCMSFields() {
107
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/PermissionCheckboxSetField.js');
108
109
		$fields = new FieldList(
110
			new TabSet("Root",
111
				new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'),
112
					new TextField("Title", $this->fieldLabel('Title')),
113
					$parentidfield = DropdownField::create(						'ParentID',
114
						$this->fieldLabel('Parent'),
115
						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...
116
					)->setEmptyString(' '),
117
					new TextareaField('Description', $this->fieldLabel('Description'))
118
				),
119
120
				$permissionsTab = new Tab('Permissions', _t('SecurityAdmin.PERMISSIONS', 'Permissions'),
121
					$permissionsField = new PermissionCheckboxSetField(
122
						'Permissions',
123
						false,
124
						'SilverStripe\\Security\\Permission',
125
						'GroupID',
126
						$this
127
					)
128
				)
129
			)
130
		);
131
132
		$parentidfield->setDescription(
133
			_t('Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles')
134
		);
135
136
		// Filter permissions
137
		// TODO SecurityAdmin coupling, not easy to get to the form fields through GridFieldDetailForm
138
		$permissionsField->setHiddenPermissions((array)Config::inst()->get('SecurityAdmin', 'hidden_permissions'));
139
140
		if($this->ID) {
141
			$group = $this;
142
			$config = GridFieldConfig_RelationEditor::create();
143
			$config->addComponent(new GridFieldButtonRow('after'));
144
			$config->addComponents(new GridFieldExportButton('buttons-after-left'));
145
			$config->addComponents(new GridFieldPrintButton('buttons-after-left'));
146
			$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...
147
				->setResultsFormat('$Title ($Email)')->setSearchFields(array('FirstName', 'Surname', 'Email'));
148
			$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...
149
				->setValidator(Member_Validator::create())
150
				->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...
151
					$record = $form->getRecord();
152
					$groupsField = $form->Fields()->dataFieldByName('DirectGroups');
153
					if($groupsField) {
154
						// If new records are created in a group context,
155
						// set this group by default.
156
						if($record && !$record->ID) {
157
							$groupsField->setValue($group->ID);
158
						} elseif($record && $record->ID) {
159
							// TODO Mark disabled once chosen.js supports it
160
							// $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...
161
							$form->Fields()->replaceField('DirectGroups',
162
								$groupsField->performReadonlyTransformation());
163
						}
164
					}
165
				});
166
			$memberList = GridField::create('Members',false, $this->DirectMembers(), $config)
167
				->addExtraClass('members_grid');
168
			// @todo Implement permission checking on GridField
169
			//$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...
170
			$fields->addFieldToTab('Root.Members', $memberList);
171
		}
172
173
		// Only add a dropdown for HTML editor configurations if more than one is available.
174
		// Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
175
		$editorConfigMap = HTMLEditorConfig::get_available_configs_map();
176
		if(count($editorConfigMap) > 1) {
177
			$fields->addFieldToTab('Root.Permissions',
178
				new DropdownField(
179
					'HtmlEditorConfig',
180
					'HTML Editor Configuration',
181
					$editorConfigMap
182
				),
183
				'Permissions'
184
			);
185
		}
186
187
		if(!Permission::check('EDIT_PERMISSIONS')) {
188
			$fields->removeFieldFromTab('Root', 'Permissions');
189
		}
190
191
		// Only show the "Roles" tab if permissions are granted to edit them,
192
		// and at least one role exists
193
		if(Permission::check('APPLY_ROLES') && DataObject::get('SilverStripe\\Security\\PermissionRole')) {
194
			$fields->findOrMakeTab('Root.Roles', _t('SecurityAdmin.ROLES', 'Roles'));
195
			$fields->addFieldToTab('Root.Roles',
196
				new LiteralField(
197
					"",
198
					"<p>" .
199
					_t(
200
						'SecurityAdmin.ROLESDESCRIPTION',
201
						"Roles are predefined sets of permissions, and can be assigned to groups.<br />"
202
						. "They are inherited from parent groups if required."
203
					) . '<br />' .
204
					sprintf(
205
						'<a href="%s" class="add-role">%s</a>',
206
						singleton('SecurityAdmin')->Link('show/root#Root_Roles'),
207
						// TODO This should include #Root_Roles to switch directly to the tab,
208
						// but tabstrip.js doesn't display tabs when directly adressed through a URL pragma
209
						_t('Group.RolesAddEditLink', 'Manage roles')
210
					) .
211
					"</p>"
212
				)
213
			);
214
215
			// Add roles (and disable all checkboxes for inherited roles)
216
			$allRoles = PermissionRole::get();
217
			if(!Permission::check('ADMIN')) {
218
				$allRoles = $allRoles->filter("OnlyAdminCanApply", 0);
219
			}
220
			if($this->ID) {
221
				$groupRoles = $this->Roles();
222
				$inheritedRoles = new ArrayList();
223
				$ancestors = $this->getAncestors();
224
				foreach($ancestors as $ancestor) {
225
					$ancestorRoles = $ancestor->Roles();
226
					if($ancestorRoles) $inheritedRoles->merge($ancestorRoles);
227
				}
228
				$groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID');
229
				$inheritedRoleIDs = $inheritedRoles->column('ID');
230
			} else {
231
				$groupRoleIDs = array();
232
				$inheritedRoleIDs = array();
233
			}
234
235
			$rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray())
236
					->setDefaultItems($groupRoleIDs)
237
					->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group'))
238
					->setDisabledItems($inheritedRoleIDs);
239
			if(!$allRoles->Count()) {
240
				$rolesField->setAttribute('data-placeholder', _t('Group.NoRoles', 'No roles found'));
241
			}
242
			$fields->addFieldToTab('Root.Roles', $rolesField);
243
		}
244
245
		$fields->push($idField = new HiddenField("ID"));
246
247
		$this->extend('updateCMSFields', $fields);
248
249
		return $fields;
250
	}
251
252
	/**
253
	 * @param bool $includerelations Indicate if the labels returned include relation fields
254
	 * @return array
255
	 */
256
	public function fieldLabels($includerelations = true) {
257
		$labels = parent::fieldLabels($includerelations);
258
		$labels['Title'] = _t('SecurityAdmin.GROUPNAME', 'Group name');
259
		$labels['Description'] = _t('Group.Description', 'Description');
260
		$labels['Code'] = _t('Group.Code', 'Group Code', 'Programmatical code identifying a group');
261
		$labels['Locked'] = _t('Group.Locked', 'Locked?', 'Group is locked in the security administration area');
262
		$labels['Sort'] = _t('Group.Sort', 'Sort Order');
263
		if($includerelations){
264
			$labels['Parent'] = _t('Group.Parent', 'Parent Group', 'One group has one parent group');
265
			$labels['Permissions'] = _t('Group.has_many_Permissions', 'Permissions', 'One group has many permissions');
266
			$labels['Members'] = _t('Group.many_many_Members', 'Members', 'One group has many members');
267
		}
268
269
		return $labels;
270
	}
271
272
	/**
273
	 * Get many-many relation to {@link Member},
274
	 * including all members which are "inherited" from children groups of this record.
275
	 * See {@link DirectMembers()} for retrieving members without any inheritance.
276
	 *
277
	 * @param String $filter
278
	 * @return ManyManyList
279
	 */
280
	public function Members($filter = '') {
281
		// First get direct members as a base result
282
		$result = $this->DirectMembers();
283
284
		// Unsaved group cannot have child groups because its ID is still 0.
285
		if(!$this->exists()) return $result;
286
287
		// Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
288
		// Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
289
		// ones.
290
		if(!($result instanceof UnsavedRelationList)) {
291
			$result = $result->alterDataQuery(function($query){
292
				$query->removeFilterOn('Group_Members');
293
			});
294
		}
295
		// Now set all children groups as a new foreign key
296
		$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...
297
		$result = $result->forForeignID($groups->column('ID'))->where($filter);
298
299
		return $result;
300
	}
301
302
	/**
303
	 * Return only the members directly added to this group
304
	 */
305
	public function DirectMembers() {
306
		return $this->getManyManyComponents('Members');
307
	}
308
309
	/**
310
	 * Return a set of this record's "family" of IDs - the IDs of
311
	 * this record and all its descendants.
312
	 *
313
	 * @return array
314
	 */
315
	public function collateFamilyIDs() {
316
		if (!$this->exists()) {
317
			throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group.");
318
		}
319
320
		$familyIDs = array();
321
		$chunkToAdd = array($this->ID);
322
323
		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...
324
			$familyIDs = array_merge($familyIDs,$chunkToAdd);
325
326
			// Get the children of *all* the groups identified in the previous chunk.
327
			// This minimises the number of SQL queries necessary
328
			$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...
329
		}
330
331
		return $familyIDs;
332
	}
333
334
	/**
335
	 * Returns an array of the IDs of this group and all its parents
336
	 */
337
	public function collateAncestorIDs() {
338
		$parent = $this;
339
		while(isset($parent) && $parent instanceof Group) {
340
			$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...
341
			$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...
342
		}
343
		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...
344
	}
345
346
	/**
347
	 * This isn't a decendant of SiteTree, but needs this in case
348
	 * the group is "reorganised";
349
	 */
350
	public function cmsCleanup_parentChanged() {
351
	}
352
353
	/**
354
	 * Override this so groups are ordered in the CMS
355
	 */
356
	public function stageChildren() {
357
		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...
358
			->filter("ParentID", $this->ID)
359
			->exclude("ID", $this->ID)
360
			->sort('"Sort"');
361
	}
362
363
	public function getTreeTitle() {
364
		if($this->hasMethod('alternateTreeTitle')) return $this->alternateTreeTitle();
365
		else return htmlspecialchars($this->Title, ENT_QUOTES);
366
	}
367
368
	/**
369
	 * Overloaded to ensure the code is always descent.
370
	 *
371
	 * @param string
372
	 */
373
	public function setCode($val){
374
		$this->setField("Code", Convert::raw2url($val));
375
	}
376
377
	public function validate() {
378
		$result = parent::validate();
379
380
		// Check if the new group hierarchy would add certain "privileged permissions",
381
		// and require an admin to perform this change in case it does.
382
		// This prevents "sub-admin" users with group editing permissions to increase their privileges.
383
		if($this->Parent()->exists() && !Permission::check('ADMIN')) {
384
			$inheritedCodes = Permission::get()
385
				->filter('GroupID', $this->Parent()->collateAncestorIDs())
386
				->column('Code');
387
			$privilegedCodes = Config::inst()->get('SilverStripe\\Security\\Permission', 'privileged_permissions');
388
			if(array_intersect($inheritedCodes, $privilegedCodes)) {
389
				$result->error(sprintf(
390
					_t(
391
						'Group.HierarchyPermsError',
392
						'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)'
393
					),
394
					$this->Parent()->Title
395
				));
396
			}
397
		}
398
399
		return $result;
400
	}
401
402
	public function onBeforeWrite() {
403
		parent::onBeforeWrite();
404
405
		// Only set code property when the group has a custom title, and no code exists.
406
		// The "Code" attribute is usually treated as a more permanent identifier than database IDs
407
		// in custom application logic, so can't be changed after its first set.
408
		if(!$this->Code && $this->Title != _t('SecurityAdmin.NEWGROUP',"New Group")) {
409
			if(!$this->Code) $this->setCode($this->Title);
410
		}
411
	}
412
413
	public function onBeforeDelete() {
414
		parent::onBeforeDelete();
415
416
		// if deleting this group, delete it's children as well
417
		foreach($this->Groups() as $group) {
418
			$group->delete();
419
		}
420
421
		// Delete associated permissions
422
		foreach($this->Permissions() as $permission) {
423
			$permission->delete();
424
		}
425
	}
426
427
	/**
428
	 * Checks for permission-code CMS_ACCESS_SecurityAdmin.
429
	 * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
430
	 *
431
	 * @param $member Member
432
	 * @return boolean
433
	 */
434
	public function canEdit($member = null) {
435
		if(!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) $member = Member::currentUser();
436
437
		// extended access checks
438
		$results = $this->extend('canEdit', $member);
439
		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...
440
441
		if(
442
			// either we have an ADMIN
443
			(bool)Permission::checkMember($member, "ADMIN")
444
			|| (
445
				// or a privileged CMS user and a group without ADMIN permissions.
446
				// without this check, a user would be able to add himself to an administrators group
447
				// with just access to the "Security" admin interface
448
				Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
449
				!Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists()
450
			)
451
		) {
452
			return true;
453
		}
454
455
		return false;
456
	}
457
458
	/**
459
	 * Checks for permission-code CMS_ACCESS_SecurityAdmin.
460
	 *
461
	 * @param $member Member
462
	 * @return boolean
463
	 */
464
	public function canView($member = null) {
465
		if(!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) $member = Member::currentUser();
466
467
		// extended access checks
468
		$results = $this->extend('canView', $member);
469
		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...
470
471
		// user needs access to CMS_ACCESS_SecurityAdmin
472
		if(Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) return true;
473
474
		return false;
475
	}
476
477
	public function canDelete($member = null) {
478
		if(!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) $member = Member::currentUser();
479
480
		// extended access checks
481
		$results = $this->extend('canDelete', $member);
482
		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...
483
484
		return $this->canEdit($member);
485
	}
486
487
	/**
488
	 * Returns all of the children for the CMS Tree.
489
	 * Filters to only those groups that the current user can edit
490
	 */
491
	public function AllChildrenIncludingDeleted() {
492
		$extInstance = $this->getExtensionInstance('SilverStripe\\ORM\\Hierarchy\\Hierarchy');
493
		$extInstance->setOwner($this);
494
		$children = $extInstance->AllChildrenIncludingDeleted();
495
		$extInstance->clearOwner();
496
497
		$filteredChildren = new ArrayList();
498
499
		if($children) foreach($children as $child) {
500
			if($child->canView()) $filteredChildren->push($child);
501
		}
502
503
		return $filteredChildren;
504
	}
505
506
	/**
507
	 * Add default records to database.
508
	 *
509
	 * This function is called whenever the database is built, after the
510
	 * database tables have all been created.
511
	 */
512
	public function requireDefaultRecords() {
513
		parent::requireDefaultRecords();
514
515
		// Add default author group if no other group exists
516
		$allGroups = DataObject::get('SilverStripe\\Security\\Group');
517
		if(!$allGroups->count()) {
518
			$authorGroup = new Group();
519
			$authorGroup->Code = 'content-authors';
520
			$authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors');
521
			$authorGroup->Sort = 1;
522
			$authorGroup->write();
523
			Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
524
			Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
525
			Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
526
			Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
527
		}
528
529
		// Add default admin group if none with permission code ADMIN exists
530
		$adminGroups = Permission::get_groups_by_permission('ADMIN');
531
		if(!$adminGroups->count()) {
532
			$adminGroup = new Group();
533
			$adminGroup->Code = 'administrators';
534
			$adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators');
535
			$adminGroup->Sort = 0;
536
			$adminGroup->write();
537
			Permission::grant($adminGroup->ID, 'ADMIN');
538
		}
539
540
		// Members are populated through Member->requireDefaultRecords()
541
	}
542
543
}
544