Completed
Push — 3 ( bc9e38...39c73e )
by Robbie
05:15 queued 10s
created

Member   F

Complexity

Total Complexity 249

Size/Duplication

Total Lines 1749
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 37

Importance

Changes 0
Metric Value
dl 0
loc 1749
rs 0.8
c 0
b 0
f 0
wmc 249
lcom 1
cbo 37

68 Methods

Rating   Name   Duplication   Size   Complexity  
A set_session_regenerate_id() 0 4 1
A populateDefaults() 0 4 1
A requireDefaultRecords() 0 5 1
A default_admin() 0 33 4
A set_login_marker_cookie() 0 4 1
B checkPassword() 0 29 7
A isDefaultAdmin() 0 4 2
A canLogIn() 0 18 2
B isLockedOut() 0 40 9
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 5
A addVisit() 0 9 2
A regenerateTempID() 0 8 2
A logged_in_session_exists() 0 9 4
C autoLogin() 0 49 12
A logOut() 0 22 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 19 1
A getMemberPasswordField() 0 23 4
A getValidator() 0 7 1
A currentUser() 0 7 3
A currentUserID() 0 9 4
A create_new_password() 0 21 3
C onBeforeWrite() 0 82 17
A onAfterWrite() 0 9 2
A onAfterDelete() 0 6 1
A deletePasswordLogs() 0 7 2
A onChangeGroups() 0 5 1
A disallowedGroups() 0 9 3
A inGroups() 0 7 4
B inGroup() 0 22 9
A addToGroupByCode() 0 18 3
A removeFromGroupByCode() 0 7 2
A set_title_columns() 0 4 2
B getTitle() 0 23 8
A get_title_sql() 0 16 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
B map_in_groups() 0 27 7
B mapInCMSGroups() 0 44 9
A memberNotInGroups() 0 12 4
C getCMSFields() 0 118 8
A fieldLabels() 0 20 2
B canView() 0 22 11
B canEdit() 0 22 11
B canDelete() 0 19 11
B validate() 0 17 10
A changePassword() 0 11 2
A registerFailedLogin() 0 14 3
A registerSuccessfulLogin() 0 9 2
A 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
 * The member class which represents the users of the system
4
 *
5
 * @package framework
6
 * @subpackage security
7
 *
8
 * @property string $FirstName
9
 * @property string $Surname
10
 * @property string $Email
11
 * @property string $Password
12
 * @property string $RememberLoginToken
13
 * @property string $TempIDHash
14
 * @property string $TempIDExpired
15
 * @property int $NumVisit @deprecated 4.0
16
 * @property string $LastVisited @deprecated 4.0
17
 * @property string $AutoLoginHash
18
 * @property string $AutoLoginExpired
19
 * @property string $PasswordEncryption
20
 * @property string $Salt
21
 * @property string $PasswordExpiry
22
 * @property string $LockedOutUntil
23
 * @property string $Locale
24
 * @property int $FailedLoginCount
25
 * @property string $DateFormat
26
 * @property string $TimeFormat
27
 */
28
class Member extends DataObject implements TemplateGlobalProvider {
29
30
	private static $db = array(
31
		'FirstName' => 'Varchar',
32
		'Surname' => 'Varchar',
33
		'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
34
		'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
35
		'TempIDExpired' => 'SS_Datetime', // Expiry of temp login
36
		'Password' => 'Varchar(160)',
37
		'RememberLoginToken' => 'Varchar(160)', // Note: this currently holds a hash, not a token.
38
		'NumVisit' => 'Int', // @deprecated 4.0
39
		'LastVisited' => 'SS_Datetime', // @deprecated 4.0
40
		'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
41
		'AutoLoginExpired' => 'SS_Datetime',
42
		// This is an arbitrary code pointing to a PasswordEncryptor instance,
43
		// not an actual encryption algorithm.
44
		// Warning: Never change this field after its the first password hashing without
45
		// providing a new cleartext password as well.
46
		'PasswordEncryption' => "Varchar(50)",
47
		'Salt' => 'Varchar(50)',
48
		'PasswordExpiry' => 'Date',
49
		'LockedOutUntil' => 'SS_Datetime',
50
		'Locale' => 'Varchar(6)',
51
		// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
52
		'FailedLoginCount' => 'Int',
53
		// In ISO format
54
		'DateFormat' => 'Varchar(30)',
55
		'TimeFormat' => 'Varchar(30)',
56
	);
57
58
	private static $belongs_many_many = array(
59
		'Groups' => 'Group',
60
	);
61
62
	private static $has_one = array();
63
64
	private static $has_many = array(
65
		'LoggedPasswords' => 'MemberPassword',
66
	);
67
68
	private static $many_many = array();
69
70
	private static $many_many_extraFields = array();
71
72
	private static $default_sort = '"Surname", "FirstName"';
73
74
	private static $indexes = array(
75
		'Email' => true,
76
		//Removed due to duplicate null values causing MSSQL problems
77
		//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true)
78
	);
79
80
	/**
81
	 * @config
82
	 * @var boolean
83
	 */
84
	private static $notify_password_change = false;
85
86
	/**
87
	 * Flag whether or not member visits should be logged (count only)
88
	 *
89
	 * @deprecated 4.0
90
	 * @var bool
91
	 * @config
92
	 */
93
	private static $log_last_visited = true;
94
95
	/**
96
	 * Flag whether we should count number of visits
97
	 *
98
	 * @deprecated 4.0
99
	 * @var bool
100
	 * @config
101
	 */
102
	private static $log_num_visits = true;
103
104
	/**
105
	 * All searchable database columns
106
	 * in this object, currently queried
107
	 * with a "column LIKE '%keywords%'
108
	 * statement.
109
	 *
110
	 * @var array
111
	 * @todo Generic implementation of $searchable_fields on DataObject,
112
	 * with definition for different searching algorithms
113
	 * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
114
	 */
115
	private static $searchable_fields = array(
116
		'FirstName',
117
		'Surname',
118
		'Email',
119
	);
120
121
	/**
122
	 * @config
123
	 * @var array
124
	 */
125
	private static $summary_fields = array(
126
		'FirstName',
127
		'Surname',
128
		'Email',
129
	);
130
131
	/**
132
	 * @config
133
	 * @var array
134
	 */
135
	private static $casting = array(
136
		'Name' => 'Varchar',
137
	);
138
139
	/**
140
	 * Internal-use only fields
141
	 *
142
	 * @config
143
	 * @var array
144
	 */
145
	private static $hidden_fields = array(
146
		'RememberLoginToken',
147
		'AutoLoginHash',
148
		'AutoLoginExpired',
149
		'PasswordEncryption',
150
		'PasswordExpiry',
151
		'LockedOutUntil',
152
		'TempIDHash',
153
		'TempIDExpired',
154
		'Salt',
155
		'NumVisit', // @deprecated 4.0
156
	);
157
158
	/**
159
	 * @config
160
	 * @var Array See {@link set_title_columns()}
161
	 */
162
	private static $title_format = null;
163
164
	/**
165
	 * The unique field used to identify this member.
166
	 * By default, it's "Email", but another common
167
	 * field could be Username.
168
	 *
169
	 * @config
170
	 * @var string
171
	 */
172
	private static $unique_identifier_field = 'Email';
173
174
	/**
175
	 * @config
176
	 * {@link PasswordValidator} object for validating user's password
177
	 */
178
	private static $password_validator = null;
179
180
	/**
181
	 * @config
182
	 * The number of days that a password should be valid for.
183
	 * By default, this is null, which means that passwords never expire
184
	 */
185
	private static $password_expiry_days = null;
186
187
	/**
188
	 * @config
189
	 * @var Int Number of incorrect logins after which
190
	 * the user is blocked from further attempts for the timespan
191
	 * defined in {@link $lock_out_delay_mins}.
192
	 */
193
	private static $lock_out_after_incorrect_logins = 10;
194
195
	/**
196
	 * @config
197
	 * @var integer Minutes of enforced lockout after incorrect password attempts.
198
	 * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
199
	 */
200
	private static $lock_out_delay_mins = 15;
201
202
	/**
203
	 * @config
204
	 * @var String If this is set, then a session cookie with the given name will be set on log-in,
205
	 * and cleared on logout.
206
	 */
207
	private static $login_marker_cookie = null;
208
209
	/**
210
	 * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
211
	 * should be called as a security precaution.
212
	 *
213
	 * This doesn't always work, especially if you're trying to set session cookies
214
	 * across an entire site using the domain parameter to session_set_cookie_params()
215
	 *
216
	 * @config
217
	 * @var boolean
218
	 */
219
	private static $session_regenerate_id = true;
220
221
222
	/**
223
	 * Default lifetime of temporary ids.
224
	 *
225
	 * This is the period within which a user can be re-authenticated within the CMS by entering only their password
226
	 * and without losing their workspace.
227
	 *
228
	 * Any session expiration outside of this time will require them to login from the frontend using their full
229
	 * username and password.
230
	 *
231
	 * Defaults to 72 hours. Set to zero to disable expiration.
232
	 *
233
	 * @config
234
	 * @var int Lifetime in seconds
235
	 */
236
	private static $temp_id_lifetime = 259200;
237
238
	/**
239
	 * @deprecated 4.0 Use the "Member.session_regenerate_id" config setting instead
240
	 */
241
	public static function set_session_regenerate_id($bool) {
242
		Deprecation::notice('4.0', 'Use the "Member.session_regenerate_id" config setting instead');
243
		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...
244
	}
245
246
	/**
247
	 * Ensure the locale is set to something sensible by default.
248
	 */
249
	public function populateDefaults() {
250
		parent::populateDefaults();
251
		$this->Locale = i18n::get_closest_translation(i18n::get_locale());
252
	}
253
254
	public function requireDefaultRecords() {
255
		parent::requireDefaultRecords();
256
		// Default groups should've been built by Group->requireDefaultRecords() already
257
		static::default_admin();
258
	}
259
260
	/**
261
	 * Get the default admin record if it exists, or creates it otherwise if enabled
262
	 *
263
	 * @return Member
264
	 */
265
	public static function default_admin() {
266
		// Check if set
267
		if(!Security::has_default_admin()) return null;
268
269
		// Find or create ADMIN group
270
		singleton('Group')->requireDefaultRecords();
271
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
272
273
		// Find member
274
		$admin = Member::get()
275
			->filter('Email', Security::default_admin_username())
276
			->first();
277
		if(!$admin) {
278
			// 'Password' is not set to avoid creating
279
			// persistent logins in the database. See Security::setDefaultAdmin().
280
			// Set 'Email' to identify this as the default admin
281
			$admin = Member::create();
282
			$admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
283
			$admin->Email = Security::default_admin_username();
284
			$admin->write();
285
		}
286
287
		// Ensure this user is in the admin group
288
		if(!$admin->inGroup($adminGroup)) {
289
			// Add member to group instead of adding group to member
290
			// This bypasses the privilege escallation code in Member_GroupSet
291
			$adminGroup
292
				->DirectMembers()
293
				->add($admin);
294
		}
295
296
		return $admin;
297
	}
298
299
	/**
300
	 * If this is called, then a session cookie will be set to "1" whenever a user
301
	 * logs in.  This lets 3rd party tools, such as apache's mod_rewrite, detect
302
	 * whether a user is logged in or not and alter behaviour accordingly.
303
	 *
304
	 * One known use of this is to bypass static caching for logged in users.  This is
305
	 * done by putting this into _config.php
306
	 * <pre>
307
	 * Member::set_login_marker_cookie("SS_LOGGED_IN");
308
	 * </pre>
309
	 *
310
	 * And then adding this condition to each of the rewrite rules that make use of
311
	 * the static cache.
312
	 * <pre>
313
	 * RewriteCond %{HTTP_COOKIE} !SS_LOGGED_IN=1
314
	 * </pre>
315
	 *
316
	 * @deprecated 4.0 Use the "Member.login_marker_cookie" config setting instead
317
	 * @param $cookieName string The name of the cookie to set.
318
	 */
319
	public static function set_login_marker_cookie($cookieName) {
320
		Deprecation::notice('4.0', 'Use the "Member.login_marker_cookie" config setting instead');
321
		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...
322
	}
323
324
	/**
325
	 * Check if the passed password matches the stored one (if the member is not locked out).
326
	 *
327
	 * @param string $password
328
	 * @return ValidationResult
329
	 */
330
	public function checkPassword($password) {
331
		$result = $this->canLogIn();
332
333
		// Short-circuit the result upon failure, no further checks needed.
334
		if (!$result->valid()) {
335
			return $result;
336
		}
337
338
		// Allow default admin to login as self
339
		if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
340
			return $result;
341
		}
342
343
		// Check a password is set on this member
344
		if(empty($this->Password) && $this->exists()) {
345
			$result->error(_t('Member.NoPassword','There is no password on this member.'));
346
			return $result;
347
		}
348
349
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
350
		if(!$e->check($this->Password, $password, $this->Salt, $this)) {
351
			$result->error(_t (
352
				'Member.ERRORWRONGCRED',
353
				'The provided details don\'t seem to be correct. Please try again.'
354
			));
355
		}
356
357
		return $result;
358
	}
359
360
	/**
361
	 * Check if this user is the currently configured default admin
362
	 *
363
	 * @return bool
364
	 */
365
	public function isDefaultAdmin() {
366
		return Security::has_default_admin()
367
			&& $this->Email === Security::default_admin_username();
368
	}
369
370
	/**
371
	 * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
372
	 * one with error messages to display if the member is locked out.
373
	 *
374
	 * You can hook into this with a "canLogIn" method on an attached extension.
375
	 *
376
	 * @return ValidationResult
377
	 */
378
	public function canLogIn() {
379
		$result = ValidationResult::create();
380
381
		if($this->isLockedOut()) {
382
			$result->error(
383
				_t(
384
					'Member.ERRORLOCKEDOUT2',
385
					'Your account has been temporarily disabled because of too many failed attempts at ' .
386
					'logging in. Please try again in {count} minutes.',
387
					null,
388
					array('count' => $this->config()->lock_out_delay_mins)
389
				)
390
			);
391
		}
392
393
		$this->extend('canLogIn', $result);
394
		return $result;
395
	}
396
397
	/**
398
	 * Returns true if this user is locked out
399
	 */
400
	public function isLockedOut() {
401
		$state = true;
402
		if ($this->LockedOutUntil && $this->dbObject('LockedOutUntil')->InFuture()) {
403
			$state = true;
404
		} elseif ($this->config()->lock_out_after_incorrect_logins <= 0) {
405
			$state = false;
406
		} else {
407
			$email = $this->{static::config()->unique_identifier_field};
408
			$attempts = LoginAttempt::getByEmail($email)
409
				->sort('Created', 'DESC')
410
				->limit($this->config()->lock_out_after_incorrect_logins);
411
412
			if ($attempts->count() < $this->config()->lock_out_after_incorrect_logins) {
413
				$state = false;
414
			} else {
415
416
				$success = false;
417
				foreach ($attempts as $attempt) {
418
					if ($attempt->Status === 'Success') {
419
						$success = true;
420
						$state = false;
421
						break;
422
					}
423
				}
424
425
				if (!$success) {
426
					$lockedOutUntil = $attempts->first()->dbObject('Created')->Format('U')
427
					                  + ($this->config()->lock_out_delay_mins * 60);
428
					if (SS_Datetime::now()->Format('U') < $lockedOutUntil) {
429
						$state = true;
430
					} else {
431
						$state = false;
432
					}
433
				}
434
			}
435
		}
436
437
		$this->extend('updateIsLockedOut', $state);
438
		return $state;
439
	}
440
441
	/**
442
	 * Regenerate the session_id.
443
	 * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
444
	 * They have caused problems in certain
445
	 * quirky problems (such as using the Windmill 0.3.6 proxy).
446
	 */
447
	public static function session_regenerate_id() {
448
		if(!self::config()->session_regenerate_id) return;
449
450
		// This can be called via CLI during testing.
451
		if(Director::is_cli()) return;
452
453
		$file = '';
454
		$line = '';
455
456
		// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
457
		// There's nothing we can do about this, because it's an operating system function!
458
		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...
459
	}
460
461
	/**
462
	 * Get the field used for uniquely identifying a member
463
	 * in the database. {@see Member::$unique_identifier_field}
464
	 *
465
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
466
	 * @return string
467
	 */
468
	public static function get_unique_identifier_field() {
469
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
470
		return Member::config()->unique_identifier_field;
471
	}
472
473
	/**
474
	 * Set the field used for uniquely identifying a member
475
	 * in the database. {@see Member::$unique_identifier_field}
476
	 *
477
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
478
	 * @param $field The field name to set as the unique field
479
	 */
480
	public static function set_unique_identifier_field($field) {
481
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
482
		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...
483
	}
484
485
	/**
486
	 * Set a {@link PasswordValidator} object to use to validate member's passwords.
487
	 */
488
	public static function set_password_validator($pv) {
489
		self::$password_validator = $pv;
490
	}
491
492
	/**
493
	 * Returns the current {@link PasswordValidator}
494
	 */
495
	public static function password_validator() {
496
		return self::$password_validator;
497
	}
498
499
	/**
500
	 * Set the number of days that a password should be valid for.
501
	 * Set to null (the default) to have passwords never expire.
502
	 *
503
	 * @deprecated 4.0 Use the "Member.password_expiry_days" config setting instead
504
	 */
505
	public static function set_password_expiry($days) {
506
		Deprecation::notice('4.0', 'Use the "Member.password_expiry_days" config setting instead');
507
		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...
508
	}
509
510
	/**
511
	 * Configure the security system to lock users out after this many incorrect logins
512
	 *
513
	 * @deprecated 4.0 Use the "Member.lock_out_after_incorrect_logins" config setting instead
514
	 */
515
	public static function lock_out_after_incorrect_logins($numLogins) {
516
		Deprecation::notice('4.0', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead');
517
		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...
518
	}
519
520
521
	public function isPasswordExpired() {
522
		if(!$this->PasswordExpiry) return false;
523
		return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
524
	}
525
526
	/**
527
	 * Logs this member in
528
	 *
529
	 * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
530
	 */
531
	public function logIn($remember = false) {
532
		$this->extend('beforeMemberLoggedIn');
533
534
		self::session_regenerate_id();
535
536
		Session::set("loggedInAs", $this->ID);
537
		// This lets apache rules detect whether the user has logged in
538
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0);
539
540
		$this->addVisit();
0 ignored issues
show
Deprecated Code introduced by
The method Member::addVisit() has been deprecated with message: 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
541
542
		// Only set the cookie if autologin is enabled
543
		if($remember && Security::config()->autologin_enabled) {
544
			// Store the hash and give the client the cookie with the token.
545
			$generator = new RandomGenerator();
546
			$token = $generator->randomToken('sha1');
547
			$hash = $this->encryptWithUserSettings($token);
548
			$this->RememberLoginToken = $hash;
549
			Cookie::set('alc_enc', $this->ID . ':' . $token, 90, null, null, null, true);
550
		} else {
551
			$this->RememberLoginToken = null;
552
			Cookie::force_expiry('alc_enc');
553
		}
554
555
		// Clear the incorrect log-in count
556
		$this->registerSuccessfulLogin();
557
558
		// Don't set column if its not built yet (the login might be precursor to a /dev/build...)
559
		if(array_key_exists('LockedOutUntil', DB::field_list('Member'))) {
560
			$this->LockedOutUntil = null;
561
		}
562
563
		$this->regenerateTempID();
564
565
		$this->write();
566
567
		// Audit logging hook
568
		$this->extend('memberLoggedIn');
569
	}
570
571
	/**
572
	 * @deprecated 4.0
573
	 */
574
	public function addVisit() {
575
		if($this->config()->log_num_visits) {
576
			Deprecation::notice(
577
				'4.0',
578
				'Member::$NumVisit is deprecated. From 4.0 onwards you should implement this as a custom extension'
579
			);
580
			$this->NumVisit++;
581
		}
582
	}
583
584
	/**
585
	 * Trigger regeneration of TempID.
586
	 *
587
	 * This should be performed any time the user presents their normal identification (normally Email)
588
	 * and is successfully authenticated.
589
	 */
590
	public function regenerateTempID() {
591
		$generator = new RandomGenerator();
592
		$this->TempIDHash = $generator->randomToken('sha1');
593
		$this->TempIDExpired = self::config()->temp_id_lifetime
594
			? date('Y-m-d H:i:s', strtotime(SS_Datetime::now()->getValue()) + self::config()->temp_id_lifetime)
595
			: null;
596
		$this->write();
597
	}
598
599
	/**
600
	 * Check if the member ID logged in session actually
601
	 * has a database record of the same ID. If there is
602
	 * no logged in user, FALSE is returned anyway.
603
	 *
604
	 * @return boolean TRUE record found FALSE no record found
605
	 */
606
	public static function logged_in_session_exists() {
607
		if($id = Member::currentUserID()) {
608
			if($member = DataObject::get_by_id('Member', $id)) {
609
				if($member->exists()) return true;
610
			}
611
		}
612
613
		return false;
614
	}
615
616
	/**
617
	 * Log the user in if the "remember login" cookie is set
618
	 *
619
	 * The <i>remember login token</i> will be changed on every successful
620
	 * auto-login.
621
	 */
622
	public static function autoLogin() {
623
		// Don't bother trying this multiple times
624
		self::$_already_tried_to_auto_log_in = true;
625
626
		if(!Security::config()->autologin_enabled
627
			|| strpos(Cookie::get('alc_enc'), ':') === false
628
			|| Session::get("loggedInAs")
629
			|| !Security::database_is_ready()
630
		) {
631
			return;
632
		}
633
634
		list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
635
636
		if (!$uid || !$token) {
637
			return;
638
		}
639
640
		$member = DataObject::get_by_id("Member", $uid);
641
642
		// check if autologin token matches
643
		if($member) {
644
			$hash = $member->encryptWithUserSettings($token);
645
			if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
646
				$member = null;
647
			}
648
		}
649
650
		if($member) {
651
			self::session_regenerate_id();
652
			Session::set("loggedInAs", $member->ID);
653
			// This lets apache rules detect whether the user has logged in
654
			if(Member::config()->login_marker_cookie) {
655
				Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
656
			}
657
658
			$generator = new RandomGenerator();
659
			$token = $generator->randomToken('sha1');
660
			$hash = $member->encryptWithUserSettings($token);
661
			$member->RememberLoginToken = $hash;
0 ignored issues
show
Documentation introduced by
The property RememberLoginToken does not exist on object<DataObject>. 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...
662
			Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
663
664
			$member->addVisit();
665
			$member->write();
666
667
			// Audit logging hook
668
			$member->extend('memberAutoLoggedIn');
669
		}
670
	}
671
672
	/**
673
	 * Logs this member out.
674
	 */
675
	public function logOut() {
676
		$this->extend('beforeMemberLoggedOut');
677
678
		Session::clear("loggedInAs");
679
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
680
681
		Session::destroy();
682
683
		$this->extend('memberLoggedOut');
684
685
		$this->RememberLoginToken = null;
686
		Cookie::force_expiry('alc_enc');
687
688
		// Switch back to live in order to avoid infinite loops when
689
		// redirecting to the login screen (if this login screen is versioned)
690
		Session::clear('readingMode');
691
692
		$this->write();
693
694
		// Audit logging hook
695
		$this->extend('memberLoggedOut');
696
	}
697
698
	/**
699
	 * Utility for generating secure password hashes for this member.
700
	 */
701
	public function encryptWithUserSettings($string) {
702
		if (!$string) return null;
703
704
		// If the algorithm or salt is not available, it means we are operating
705
		// on legacy account with unhashed password. Do not hash the string.
706
		if (!$this->PasswordEncryption) {
707
			return $string;
708
		}
709
710
		// We assume we have PasswordEncryption and Salt available here.
711
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
712
		return $e->encrypt($string, $this->Salt);
713
714
	}
715
716
	/**
717
	 * Generate an auto login token which can be used to reset the password,
718
	 * at the same time hashing it and storing in the database.
719
	 *
720
	 * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
721
	 *
722
	 * @returns string Token that should be passed to the client (but NOT persisted).
723
	 *
724
	 * @todo Make it possible to handle database errors such as a "duplicate key" error
725
	 */
726
	public function generateAutologinTokenAndStoreHash($lifetime = 2) {
727
		do {
728
			$generator = new RandomGenerator();
729
			$token = $generator->randomToken();
730
			$hash = $this->encryptWithUserSettings($token);
731
		} while(DataObject::get_one('Member', array(
732
			'"Member"."AutoLoginHash"' => $hash
733
		)));
734
735
		$this->AutoLoginHash = $hash;
736
		$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
737
738
		$this->write();
739
740
		return $token;
741
	}
742
743
	/**
744
	 * Check the token against the member.
745
	 *
746
	 * @param string $autologinToken
747
	 *
748
	 * @returns bool Is token valid?
749
	 */
750
	public function validateAutoLoginToken($autologinToken) {
751
		$hash = $this->encryptWithUserSettings($autologinToken);
752
		$member = self::member_from_autologinhash($hash, false);
753
		return (bool)$member;
754
	}
755
756
	/**
757
	 * Return the member for the auto login hash
758
	 *
759
	 * @param string $hash The hash key
760
	 * @param bool $login Should the member be logged in?
761
	 *
762
	 * @return Member the matching member, if valid
763
	 * @return Member
764
	 */
765
	public static function member_from_autologinhash($hash, $login = false) {
766
767
		$nowExpression = DB::get_conn()->now();
768
		$member = DataObject::get_one('Member', array(
769
			"\"Member\".\"AutoLoginHash\"" => $hash,
770
			"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
771
		));
772
773
		if($login && $member) $member->logIn();
774
775
		return $member;
776
	}
777
778
	/**
779
	 * Find a member record with the given TempIDHash value
780
	 *
781
	 * @param string $tempid
782
	 * @return Member
783
	 */
784
	public static function member_from_tempid($tempid) {
785
		$members = Member::get()
786
			->filter('TempIDHash', $tempid);
787
788
		// Exclude expired
789
		if(static::config()->temp_id_lifetime) {
790
			$members = $members->filter('TempIDExpired:GreaterThan', SS_Datetime::now()->getValue());
791
		}
792
793
		return $members->first();
794
	}
795
796
	/**
797
	 * Returns the fields for the member form - used in the registration/profile module.
798
	 * It should return fields that are editable by the admin and the logged-in user.
799
	 *
800
	 * @return FieldList Returns a {@link FieldList} containing the fields for
801
	 *                   the member form.
802
	 */
803
	public function getMemberFormFields() {
804
		$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...
805
806
		$fields->replaceField('Password', $this->getMemberPasswordField());
807
808
		$fields->replaceField('Locale', new DropdownField (
809
			'Locale',
810
			$this->fieldLabel('Locale'),
811
			i18n::get_existing_translations()
812
		));
813
814
		$fields->removeByName(static::config()->hidden_fields);
815
		$fields->removeByName('LastVisited');
816
		$fields->removeByName('FailedLoginCount');
817
818
819
		$this->extend('updateMemberFormFields', $fields);
820
		return $fields;
821
	}
822
823
	/**
824
	 * Builds "Change / Create Password" field for this member
825
	 *
826
	 * @return ConfirmedPasswordField
827
	 */
828
	public function getMemberPasswordField() {
829
		$editingPassword = $this->isInDB();
830
		$label = $editingPassword
831
			? _t('Member.EDIT_PASSWORD', 'New Password')
832
			: $this->fieldLabel('Password');
833
		/** @var ConfirmedPasswordField $password */
834
		$password = ConfirmedPasswordField::create(
835
			'Password',
836
			$label,
837
			null,
838
			null,
839
			$editingPassword
840
		);
841
842
		// If editing own password, require confirmation of existing
843
		if($editingPassword && $this->ID == Member::currentUserID()) {
844
			$password->setRequireExistingPassword(true);
845
		}
846
847
		$password->setCanBeEmpty(true);
848
		$this->extend('updateMemberPasswordField', $password);
849
		return $password;
850
	}
851
852
853
	/**
854
	 * Returns the {@link RequiredFields} instance for the Member object. This
855
	 * Validator is used when saving a {@link CMSProfileController} or added to
856
	 * any form responsible for saving a users data.
857
	 *
858
	 * To customize the required fields, add a {@link DataExtension} to member
859
	 * calling the `updateValidator()` method.
860
	 *
861
	 * @return Member_Validator
862
	 */
863
	public function getValidator() {
864
		$validator = Injector::inst()->create('Member_Validator');
865
		$validator->setForMember($this);
866
		$this->extend('updateValidator', $validator);
867
868
		return $validator;
869
	}
870
871
872
	/**
873
	 * Returns the current logged in user
874
	 *
875
	 * @return Member|null
876
	 */
877
	public static function currentUser() {
878
		$id = Member::currentUserID();
879
880
		if($id) {
881
			return DataObject::get_by_id('Member', $id) ?: null;
882
		}
883
	}
884
885
	/**
886
	 * Get the ID of the current logged in user
887
	 *
888
	 * @return int Returns the ID of the current logged in user or 0.
889
	 */
890
	public static function currentUserID() {
891
		$id = Session::get("loggedInAs");
892
		if(!$id && !self::$_already_tried_to_auto_log_in) {
893
			self::autoLogin();
894
			$id = Session::get("loggedInAs");
895
		}
896
897
		return is_numeric($id) ? (int) $id : 0;
898
	}
899
	private static $_already_tried_to_auto_log_in = false;
900
901
902
	/*
903
	 * Generate a random password, with randomiser to kick in if there's no words file on the
904
	 * filesystem.
905
	 *
906
	 * @return string Returns a random password.
907
	 * @deprecated 3.6.0..4.0.0
908
	 */
909
	public static function create_new_password() {
910
		Deprecation::notice('4.0', 'Please use Security/lostpassword to reset a password');
911
		$words = Config::inst()->get('Security', 'word_list');
912
913
		if($words && file_exists($words)) {
914
			$words = file($words);
915
916
			list($usec, $sec) = explode(' ', microtime());
917
			srand($sec + ((float) $usec * 100000));
918
919
			$word = trim($words[rand(0,sizeof($words)-1)]);
920
			$number = rand(10,999);
921
922
			return $word . $number;
923
		} else {
924
			$random = rand();
925
			$string = md5($random);
926
			$output = substr($string, 0, 8);
927
			return $output;
928
		}
929
	}
930
931
	/**
932
	 * Event handler called before writing to the database.
933
	 */
934
	public function onBeforeWrite() {
935
		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...
936
937
		// If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
938
		// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
939
		// but rather a last line of defense against data inconsistencies.
940
		$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...
941
		if($this->$identifierField) {
942
943
			// Note: Same logic as Member_Validator class
944
			$filter = array("\"$identifierField\"" => $this->$identifierField);
945
			if($this->ID) {
946
				$filter[] = array('"Member"."ID" <> ?' => $this->ID);
947
			}
948
			$existingRecord = DataObject::get_one('Member', $filter);
949
950
			if($existingRecord) {
951
				throw new ValidationException(ValidationResult::create(false, _t(
952
					'Member.ValidationIdentifierFailed',
953
					'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
954
					'Values in brackets show "fieldname = value", usually denoting an existing email address',
955
					array(
956
						'id' => $existingRecord->ID,
957
						'name' => $identifierField,
958
						'value' => $this->$identifierField
959
					)
960
				)));
961
			}
962
		}
963
964
		// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
965
		// However, if TestMailer is in use this isn't a risk.
966
		if(
967
			(Director::isLive() || Email::mailer() instanceof TestMailer)
968
			&& $this->isChanged('Password')
969
			&& $this->record['Password']
970
			&& $this->config()->notify_password_change
971
		) {
972
			$e = Member_ChangePasswordEmail::create();
973
			$e->populateTemplate($this);
974
			$e->setTo($this->Email);
975
			$e->send();
976
		}
977
978
		// The test on $this->ID is used for when records are initially created.
979
		// Note that this only works with cleartext passwords, as we can't rehash
980
		// existing passwords.
981
		if((!$this->ID && $this->Password) || $this->isChanged('Password')) {
982
			//reset salt so that it gets regenerated - this will invalidate any persistant login cookies
983
			// or other information encrypted with this Member's settings (see self::encryptWithUserSettings)
984
			$this->Salt = '';
985
			// Password was changed: encrypt the password according the settings
986
			$encryption_details = Security::encrypt_password(
987
				$this->Password, // this is assumed to be cleartext
988
				$this->Salt,
989
				$this->isChanged('PasswordEncryption') ? $this->PasswordEncryption : null,
990
				$this
991
			);
992
993
			// Overwrite the Password property with the hashed value
994
			$this->Password = $encryption_details['password'];
995
			$this->Salt = $encryption_details['salt'];
996
			$this->PasswordEncryption = $encryption_details['algorithm'];
997
998
			// If we haven't manually set a password expiry
999
			if(!$this->isChanged('PasswordExpiry')) {
1000
				// then set it for us
1001
				if(self::config()->password_expiry_days) {
1002
					$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
1003
				} else {
1004
					$this->PasswordExpiry = null;
1005
				}
1006
			}
1007
		}
1008
1009
		// save locale
1010
		if(!$this->Locale) {
1011
			$this->Locale = i18n::get_locale();
1012
		}
1013
1014
		parent::onBeforeWrite();
1015
	}
1016
1017
	public function onAfterWrite() {
1018
		parent::onAfterWrite();
1019
1020
		Permission::flush_permission_cache();
1021
1022
		if($this->isChanged('Password')) {
1023
			MemberPassword::log($this);
1024
		}
1025
	}
1026
1027
	public function onAfterDelete() {
1028
		parent::onAfterDelete();
1029
1030
		//prevent orphaned records remaining in the DB
1031
		$this->deletePasswordLogs();
1032
	}
1033
1034
	/**
1035
	 * Delete the MemberPassword objects that are associated to this user
1036
	 *
1037
	 * @return self
1038
	 */
1039
	protected function deletePasswordLogs() {
1040
		foreach ($this->LoggedPasswords() as $password) {
1041
			$password->delete();
1042
			$password->destroy();
1043
		}
1044
		return $this;
1045
	}
1046
1047
	/**
1048
	 * Filter out admin groups to avoid privilege escalation,
1049
	 * If any admin groups are requested, deny the whole save operation.
1050
	 *
1051
	 * @param Array $ids Database IDs of Group records
1052
	 * @return boolean True if the change can be accepted
1053
	 */
1054
	public function onChangeGroups($ids) {
1055
		// Ensure none of these match disallowed list
1056
		$disallowedGroupIDs = $this->disallowedGroups();
1057
		return count(array_intersect($ids, $disallowedGroupIDs)) == 0;
1058
	}
1059
1060
	/**
1061
	 * List of group IDs this user is disallowed from
1062
	 *
1063
	 * @return int[] List of group IDs
1064
	 */
1065
	public function disallowedGroups() {
1066
		// unless the current user is an admin already OR the logged in user is an admin
1067
		if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
1068
			return array();
1069
		}
1070
1071
		// Non-admins may not belong to admin groups
1072
		return Permission::get_groups_by_permission('ADMIN')->column('ID');
1073
	}
1074
1075
1076
	/**
1077
	 * Check if the member is in one of the given groups.
1078
	 *
1079
	 * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
1080
	 * @param boolean $strict Only determine direct group membership if set to true (Default: false)
1081
	 * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
1082
	 */
1083
	public function inGroups($groups, $strict = false) {
1084
		if($groups) foreach($groups as $group) {
1085
			if($this->inGroup($group, $strict)) return true;
1086
		}
1087
1088
		return false;
1089
	}
1090
1091
1092
	/**
1093
	 * Check if the member is in the given group or any parent groups.
1094
	 *
1095
	 * @param int|Group|string $group Group instance, Group Code or ID
1096
	 * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
1097
	 * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
1098
	 */
1099
	public function inGroup($group, $strict = false) {
1100
		if(is_numeric($group)) {
1101
			$groupCheckObj = DataObject::get_by_id('Group', $group);
1102
		} elseif(is_string($group)) {
1103
			$groupCheckObj = DataObject::get_one('Group', array(
1104
				'"Group"."Code"' => $group
1105
			));
1106
		} elseif($group instanceof Group) {
1107
			$groupCheckObj = $group;
1108
		} else {
1109
			user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
1110
		}
1111
1112
		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...
1113
1114
		$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1115
		if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
1116
			if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
1117
		}
1118
1119
		return false;
1120
	}
1121
1122
	/**
1123
	 * Adds the member to a group. This will create the group if the given
1124
	 * group code does not return a valid group object.
1125
	 *
1126
	 * @param string $groupcode
1127
	 * @param string Title of the group
1128
	 */
1129
	public function addToGroupByCode($groupcode, $title = "") {
1130
		$group = DataObject::get_one('Group', array(
1131
			'"Group"."Code"' => $groupcode
1132
		));
1133
1134
		if($group) {
1135
			$this->Groups()->add($group);
1136
		} else {
1137
			if(!$title) $title = $groupcode;
1138
1139
			$group = new Group();
1140
			$group->Code = $groupcode;
1141
			$group->Title = $title;
1142
			$group->write();
1143
1144
			$this->Groups()->add($group);
1145
		}
1146
	}
1147
1148
	/**
1149
	 * Removes a member from a group.
1150
	 *
1151
	 * @param string $groupcode
1152
	 */
1153
	public function removeFromGroupByCode($groupcode) {
1154
		$group = Group::get()->filter(array('Code' => $groupcode))->first();
1155
1156
		if($group) {
1157
			$this->Groups()->remove($group);
1158
		}
1159
	}
1160
1161
	/**
1162
	 * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
1163
	 * @param String $sep Separator
1164
	 */
1165
	public static function set_title_columns($columns, $sep = ' ') {
1166
		if (!is_array($columns)) $columns = array($columns);
1167
		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...
1168
	}
1169
1170
	//------------------- HELPER METHODS -----------------------------------//
1171
1172
	/**
1173
	 * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1174
	 * Falls back to showing either field on its own.
1175
	 *
1176
	 * You can overload this getter with {@link set_title_format()}
1177
	 * and {@link set_title_sql()}.
1178
	 *
1179
	 * @return string Returns the first- and surname of the member. If the ID
1180
	 *  of the member is equal 0, only the surname is returned.
1181
	 */
1182
	public function getTitle() {
1183
		$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...
1184
		if ($format) {
1185
			$values = array();
1186
			foreach($format['columns'] as $col) {
1187
				$values[] = $this->getField($col);
1188
			}
1189
			return join($format['sep'], $values);
1190
		}
1191
		if($this->getField('ID') === 0)
1192
			return $this->getField('Surname');
1193
		else{
1194
			if($this->getField('Surname') && $this->getField('FirstName')){
1195
				return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1196
			}elseif($this->getField('Surname')){
1197
				return $this->getField('Surname');
1198
			}elseif($this->getField('FirstName')){
1199
				return $this->getField('FirstName');
1200
			}else{
1201
				return null;
1202
			}
1203
		}
1204
	}
1205
1206
	/**
1207
	 * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1208
	 * Useful for custom queries which assume a certain member title format.
1209
	 *
1210
	 * @param String $tableName
1211
	 * @return String SQL
1212
	 */
1213
	public static function get_title_sql($tableName = 'Member') {
1214
		// This should be abstracted to SSDatabase concatOperator or similar.
1215
		$op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || ";
0 ignored issues
show
Bug introduced by
The class 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...
1216
1217
		$format = self::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...
1218
		if ($format) {
1219
			$columnsWithTablename = array();
1220
			foreach($format['columns'] as $column) {
1221
				$columnsWithTablename[] = "\"$tableName\".\"$column\"";
1222
			}
1223
1224
			return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")";
1225
		} else {
1226
			return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")";
1227
		}
1228
	}
1229
1230
1231
	/**
1232
	 * Get the complete name of the member
1233
	 *
1234
	 * @return string Returns the first- and surname of the member.
1235
	 */
1236
	public function getName() {
1237
		return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1238
	}
1239
1240
1241
	/**
1242
	 * Set first- and surname
1243
	 *
1244
	 * This method assumes that the last part of the name is the surname, e.g.
1245
	 * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1246
	 *
1247
	 * @param string $name The name
1248
	 */
1249
	public function setName($name) {
1250
		$nameParts = explode(' ', $name);
1251
		$this->Surname = array_pop($nameParts);
1252
		$this->FirstName = join(' ', $nameParts);
1253
	}
1254
1255
1256
	/**
1257
	 * Alias for {@link setName}
1258
	 *
1259
	 * @param string $name The name
1260
	 * @see setName()
1261
	 */
1262
	public function splitName($name) {
1263
		return $this->setName($name);
1264
	}
1265
1266
	/**
1267
	 * Override the default getter for DateFormat so the
1268
	 * default format for the user's locale is used
1269
	 * if the user has not defined their own.
1270
	 *
1271
	 * @return string ISO date format
1272
	 */
1273
	public function getDateFormat() {
1274
		if($this->getField('DateFormat')) {
1275
			return $this->getField('DateFormat');
1276
		} else {
1277
			return Config::inst()->get('i18n', 'date_format');
1278
		}
1279
	}
1280
1281
	/**
1282
	 * Override the default getter for TimeFormat so the
1283
	 * default format for the user's locale is used
1284
	 * if the user has not defined their own.
1285
	 *
1286
	 * @return string ISO date format
1287
	 */
1288
	public function getTimeFormat() {
1289
		if($this->getField('TimeFormat')) {
1290
			return $this->getField('TimeFormat');
1291
		} else {
1292
			return Config::inst()->get('i18n', 'time_format');
1293
		}
1294
	}
1295
1296
	//---------------------------------------------------------------------//
1297
1298
1299
	/**
1300
	 * Get a "many-to-many" map that holds for all members their group memberships,
1301
	 * including any parent groups where membership is implied.
1302
	 * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1303
	 *
1304
	 * @todo Push all this logic into Member_GroupSet's getIterator()?
1305
	 * @return Member_Groupset
1306
	 */
1307
	public function Groups() {
1308
		$groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID');
1309
		$groups = $groups->forForeignID($this->ID);
1310
1311
		$this->extend('updateGroups', $groups);
1312
1313
		return $groups;
1314
	}
1315
1316
	/**
1317
	 * @return ManyManyList
1318
	 */
1319
	public function DirectGroups() {
1320
		return $this->getManyManyComponents('Groups');
1321
	}
1322
1323
	/**
1324
	 * Get a member SQLMap of members in specific groups
1325
	 *
1326
	 * If no $groups is passed, all members will be returned
1327
	 *
1328
	 * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1329
	 * @return SQLMap Returns an SQLMap that returns all Member data.
1330
	 * @see map()
1331
	 */
1332
	public static function map_in_groups($groups = null) {
1333
		$groupIDList = array();
1334
1335
		if($groups instanceof SS_List) {
1336
			foreach( $groups as $group ) {
1337
				$groupIDList[] = $group->ID;
1338
			}
1339
		} elseif(is_array($groups)) {
1340
			$groupIDList = $groups;
1341
		} elseif($groups) {
1342
			$groupIDList[] = $groups;
1343
		}
1344
1345
		// No groups, return all Members
1346
		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...
1347
			return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
1348
		}
1349
1350
		$membersList = new ArrayList();
1351
		// This is a bit ineffective, but follow the ORM style
1352
		foreach(Group::get()->byIDs($groupIDList) as $group) {
1353
			$membersList->merge($group->Members());
1354
		}
1355
1356
		$membersList->removeDuplicates('ID');
1357
		return $membersList->map();
1358
	}
1359
1360
1361
	/**
1362
	 * Get a map of all members in the groups given that have CMS permissions
1363
	 *
1364
	 * If no groups are passed, all groups with CMS permissions will be used.
1365
	 *
1366
	 * @param array $groups Groups to consider or NULL to use all groups with
1367
	 *                      CMS permissions.
1368
	 * @return SS_Map Returns a map of all members in the groups given that
1369
	 *                have CMS permissions.
1370
	 */
1371
	public static function mapInCMSGroups($groups = null) {
1372
		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...
1373
			$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1374
1375
			if(class_exists('CMSMain')) {
1376
				$cmsPerms = singleton('CMSMain')->providePermissions();
1377
			} else {
1378
				$cmsPerms = singleton('LeftAndMain')->providePermissions();
1379
			}
1380
1381
			if(!empty($cmsPerms)) {
1382
				$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1383
			}
1384
1385
			$permsClause = DB::placeholders($perms);
1386
			$groups = DataObject::get('Group')
1387
				->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1388
				->where(array(
1389
					"\"Permission\".\"Code\" IN ($permsClause)" => $perms
1390
				));
1391
		}
1392
1393
		$groupIDList = array();
1394
1395
		if(is_a($groups, 'SS_List')) {
1396
			foreach($groups as $group) {
1397
				$groupIDList[] = $group->ID;
1398
			}
1399
		} elseif(is_array($groups)) {
1400
			$groupIDList = $groups;
1401
		}
1402
1403
		$members = Member::get()
1404
			->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1405
			->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1406
		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...
1407
			$groupClause = DB::placeholders($groupIDList);
1408
			$members = $members->where(array(
1409
				"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1410
			));
1411
		}
1412
1413
		return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1414
	}
1415
1416
1417
	/**
1418
	 * Get the groups in which the member is NOT in
1419
	 *
1420
	 * When passed an array of groups, and a component set of groups, this
1421
	 * function will return the array of groups the member is NOT in.
1422
	 *
1423
	 * @param array $groupList An array of group code names.
1424
	 * @param array $memberGroups A component set of groups (if set to NULL,
1425
	 *                            $this->groups() will be used)
1426
	 * @return array Groups in which the member is NOT in.
1427
	 */
1428
	public function memberNotInGroups($groupList, $memberGroups = null){
1429
		if(!$memberGroups) $memberGroups = $this->Groups();
1430
1431
		foreach($memberGroups as $group) {
1432
			if(in_array($group->Code, $groupList)) {
1433
				$index = array_search($group->Code, $groupList);
1434
				unset($groupList[$index]);
1435
			}
1436
		}
1437
1438
		return $groupList;
1439
	}
1440
1441
1442
	/**
1443
	 * Return a {@link FieldList} of fields that would appropriate for editing
1444
	 * this member.
1445
	 *
1446
	 * @return FieldList Return a FieldList of fields that would appropriate for
1447
	 *                   editing this member.
1448
	 */
1449
	public function getCMSFields() {
1450
		require_once 'Zend/Date.php';
1451
1452
		$self = $this;
1453
		$this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) {
1454
			/** @var FieldList $mainFields */
1455
			$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
1456
1457
			// Build change password field
1458
			$mainFields->replaceField('Password', $self->getMemberPasswordField());
1459
1460
			$mainFields->replaceField('Locale', new DropdownField(
1461
				"Locale",
1462
				_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1463
				i18n::get_existing_translations()
1464
			));
1465
1466
			$mainFields->removeByName($self->config()->hidden_fields);
1467
1468
			// make sure that the "LastVisited" field exists
1469
			// it may have been removed using $self->config()->hidden_fields
1470
			if($mainFields->fieldByName("LastVisited")){
1471
			$mainFields->makeFieldReadonly('LastVisited');
1472
			}
1473
1474
			if( ! $self->config()->lock_out_after_incorrect_logins) {
1475
				$mainFields->removeByName('FailedLoginCount');
1476
			}
1477
1478
1479
			// Groups relation will get us into logical conflicts because
1480
			// Members are displayed within  group edit form in SecurityAdmin
1481
			$fields->removeByName('Groups');
1482
1483
			// Members shouldn't be able to directly view/edit logged passwords
1484
			$fields->removeByName('LoggedPasswords');
1485
1486
			if(Permission::check('EDIT_PERMISSIONS')) {
1487
                // Filter allowed groups
1488
                $groups = Group::get();
1489
                $disallowedGroupIDs = $self->disallowedGroups();
1490
                if ($disallowedGroupIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $disallowedGroupIDs of type integer[] 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...
1491
                    $groups = $groups->exclude('ID', $disallowedGroupIDs);
1492
                }
1493
                $groupsMap = array();
1494
                foreach ($groups as $group) {
1495
                    // Listboxfield values are escaped, use ASCII char instead of &raquo;
1496
                    $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1497
                }
1498
                asort($groupsMap);
1499
				$fields->addFieldToTab('Root.Main',
1500
					ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name())
1501
						->setMultiple(true)
1502
						->setSource($groupsMap)
1503
						->setAttribute(
1504
							'data-placeholder',
1505
							_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1506
						)
1507
				);
1508
1509
1510
				// Add permission field (readonly to avoid complicated group assignment logic).
1511
				// This should only be available for existing records, as new records start
1512
				// with no permissions until they have a group assignment anyway.
1513
				if($self->ID) {
1514
					$permissionsField = new PermissionCheckboxSetField_Readonly(
1515
						'Permissions',
1516
						false,
1517
						'Permission',
1518
						'GroupID',
1519
						// we don't want parent relationships, they're automatically resolved in the field
1520
						$self->getManyManyComponents('Groups')
1521
					);
1522
					$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1523
					$fields->addFieldToTab('Root.Permissions', $permissionsField);
1524
				}
1525
			}
1526
1527
			$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1528
			if($permissionsTab) $permissionsTab->addExtraClass('readonly');
1529
1530
			$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
1531
			$dateFormatMap = array(
1532
				'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
1533
				'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
1534
				'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
1535
				'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
1536
			);
1537
			$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
1538
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1539
			$mainFields->push(
1540
				$dateFormatField = new MemberDatetimeOptionsetField(
1541
					'DateFormat',
1542
					$self->fieldLabel('DateFormat'),
1543
					$dateFormatMap
1544
				)
1545
			);
1546
			$dateFormatField->setValue($self->DateFormat);
1547
1548
			$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
1549
			$timeFormatMap = array(
1550
				'h:mm a' => Zend_Date::now()->toString('h:mm a'),
1551
				'H:mm' => Zend_Date::now()->toString('H:mm'),
1552
			);
1553
			$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
1554
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1555
			$mainFields->push(
1556
				$timeFormatField = new MemberDatetimeOptionsetField(
1557
					'TimeFormat',
1558
					$self->fieldLabel('TimeFormat'),
1559
					$timeFormatMap
1560
				)
1561
			);
1562
			$timeFormatField->setValue($self->TimeFormat);
1563
		});
1564
1565
		return parent::getCMSFields();
1566
	}
1567
1568
	/**
1569
	 *
1570
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1571
	 *
1572
	 */
1573
	public function fieldLabels($includerelations = true) {
1574
		$labels = parent::fieldLabels($includerelations);
1575
1576
		$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1577
		$labels['Surname'] = _t('Member.SURNAME', 'Surname');
1578
		$labels['Email'] = _t('Member.EMAIL', 'Email');
1579
		$labels['Password'] = _t('Member.db_Password', 'Password');
1580
		$labels['NumVisit'] = _t('Member.db_NumVisit', 'Number of Visits');
1581
		$labels['LastVisited'] = _t('Member.db_LastVisited', 'Last Visited Date');
1582
		$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1583
		$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1584
		$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1585
		$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
1586
		$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
1587
		if($includerelations){
1588
			$labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups',
1589
				'Security Groups this member belongs to');
1590
		}
1591
		return $labels;
1592
	}
1593
1594
	/**
1595
	 * Users can view their own record.
1596
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1597
	 * This is likely to be customized for social sites etc. with a looser permission model.
1598
	 */
1599
	public function canView($member = null) {
1600
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1601
1602
		// extended access checks
1603
		$results = $this->extend('canView', $member);
1604
		if($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1605
			if(!min($results)) return false;
1606
			else return true;
1607
		}
1608
1609
		// members can usually edit their own record
1610
		if($member && $this->ID == $member->ID) return true;
1611
1612
		if(
1613
			Permission::checkMember($member, 'ADMIN')
1614
			|| Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin')
1615
		) {
1616
			return true;
1617
		}
1618
1619
		return false;
1620
	}
1621
1622
	/**
1623
	 * Users can edit their own record.
1624
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1625
	 */
1626
	public function canEdit($member = null) {
1627
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1628
1629
		// extended access checks
1630
		$results = $this->extend('canEdit', $member);
1631
		if($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1632
			if(!min($results)) return false;
1633
			else return true;
1634
		}
1635
1636
		// No member found
1637
		if(!($member && $member->exists())) return false;
1638
1639
		// If the requesting member is not an admin, but has access to manage members,
1640
		// they still can't edit other members with ADMIN permission.
1641
		// This is a bit weak, strictly speaking they shouldn't be allowed to
1642
		// perform any action that could change the password on a member
1643
		// with "higher" permissions than himself, but thats hard to determine.
1644
		if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) return false;
1645
1646
		return $this->canView($member);
1647
	}
1648
1649
	/**
1650
	 * Users can edit their own record.
1651
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1652
	 */
1653
	public function canDelete($member = null) {
1654
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1655
1656
		// extended access checks
1657
		$results = $this->extend('canDelete', $member);
1658
		if($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1659
			if(!min($results)) return false;
1660
			else return true;
1661
		}
1662
1663
		// No member found
1664
		if(!($member && $member->exists())) return false;
1665
1666
		// Members are not allowed to remove themselves,
1667
		// since it would create inconsistencies in the admin UIs.
1668
		if($this->ID && $member->ID == $this->ID) return false;
1669
1670
		return $this->canEdit($member);
1671
	}
1672
1673
1674
	/**
1675
	 * Validate this member object.
1676
	 */
1677
	public function validate() {
1678
		$valid = parent::validate();
1679
1680
		if(!$this->ID || $this->isChanged('Password')) {
1681
			if($this->Password && self::$password_validator) {
1682
				$valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1683
			}
1684
		}
1685
1686
		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...
1687
			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...
1688
				$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...
1689
			}
1690
		}
1691
1692
		return $valid;
1693
	}
1694
1695
	/**
1696
	 * Change password. This will cause rehashing according to
1697
	 * the `PasswordEncryption` property.
1698
	 *
1699
	 * @param String $password Cleartext password
1700
	 */
1701
	public function changePassword($password) {
1702
		$this->Password = $password;
1703
		$valid = $this->validate();
1704
1705
		if($valid->valid()) {
1706
			$this->AutoLoginHash = null;
1707
			$this->write();
1708
		}
1709
1710
		return $valid;
1711
	}
1712
1713
	/**
1714
	 * Tell this member that someone made a failed attempt at logging in as them.
1715
	 * This can be used to lock the user out temporarily if too many failed attempts are made.
1716
	 */
1717
	public function registerFailedLogin() {
1718
		if(self::config()->lock_out_after_incorrect_logins) {
1719
			// Keep a tally of the number of failed log-ins so that we can lock people out
1720
			++$this->FailedLoginCount;
1721
1722
			if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1723
				$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...
1724
				$this->LockedOutUntil = date('Y-m-d H:i:s', SS_Datetime::now()->Format('U') + $lockoutMins*60);
1725
				$this->FailedLoginCount = 0;
1726
			}
1727
		}
1728
		$this->extend('registerFailedLogin');
1729
		$this->write();
1730
	}
1731
1732
	/**
1733
	 * Tell this member that a successful login has been made
1734
	 */
1735
	public function registerSuccessfulLogin() {
1736
		if(self::config()->lock_out_after_incorrect_logins) {
1737
			// Forgive all past login failures
1738
			$this->FailedLoginCount = 0;
1739
			$this->LockedOutUntil = null;
1740
			$this->write();
1741
		}
1742
        $this->extend('onAfterRegisterSuccessfulLogin');
1743
	}
1744
	/**
1745
	 * Get the HtmlEditorConfig for this user to be used in the CMS.
1746
	 * This is set by the group. If multiple configurations are set,
1747
	 * the one with the highest priority wins.
1748
	 *
1749
	 * @return string
1750
	 */
1751
	public function getHtmlEditorConfigForCMS() {
1752
		$currentName = '';
1753
		$currentPriority = 0;
1754
1755
		foreach($this->Groups() as $group) {
1756
			$configName = $group->HtmlEditorConfig;
1757
			if($configName) {
1758
				$config = HtmlEditorConfig::get($group->HtmlEditorConfig);
1759
				if($config && $config->getOption('priority') > $currentPriority) {
1760
					$currentName = $configName;
1761
					$currentPriority = $config->getOption('priority');
1762
				}
1763
			}
1764
		}
1765
1766
		// If can't find a suitable editor, just default to cms
1767
		return $currentName ? $currentName : 'cms';
1768
	}
1769
1770
	public static function get_template_global_variables() {
1771
		return array(
1772
			'CurrentMember' => 'currentUser',
1773
			'currentUser',
1774
		);
1775
	}
1776
}
1777
1778
/**
1779
 * Represents a set of Groups attached to a member.
1780
 * Handles the hierarchy logic.
1781
 * @package framework
1782
 * @subpackage security
1783
 */
1784
class Member_GroupSet extends ManyManyList {
1785
1786
	protected function linkJoinTable() {
1787
		// Do not join the table directly
1788
		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...
1789
			user_error('Member_GroupSet does not support many_many_extraFields', E_USER_ERROR);
1790
		}
1791
	}
1792
1793
	/**
1794
	 * Link this group set to a specific member.
1795
	 *
1796
	 * Recursively selects all groups applied to this member, as well as any
1797
	 * parent groups of any applied groups
1798
	 *
1799
	 * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
1800
	 * ids as per getForeignID
1801
	 * @return array Condition In array(SQL => parameters format)
1802
	 */
1803
	public function foreignIDFilter($id = null) {
1804
		if ($id === null) $id = $this->getForeignID();
1805
1806
		// Find directly applied groups
1807
		$manyManyFilter = parent::foreignIDFilter($id);
1808
		$query = new SQLQuery('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter);
0 ignored issues
show
Bug introduced by
It seems like $manyManyFilter defined by parent::foreignIDFilter($id) on line 1807 can also be of type null; however, SQLQuery::__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...
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
1809
		$groupIDs = $query->execute()->column();
1810
1811
		// Get all ancestors, iteratively merging these into the master set
1812
		$allGroupIDs = array();
1813
		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...
1814
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1815
			$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
1816
			$groupIDs = array_filter($groupIDs);
1817
		}
1818
1819
		// Add a filter to this DataList
1820
		if(!empty($allGroupIDs)) {
1821
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1822
			return array("\"Group\".\"ID\" IN ($allGroupIDsPlaceholders)" => $allGroupIDs);
1823
		} else {
1824
			return array('"Group"."ID"' => 0);
1825
		}
1826
	}
1827
1828
	public function foreignIDWriteFilter($id = null) {
1829
		// Use the ManyManyList::foreignIDFilter rather than the one
1830
		// in this class, otherwise we end up selecting all inherited groups
1831
		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...
1832
	}
1833
1834
	public function add($item, $extraFields = null) {
1835
		// Get Group.ID
1836
		$itemID = null;
1837
		if(is_numeric($item)) {
1838
			$itemID = $item;
1839
		} else if($item instanceof Group) {
1840
			$itemID = $item->ID;
1841
		}
1842
1843
		// Check if this group is allowed to be added
1844
		if($this->canAddGroups(array($itemID))) {
1845
			parent::add($item, $extraFields);
1846
		}
1847
	}
1848
1849
	public function removeAll() {
1850
		$base = ClassInfo::baseDataClass($this->dataClass());
1851
1852
		// Remove the join to the join table to avoid MySQL row locking issues.
1853
		$query = $this->dataQuery();
1854
		$foreignFilter = $query->getQueryParam('Foreign.Filter');
1855
		$query->removeFilterOn($foreignFilter);
1856
1857
		$selectQuery = $query->query();
1858
		$selectQuery->setSelect("\"{$base}\".\"ID\"");
1859
1860
		$from = $selectQuery->getFrom();
1861
		unset($from[$this->joinTable]);
1862
		$selectQuery->setFrom($from);
1863
		$selectQuery->setOrderBy(); // ORDER BY in subselects breaks MS SQL Server and is not necessary here
1864
		$selectQuery->setDistinct(false);
1865
1866
		// Use a sub-query as SQLite does not support setting delete targets in
1867
		// joined queries.
1868
		$delete = new SQLDelete();
1869
		$delete->setFrom("\"{$this->joinTable}\"");
1870
		// Use ManyManyList::foreignIDFilter() rather than the one in this class
1871
		// otherwise we end up selecting the wrong columns
1872
		$delete->addWhere(parent::foreignIDFilter());
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (foreignIDFilter() instead of removeAll()). 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...
1873
		$subSelect = $selectQuery->sql($parameters);
1874
		$delete->addWhere(array(
1875
			"\"{$this->joinTable}\".\"{$this->localKey}\" IN ($subSelect)" => $parameters
1876
		));
1877
		$delete->execute();
1878
	}
1879
1880
	/**
1881
	 * Determine if the following groups IDs can be added
1882
	 *
1883
	 * @param array $itemIDs
1884
	 * @return boolean
1885
	 */
1886
	protected function canAddGroups($itemIDs) {
1887
		if(empty($itemIDs)) {
1888
			return true;
1889
		}
1890
		$member = $this->getMember();
1891
		return empty($member) || $member->onChangeGroups($itemIDs);
1892
	}
1893
1894
	/**
1895
	 * Get foreign member record for this relation
1896
	 *
1897
	 * @return Member
1898
	 */
1899
	protected function getMember() {
1900
		$id = $this->getForeignID();
1901
		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...
1902
			return DataObject::get_by_id('Member', $id);
1903
		}
1904
	}
1905
}
1906
1907
/**
1908
 * Class used as template to send an email saying that the password has been
1909
 * changed.
1910
 *
1911
 * @package framework
1912
 * @subpackage security
1913
 */
1914
class Member_ChangePasswordEmail extends Email {
1915
1916
	protected $from = '';   // setting a blank from address uses the site's default administrator email
1917
	protected $subject = '';
1918
	protected $ss_template = 'ChangePasswordEmail';
1919
1920
	public function __construct() {
1921
		parent::__construct();
1922
1923
		$this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject');
1924
	}
1925
}
1926
1927
1928
1929
/**
1930
 * Class used as template to send the forgot password email
1931
 *
1932
 * @package framework
1933
 * @subpackage security
1934
 */
1935
class Member_ForgotPasswordEmail extends Email {
1936
	protected $from = '';  // setting a blank from address uses the site's default administrator email
1937
	protected $subject = '';
1938
	protected $ss_template = 'ForgotPasswordEmail';
1939
1940
	public function __construct() {
1941
		parent::__construct();
1942
1943
		$this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject');
1944
	}
1945
}
1946
1947
/**
1948
 * Member Validator
1949
 *
1950
 * Custom validation for the Member object can be achieved either through an
1951
 * {@link DataExtension} on the Member_Validator object or, by specifying a subclass of
1952
 * {@link Member_Validator} through the {@link Injector} API.
1953
 * The Validator can also be modified by adding an Extension to Member and implement the
1954
 * <code>updateValidator</code> hook.
1955
 * {@see Member::getValidator()}
1956
 *
1957
 * Additional required fields can also be set via config API, eg.
1958
 * <code>
1959
 * Member_Validator:
1960
 *   customRequired:
1961
 *     - Surname
1962
 * </code>
1963
 *
1964
 * @package framework
1965
 * @subpackage security
1966
 */
1967
class Member_Validator extends RequiredFields
1968
{
1969
	/**
1970
	 * Fields that are required by this validator
1971
	 * @config
1972
	 * @var array
1973
	 */
1974
	protected $customRequired = array(
1975
		'FirstName',
1976
		'Email'
1977
	);
1978
1979
	/**
1980
	 * Determine what member this validator is meant for
1981
	 * @var Member
1982
	 */
1983
	protected $forMember = null;
1984
1985
	/**
1986
	 * Constructor
1987
	 */
1988
	public function __construct() {
1989
		$required = func_get_args();
1990
1991
		if(isset($required[0]) && is_array($required[0])) {
1992
			$required = $required[0];
1993
		}
1994
1995
		$required = array_merge($required, $this->customRequired);
1996
1997
		// check for config API values and merge them in
1998
		$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...
1999
		if(is_array($config)){
2000
			$required = array_merge($required, $config);
2001
		}
2002
2003
		parent::__construct(array_unique($required));
2004
	}
2005
2006
	/**
2007
	 * Get the member this validator applies to.
2008
	 * @return Member
2009
	 */
2010
	public function getForMember()
2011
	{
2012
		return $this->forMember;
2013
	}
2014
2015
	/**
2016
	 * Set the Member this validator applies to.
2017
	 * @param Member $value
2018
	 * @return $this
2019
	 */
2020
	public function setForMember(Member $value)
2021
	{
2022
		$this->forMember = $value;
2023
		return $this;
2024
	}
2025
2026
	/**
2027
	 * Check if the submitted member data is valid (server-side)
2028
	 *
2029
	 * Check if a member with that email doesn't already exist, or if it does
2030
	 * that it is this member.
2031
	 *
2032
	 * @param array $data Submitted data
2033
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
2034
	 *              FALSE.
2035
	 */
2036
	public function php($data)
2037
	{
2038
		$valid = parent::php($data);
2039
2040
		$identifierField = (string)Member::config()->unique_identifier_field;
2041
2042
		// Only validate identifier field if it's actually set. This could be the case if
2043
		// somebody removes `Email` from the list of required fields.
2044
		if(isset($data[$identifierField])){
2045
			$id = isset($data['ID']) ? (int)$data['ID'] : 0;
2046
			if(!$id && ($ctrl = $this->form->getController())){
2047
				// get the record when within GridField (Member editing page in CMS)
2048
				if($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()){
2049
					$id = $record->ID;
2050
				}
2051
			}
2052
2053
			// If there's no ID passed via controller or form-data, use the assigned member (if available)
2054
			if(!$id && ($member = $this->getForMember())){
2055
				$id = $member->exists() ? $member->ID : 0;
2056
			}
2057
2058
			// set the found ID to the data array, so that extensions can also use it
2059
			$data['ID'] = $id;
2060
2061
			$members = Member::get()->filter($identifierField, $data[$identifierField]);
2062
			if($id) {
2063
				$members = $members->exclude('ID', $id);
2064
			}
2065
2066
			if($members->count() > 0) {
2067
				$this->validationError(
2068
					$identifierField,
2069
					_t(
2070
						'Member.VALIDATIONMEMBEREXISTS',
2071
						'A member already exists with the same {identifier}',
2072
						array('identifier' => Member::singleton()->fieldLabel($identifierField))
2073
					),
2074
					'required'
2075
				);
2076
				$valid = false;
2077
			}
2078
		}
2079
2080
2081
		// Execute the validators on the extensions
2082
		$results = $this->extend('updatePHP', $data, $this->form);
2083
		$results[] = $valid;
2084
		return min($results);
2085
	}
2086
}
2087