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

Member   F

Complexity

Total Complexity 228

Size/Duplication

Total Lines 1701
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 38

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 1701
rs 0.5217
wmc 228
lcom 1
cbo 38

66 Methods

Rating   Name   Duplication   Size   Complexity  
A set_session_regenerate_id() 0 4 1
A populateDefaults() 0 4 1
A requireDefaultRecords() 0 5 1
B default_admin() 0 33 4
A set_login_marker_cookie() 0 4 1
C checkPassword() 0 29 7
A isDefaultAdmin() 0 4 2
A canLogIn() 0 18 2
A isLockedOut() 0 3 2
A session_regenerate_id() 0 13 4
A get_unique_identifier_field() 0 4 1
A set_unique_identifier_field() 0 4 1
A set_password_validator() 0 3 1
A password_validator() 0 3 1
A set_password_expiry() 0 4 1
A lock_out_after_incorrect_logins() 0 4 1
A isPasswordExpired() 0 4 2
B logIn() 0 39 4
A regenerateTempID() 0 8 2
A logged_in_session_exists() 0 9 4
C autoLogin() 0 70 17
B logOut() 0 27 2
A encryptWithUserSettings() 0 14 3
A generateAutologinTokenAndStoreHash() 0 16 2
A validateAutoLoginToken() 0 5 1
A member_from_autologinhash() 0 12 3
A member_from_tempid() 0 11 2
A getMemberFormFields() 0 18 1
B getMemberPasswordField() 0 23 4
A getValidator() 0 7 1
A currentUser() 0 7 2
A currentUserID() 0 9 4
A create_new_password() 0 20 3
D onBeforeWrite() 0 83 17
A onAfterWrite() 0 9 2
A onAfterDelete() 0 6 1
A deletePasswordLogs() 0 7 2
A onChangeGroups() 0 11 4
A inGroups() 0 7 4
C inGroup() 0 22 9
A addToGroupByCode() 0 18 3
A removeFromGroupByCode() 0 7 2
A set_title_columns() 0 4 2
C getTitle() 0 23 8
A get_title_sql() 0 21 4
A getName() 0 3 2
A setName() 0 5 1
A splitName() 0 3 1
A getDateFormat() 0 7 2
A getTimeFormat() 0 7 2
A Groups() 0 8 1
A DirectGroups() 0 3 1
C map_in_groups() 0 27 7
C mapInCMSGroups() 0 46 9
A memberNotInGroups() 0 12 4
B getCMSFields() 0 108 6
A fieldLabels() 0 18 2
B canView() 0 22 5
C canEdit() 0 27 7
C canDelete() 0 31 8
B validate() 0 17 10
A changePassword() 0 11 2
A registerFailedLogin() 0 14 3
A registerSuccessfulLogin() 0 7 2
B getHtmlEditorConfigForCMS() 0 18 6
A get_template_global_variables() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Member 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 Member, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\ORM\SS_Map;
6
use SilverStripe\ORM\ValidationResult;
7
use SilverStripe\ORM\FieldType\DBDatetime;
8
use SilverStripe\ORM\DB;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\ValidationException;
11
use SilverStripe\ORM\SS_List;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\Queries\SQLSelect;
14
use SilverStripe\ORM\ManyManyList;
15
use SilverStripe\MSSQL\MSSQLDatabase;
16
use TemplateGlobalProvider;
17
use Deprecation;
18
use i18n;
19
use Director;
20
use Session;
21
use Cookie;
22
use Config;
23
use SapphireTest;
24
use DateTime;
25
use DropdownField;
26
use ConfirmedPasswordField;
27
use Injector;
28
use TestMailer;
29
use Email;
30
use FieldList;
31
use ListboxField;
32
use Zend_Locale_Format;
33
use Zend_Locale;
34
use Zend_Date;
35
use MemberDatetimeOptionsetField;
36
use HTMLEditorConfig;
37
use RequiredFields;
38
use GridFieldDetailForm_ItemRequest;
39
40
/**
41
 * The member class which represents the users of the system
42
 *
43
 * @package framework
44
 * @subpackage security
45
 *
46
 * @property string $FirstName
47
 * @property string $Surname
48
 * @property string $Email
49
 * @property string $Password
50
 * @property string $TempIDHash
51
 * @property string $TempIDExpired
52
 * @property string $AutoLoginHash
53
 * @property string $AutoLoginExpired
54
 * @property string $PasswordEncryption
55
 * @property string $Salt
56
 * @property string $PasswordExpiry
57
 * @property string $LockedOutUntil
58
 * @property string $Locale
59
 * @property int $FailedLoginCount
60
 * @property string $DateFormat
61
 * @property string $TimeFormat
62
 */
63
class Member extends DataObject implements TemplateGlobalProvider {
64
65
	private static $db = array(
66
		'FirstName' => 'Varchar',
67
		'Surname' => 'Varchar',
68
		'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
69
		'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
70
		'TempIDExpired' => 'Datetime', // Expiry of temp login
71
		'Password' => 'Varchar(160)',
72
		'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
73
		'AutoLoginExpired' => 'Datetime',
74
		// This is an arbitrary code pointing to a PasswordEncryptor instance,
75
		// not an actual encryption algorithm.
76
		// Warning: Never change this field after its the first password hashing without
77
		// providing a new cleartext password as well.
78
		'PasswordEncryption' => "Varchar(50)",
79
		'Salt' => 'Varchar(50)',
80
		'PasswordExpiry' => 'Date',
81
		'LockedOutUntil' => 'Datetime',
82
		'Locale' => 'Varchar(6)',
83
		// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
84
		'FailedLoginCount' => 'Int',
85
		// In ISO format
86
		'DateFormat' => 'Varchar(30)',
87
		'TimeFormat' => 'Varchar(30)',
88
	);
89
90
	private static $belongs_many_many = array(
91
		'Groups' => 'SilverStripe\\Security\\Group',
92
	);
93
94
	private static $has_many = array(
95
		'LoggedPasswords' => 'SilverStripe\\Security\\MemberPassword',
96
		'RememberLoginHashes' => 'SilverStripe\\Security\\RememberLoginHash'
97
	);
98
99
	private static $table_name = "Member";
100
101
	private static $default_sort = '"Surname", "FirstName"';
102
103
	private static $indexes = array(
104
		'Email' => true,
105
		//Removed due to duplicate null values causing MSSQL problems
106
		//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true)
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
107
	);
108
109
	/**
110
	 * @config
111
	 * @var boolean
112
	 */
113
	private static $notify_password_change = false;
114
115
	/**
116
	 * All searchable database columns
117
	 * in this object, currently queried
118
	 * with a "column LIKE '%keywords%'
119
	 * statement.
120
	 *
121
	 * @var array
122
	 * @todo Generic implementation of $searchable_fields on DataObject,
123
	 * with definition for different searching algorithms
124
	 * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
125
	 */
126
	private static $searchable_fields = array(
127
		'FirstName',
128
		'Surname',
129
		'Email',
130
	);
131
132
	private static $summary_fields = array(
133
		'FirstName',
134
		'Surname',
135
		'Email',
136
	);
137
138
	/**
139
	 * Internal-use only fields
140
	 *
141
	 * @config
142
	 * @var array
143
	 */
144
	private static $hidden_fields = array(
145
		'AutoLoginHash',
146
		'AutoLoginExpired',
147
		'PasswordEncryption',
148
		'PasswordExpiry',
149
		'LockedOutUntil',
150
		'TempIDHash',
151
		'TempIDExpired',
152
		'Salt',
153
	);
154
155
	/**
156
	 * @config
157
	 * @var array See {@link set_title_columns()}
158
	 */
159
	private static $title_format = null;
160
161
	/**
162
	 * The unique field used to identify this member.
163
	 * By default, it's "Email", but another common
164
	 * field could be Username.
165
	 *
166
	 * @config
167
	 * @var string
168
	 */
169
	private static $unique_identifier_field = 'Email';
170
171
	/**
172
	 * Object for validating user's password
173
	 *
174
	 * @config
175
	 * @var PasswordValidator
176
	 */
177
	private static $password_validator = null;
178
179
	/**
180
	 * @config
181
	 * The number of days that a password should be valid for.
182
	 * By default, this is null, which means that passwords never expire
183
	 */
184
	private static $password_expiry_days = null;
185
186
	/**
187
	 * @config
188
	 * @var Int Number of incorrect logins after which
189
	 * the user is blocked from further attempts for the timespan
190
	 * defined in {@link $lock_out_delay_mins}.
191
	 */
192
	private static $lock_out_after_incorrect_logins = 10;
193
194
	/**
195
	 * @config
196
	 * @var integer Minutes of enforced lockout after incorrect password attempts.
197
	 * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
198
	 */
199
	private static $lock_out_delay_mins = 15;
200
201
	/**
202
	 * @config
203
	 * @var String If this is set, then a session cookie with the given name will be set on log-in,
204
	 * and cleared on logout.
205
	 */
206
	private static $login_marker_cookie = null;
207
208
	/**
209
	 * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
210
	 * should be called as a security precaution.
211
	 *
212
	 * This doesn't always work, especially if you're trying to set session cookies
213
	 * across an entire site using the domain parameter to session_set_cookie_params()
214
	 *
215
	 * @config
216
	 * @var boolean
217
	 */
218
	private static $session_regenerate_id = true;
219
220
221
	/**
222
	 * Default lifetime of temporary ids.
223
	 *
224
	 * This is the period within which a user can be re-authenticated within the CMS by entering only their password
225
	 * and without losing their workspace.
226
	 *
227
	 * Any session expiration outside of this time will require them to login from the frontend using their full
228
	 * username and password.
229
	 *
230
	 * Defaults to 72 hours. Set to zero to disable expiration.
231
	 *
232
	 * @config
233
	 * @var int Lifetime in seconds
234
	 */
235
	private static $temp_id_lifetime = 259200;
236
237
	/**
238
	 * @deprecated 4.0 Use the "Member.session_regenerate_id" config setting instead
239
	 */
240
	public static function set_session_regenerate_id($bool) {
241
		Deprecation::notice('4.0', 'Use the "Member.session_regenerate_id" config setting instead');
242
		self::config()->session_regenerate_id = $bool;
0 ignored issues
show
Documentation introduced by
The property session_regenerate_id does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
243
	}
244
245
	/**
246
	 * Ensure the locale is set to something sensible by default.
247
	 */
248
	public function populateDefaults() {
249
		parent::populateDefaults();
250
		$this->Locale = i18n::get_closest_translation(i18n::get_locale());
251
	}
252
253
	public function requireDefaultRecords() {
254
		parent::requireDefaultRecords();
255
		// Default groups should've been built by Group->requireDefaultRecords() already
256
		static::default_admin();
257
	}
258
259
	/**
260
	 * Get the default admin record if it exists, or creates it otherwise if enabled
261
	 *
262
	 * @return Member
263
	 */
264
	public static function default_admin() {
265
		// Check if set
266
		if(!Security::has_default_admin()) return null;
267
268
		// Find or create ADMIN group
269
		Group::singleton()->requireDefaultRecords();
270
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
271
272
		// Find member
273
		$admin = Member::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...
274
			->filter('Email', Security::default_admin_username())
275
			->first();
276
		if(!$admin) {
277
			// 'Password' is not set to avoid creating
278
			// persistent logins in the database. See Security::setDefaultAdmin().
279
			// Set 'Email' to identify this as the default admin
280
			$admin = Member::create();
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...
281
			$admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
282
			$admin->Email = Security::default_admin_username();
283
			$admin->write();
284
		}
285
286
		// Ensure this user is in the admin group
287
		if(!$admin->inGroup($adminGroup)) {
288
			// Add member to group instead of adding group to member
289
			// This bypasses the privilege escallation code in Member_GroupSet
290
			$adminGroup
291
				->DirectMembers()
292
				->add($admin);
293
		}
294
295
		return $admin;
296
	}
297
298
	/**
299
	 * If this is called, then a session cookie will be set to "1" whenever a user
300
	 * logs in.  This lets 3rd party tools, such as apache's mod_rewrite, detect
301
	 * whether a user is logged in or not and alter behaviour accordingly.
302
	 *
303
	 * One known use of this is to bypass static caching for logged in users.  This is
304
	 * done by putting this into _config.php
305
	 * <pre>
306
	 * Member::set_login_marker_cookie("SS_LOGGED_IN");
307
	 * </pre>
308
	 *
309
	 * And then adding this condition to each of the rewrite rules that make use of
310
	 * the static cache.
311
	 * <pre>
312
	 * RewriteCond %{HTTP_COOKIE} !SS_LOGGED_IN=1
313
	 * </pre>
314
	 *
315
	 * @deprecated 4.0 Use the "Member.login_marker_cookie" config setting instead
316
	 * @param $cookieName string The name of the cookie to set.
317
	 */
318
	public static function set_login_marker_cookie($cookieName) {
319
		Deprecation::notice('4.0', 'Use the "Member.login_marker_cookie" config setting instead');
320
		self::config()->login_marker_cookie = $cookieName;
0 ignored issues
show
Documentation introduced by
The property login_marker_cookie does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
321
	}
322
323
	/**
324
	 * Check if the passed password matches the stored one (if the member is not locked out).
325
	 *
326
	 * @param string $password
327
	 * @return ValidationResult
328
	 */
329
	public function checkPassword($password) {
330
		$result = $this->canLogIn();
331
332
		// Short-circuit the result upon failure, no further checks needed.
333
		if (!$result->valid()) {
334
			return $result;
335
		}
336
337
		// Allow default admin to login as self
338
		if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
339
			return $result;
340
		}
341
342
		// Check a password is set on this member
343
		if(empty($this->Password) && $this->exists()) {
344
			$result->error(_t('Member.NoPassword','There is no password on this member.'));
345
			return $result;
346
		}
347
348
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
349
		if(!$e->check($this->Password, $password, $this->Salt, $this)) {
350
			$result->error(_t (
351
				'Member.ERRORWRONGCRED',
352
				'The provided details don\'t seem to be correct. Please try again.'
353
			));
354
		}
355
356
		return $result;
357
	}
358
359
	/**
360
	 * Check if this user is the currently configured default admin
361
	 *
362
	 * @return bool
363
	 */
364
	public function isDefaultAdmin() {
365
		return Security::has_default_admin()
366
			&& $this->Email === Security::default_admin_username();
367
	}
368
369
	/**
370
	 * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
371
	 * one with error messages to display if the member is locked out.
372
	 *
373
	 * You can hook into this with a "canLogIn" method on an attached extension.
374
	 *
375
	 * @return ValidationResult
376
	 */
377
	public function canLogIn() {
378
		$result = ValidationResult::create();
379
380
		if($this->isLockedOut()) {
381
			$result->error(
382
				_t(
383
					'Member.ERRORLOCKEDOUT2',
384
					'Your account has been temporarily disabled because of too many failed attempts at ' .
385
					'logging in. Please try again in {count} minutes.',
386
					null,
387
					array('count' => $this->config()->lock_out_delay_mins)
388
				)
389
			);
390
		}
391
392
		$this->extend('canLogIn', $result);
393
		return $result;
394
	}
395
396
	/**
397
	 * Returns true if this user is locked out
398
	 */
399
	public function isLockedOut() {
400
		return $this->LockedOutUntil && DBDatetime::now()->Format('U') < strtotime($this->LockedOutUntil);
401
	}
402
403
	/**
404
	 * Regenerate the session_id.
405
	 * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
406
	 * They have caused problems in certain
407
	 * quirky problems (such as using the Windmill 0.3.6 proxy).
408
	 */
409
	public static function session_regenerate_id() {
410
		if(!self::config()->session_regenerate_id) return;
411
412
		// This can be called via CLI during testing.
413
		if(Director::is_cli()) return;
414
415
		$file = '';
416
		$line = '';
417
418
		// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
419
		// There's nothing we can do about this, because it's an operating system function!
420
		if(!headers_sent($file, $line)) @session_regenerate_id(true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
421
	}
422
423
	/**
424
	 * Get the field used for uniquely identifying a member
425
	 * in the database. {@see Member::$unique_identifier_field}
426
	 *
427
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
428
	 * @return string
429
	 */
430
	public static function get_unique_identifier_field() {
431
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
432
		return Member::config()->unique_identifier_field;
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...
433
	}
434
435
	/**
436
	 * Set the field used for uniquely identifying a member
437
	 * in the database. {@see Member::$unique_identifier_field}
438
	 *
439
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
440
	 * @param $field The field name to set as the unique field
441
	 */
442
	public static function set_unique_identifier_field($field) {
443
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
444
		Member::config()->unique_identifier_field = $field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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...
445
	}
446
447
	/**
448
	 * Set a {@link PasswordValidator} object to use to validate member's passwords.
449
	 *
450
	 * @param PasswordValidator $pv
451
	 */
452
	public static function set_password_validator($pv) {
453
		self::$password_validator = $pv;
454
	}
455
456
	/**
457
	 * Returns the current {@link PasswordValidator}
458
	 *
459
	 * @return PasswordValidator
460
	 */
461
	public static function password_validator() {
462
		return self::$password_validator;
463
	}
464
465
	/**
466
	 * Set the number of days that a password should be valid for.
467
	 * Set to null (the default) to have passwords never expire.
468
	 *
469
	 * @deprecated 4.0 Use the "Member.password_expiry_days" config setting instead
470
	 */
471
	public static function set_password_expiry($days) {
472
		Deprecation::notice('4.0', 'Use the "Member.password_expiry_days" config setting instead');
473
		self::config()->password_expiry_days = $days;
0 ignored issues
show
Documentation introduced by
The property password_expiry_days does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
474
	}
475
476
	/**
477
	 * Configure the security system to lock users out after this many incorrect logins
478
	 *
479
	 * @deprecated 4.0 Use the "Member.lock_out_after_incorrect_logins" config setting instead
480
	 */
481
	public static function lock_out_after_incorrect_logins($numLogins) {
482
		Deprecation::notice('4.0', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead');
483
		self::config()->lock_out_after_incorrect_logins = $numLogins;
0 ignored issues
show
Documentation introduced by
The property lock_out_after_incorrect_logins does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
484
	}
485
486
487
	public function isPasswordExpired() {
488
		if(!$this->PasswordExpiry) return false;
489
		return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
490
	}
491
492
	/**
493
	 * Logs this member in
494
	 *
495
	 * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
496
	 */
497
	public function logIn($remember = false) {
498
		$this->extend('beforeMemberLoggedIn');
499
500
		self::session_regenerate_id();
501
502
		Session::set("loggedInAs", $this->ID);
503
		// This lets apache rules detect whether the user has logged in
504
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0);
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...
505
506
		// Cleans up any potential previous hash for this member on this device
507
		if ($alcDevice = Cookie::get('alc_device')) {
508
			RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
509
		}
510
		if($remember) {
511
			$rememberLoginHash = RememberLoginHash::generate($this);
512
			$tokenExpiryDays = Config::inst()->get('SilverStripe\\Security\\RememberLoginHash', 'token_expiry_days');
513
			$deviceExpiryDays = Config::inst()->get('SilverStripe\\Security\\RememberLoginHash', 'device_expiry_days');
514
			Cookie::set('alc_enc', $this->ID . ':' . $rememberLoginHash->getToken(),
515
				$tokenExpiryDays, null, null, null, true);
516
			Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true);
517
		} else {
518
			Cookie::set('alc_enc', null);
519
			Cookie::set('alc_device', null);
520
			Cookie::force_expiry('alc_enc');
521
			Cookie::force_expiry('alc_device');
522
		}
523
524
		// Clear the incorrect log-in count
525
		$this->registerSuccessfulLogin();
526
527
		$this->LockedOutUntil = null;
528
529
		$this->regenerateTempID();
530
531
		$this->write();
532
533
		// Audit logging hook
534
		$this->extend('memberLoggedIn');
535
	}
536
537
	/**
538
	 * Trigger regeneration of TempID.
539
	 *
540
	 * This should be performed any time the user presents their normal identification (normally Email)
541
	 * and is successfully authenticated.
542
	 */
543
	public function regenerateTempID() {
544
		$generator = new RandomGenerator();
545
		$this->TempIDHash = $generator->randomToken('sha1');
546
		$this->TempIDExpired = self::config()->temp_id_lifetime
547
			? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime)
548
			: null;
549
		$this->write();
550
	}
551
552
	/**
553
	 * Check if the member ID logged in session actually
554
	 * has a database record of the same ID. If there is
555
	 * no logged in user, FALSE is returned anyway.
556
	 *
557
	 * @return boolean TRUE record found FALSE no record found
558
	 */
559
	public static function logged_in_session_exists() {
560
		if($id = Member::currentUserID()) {
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...
561
			if($member = DataObject::get_by_id('SilverStripe\\Security\\Member', $id)) {
562
				if($member->exists()) return true;
563
			}
564
		}
565
566
		return false;
567
	}
568
569
	/**
570
	 * Log the user in if the "remember login" cookie is set
571
	 *
572
	 * The <i>remember login token</i> will be changed on every successful
573
	 * auto-login.
574
	 */
575
	public static function autoLogin() {
576
		// Don't bother trying this multiple times
577
		if (!class_exists('SapphireTest', false) || !SapphireTest::is_running_test()) {
578
			self::$_already_tried_to_auto_log_in = true;
579
		}
580
581
		if(strpos(Cookie::get('alc_enc'), ':') === false
582
			|| Session::get("loggedInAs")
583
			|| !Security::database_is_ready()
584
		) {
585
			return;
586
		}
587
588
		if(strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Cookie::get('alc_device') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
589
			list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
590
591
			if (!$uid || !$token) {
592
				return;
593
			}
594
595
			$deviceID = Cookie::get('alc_device');
596
597
			$member = Member::get()->byID($uid);
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...
598
599
			$rememberLoginHash = null;
600
601
			// check if autologin token matches
602
			if($member) {
603
				$hash = $member->encryptWithUserSettings($token);
604
				$rememberLoginHash = RememberLoginHash::get()
605
					->filter(array(
606
						'MemberID' => $member->ID,
607
						'DeviceID' => $deviceID,
608
						'Hash' => $hash
609
					))->First();
610
				if(!$rememberLoginHash) {
611
					$member = null;
612
				} else {
613
					// Check for expired token
614
					$expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
615
					$now = DBDatetime::now();
616
					$now = new DateTime($now->Rfc2822());
617
					if ($now > $expiryDate) {
618
						$member = null;
619
					}
620
				}
621
			}
622
623
			if($member) {
624
				self::session_regenerate_id();
625
				Session::set("loggedInAs", $member->ID);
626
				// This lets apache rules detect whether the user has logged in
627
				if(Member::config()->login_marker_cookie) {
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...
628
					Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
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...
629
				}
630
631
				if ($rememberLoginHash) {
632
					$rememberLoginHash->renew();
633
					$tokenExpiryDays = Config::inst()->get('SilverStripe\\Security\\RememberLoginHash', 'token_expiry_days');
634
					Cookie::set('alc_enc', $member->ID . ':' . $rememberLoginHash->getToken(),
635
						$tokenExpiryDays, null, null, false, true);
636
				}
637
638
				$member->write();
639
640
				// Audit logging hook
641
				$member->extend('memberAutoLoggedIn');
642
			}
643
		}
644
	}
645
646
	/**
647
	 * Logs this member out.
648
	 */
649
	public function logOut() {
650
		$this->extend('beforeMemberLoggedOut');
651
652
		Session::clear("loggedInAs");
653
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
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...
654
655
		Session::destroy();
656
657
		$this->extend('memberLoggedOut');
658
659
		// Clears any potential previous hashes for this member
660
		RememberLoginHash::clear($this, Cookie::get('alc_device'));
661
662
		Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
663
		Cookie::force_expiry('alc_enc');
664
		Cookie::set('alc_device', null);
665
		Cookie::force_expiry('alc_device');
666
667
		// Switch back to live in order to avoid infinite loops when
668
		// redirecting to the login screen (if this login screen is versioned)
669
		Session::clear('readingMode');
670
671
		$this->write();
672
673
		// Audit logging hook
674
		$this->extend('memberLoggedOut');
675
	}
676
677
	/**
678
	 * Utility for generating secure password hashes for this member.
679
	 *
680
	 * @param string $string
681
	 * @return string
682
	 * @throws PasswordEncryptor_NotFoundException
683
	 */
684
	public function encryptWithUserSettings($string) {
685
		if (!$string) return null;
686
687
		// If the algorithm or salt is not available, it means we are operating
688
		// on legacy account with unhashed password. Do not hash the string.
689
		if (!$this->PasswordEncryption) {
690
			return $string;
691
		}
692
693
		// We assume we have PasswordEncryption and Salt available here.
694
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
695
		return $e->encrypt($string, $this->Salt);
696
697
	}
698
699
	/**
700
	 * Generate an auto login token which can be used to reset the password,
701
	 * at the same time hashing it and storing in the database.
702
	 *
703
	 * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
704
	 *
705
	 * @returns string Token that should be passed to the client (but NOT persisted).
706
	 *
707
	 * @todo Make it possible to handle database errors such as a "duplicate key" error
708
	 */
709
	public function generateAutologinTokenAndStoreHash($lifetime = 2) {
710
		do {
711
			$generator = new RandomGenerator();
712
			$token = $generator->randomToken();
713
			$hash = $this->encryptWithUserSettings($token);
714
		} while(DataObject::get_one('SilverStripe\\Security\\Member', array(
715
			'"Member"."AutoLoginHash"' => $hash
716
		)));
717
718
		$this->AutoLoginHash = $hash;
719
		$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
720
721
		$this->write();
722
723
		return $token;
724
	}
725
726
	/**
727
	 * Check the token against the member.
728
	 *
729
	 * @param string $autologinToken
730
	 *
731
	 * @returns bool Is token valid?
732
	 */
733
	public function validateAutoLoginToken($autologinToken) {
734
		$hash = $this->encryptWithUserSettings($autologinToken);
735
		$member = self::member_from_autologinhash($hash, false);
736
		return (bool)$member;
737
	}
738
739
	/**
740
	 * Return the member for the auto login hash
741
	 *
742
	 * @param string $hash The hash key
743
	 * @param bool $login Should the member be logged in?
744
	 *
745
	 * @return Member the matching member, if valid
746
	 * @return Member
747
	 */
748
	public static function member_from_autologinhash($hash, $login = false) {
749
750
		$nowExpression = DB::get_conn()->now();
751
		$member = DataObject::get_one('SilverStripe\\Security\\Member', array(
752
			"\"Member\".\"AutoLoginHash\"" => $hash,
753
			"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
754
		));
755
756
		if($login && $member) $member->logIn();
757
758
		return $member;
759
	}
760
761
	/**
762
	 * Find a member record with the given TempIDHash value
763
	 *
764
	 * @param string $tempid
765
	 * @return Member
766
	 */
767
	public static function member_from_tempid($tempid) {
768
		$members = Member::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...
769
			->filter('TempIDHash', $tempid);
770
771
		// Exclude expired
772
		if(static::config()->temp_id_lifetime) {
773
			$members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
774
		}
775
776
		return $members->first();
777
	}
778
779
	/**
780
	 * Returns the fields for the member form - used in the registration/profile module.
781
	 * It should return fields that are editable by the admin and the logged-in user.
782
	 *
783
	 * @return FieldList Returns a {@link FieldList} containing the fields for
784
	 *                   the member form.
785
	 */
786
	public function getMemberFormFields() {
787
		$fields = parent::getFrontendFields();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (getFrontendFields() instead of getMemberFormFields()). Are you sure this is correct? If so, you might want to change this to $this->getFrontendFields().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
788
789
		$fields->replaceField('Password', $this->getMemberPasswordField());
790
791
		$fields->replaceField('Locale', new DropdownField (
792
			'Locale',
793
			$this->fieldLabel('Locale'),
794
			i18n::get_existing_translations()
795
		));
796
797
		$fields->removeByName(static::config()->hidden_fields);
798
		$fields->removeByName('FailedLoginCount');
799
800
801
		$this->extend('updateMemberFormFields', $fields);
802
		return $fields;
803
	}
804
805
	/**
806
	 * Builds "Change / Create Password" field for this member
807
	 *
808
	 * @return ConfirmedPasswordField
809
	 */
810
	public function getMemberPasswordField() {
811
		$editingPassword = $this->isInDB();
812
		$label = $editingPassword
813
			? _t('Member.EDIT_PASSWORD', 'New Password')
814
			: $this->fieldLabel('Password');
815
		/** @var ConfirmedPasswordField $password */
816
		$password = ConfirmedPasswordField::create(
817
			'Password',
818
			$label,
819
			null,
820
			null,
821
			$editingPassword
822
		);
823
824
		// If editing own password, require confirmation of existing
825
		if($editingPassword && $this->ID == Member::currentUserID()) {
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...
826
			$password->setRequireExistingPassword(true);
827
		}
828
829
		$password->setCanBeEmpty(true);
830
		$this->extend('updateMemberPasswordField', $password);
831
		return $password;
832
	}
833
834
835
	/**
836
	 * Returns the {@link RequiredFields} instance for the Member object. This
837
	 * Validator is used when saving a {@link CMSProfileController} or added to
838
	 * any form responsible for saving a users data.
839
	 *
840
	 * To customize the required fields, add a {@link DataExtension} to member
841
	 * calling the `updateValidator()` method.
842
	 *
843
	 * @return Member_Validator
844
	 */
845
	public function getValidator() {
846
		$validator = Injector::inst()->create('SilverStripe\\Security\\Member_Validator');
847
		$validator->setForMember($this);
848
		$this->extend('updateValidator', $validator);
849
850
		return $validator;
851
	}
852
853
854
	/**
855
	 * Returns the current logged in user
856
	 *
857
	 * @return Member
858
	 */
859
	public static function currentUser() {
860
		$id = Member::currentUserID();
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...
861
862
		if($id) {
863
			return Member::get()->byID($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...
864
		}
865
	}
866
867
	/**
868
	 * Get the ID of the current logged in user
869
	 *
870
	 * @return int Returns the ID of the current logged in user or 0.
871
	 */
872
	public static function currentUserID() {
873
		$id = Session::get("loggedInAs");
874
		if(!$id && !self::$_already_tried_to_auto_log_in) {
875
			self::autoLogin();
876
			$id = Session::get("loggedInAs");
877
		}
878
879
		return is_numeric($id) ? $id : 0;
880
	}
881
	private static $_already_tried_to_auto_log_in = false;
882
883
884
	/*
885
	 * Generate a random password, with randomiser to kick in if there's no words file on the
886
	 * filesystem.
887
	 *
888
	 * @return string Returns a random password.
889
	 */
890
	public static function create_new_password() {
891
		$words = Config::inst()->get('SilverStripe\\Security\\Security', 'word_list');
892
893
		if($words && file_exists($words)) {
894
			$words = file($words);
895
896
			list($usec, $sec) = explode(' ', microtime());
897
			srand($sec + ((float) $usec * 100000));
898
899
			$word = trim($words[rand(0,sizeof($words)-1)]);
900
			$number = rand(10,999);
901
902
			return $word . $number;
903
		} else {
904
			$random = rand();
905
			$string = md5($random);
906
			$output = substr($string, 0, 6);
907
			return $output;
908
		}
909
	}
910
911
	/**
912
	 * Event handler called before writing to the database.
913
	 */
914
	public function onBeforeWrite() {
915
		if($this->SetPassword) $this->Password = $this->SetPassword;
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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...
916
917
		// If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
918
		// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
919
		// but rather a last line of defense against data inconsistencies.
920
		$identifierField = Member::config()->unique_identifier_field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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...
921
		if($this->$identifierField) {
922
923
			// Note: Same logic as Member_Validator class
924
			$filter = array("\"$identifierField\"" => $this->$identifierField);
925
			if($this->ID) {
926
				$filter[] = array('"Member"."ID" <> ?' => $this->ID);
927
			}
928
			$existingRecord = DataObject::get_one('SilverStripe\\Security\\Member', $filter);
929
930
			if($existingRecord) {
931
				throw new ValidationException(ValidationResult::create(false, _t(
932
					'Member.ValidationIdentifierFailed',
933
					'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
934
					'Values in brackets show "fieldname = value", usually denoting an existing email address',
935
					array(
936
						'id' => $existingRecord->ID,
937
						'name' => $identifierField,
938
						'value' => $this->$identifierField
939
					)
940
				)));
941
			}
942
		}
943
944
		// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
945
		// However, if TestMailer is in use this isn't a risk.
946
		if(
947
			(Director::isLive() || Email::mailer() instanceof TestMailer)
948
			&& $this->isChanged('Password')
949
			&& $this->record['Password']
950
			&& $this->config()->notify_password_change
951
		) {
952
			/** @var Email $e */
953
			$e = Email::create();
954
			$e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'));
955
			$e->setTemplate('ChangePasswordEmail');
956
			$e->populateTemplate($this);
957
			$e->setTo($this->Email);
958
			$e->send();
959
		}
960
961
		// The test on $this->ID is used for when records are initially created.
962
		// Note that this only works with cleartext passwords, as we can't rehash
963
		// existing passwords.
964
		if((!$this->ID && $this->Password) || $this->isChanged('Password')) {
965
			// Password was changed: encrypt the password according the settings
966
			$encryption_details = Security::encrypt_password(
967
				$this->Password, // this is assumed to be cleartext
968
				$this->Salt,
969
				($this->PasswordEncryption) ?
970
					$this->PasswordEncryption : Security::config()->password_encryption_algorithm,
971
				$this
972
			);
973
974
			// Overwrite the Password property with the hashed value
975
			$this->Password = $encryption_details['password'];
976
			$this->Salt = $encryption_details['salt'];
977
			$this->PasswordEncryption = $encryption_details['algorithm'];
978
979
			// If we haven't manually set a password expiry
980
			if(!$this->isChanged('PasswordExpiry')) {
981
				// then set it for us
982
				if(self::config()->password_expiry_days) {
983
					$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
984
				} else {
985
					$this->PasswordExpiry = null;
986
				}
987
			}
988
		}
989
990
		// save locale
991
		if(!$this->Locale) {
992
			$this->Locale = i18n::get_locale();
993
		}
994
995
		parent::onBeforeWrite();
996
	}
997
998
	public function onAfterWrite() {
999
		parent::onAfterWrite();
1000
1001
		Permission::flush_permission_cache();
1002
1003
		if($this->isChanged('Password')) {
1004
			MemberPassword::log($this);
1005
		}
1006
	}
1007
1008
	public function onAfterDelete() {
1009
		parent::onAfterDelete();
1010
1011
		//prevent orphaned records remaining in the DB
1012
		$this->deletePasswordLogs();
1013
	}
1014
1015
	/**
1016
	 * Delete the MemberPassword objects that are associated to this user
1017
	 *
1018
	 * @return self
1019
	 */
1020
	protected function deletePasswordLogs() {
1021
		foreach ($this->LoggedPasswords() as $password) {
1022
			$password->delete();
1023
			$password->destroy();
1024
		}
1025
		return $this;
1026
	}
1027
1028
	/**
1029
	 * Filter out admin groups to avoid privilege escalation,
1030
	 * If any admin groups are requested, deny the whole save operation.
1031
	 *
1032
	 * @param array $ids Database IDs of Group records
1033
	 * @return bool True if the change can be accepted
1034
	 */
1035
	public function onChangeGroups($ids) {
1036
		// unless the current user is an admin already OR the logged in user is an admin
1037
		if(Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
1038
			return true;
1039
		}
1040
1041
		// If there are no admin groups in this set then it's ok
1042
		$adminGroups = Permission::get_groups_by_permission('ADMIN');
1043
		$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
1044
		return count(array_intersect($ids, $adminGroupIDs)) == 0;
1045
	}
1046
1047
1048
	/**
1049
	 * Check if the member is in one of the given groups.
1050
	 *
1051
	 * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
1052
	 * @param boolean $strict Only determine direct group membership if set to true (Default: false)
1053
	 * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
1054
	 */
1055
	public function inGroups($groups, $strict = false) {
1056
		if($groups) foreach($groups as $group) {
1057
			if($this->inGroup($group, $strict)) return true;
1058
		}
1059
1060
		return false;
1061
	}
1062
1063
1064
	/**
1065
	 * Check if the member is in the given group or any parent groups.
1066
	 *
1067
	 * @param int|Group|string $group Group instance, Group Code or ID
1068
	 * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
1069
	 * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
1070
	 */
1071
	public function inGroup($group, $strict = false) {
1072
		if(is_numeric($group)) {
1073
			$groupCheckObj = DataObject::get_by_id('SilverStripe\\Security\\Group', $group);
1074
		} elseif(is_string($group)) {
1075
			$groupCheckObj = DataObject::get_one('SilverStripe\\Security\\Group', array(
1076
				'"Group"."Code"' => $group
1077
			));
1078
		} elseif($group instanceof Group) {
1079
			$groupCheckObj = $group;
1080
		} else {
1081
			user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
1082
		}
1083
1084
		if(!$groupCheckObj) return false;
0 ignored issues
show
Bug introduced by
The variable $groupCheckObj 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...
1085
1086
		$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1087
		if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
1088
			if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
1089
		}
1090
1091
		return false;
1092
	}
1093
1094
	/**
1095
	 * Adds the member to a group. This will create the group if the given
1096
	 * group code does not return a valid group object.
1097
	 *
1098
	 * @param string $groupcode
1099
	 * @param string $title Title of the group
1100
	 */
1101
	public function addToGroupByCode($groupcode, $title = "") {
1102
		$group = DataObject::get_one('SilverStripe\\Security\\Group', array(
1103
			'"Group"."Code"' => $groupcode
1104
		));
1105
1106
		if($group) {
1107
			$this->Groups()->add($group);
1108
		} else {
1109
			if(!$title) $title = $groupcode;
1110
1111
			$group = new Group();
1112
			$group->Code = $groupcode;
1113
			$group->Title = $title;
1114
			$group->write();
1115
1116
			$this->Groups()->add($group);
1117
		}
1118
	}
1119
1120
	/**
1121
	 * Removes a member from a group.
1122
	 *
1123
	 * @param string $groupcode
1124
	 */
1125
	public function removeFromGroupByCode($groupcode) {
1126
		$group = Group::get()->filter(array('Code' => $groupcode))->first();
1127
1128
		if($group) {
1129
			$this->Groups()->remove($group);
1130
		}
1131
	}
1132
1133
	/**
1134
	 * @param array $columns Column names on the Member record to show in {@link getTitle()}.
1135
	 * @param String $sep Separator
1136
	 */
1137
	public static function set_title_columns($columns, $sep = ' ') {
1138
		if (!is_array($columns)) $columns = array($columns);
1139
		self::config()->title_format = array('columns' => $columns, 'sep' => $sep);
0 ignored issues
show
Documentation introduced by
The property title_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1140
	}
1141
1142
	//------------------- HELPER METHODS -----------------------------------//
1143
1144
	/**
1145
	 * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1146
	 * Falls back to showing either field on its own.
1147
	 *
1148
	 * You can overload this getter with {@link set_title_format()}
1149
	 * and {@link set_title_sql()}.
1150
	 *
1151
	 * @return string Returns the first- and surname of the member. If the ID
1152
	 *  of the member is equal 0, only the surname is returned.
1153
	 */
1154
	public function getTitle() {
1155
		$format = $this->config()->title_format;
0 ignored issues
show
Documentation introduced by
The property title_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1156
		if ($format) {
1157
			$values = array();
1158
			foreach($format['columns'] as $col) {
1159
				$values[] = $this->getField($col);
1160
			}
1161
			return join($format['sep'], $values);
1162
		}
1163
		if($this->getField('ID') === 0)
1164
			return $this->getField('Surname');
1165
		else{
1166
			if($this->getField('Surname') && $this->getField('FirstName')){
1167
				return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1168
			}elseif($this->getField('Surname')){
1169
				return $this->getField('Surname');
1170
			}elseif($this->getField('FirstName')){
1171
				return $this->getField('FirstName');
1172
			}else{
1173
				return null;
1174
			}
1175
		}
1176
	}
1177
1178
	/**
1179
	 * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1180
	 * Useful for custom queries which assume a certain member title format.
1181
	 *
1182
	 * @return String SQL
1183
	 */
1184
	public static function get_title_sql() {
1185
		// This should be abstracted to SSDatabase concatOperator or similar.
1186
		$op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || ";
0 ignored issues
show
Bug introduced by
The class SilverStripe\MSSQL\MSSQLDatabase does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1187
1188
		// Get title_format with fallback to default
1189
		$format = static::config()->title_format;
0 ignored issues
show
Documentation introduced by
The property title_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1190
		if (!$format) {
1191
			$format = [
1192
				'columns' => ['Surname', 'FirstName'],
1193
				'sep' => ' ',
1194
			];
1195
		}
1196
1197
		$columnsWithTablename = array();
1198
		foreach($format['columns'] as $column) {
1199
			$columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column);
1200
		}
1201
1202
		$sepSQL = \Convert::raw2sql($format['sep'], true);
1203
		return "(".join(" $op $sepSQL $op ", $columnsWithTablename).")";
1204
	}
1205
1206
1207
	/**
1208
	 * Get the complete name of the member
1209
	 *
1210
	 * @return string Returns the first- and surname of the member.
1211
	 */
1212
	public function getName() {
1213
		return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1214
	}
1215
1216
1217
	/**
1218
	 * Set first- and surname
1219
	 *
1220
	 * This method assumes that the last part of the name is the surname, e.g.
1221
	 * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1222
	 *
1223
	 * @param string $name The name
1224
	 */
1225
	public function setName($name) {
1226
		$nameParts = explode(' ', $name);
1227
		$this->Surname = array_pop($nameParts);
1228
		$this->FirstName = join(' ', $nameParts);
1229
	}
1230
1231
1232
	/**
1233
	 * Alias for {@link setName}
1234
	 *
1235
	 * @param string $name The name
1236
	 * @see setName()
1237
	 */
1238
	public function splitName($name) {
1239
		return $this->setName($name);
1240
	}
1241
1242
	/**
1243
	 * Override the default getter for DateFormat so the
1244
	 * default format for the user's locale is used
1245
	 * if the user has not defined their own.
1246
	 *
1247
	 * @return string ISO date format
1248
	 */
1249
	public function getDateFormat() {
1250
		if($this->getField('DateFormat')) {
1251
			return $this->getField('DateFormat');
1252
		} else {
1253
			return Config::inst()->get('i18n', 'date_format');
1254
		}
1255
	}
1256
1257
	/**
1258
	 * Override the default getter for TimeFormat so the
1259
	 * default format for the user's locale is used
1260
	 * if the user has not defined their own.
1261
	 *
1262
	 * @return string ISO date format
1263
	 */
1264
	public function getTimeFormat() {
1265
		if($this->getField('TimeFormat')) {
1266
			return $this->getField('TimeFormat');
1267
		} else {
1268
			return Config::inst()->get('i18n', 'time_format');
1269
		}
1270
	}
1271
1272
	//---------------------------------------------------------------------//
1273
1274
1275
	/**
1276
	 * Get a "many-to-many" map that holds for all members their group memberships,
1277
	 * including any parent groups where membership is implied.
1278
	 * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1279
	 *
1280
	 * @todo Push all this logic into Member_GroupSet's getIterator()?
1281
	 * @return Member_Groupset
1282
	 */
1283
	public function Groups() {
1284
		$groups = Member_GroupSet::create('SilverStripe\\Security\\Group', 'Group_Members', 'GroupID', 'MemberID');
1285
		$groups = $groups->forForeignID($this->ID);
1286
1287
		$this->extend('updateGroups', $groups);
1288
1289
		return $groups;
1290
	}
1291
1292
	/**
1293
	 * @return ManyManyList
1294
	 */
1295
	public function DirectGroups() {
1296
		return $this->getManyManyComponents('Groups');
1297
	}
1298
1299
	/**
1300
	 * Get a member SQLMap of members in specific groups
1301
	 *
1302
	 * If no $groups is passed, all members will be returned
1303
	 *
1304
	 * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1305
	 * @return SS_Map Returns an SS_Map that returns all Member data.
1306
	 */
1307
	public static function map_in_groups($groups = null) {
1308
		$groupIDList = array();
1309
1310
		if($groups instanceof SS_List) {
1311
			foreach( $groups as $group ) {
1312
				$groupIDList[] = $group->ID;
1313
			}
1314
		} elseif(is_array($groups)) {
1315
			$groupIDList = $groups;
1316
		} elseif($groups) {
1317
			$groupIDList[] = $groups;
1318
		}
1319
1320
		// No groups, return all Members
1321
		if(!$groupIDList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIDList 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...
1322
			return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
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...
1323
		}
1324
1325
		$membersList = new ArrayList();
1326
		// This is a bit ineffective, but follow the ORM style
1327
		foreach(Group::get()->byIDs($groupIDList) as $group) {
1328
			$membersList->merge($group->Members());
1329
		}
1330
1331
		$membersList->removeDuplicates('ID');
1332
		return $membersList->map();
1333
	}
1334
1335
1336
	/**
1337
	 * Get a map of all members in the groups given that have CMS permissions
1338
	 *
1339
	 * If no groups are passed, all groups with CMS permissions will be used.
1340
	 *
1341
	 * @param array $groups Groups to consider or NULL to use all groups with
1342
	 *                      CMS permissions.
1343
	 * @return SS_Map Returns a map of all members in the groups given that
1344
	 *                have CMS permissions.
1345
	 */
1346
	public static function mapInCMSGroups($groups = null) {
1347
		if(!$groups || $groups->Count() == 0) {
0 ignored issues
show
Bug introduced by
The method Count cannot be called on $groups (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1348
			$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1349
1350
			if(class_exists('CMSMain')) {
1351
				$cmsPerms = singleton('CMSMain')->providePermissions();
1352
			} else {
1353
				$cmsPerms = singleton('LeftAndMain')->providePermissions();
1354
			}
1355
1356
			if(!empty($cmsPerms)) {
1357
				$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1358
			}
1359
1360
			$permsClause = DB::placeholders($perms);
1361
			/** @skipUpgrade */
1362
			$groups = DataObject::get('SilverStripe\\Security\\Group')
1363
				->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1364
				->where(array(
1365
					"\"Permission\".\"Code\" IN ($permsClause)" => $perms
1366
				));
1367
		}
1368
1369
		$groupIDList = array();
1370
1371
		if(is_a($groups, 'SilverStripe\\ORM\\SS_List')) {
1372
			foreach($groups as $group) {
1373
				$groupIDList[] = $group->ID;
1374
			}
1375
		} elseif(is_array($groups)) {
1376
			$groupIDList = $groups;
1377
		}
1378
1379
		/** @skipUpgrade */
1380
		$members = Member::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...
1381
			->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1382
			->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1383
		if($groupIDList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIDList 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...
1384
			$groupClause = DB::placeholders($groupIDList);
1385
			$members = $members->where(array(
1386
				"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1387
			));
1388
		}
1389
1390
		return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1391
	}
1392
1393
1394
	/**
1395
	 * Get the groups in which the member is NOT in
1396
	 *
1397
	 * When passed an array of groups, and a component set of groups, this
1398
	 * function will return the array of groups the member is NOT in.
1399
	 *
1400
	 * @param array $groupList An array of group code names.
1401
	 * @param array $memberGroups A component set of groups (if set to NULL,
1402
	 *                            $this->groups() will be used)
1403
	 * @return array Groups in which the member is NOT in.
1404
	 */
1405
	public function memberNotInGroups($groupList, $memberGroups = null){
1406
		if(!$memberGroups) $memberGroups = $this->Groups();
1407
1408
		foreach($memberGroups as $group) {
1409
			if(in_array($group->Code, $groupList)) {
1410
				$index = array_search($group->Code, $groupList);
1411
				unset($groupList[$index]);
1412
			}
1413
		}
1414
1415
		return $groupList;
1416
	}
1417
1418
1419
	/**
1420
	 * Return a {@link FieldList} of fields that would appropriate for editing
1421
	 * this member.
1422
	 *
1423
	 * @return FieldList Return a FieldList of fields that would appropriate for
1424
	 *                   editing this member.
1425
	 */
1426
	public function getCMSFields() {
1427
		require_once 'Zend/Date.php';
1428
1429
		$self = $this;
1430
		$this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) {
1431
			/** @var FieldList $mainFields */
1432
			$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
1433
1434
			// Build change password field
1435
			$mainFields->replaceField('Password', $self->getMemberPasswordField());
1436
1437
			$mainFields->replaceField('Locale', new DropdownField(
1438
				"Locale",
1439
				_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1440
				i18n::get_existing_translations()
1441
			));
1442
			$mainFields->removeByName($self->config()->hidden_fields);
1443
1444
			if( ! $self->config()->lock_out_after_incorrect_logins) {
1445
				$mainFields->removeByName('FailedLoginCount');
1446
			}
1447
1448
1449
			// Groups relation will get us into logical conflicts because
1450
			// Members are displayed within  group edit form in SecurityAdmin
1451
			$fields->removeByName('Groups');
1452
1453
			// Members shouldn't be able to directly view/edit logged passwords
1454
			$fields->removeByName('LoggedPasswords');
1455
1456
			$fields->removeByName('RememberLoginHashes');
1457
1458
			if(Permission::check('EDIT_PERMISSIONS')) {
1459
				$groupsMap = array();
1460
				foreach(Group::get() as $group) {
1461
					// Listboxfield values are escaped, use ASCII char instead of &raquo;
1462
					$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1463
				}
1464
				asort($groupsMap);
1465
				$fields->addFieldToTab('Root.Main',
1466
					ListboxField::create('DirectGroups', singleton('SilverStripe\\Security\\Group')->i18n_plural_name())
1467
						->setSource($groupsMap)
1468
						->setAttribute(
1469
							'data-placeholder',
1470
							_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1471
						)
1472
				);
1473
1474
1475
				// Add permission field (readonly to avoid complicated group assignment logic).
1476
				// This should only be available for existing records, as new records start
1477
				// with no permissions until they have a group assignment anyway.
1478
				if($self->ID) {
1479
					$permissionsField = new PermissionCheckboxSetField_Readonly(
1480
						'Permissions',
1481
						false,
1482
						'SilverStripe\\Security\\Permission',
1483
						'GroupID',
1484
						// we don't want parent relationships, they're automatically resolved in the field
1485
						$self->getManyManyComponents('Groups')
1486
					);
1487
					$fields->findOrMakeTab('Root.Permissions', singleton('SilverStripe\\Security\\Permission')->i18n_plural_name());
1488
					$fields->addFieldToTab('Root.Permissions', $permissionsField);
1489
				}
1490
			}
1491
1492
			$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1493
			if($permissionsTab) $permissionsTab->addExtraClass('readonly');
1494
1495
			$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
1496
			$dateFormatMap = array(
1497
				'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
1498
				'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
1499
				'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
1500
				'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
1501
			);
1502
			$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
1503
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1504
			$mainFields->push(
1505
				$dateFormatField = new MemberDatetimeOptionsetField(
1506
					'DateFormat',
1507
					$self->fieldLabel('DateFormat'),
1508
					$dateFormatMap
1509
				)
1510
			);
1511
			$dateFormatField->setValue($self->DateFormat);
1512
			$dateFormatField->setDescriptionTemplate('MemberDatetimeOptionsetField_description_date');
1513
1514
			$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
1515
			$timeFormatMap = array(
1516
				'h:mm a' => Zend_Date::now()->toString('h:mm a'),
1517
				'H:mm' => Zend_Date::now()->toString('H:mm'),
1518
			);
1519
			$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
1520
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1521
			$mainFields->push(
1522
				$timeFormatField = new MemberDatetimeOptionsetField(
1523
					'TimeFormat',
1524
					$self->fieldLabel('TimeFormat'),
1525
					$timeFormatMap
1526
				)
1527
			);
1528
			$timeFormatField->setValue($self->TimeFormat);
1529
			$timeFormatField->setDescriptionTemplate('MemberDatetimeOptionsetField_description_time');
1530
		});
1531
1532
		return parent::getCMSFields();
1533
	}
1534
1535
	/**
1536
	 * @param bool $includerelations Indicate if the labels returned include relation fields
1537
	 * @return array
1538
	 */
1539
	public function fieldLabels($includerelations = true) {
1540
		$labels = parent::fieldLabels($includerelations);
1541
1542
		$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1543
		$labels['Surname'] = _t('Member.SURNAME', 'Surname');
1544
		$labels['Email'] = _t('Member.EMAIL', 'Email');
1545
		$labels['Password'] = _t('Member.db_Password', 'Password');
1546
		$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1547
		$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1548
		$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1549
		$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
1550
		$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
1551
		if($includerelations){
1552
			$labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups',
1553
				'Security Groups this member belongs to');
1554
		}
1555
		return $labels;
1556
	}
1557
1558
	/**
1559
	 * Users can view their own record.
1560
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1561
	 * This is likely to be customized for social sites etc. with a looser permission model.
1562
	 *
1563
	 * @param Member $member
1564
	 * @return bool
1565
	 */
1566
    public function canView($member = null) {
1567
        //get member
1568
        if(!($member instanceof Member)) {
1569
            $member = Member::currentUser();
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...
1570
        }
1571
        //check for extensions, we do this first as they can overrule everything
1572
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1573
        if($extended !== null) {
1574
            return $extended;
1575
        }
1576
1577
        //need to be logged in and/or most checks below rely on $member being a Member
1578
        if(!$member) {
1579
            return false;
1580
        }
1581
        // members can usually view their own record
1582
        if($this->ID == $member->ID) {
1583
            return true;
1584
        }
1585
        //standard check
1586
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1587
    }
1588
1589
	/**
1590
	 * Users can edit their own record.
1591
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1592
	 *
1593
	 * @param Member $member
1594
	 * @return bool
1595
	 */
1596
    public function canEdit($member = null) {
1597
        //get member
1598
        if(!($member instanceof Member)) {
1599
            $member = Member::currentUser();
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...
1600
        }
1601
        //check for extensions, we do this first as they can overrule everything
1602
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1603
        if($extended !== null) {
1604
            return $extended;
1605
        }
1606
1607
        //need to be logged in and/or most checks below rely on $member being a Member
1608
        if(!$member) {
1609
            return false;
1610
        }
1611
1612
        // HACK: we should not allow for an non-Admin to edit an Admin
1613
        if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
1614
            return false;
1615
        }
1616
        // members can usually edit their own record
1617
        if($this->ID == $member->ID) {
1618
            return true;
1619
        }
1620
        //standard check
1621
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1622
    }
1623
    /**
1624
     * Users can edit their own record.
1625
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1626
	 *
1627
	 * @param Member $member
1628
	 * @return bool
1629
     */
1630
    public function canDelete($member = null) {
1631
        if(!($member instanceof Member)) {
1632
            $member = Member::currentUser();
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...
1633
        }
1634
        //check for extensions, we do this first as they can overrule everything
1635
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1636
        if($extended !== null) {
1637
            return $extended;
1638
        }
1639
1640
        //need to be logged in and/or most checks below rely on $member being a Member
1641
        if(!$member) {
1642
            return false;
1643
        }
1644
        // Members are not allowed to remove themselves,
1645
        // since it would create inconsistencies in the admin UIs.
1646
        if($this->ID && $member->ID == $this->ID) {
1647
            return false;
1648
        }
1649
1650
        // HACK: if you want to delete a member, you have to be a member yourself.
1651
        // this is a hack because what this should do is to stop a user
1652
        // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
1653
        if(Permission::checkMember($this, 'ADMIN')) {
1654
            if( ! Permission::checkMember($member, 'ADMIN')) {
1655
                return false;
1656
            }
1657
        }
1658
        //standard check
1659
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1660
    }
1661
1662
	/**
1663
	 * Validate this member object.
1664
	 */
1665
	public function validate() {
1666
		$valid = parent::validate();
1667
1668
		if(!$this->ID || $this->isChanged('Password')) {
1669
			if($this->Password && self::$password_validator) {
1670
				$valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1671
			}
1672
		}
1673
1674
		if((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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...
1675
			if($this->SetPassword && self::$password_validator) {
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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...
1676
				$valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this));
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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...
1677
			}
1678
		}
1679
1680
		return $valid;
1681
	}
1682
1683
	/**
1684
	 * Change password. This will cause rehashing according to
1685
	 * the `PasswordEncryption` property.
1686
	 *
1687
	 * @param string $password Cleartext password
1688
	 * @return ValidationResult
1689
	 */
1690
	public function changePassword($password) {
1691
		$this->Password = $password;
1692
		$valid = $this->validate();
1693
1694
		if($valid->valid()) {
1695
			$this->AutoLoginHash = null;
1696
			$this->write();
1697
		}
1698
1699
		return $valid;
1700
	}
1701
1702
	/**
1703
	 * Tell this member that someone made a failed attempt at logging in as them.
1704
	 * This can be used to lock the user out temporarily if too many failed attempts are made.
1705
	 */
1706
	public function registerFailedLogin() {
1707
		if(self::config()->lock_out_after_incorrect_logins) {
1708
			// Keep a tally of the number of failed log-ins so that we can lock people out
1709
			$this->FailedLoginCount = $this->FailedLoginCount + 1;
1710
1711
			if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1712
				$lockoutMins = self::config()->lock_out_delay_mins;
0 ignored issues
show
Documentation introduced by
The property lock_out_delay_mins does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1713
				$this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->Format('U') + $lockoutMins*60);
1714
				$this->FailedLoginCount = 0;
1715
			}
1716
		}
1717
		$this->extend('registerFailedLogin');
1718
		$this->write();
1719
	}
1720
1721
	/**
1722
	 * Tell this member that a successful login has been made
1723
	 */
1724
	public function registerSuccessfulLogin() {
1725
		if(self::config()->lock_out_after_incorrect_logins) {
1726
			// Forgive all past login failures
1727
			$this->FailedLoginCount = 0;
1728
			$this->write();
1729
		}
1730
	}
1731
	/**
1732
	 * Get the HtmlEditorConfig for this user to be used in the CMS.
1733
	 * This is set by the group. If multiple configurations are set,
1734
	 * the one with the highest priority wins.
1735
	 *
1736
	 * @return string
1737
	 */
1738
	public function getHtmlEditorConfigForCMS() {
1739
		$currentName = '';
1740
		$currentPriority = 0;
1741
1742
		foreach($this->Groups() as $group) {
1743
			$configName = $group->HtmlEditorConfig;
1744
			if($configName) {
1745
				$config = HTMLEditorConfig::get($group->HtmlEditorConfig);
1746
				if($config && $config->getOption('priority') > $currentPriority) {
1747
					$currentName = $configName;
1748
					$currentPriority = $config->getOption('priority');
1749
				}
1750
			}
1751
		}
1752
1753
		// If can't find a suitable editor, just default to cms
1754
		return $currentName ? $currentName : 'cms';
1755
	}
1756
1757
	public static function get_template_global_variables() {
1758
		return array(
1759
			'CurrentMember' => 'currentUser',
1760
			'currentUser',
1761
		);
1762
	}
1763
}
1764
1765
/**
1766
 * Represents a set of Groups attached to a member.
1767
 * Handles the hierarchy logic.
1768
 * @package framework
1769
 * @subpackage security
1770
 */
1771
class Member_GroupSet extends ManyManyList {
1772
1773
	protected function linkJoinTable() {
1774
		// Do not join the table directly
1775
		if($this->extraFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraFields 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...
1776
			user_error('Member_GroupSet does not support many_many_extraFields', E_USER_ERROR);
1777
		}
1778
	}
1779
1780
	/**
1781
	 * Link this group set to a specific member.
1782
	 *
1783
	 * Recursively selects all groups applied to this member, as well as any
1784
	 * parent groups of any applied groups
1785
	 *
1786
	 * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
1787
	 * ids as per getForeignID
1788
	 * @return array Condition In array(SQL => parameters format)
1789
	 */
1790
	public function foreignIDFilter($id = null) {
1791
		if ($id === null) $id = $this->getForeignID();
1792
1793
		// Find directly applied groups
1794
		$manyManyFilter = parent::foreignIDFilter($id);
1795
		$query = new SQLSelect('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter);
0 ignored issues
show
Bug introduced by
It seems like $manyManyFilter defined by parent::foreignIDFilter($id) on line 1794 can also be of type null; however, SilverStripe\ORM\Queries\SQLSelect::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1796
		$groupIDs = $query->execute()->column();
1797
1798
		// Get all ancestors, iteratively merging these into the master set
1799
		$allGroupIDs = array();
1800
		while($groupIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIDs 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...
1801
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1802
			$groupIDs = DataObject::get("SilverStripe\\Security\\Group")->byIDs($groupIDs)->column("ParentID");
1803
			$groupIDs = array_filter($groupIDs);
1804
		}
1805
1806
		// Add a filter to this DataList
1807
		if(!empty($allGroupIDs)) {
1808
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1809
			return array("\"Group\".\"ID\" IN ($allGroupIDsPlaceholders)" => $allGroupIDs);
1810
		} else {
1811
			return array('"Group"."ID"' => 0);
1812
		}
1813
	}
1814
1815
	public function foreignIDWriteFilter($id = null) {
1816
		// Use the ManyManyList::foreignIDFilter rather than the one
1817
		// in this class, otherwise we end up selecting all inherited groups
1818
		return parent::foreignIDFilter($id);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (foreignIDFilter() instead of foreignIDWriteFilter()). Are you sure this is correct? If so, you might want to change this to $this->foreignIDFilter().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1819
	}
1820
1821
	public function add($item, $extraFields = null) {
1822
		// Get Group.ID
1823
		$itemID = null;
1824
		if(is_numeric($item)) {
1825
			$itemID = $item;
1826
		} else if($item instanceof Group) {
1827
			$itemID = $item->ID;
1828
		}
1829
1830
		// Check if this group is allowed to be added
1831
		if($this->canAddGroups(array($itemID))) {
1832
			parent::add($item, $extraFields);
1833
		}
1834
	}
1835
1836
	/**
1837
	 * Determine if the following groups IDs can be added
1838
	 *
1839
	 * @param array $itemIDs
1840
	 * @return boolean
1841
	 */
1842
	protected function canAddGroups($itemIDs) {
1843
		if(empty($itemIDs)) {
1844
			return true;
1845
		}
1846
		$member = $this->getMember();
1847
		return empty($member) || $member->onChangeGroups($itemIDs);
1848
	}
1849
1850
	/**
1851
	 * Get foreign member record for this relation
1852
	 *
1853
	 * @return Member
1854
	 */
1855
	protected function getMember() {
1856
		$id = $this->getForeignID();
1857
		if($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1858
			return DataObject::get_by_id('SilverStripe\\Security\\Member', $id);
1859
		}
1860
	}
1861
}
1862
1863
/**
1864
 * Member Validator
1865
 *
1866
 * Custom validation for the Member object can be achieved either through an
1867
 * {@link DataExtension} on the Member_Validator object or, by specifying a subclass of
1868
 * {@link Member_Validator} through the {@link Injector} API.
1869
 * The Validator can also be modified by adding an Extension to Member and implement the
1870
 * <code>updateValidator</code> hook.
1871
 * {@see Member::getValidator()}
1872
 *
1873
 * Additional required fields can also be set via config API, eg.
1874
 * <code>
1875
 * Member_Validator:
1876
 *   customRequired:
1877
 *     - Surname
1878
 * </code>
1879
 *
1880
 * @package framework
1881
 * @subpackage security
1882
 */
1883
class Member_Validator extends RequiredFields
1884
{
1885
	/**
1886
	 * Fields that are required by this validator
1887
	 * @config
1888
	 * @var array
1889
	 */
1890
	protected $customRequired = array(
1891
		'FirstName',
1892
		'Email'
1893
	);
1894
1895
	/**
1896
	 * Determine what member this validator is meant for
1897
	 * @var Member
1898
	 */
1899
	protected $forMember = null;
1900
1901
	/**
1902
	 * Constructor
1903
	 */
1904
	public function __construct() {
1905
		$required = func_get_args();
1906
1907
		if(isset($required[0]) && is_array($required[0])) {
1908
			$required = $required[0];
1909
		}
1910
1911
		$required = array_merge($required, $this->customRequired);
1912
1913
		// check for config API values and merge them in
1914
		$config = $this->config()->customRequired;
0 ignored issues
show
Documentation introduced by
The property customRequired does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1915
		if(is_array($config)){
1916
			$required = array_merge($required, $config);
1917
		}
1918
1919
		parent::__construct(array_unique($required));
1920
	}
1921
1922
	/**
1923
	 * Get the member this validator applies to.
1924
	 * @return Member
1925
	 */
1926
	public function getForMember()
1927
	{
1928
		return $this->forMember;
1929
	}
1930
1931
	/**
1932
	 * Set the Member this validator applies to.
1933
	 * @param Member $value
1934
	 * @return $this
1935
	 */
1936
	public function setForMember(Member $value)
1937
	{
1938
		$this->forMember = $value;
1939
		return $this;
1940
	}
1941
1942
	/**
1943
	 * Check if the submitted member data is valid (server-side)
1944
	 *
1945
	 * Check if a member with that email doesn't already exist, or if it does
1946
	 * that it is this member.
1947
	 *
1948
	 * @param array $data Submitted data
1949
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
1950
	 *              FALSE.
1951
	 */
1952
	public function php($data)
1953
	{
1954
		$valid = parent::php($data);
1955
1956
		$identifierField = (string)Member::config()->unique_identifier_field;
1957
1958
		// Only validate identifier field if it's actually set. This could be the case if
1959
		// somebody removes `Email` from the list of required fields.
1960
		if(isset($data[$identifierField])){
1961
			$id = isset($data['ID']) ? (int)$data['ID'] : 0;
1962
			if(!$id && ($ctrl = $this->form->getController())){
1963
				// get the record when within GridField (Member editing page in CMS)
1964
				if($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()){
1965
					$id = $record->ID;
1966
				}
1967
			}
1968
1969
			// If there's no ID passed via controller or form-data, use the assigned member (if available)
1970
			if(!$id && ($member = $this->getForMember())){
1971
				$id = $member->exists() ? $member->ID : 0;
1972
			}
1973
1974
			// set the found ID to the data array, so that extensions can also use it
1975
			$data['ID'] = $id;
1976
1977
			$members = Member::get()->filter($identifierField, $data[$identifierField]);
1978
			if($id) {
1979
				$members = $members->exclude('ID', $id);
1980
			}
1981
1982
			if($members->count() > 0) {
1983
				$this->validationError(
1984
					$identifierField,
1985
					_t(
1986
						'Member.VALIDATIONMEMBEREXISTS',
1987
						'A member already exists with the same {identifier}',
1988
						array('identifier' => Member::singleton()->fieldLabel($identifierField))
1989
					),
1990
					'required'
1991
				);
1992
				$valid = false;
1993
			}
1994
		}
1995
1996
1997
		// Execute the validators on the extensions
1998
		$results = $this->extend('updatePHP', $data, $this->form);
1999
		$results[] = $valid;
2000
		return min($results);
2001
	}
2002
}
2003