Completed
Push — namespace-template ( 7967f2...367a36 )
by Sam
10:48
created

Member::getMemberFormFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 0
dl 0
loc 18
rs 9.4285
c 1
b 0
f 0
1
<?php
2
3
use SilverStripe\Model\FieldType\DBDatetime;
4
5
/**
6
 * The member class which represents the users of the system
7
 *
8
 * @package framework
9
 * @subpackage security
10
 *
11
 * @property string $FirstName
12
 * @property string $Surname
13
 * @property string $Email
14
 * @property string $Password
15
 * @property string $TempIDHash
16
 * @property string $TempIDExpired
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(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
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
		'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
38
		'AutoLoginExpired' => 'SS_Datetime',
39
		// This is an arbitrary code pointing to a PasswordEncryptor instance,
40
		// not an actual encryption algorithm.
41
		// Warning: Never change this field after its the first password hashing without
42
		// providing a new cleartext password as well.
43
		'PasswordEncryption' => "Varchar(50)",
44
		'Salt' => 'Varchar(50)',
45
		'PasswordExpiry' => 'Date',
46
		'LockedOutUntil' => 'SS_Datetime',
47
		'Locale' => 'Varchar(6)',
48
		// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
49
		'FailedLoginCount' => 'Int',
50
		// In ISO format
51
		'DateFormat' => 'Varchar(30)',
52
		'TimeFormat' => 'Varchar(30)',
53
	);
54
55
	private static $belongs_many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
56
		'Groups' => 'Group',
57
	);
58
59
	private static $has_one = array();
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
60
61
	private static $has_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
62
		'LoggedPasswords' => 'MemberPassword',
63
		'RememberLoginHashes' => 'RememberLoginHash'
64
	);
65
66
	private static $many_many = array();
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
67
68
	private static $many_many_extraFields = array();
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
69
70
	private static $default_sort = '"Surname", "FirstName"';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
71
72
	private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
73
		'Email' => true,
74
		//Removed due to duplicate null values causing MSSQL problems
75
		//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true)
76
	);
77
78
	/**
79
	 * @config
80
	 * @var boolean
81
	 */
82
	private static $notify_password_change = false;
83
84
	/**
85
	 * All searchable database columns
86
	 * in this object, currently queried
87
	 * with a "column LIKE '%keywords%'
88
	 * statement.
89
	 *
90
	 * @var array
91
	 * @todo Generic implementation of $searchable_fields on DataObject,
92
	 * with definition for different searching algorithms
93
	 * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
94
	 */
95
	private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
96
		'FirstName',
97
		'Surname',
98
		'Email',
99
	);
100
101
	private static $summary_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
102
		'FirstName',
103
		'Surname',
104
		'Email',
105
	);
106
107
	/**
108
	 * Internal-use only fields
109
	 *
110
	 * @config
111
	 * @var array
112
	 */
113
	private static $hidden_fields = array(
114
		'AutoLoginHash',
115
		'AutoLoginExpired',
116
		'PasswordEncryption',
117
		'PasswordExpiry',
118
		'LockedOutUntil',
119
		'TempIDHash',
120
		'TempIDExpired',
121
		'Salt',
122
	);
123
124
	/**
125
	 * @config
126
	 * @var Array See {@link set_title_columns()}
127
	 */
128
	private static $title_format = null;
129
130
	/**
131
	 * The unique field used to identify this member.
132
	 * By default, it's "Email", but another common
133
	 * field could be Username.
134
	 *
135
	 * @config
136
	 * @var string
137
	 */
138
	private static $unique_identifier_field = 'Email';
139
140
	/**
141
	 * @config
142
	 * {@link PasswordValidator} object for validating user's password
143
	 */
144
	private static $password_validator = null;
145
146
	/**
147
	 * @config
148
	 * The number of days that a password should be valid for.
149
	 * By default, this is null, which means that passwords never expire
150
	 */
151
	private static $password_expiry_days = null;
152
153
	/**
154
	 * @config
155
	 * @var Int Number of incorrect logins after which
156
	 * the user is blocked from further attempts for the timespan
157
	 * defined in {@link $lock_out_delay_mins}.
158
	 */
159
	private static $lock_out_after_incorrect_logins = 10;
160
161
	/**
162
	 * @config
163
	 * @var integer Minutes of enforced lockout after incorrect password attempts.
164
	 * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
165
	 */
166
	private static $lock_out_delay_mins = 15;
167
168
	/**
169
	 * @config
170
	 * @var String If this is set, then a session cookie with the given name will be set on log-in,
171
	 * and cleared on logout.
172
	 */
173
	private static $login_marker_cookie = null;
174
175
	/**
176
	 * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
177
	 * should be called as a security precaution.
178
	 *
179
	 * This doesn't always work, especially if you're trying to set session cookies
180
	 * across an entire site using the domain parameter to session_set_cookie_params()
181
	 *
182
	 * @config
183
	 * @var boolean
184
	 */
185
	private static $session_regenerate_id = true;
186
187
188
	/**
189
	 * Default lifetime of temporary ids.
190
	 *
191
	 * This is the period within which a user can be re-authenticated within the CMS by entering only their password
192
	 * and without losing their workspace.
193
	 *
194
	 * Any session expiration outside of this time will require them to login from the frontend using their full
195
	 * username and password.
196
	 *
197
	 * Defaults to 72 hours. Set to zero to disable expiration.
198
	 *
199
	 * @config
200
	 * @var int Lifetime in seconds
201
	 */
202
	private static $temp_id_lifetime = 259200;
203
204
	/**
205
	 * @deprecated 4.0 Use the "Member.session_regenerate_id" config setting instead
206
	 */
207
	public static function set_session_regenerate_id($bool) {
208
		Deprecation::notice('4.0', 'Use the "Member.session_regenerate_id" config setting instead');
209
		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...
210
	}
211
212
	/**
213
	 * Ensure the locale is set to something sensible by default.
214
	 */
215
	public function populateDefaults() {
216
		parent::populateDefaults();
217
		$this->Locale = i18n::get_closest_translation(i18n::get_locale());
218
	}
219
220
	public function requireDefaultRecords() {
221
		parent::requireDefaultRecords();
222
		// Default groups should've been built by Group->requireDefaultRecords() already
223
		static::default_admin();
224
	}
225
226
	/**
227
	 * Get the default admin record if it exists, or creates it otherwise if enabled
228
	 *
229
	 * @return Member
230
	 */
231
	public static function default_admin() {
232
		// Check if set
233
		if(!Security::has_default_admin()) return null;
234
235
		// Find or create ADMIN group
236
		singleton('Group')->requireDefaultRecords();
237
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
238
239
		// Find member
240
		$admin = Member::get()
241
			->filter('Email', Security::default_admin_username())
242
			->first();
243
		if(!$admin) {
244
			// 'Password' is not set to avoid creating
245
			// persistent logins in the database. See Security::setDefaultAdmin().
246
			// Set 'Email' to identify this as the default admin
247
			$admin = Member::create();
248
			$admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
249
			$admin->Email = Security::default_admin_username();
250
			$admin->write();
251
		}
252
253
		// Ensure this user is in the admin group
254
		if(!$admin->inGroup($adminGroup)) {
255
			// Add member to group instead of adding group to member
256
			// This bypasses the privilege escallation code in Member_GroupSet
257
			$adminGroup
258
				->DirectMembers()
259
				->add($admin);
260
		}
261
262
		return $admin;
263
	}
264
265
	/**
266
	 * If this is called, then a session cookie will be set to "1" whenever a user
267
	 * logs in.  This lets 3rd party tools, such as apache's mod_rewrite, detect
268
	 * whether a user is logged in or not and alter behaviour accordingly.
269
	 *
270
	 * One known use of this is to bypass static caching for logged in users.  This is
271
	 * done by putting this into _config.php
272
	 * <pre>
273
	 * Member::set_login_marker_cookie("SS_LOGGED_IN");
274
	 * </pre>
275
	 *
276
	 * And then adding this condition to each of the rewrite rules that make use of
277
	 * the static cache.
278
	 * <pre>
279
	 * RewriteCond %{HTTP_COOKIE} !SS_LOGGED_IN=1
280
	 * </pre>
281
	 *
282
	 * @deprecated 4.0 Use the "Member.login_marker_cookie" config setting instead
283
	 * @param $cookieName string The name of the cookie to set.
284
	 */
285
	public static function set_login_marker_cookie($cookieName) {
286
		Deprecation::notice('4.0', 'Use the "Member.login_marker_cookie" config setting instead');
287
		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...
288
	}
289
290
	/**
291
	 * Check if the passed password matches the stored one (if the member is not locked out).
292
	 *
293
	 * @param string $password
294
	 * @return ValidationResult
295
	 */
296
	public function checkPassword($password) {
297
		$result = $this->canLogIn();
298
299
		// Short-circuit the result upon failure, no further checks needed.
300
		if (!$result->valid()) {
301
			return $result;
302
		}
303
304
		// Allow default admin to login as self
305
		if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
306
			return $result;
307
		}
308
309
		// Check a password is set on this member
310
		if(empty($this->Password) && $this->exists()) {
311
			$result->error(_t('Member.NoPassword','There is no password on this member.'));
312
			return $result;
313
		}
314
315
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
316
		if(!$e->check($this->Password, $password, $this->Salt, $this)) {
317
			$result->error(_t (
318
				'Member.ERRORWRONGCRED',
319
				'The provided details don\'t seem to be correct. Please try again.'
320
			));
321
		}
322
323
		return $result;
324
	}
325
326
	/**
327
	 * Check if this user is the currently configured default admin
328
	 *
329
	 * @return bool
330
	 */
331
	public function isDefaultAdmin() {
332
		return Security::has_default_admin()
333
			&& $this->Email === Security::default_admin_username();
334
	}
335
336
	/**
337
	 * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
338
	 * one with error messages to display if the member is locked out.
339
	 *
340
	 * You can hook into this with a "canLogIn" method on an attached extension.
341
	 *
342
	 * @return ValidationResult
343
	 */
344
	public function canLogIn() {
345
		$result = ValidationResult::create();
346
347
		if($this->isLockedOut()) {
348
			$result->error(
349
				_t(
350
					'Member.ERRORLOCKEDOUT2',
351
					'Your account has been temporarily disabled because of too many failed attempts at ' .
352
					'logging in. Please try again in {count} minutes.',
353
					null,
354
					array('count' => $this->config()->lock_out_delay_mins)
0 ignored issues
show
Documentation introduced by
array('count' => $this->...)->lock_out_delay_mins) is of type array<string,?,{"count":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
355
				)
356
			);
357
		}
358
359
		$this->extend('canLogIn', $result);
360
		return $result;
361
	}
362
363
	/**
364
	 * Returns true if this user is locked out
365
	 */
366
	public function isLockedOut() {
367
		return $this->LockedOutUntil && SS_Datetime::now()->Format('U') < strtotime($this->LockedOutUntil);
368
	}
369
370
	/**
371
	 * Regenerate the session_id.
372
	 * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
373
	 * They have caused problems in certain
374
	 * quirky problems (such as using the Windmill 0.3.6 proxy).
375
	 */
376
	public static function session_regenerate_id() {
377
		if(!self::config()->session_regenerate_id) return;
378
379
		// This can be called via CLI during testing.
380
		if(Director::is_cli()) return;
381
382
		$file = '';
383
		$line = '';
384
385
		// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
386
		// There's nothing we can do about this, because it's an operating system function!
387
		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...
388
	}
389
390
	/**
391
	 * Get the field used for uniquely identifying a member
392
	 * in the database. {@see Member::$unique_identifier_field}
393
	 *
394
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
395
	 * @return string
396
	 */
397
	public static function get_unique_identifier_field() {
398
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
399
		return Member::config()->unique_identifier_field;
400
	}
401
402
	/**
403
	 * Set the field used for uniquely identifying a member
404
	 * in the database. {@see Member::$unique_identifier_field}
405
	 *
406
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
407
	 * @param $field The field name to set as the unique field
408
	 */
409
	public static function set_unique_identifier_field($field) {
410
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
411
		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...
412
	}
413
414
	/**
415
	 * Set a {@link PasswordValidator} object to use to validate member's passwords.
416
	 */
417
	public static function set_password_validator($pv) {
418
		self::$password_validator = $pv;
419
	}
420
421
	/**
422
	 * Returns the current {@link PasswordValidator}
423
	 */
424
	public static function password_validator() {
425
		return self::$password_validator;
426
	}
427
428
	/**
429
	 * Set the number of days that a password should be valid for.
430
	 * Set to null (the default) to have passwords never expire.
431
	 *
432
	 * @deprecated 4.0 Use the "Member.password_expiry_days" config setting instead
433
	 */
434
	public static function set_password_expiry($days) {
435
		Deprecation::notice('4.0', 'Use the "Member.password_expiry_days" config setting instead');
436
		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...
437
	}
438
439
	/**
440
	 * Configure the security system to lock users out after this many incorrect logins
441
	 *
442
	 * @deprecated 4.0 Use the "Member.lock_out_after_incorrect_logins" config setting instead
443
	 */
444
	public static function lock_out_after_incorrect_logins($numLogins) {
445
		Deprecation::notice('4.0', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead');
446
		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...
447
	}
448
449
450
	public function isPasswordExpired() {
451
		if(!$this->PasswordExpiry) return false;
452
		return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
453
	}
454
455
	/**
456
	 * Logs this member in
457
	 *
458
	 * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
459
	 */
460
	public function logIn($remember = false) {
461
		$this->extend('beforeMemberLoggedIn');
462
463
		self::session_regenerate_id();
464
465
		Session::set("loggedInAs", $this->ID);
466
		// This lets apache rules detect whether the user has logged in
467
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0);
468
469
		// Cleans up any potential previous hash for this member on this device
470
		if ($alcDevice = Cookie::get('alc_device')) {
471
			RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
472
		}
473
		if($remember) {
474
			$rememberLoginHash = RememberLoginHash::generate($this);
475
			$tokenExpiryDays = Config::inst()->get('RememberLoginHash', 'token_expiry_days');
476
			$deviceExpiryDays = Config::inst()->get('RememberLoginHash', 'device_expiry_days');
477
			Cookie::set('alc_enc', $this->ID . ':' . $rememberLoginHash->getToken(),
478
				$tokenExpiryDays, null, null, null, true);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
479
			Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
480
		} else {
481
			Cookie::set('alc_enc', null);
482
			Cookie::set('alc_device', null);
483
			Cookie::force_expiry('alc_enc');
484
			Cookie::force_expiry('alc_device');
485
		}
486
487
		// Clear the incorrect log-in count
488
		$this->registerSuccessfulLogin();
489
490
		// Don't set column if its not built yet (the login might be precursor to a /dev/build...)
491
		if(array_key_exists('LockedOutUntil', DB::field_list('Member'))) {
492
			$this->LockedOutUntil = null;
493
		}
494
495
		$this->regenerateTempID();
496
497
		$this->write();
498
499
		// Audit logging hook
500
		$this->extend('memberLoggedIn');
501
	}
502
503
	/**
504
	 * Trigger regeneration of TempID.
505
	 *
506
	 * This should be performed any time the user presents their normal identification (normally Email)
507
	 * and is successfully authenticated.
508
	 */
509
	public function regenerateTempID() {
510
		$generator = new RandomGenerator();
511
		$this->TempIDHash = $generator->randomToken('sha1');
512
		$this->TempIDExpired = self::config()->temp_id_lifetime
513
			? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime)
514
			: null;
515
		$this->write();
516
	}
517
518
	/**
519
	 * Check if the member ID logged in session actually
520
	 * has a database record of the same ID. If there is
521
	 * no logged in user, FALSE is returned anyway.
522
	 *
523
	 * @return boolean TRUE record found FALSE no record found
524
	 */
525
	public static function logged_in_session_exists() {
526
		if($id = Member::currentUserID()) {
527
			if($member = DataObject::get_by_id('Member', $id)) {
528
				if($member->exists()) return true;
529
			}
530
		}
531
532
		return false;
533
	}
534
535
	/**
536
	 * Log the user in if the "remember login" cookie is set
537
	 *
538
	 * The <i>remember login token</i> will be changed on every successful
539
	 * auto-login.
540
	 */
541
	public static function autoLogin() {
542
		// Don't bother trying this multiple times
543
		if (!class_exists('SapphireTest', false) || !SapphireTest::is_running_test()) {
544
			self::$_already_tried_to_auto_log_in = true;
545
		}
546
547
		if(strpos(Cookie::get('alc_enc'), ':') === false
548
			|| Session::get("loggedInAs")
549
			|| !Security::database_is_ready()
550
		) {
551
			return;
552
		}
553
554
		if(strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Cookie::get('alc_device') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
555
			list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
556
			
557
			if (!$uid || !$token) {
558
				return;
559
			}
560
			
561
			$deviceID = Cookie::get('alc_device');
562
563
			$member = Member::get()->byId($uid);
564
565
			$rememberLoginHash = null;
566
567
			// check if autologin token matches
568
			if($member) {
569
				$hash = $member->encryptWithUserSettings($token);
570
				$rememberLoginHash = RememberLoginHash::get()
571
					->filter(array(
572
						'MemberID' => $member->ID,
573
						'DeviceID' => $deviceID,
574
						'Hash' => $hash
575
					))->First();
576
				if(!$rememberLoginHash) {
577
					$member = null;
578
				} else {
579
					// Check for expired token
580
					$expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
581
					$now = SS_Datetime::now();
582
					$now = new DateTime($now->Rfc2822());
583
					if ($now > $expiryDate) {
584
						$member = null;
585
					}
586
				}
587
			}
588
589
			if($member) {
590
				self::session_regenerate_id();
591
				Session::set("loggedInAs", $member->ID);
592
				// This lets apache rules detect whether the user has logged in
593
				if(Member::config()->login_marker_cookie) {
594
					Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
595
				}
596
597
				if ($rememberLoginHash) {
598
					$rememberLoginHash->renew();
599
					$tokenExpiryDays = Config::inst()->get('RememberLoginHash', 'token_expiry_days');
600
					Cookie::set('alc_enc', $member->ID . ':' . $rememberLoginHash->getToken(),
601
						$tokenExpiryDays, null, null, false, true);
602
				}
603
604
				$member->write();
605
606
				// Audit logging hook
607
				$member->extend('memberAutoLoggedIn');
608
			}
609
		}
610
	}
611
612
	/**
613
	 * Logs this member out.
614
	 */
615
	public function logOut() {
616
		$this->extend('beforeMemberLoggedOut');
617
618
		Session::clear("loggedInAs");
619
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
620
621
		Session::destroy();
622
623
		$this->extend('memberLoggedOut');
624
625
		// Clears any potential previous hashes for this member
626
		RememberLoginHash::clear($this, Cookie::get('alc_device'));
627
628
		Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
629
		Cookie::force_expiry('alc_enc');
630
		Cookie::set('alc_device', null);
631
		Cookie::force_expiry('alc_device');
632
633
		// Switch back to live in order to avoid infinite loops when
634
		// redirecting to the login screen (if this login screen is versioned)
635
		Session::clear('readingMode');
636
637
		$this->write();
638
639
		// Audit logging hook
640
		$this->extend('memberLoggedOut');
641
	}
642
643
	/**
644
	 * Utility for generating secure password hashes for this member.
645
	 */
646
	public function encryptWithUserSettings($string) {
647
		if (!$string) return null;
648
649
		// If the algorithm or salt is not available, it means we are operating
650
		// on legacy account with unhashed password. Do not hash the string.
651
		if (!$this->PasswordEncryption) {
652
			return $string;
653
		}
654
655
		// We assume we have PasswordEncryption and Salt available here.
656
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
657
		return $e->encrypt($string, $this->Salt);
658
659
	}
660
661
	/**
662
	 * Generate an auto login token which can be used to reset the password,
663
	 * at the same time hashing it and storing in the database.
664
	 *
665
	 * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
666
	 *
667
	 * @returns string Token that should be passed to the client (but NOT persisted).
668
	 *
669
	 * @todo Make it possible to handle database errors such as a "duplicate key" error
670
	 */
671
	public function generateAutologinTokenAndStoreHash($lifetime = 2) {
672
		do {
673
			$generator = new RandomGenerator();
674
			$token = $generator->randomToken();
675
			$hash = $this->encryptWithUserSettings($token);
676
		} while(DataObject::get_one('Member', array(
677
			'"Member"."AutoLoginHash"' => $hash
678
		)));
679
680
		$this->AutoLoginHash = $hash;
681
		$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
682
683
		$this->write();
684
685
		return $token;
686
	}
687
688
	/**
689
	 * Check the token against the member.
690
	 *
691
	 * @param string $autologinToken
692
	 *
693
	 * @returns bool Is token valid?
694
	 */
695
	public function validateAutoLoginToken($autologinToken) {
696
		$hash = $this->encryptWithUserSettings($autologinToken);
697
		$member = self::member_from_autologinhash($hash, false);
698
		return (bool)$member;
699
	}
700
701
	/**
702
	 * Return the member for the auto login hash
703
	 *
704
	 * @param string $hash The hash key
705
	 * @param bool $login Should the member be logged in?
706
	 *
707
	 * @return Member the matching member, if valid
708
	 * @return Member
709
	 */
710
	public static function member_from_autologinhash($hash, $login = false) {
711
712
		$nowExpression = DB::get_conn()->now();
713
		$member = DataObject::get_one('Member', array(
714
			"\"Member\".\"AutoLoginHash\"" => $hash,
715
			"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
716
		));
717
718
		if($login && $member) $member->logIn();
719
720
		return $member;
721
	}
722
723
	/**
724
	 * Find a member record with the given TempIDHash value
725
	 *
726
	 * @param string $tempid
727
	 * @return Member
728
	 */
729
	public static function member_from_tempid($tempid) {
730
		$members = Member::get()
731
			->filter('TempIDHash', $tempid);
732
733
		// Exclude expired
734
		if(static::config()->temp_id_lifetime) {
735
			$members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
736
		}
737
738
		return $members->first();
739
	}
740
741
	/**
742
	 * Returns the fields for the member form - used in the registration/profile module.
743
	 * It should return fields that are editable by the admin and the logged-in user.
744
	 *
745
	 * @return FieldList Returns a {@link FieldList} containing the fields for
746
	 *                   the member form.
747
	 */
748
	public function getMemberFormFields() {
749
		$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...
750
751
		$fields->replaceField('Password', $this->getMemberPasswordField());
752
753
		$fields->replaceField('Locale', new DropdownField (
754
			'Locale',
755
			$this->fieldLabel('Locale'),
756
			i18n::get_existing_translations()
757
		));
758
759
		$fields->removeByName(static::config()->hidden_fields);
760
		$fields->removeByName('FailedLoginCount');
761
762
763
		$this->extend('updateMemberFormFields', $fields);
764
		return $fields;
765
	}
766
767
	/**
768
	 * Builds "Change / Create Password" field for this member
769
	 *
770
	 * @return ConfirmedPasswordField
771
	 */
772
	public function getMemberPasswordField() {
773
		$editingPassword = $this->isInDB();
774
		$label = $editingPassword
775
			? _t('Member.EDIT_PASSWORD', 'New Password')
776
			: $this->fieldLabel('Password');
777
		/** @var ConfirmedPasswordField $password */
778
		$password = ConfirmedPasswordField::create(
779
			'Password',
780
			$label,
781
			null,
782
			null,
783
			$editingPassword
784
		);
785
786
		// If editing own password, require confirmation of existing
787
		if($editingPassword && $this->ID == Member::currentUserID()) {
788
			$password->setRequireExistingPassword(true);
789
		}
790
791
		$password->setCanBeEmpty(true);
792
		$this->extend('updateMemberPasswordField', $password);
793
		return $password;
794
	}
795
796
797
	/**
798
	 * Returns the {@link RequiredFields} instance for the Member object. This
799
	 * Validator is used when saving a {@link CMSProfileController} or added to
800
	 * any form responsible for saving a users data.
801
	 *
802
	 * To customize the required fields, add a {@link DataExtension} to member
803
	 * calling the `updateValidator()` method.
804
	 *
805
	 * @return Member_Validator
806
	 */
807
	public function getValidator() {
808
		$validator = Injector::inst()->create('Member_Validator');
809
		$validator->setForMember($this);
810
		$this->extend('updateValidator', $validator);
811
812
		return $validator;
813
	}
814
815
816
	/**
817
	 * Returns the current logged in user
818
	 *
819
	 * @return Member|null
820
	 */
821
	public static function currentUser() {
822
		$id = Member::currentUserID();
823
824
		if($id) {
825
			return Member::get()->byId($id);
826
		}
827
	}
828
829
	/**
830
	 * Get the ID of the current logged in user
831
	 *
832
	 * @return int Returns the ID of the current logged in user or 0.
833
	 */
834
	public static function currentUserID() {
835
		$id = Session::get("loggedInAs");
836
		if(!$id && !self::$_already_tried_to_auto_log_in) {
837
			self::autoLogin();
838
			$id = Session::get("loggedInAs");
839
		}
840
841
		return is_numeric($id) ? $id : 0;
842
	}
843
	private static $_already_tried_to_auto_log_in = false;
844
845
846
	/*
847
	 * Generate a random password, with randomiser to kick in if there's no words file on the
848
	 * filesystem.
849
	 *
850
	 * @return string Returns a random password.
851
	 */
852
	public static function create_new_password() {
853
		$words = Config::inst()->get('Security', 'word_list');
854
855
		if($words && file_exists($words)) {
856
			$words = file($words);
857
858
			list($usec, $sec) = explode(' ', microtime());
859
			srand($sec + ((float) $usec * 100000));
860
861
			$word = trim($words[rand(0,sizeof($words)-1)]);
862
			$number = rand(10,999);
863
864
			return $word . $number;
865
		} else {
866
			$random = rand();
867
			$string = md5($random);
868
			$output = substr($string, 0, 6);
869
			return $output;
870
		}
871
	}
872
873
	/**
874
	 * Event handler called before writing to the database.
875
	 */
876
	public function onBeforeWrite() {
877
		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...
878
879
		// If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
880
		// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
881
		// but rather a last line of defense against data inconsistencies.
882
		$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...
883
		if($this->$identifierField) {
884
885
			// Note: Same logic as Member_Validator class
886
			$filter = array("\"$identifierField\"" => $this->$identifierField);
887
			if($this->ID) {
888
				$filter[] = array('"Member"."ID" <> ?' => $this->ID);
889
			}
890
			$existingRecord = DataObject::get_one('Member', $filter);
891
892
			if($existingRecord) {
893
				throw new ValidationException(ValidationResult::create(false, _t(
894
					'Member.ValidationIdentifierFailed',
895
					'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
896
					'Values in brackets show "fieldname = value", usually denoting an existing email address',
897
					array(
0 ignored issues
show
Documentation introduced by
array('id' => $existingR...is->{$identifierField}) is of type array<string,?,{"id":"in...name":"?","value":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
898
						'id' => $existingRecord->ID,
899
						'name' => $identifierField,
900
						'value' => $this->$identifierField
901
					)
902
				)));
903
			}
904
		}
905
906
		// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
907
		// However, if TestMailer is in use this isn't a risk.
908
		if(
909
			(Director::isLive() || Email::mailer() instanceof TestMailer)
910
			&& $this->isChanged('Password')
911
			&& $this->record['Password']
912
			&& $this->config()->notify_password_change
913
		) {
914
			/** @var Email $e */
915
			$e = Email::create();
916
			$e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'));
917
			$e->setTemplate('ChangePasswordEmail');
918
			$e->populateTemplate($this);
919
			$e->setTo($this->Email);
920
			$e->send();
921
		}
922
923
		// The test on $this->ID is used for when records are initially created.
924
		// Note that this only works with cleartext passwords, as we can't rehash
925
		// existing passwords.
926
		if((!$this->ID && $this->Password) || $this->isChanged('Password')) {
927
			// Password was changed: encrypt the password according the settings
928
			$encryption_details = Security::encrypt_password(
929
				$this->Password, // this is assumed to be cleartext
930
				$this->Salt,
931
				($this->PasswordEncryption) ?
932
					$this->PasswordEncryption : Security::config()->password_encryption_algorithm,
933
				$this
934
			);
935
936
			// Overwrite the Password property with the hashed value
937
			$this->Password = $encryption_details['password'];
938
			$this->Salt = $encryption_details['salt'];
939
			$this->PasswordEncryption = $encryption_details['algorithm'];
940
941
			// If we haven't manually set a password expiry
942
			if(!$this->isChanged('PasswordExpiry')) {
943
				// then set it for us
944
				if(self::config()->password_expiry_days) {
945
					$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
946
				} else {
947
					$this->PasswordExpiry = null;
948
				}
949
			}
950
		}
951
952
		// save locale
953
		if(!$this->Locale) {
954
			$this->Locale = i18n::get_locale();
955
		}
956
957
		parent::onBeforeWrite();
958
	}
959
960
	public function onAfterWrite() {
961
		parent::onAfterWrite();
962
963
		Permission::flush_permission_cache();
964
965
		if($this->isChanged('Password')) {
966
			MemberPassword::log($this);
967
		}
968
	}
969
970
	public function onAfterDelete() {
971
		parent::onAfterDelete();
972
973
		//prevent orphaned records remaining in the DB
974
		$this->deletePasswordLogs();
975
	}
976
977
	/**
978
	 * Delete the MemberPassword objects that are associated to this user
979
	 *
980
	 * @return self
981
	 */
982
	protected function deletePasswordLogs() {
983
		foreach ($this->LoggedPasswords() as $password) {
984
			$password->delete();
985
			$password->destroy();
986
		}
987
		return $this;
988
	}
989
990
	/**
991
	 * Filter out admin groups to avoid privilege escalation,
992
	 * If any admin groups are requested, deny the whole save operation.
993
	 *
994
	 * @param Array $ids Database IDs of Group records
995
	 * @return boolean True if the change can be accepted
996
	 */
997
	public function onChangeGroups($ids) {
998
		// unless the current user is an admin already OR the logged in user is an admin
999
		if(Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
1000
			return true;
1001
		}
1002
1003
		// If there are no admin groups in this set then it's ok
1004
		$adminGroups = Permission::get_groups_by_permission('ADMIN');
1005
		$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
1006
		return count(array_intersect($ids, $adminGroupIDs)) == 0;
1007
	}
1008
1009
1010
	/**
1011
	 * Check if the member is in one of the given groups.
1012
	 *
1013
	 * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
1014
	 * @param boolean $strict Only determine direct group membership if set to true (Default: false)
1015
	 * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
1016
	 */
1017
	public function inGroups($groups, $strict = false) {
1018
		if($groups) foreach($groups as $group) {
1019
			if($this->inGroup($group, $strict)) return true;
1020
		}
1021
1022
		return false;
1023
	}
1024
1025
1026
	/**
1027
	 * Check if the member is in the given group or any parent groups.
1028
	 *
1029
	 * @param int|Group|string $group Group instance, Group Code or ID
1030
	 * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
1031
	 * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
1032
	 */
1033
	public function inGroup($group, $strict = false) {
1034
		if(is_numeric($group)) {
1035
			$groupCheckObj = DataObject::get_by_id('Group', $group);
1036
		} elseif(is_string($group)) {
1037
			$groupCheckObj = DataObject::get_one('Group', array(
1038
				'"Group"."Code"' => $group
1039
			));
1040
		} elseif($group instanceof Group) {
1041
			$groupCheckObj = $group;
1042
		} else {
1043
			user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
1044
		}
1045
1046
		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...
1047
1048
		$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1049
		if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
1050
			if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
1051
		}
1052
1053
		return false;
1054
	}
1055
1056
	/**
1057
	 * Adds the member to a group. This will create the group if the given
1058
	 * group code does not return a valid group object.
1059
	 *
1060
	 * @param string $groupcode
1061
	 * @param string Title of the group
1062
	 */
1063
	public function addToGroupByCode($groupcode, $title = "") {
1064
		$group = DataObject::get_one('Group', array(
1065
			'"Group"."Code"' => $groupcode
1066
		));
1067
1068
		if($group) {
1069
			$this->Groups()->add($group);
1070
		} else {
1071
			if(!$title) $title = $groupcode;
1072
1073
			$group = new Group();
1074
			$group->Code = $groupcode;
1075
			$group->Title = $title;
1076
			$group->write();
1077
1078
			$this->Groups()->add($group);
1079
		}
1080
	}
1081
1082
	/**
1083
	 * Removes a member from a group.
1084
	 *
1085
	 * @param string $groupcode
1086
	 */
1087
	public function removeFromGroupByCode($groupcode) {
1088
		$group = Group::get()->filter(array('Code' => $groupcode))->first();
1089
1090
		if($group) {
1091
			$this->Groups()->remove($group);
1092
		}
1093
	}
1094
1095
	/**
1096
	 * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
1097
	 * @param String $sep Separator
1098
	 */
1099
	public static function set_title_columns($columns, $sep = ' ') {
1100
		if (!is_array($columns)) $columns = array($columns);
1101
		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...
1102
	}
1103
1104
	//------------------- HELPER METHODS -----------------------------------//
1105
1106
	/**
1107
	 * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1108
	 * Falls back to showing either field on its own.
1109
	 *
1110
	 * You can overload this getter with {@link set_title_format()}
1111
	 * and {@link set_title_sql()}.
1112
	 *
1113
	 * @return string Returns the first- and surname of the member. If the ID
1114
	 *  of the member is equal 0, only the surname is returned.
1115
	 */
1116
	public function getTitle() {
1117
		$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...
1118
		if ($format) {
1119
			$values = array();
1120
			foreach($format['columns'] as $col) {
1121
				$values[] = $this->getField($col);
1122
			}
1123
			return join($format['sep'], $values);
1124
		}
1125
		if($this->getField('ID') === 0)
1126
			return $this->getField('Surname');
1127
		else{
1128
			if($this->getField('Surname') && $this->getField('FirstName')){
1129
				return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1130
			}elseif($this->getField('Surname')){
1131
				return $this->getField('Surname');
1132
			}elseif($this->getField('FirstName')){
1133
				return $this->getField('FirstName');
1134
			}else{
1135
				return null;
1136
			}
1137
		}
1138
	}
1139
1140
	/**
1141
	 * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1142
	 * Useful for custom queries which assume a certain member title format.
1143
	 *
1144
	 * @param String $tableName
1145
	 * @return String SQL
1146
	 */
1147
	public static function get_title_sql($tableName = 'Member') {
1148
		// This should be abstracted to SSDatabase concatOperator or similar.
1149
		$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...
1150
1151
		$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...
1152
		if ($format) {
1153
			$columnsWithTablename = array();
1154
			foreach($format['columns'] as $column) {
1155
				$columnsWithTablename[] = "\"$tableName\".\"$column\"";
1156
			}
1157
1158
			return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")";
1159
		} else {
1160
			return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")";
1161
		}
1162
	}
1163
1164
1165
	/**
1166
	 * Get the complete name of the member
1167
	 *
1168
	 * @return string Returns the first- and surname of the member.
1169
	 */
1170
	public function getName() {
1171
		return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1172
	}
1173
1174
1175
	/**
1176
	 * Set first- and surname
1177
	 *
1178
	 * This method assumes that the last part of the name is the surname, e.g.
1179
	 * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1180
	 *
1181
	 * @param string $name The name
1182
	 */
1183
	public function setName($name) {
1184
		$nameParts = explode(' ', $name);
1185
		$this->Surname = array_pop($nameParts);
1186
		$this->FirstName = join(' ', $nameParts);
1187
	}
1188
1189
1190
	/**
1191
	 * Alias for {@link setName}
1192
	 *
1193
	 * @param string $name The name
1194
	 * @see setName()
1195
	 */
1196
	public function splitName($name) {
1197
		return $this->setName($name);
1198
	}
1199
1200
	/**
1201
	 * Override the default getter for DateFormat so the
1202
	 * default format for the user's locale is used
1203
	 * if the user has not defined their own.
1204
	 *
1205
	 * @return string ISO date format
1206
	 */
1207
	public function getDateFormat() {
1208
		if($this->getField('DateFormat')) {
1209
			return $this->getField('DateFormat');
1210
		} else {
1211
			return Config::inst()->get('i18n', 'date_format');
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Config::inst()->...'i18n', 'date_format'); (array|integer|double|string|boolean) is incompatible with the return type documented by Member::getDateFormat of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1212
		}
1213
	}
1214
1215
	/**
1216
	 * Override the default getter for TimeFormat so the
1217
	 * default format for the user's locale is used
1218
	 * if the user has not defined their own.
1219
	 *
1220
	 * @return string ISO date format
1221
	 */
1222
	public function getTimeFormat() {
1223
		if($this->getField('TimeFormat')) {
1224
			return $this->getField('TimeFormat');
1225
		} else {
1226
			return Config::inst()->get('i18n', 'time_format');
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Config::inst()->...'i18n', 'time_format'); (array|integer|double|string|boolean) is incompatible with the return type documented by Member::getTimeFormat of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1227
		}
1228
	}
1229
1230
	//---------------------------------------------------------------------//
1231
1232
1233
	/**
1234
	 * Get a "many-to-many" map that holds for all members their group memberships,
1235
	 * including any parent groups where membership is implied.
1236
	 * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1237
	 *
1238
	 * @todo Push all this logic into Member_GroupSet's getIterator()?
1239
	 * @return Member_Groupset
1240
	 */
1241
	public function Groups() {
1242
		$groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID');
1243
		$groups = $groups->forForeignID($this->ID);
1244
1245
		$this->extend('updateGroups', $groups);
1246
1247
		return $groups;
1248
	}
1249
1250
	/**
1251
	 * @return ManyManyList
1252
	 */
1253
	public function DirectGroups() {
1254
		return $this->getManyManyComponents('Groups');
1255
	}
1256
1257
	/**
1258
	 * Get a member SQLMap of members in specific groups
1259
	 *
1260
	 * If no $groups is passed, all members will be returned
1261
	 *
1262
	 * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1263
	 * @return SS_Map Returns an SS_Map that returns all Member data.
1264
	 */
1265
	public static function map_in_groups($groups = null) {
1266
		$groupIDList = array();
1267
1268
		if($groups instanceof SS_List) {
1269
			foreach( $groups as $group ) {
1270
				$groupIDList[] = $group->ID;
1271
			}
1272
		} elseif(is_array($groups)) {
1273
			$groupIDList = $groups;
1274
		} elseif($groups) {
1275
			$groupIDList[] = $groups;
1276
		}
1277
1278
		// No groups, return all Members
1279
		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...
1280
			return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
1281
		}
1282
1283
		$membersList = new ArrayList();
1284
		// This is a bit ineffective, but follow the ORM style
1285
		foreach(Group::get()->byIDs($groupIDList) as $group) {
1286
			$membersList->merge($group->Members());
1287
		}
1288
1289
		$membersList->removeDuplicates('ID');
1290
		return $membersList->map();
1291
	}
1292
1293
1294
	/**
1295
	 * Get a map of all members in the groups given that have CMS permissions
1296
	 *
1297
	 * If no groups are passed, all groups with CMS permissions will be used.
1298
	 *
1299
	 * @param array $groups Groups to consider or NULL to use all groups with
1300
	 *                      CMS permissions.
1301
	 * @return SS_Map Returns a map of all members in the groups given that
1302
	 *                have CMS permissions.
1303
	 */
1304
	public static function mapInCMSGroups($groups = null) {
1305
		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...
1306
			$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1307
1308
			if(class_exists('CMSMain')) {
1309
				$cmsPerms = singleton('CMSMain')->providePermissions();
1310
			} else {
1311
				$cmsPerms = singleton('LeftAndMain')->providePermissions();
1312
			}
1313
1314
			if(!empty($cmsPerms)) {
1315
				$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1316
			}
1317
1318
			$permsClause = DB::placeholders($perms);
1319
			$groups = DataObject::get('Group')
1320
				->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1321
				->where(array(
1322
					"\"Permission\".\"Code\" IN ($permsClause)" => $perms
1323
				));
1324
		}
1325
1326
		$groupIDList = array();
1327
1328
		if(is_a($groups, 'SS_List')) {
1329
			foreach($groups as $group) {
1330
				$groupIDList[] = $group->ID;
1331
			}
1332
		} elseif(is_array($groups)) {
1333
			$groupIDList = $groups;
1334
		}
1335
1336
		$members = Member::get()
1337
			->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1338
			->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1339
		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...
1340
			$groupClause = DB::placeholders($groupIDList);
1341
			$members = $members->where(array(
1342
				"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1343
			));
1344
		}
1345
1346
		return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1347
	}
1348
1349
1350
	/**
1351
	 * Get the groups in which the member is NOT in
1352
	 *
1353
	 * When passed an array of groups, and a component set of groups, this
1354
	 * function will return the array of groups the member is NOT in.
1355
	 *
1356
	 * @param array $groupList An array of group code names.
1357
	 * @param array $memberGroups A component set of groups (if set to NULL,
1358
	 *                            $this->groups() will be used)
1359
	 * @return array Groups in which the member is NOT in.
1360
	 */
1361
	public function memberNotInGroups($groupList, $memberGroups = null){
1362
		if(!$memberGroups) $memberGroups = $this->Groups();
1363
1364
		foreach($memberGroups as $group) {
1365
			if(in_array($group->Code, $groupList)) {
1366
				$index = array_search($group->Code, $groupList);
1367
				unset($groupList[$index]);
1368
			}
1369
		}
1370
1371
		return $groupList;
1372
	}
1373
1374
1375
	/**
1376
	 * Return a {@link FieldList} of fields that would appropriate for editing
1377
	 * this member.
1378
	 *
1379
	 * @return FieldList Return a FieldList of fields that would appropriate for
1380
	 *                   editing this member.
1381
	 */
1382
	public function getCMSFields() {
1383
		require_once 'Zend/Date.php';
1384
1385
		$self = $this;
1386
		$this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) {
1387
			/** @var FieldList $mainFields */
1388
			$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
1389
1390
			// Build change password field
1391
			$mainFields->replaceField('Password', $self->getMemberPasswordField());
1392
1393
			$mainFields->replaceField('Locale', new DropdownField(
1394
				"Locale",
1395
				_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1396
				i18n::get_existing_translations()
1397
			));
1398
			$mainFields->removeByName($self->config()->hidden_fields);
1399
1400
			if( ! $self->config()->lock_out_after_incorrect_logins) {
1401
				$mainFields->removeByName('FailedLoginCount');
1402
			}
1403
1404
1405
			// Groups relation will get us into logical conflicts because
1406
			// Members are displayed within  group edit form in SecurityAdmin
1407
			$fields->removeByName('Groups');
1408
1409
			// Members shouldn't be able to directly view/edit logged passwords
1410
			$fields->removeByName('LoggedPasswords');
1411
1412
			$fields->removeByName('RememberLoginHashes');
1413
1414
			if(Permission::check('EDIT_PERMISSIONS')) {
1415
				$groupsMap = array();
1416
				foreach(Group::get() as $group) {
1417
					// Listboxfield values are escaped, use ASCII char instead of &raquo;
1418
					$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1419
				}
1420
				asort($groupsMap);
1421
				$fields->addFieldToTab('Root.Main',
1422
					ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name())
1423
						->setSource($groupsMap)
1424
						->setAttribute(
1425
							'data-placeholder',
1426
							_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1427
						)
1428
				);
1429
1430
1431
				// Add permission field (readonly to avoid complicated group assignment logic).
1432
				// This should only be available for existing records, as new records start
1433
				// with no permissions until they have a group assignment anyway.
1434
				if($self->ID) {
1435
					$permissionsField = new PermissionCheckboxSetField_Readonly(
1436
						'Permissions',
1437
						false,
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1438
						'Permission',
1439
						'GroupID',
1440
						// we don't want parent relationships, they're automatically resolved in the field
1441
						$self->getManyManyComponents('Groups')
1442
					);
1443
					$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1444
					$fields->addFieldToTab('Root.Permissions', $permissionsField);
1445
				}
1446
			}
1447
1448
			$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1449
			if($permissionsTab) $permissionsTab->addExtraClass('readonly');
1450
1451
			$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
1452
			$dateFormatMap = array(
1453
				'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
1454
				'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
1455
				'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
1456
				'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
1457
			);
1458
			$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
1459
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1460
			$mainFields->push(
1461
				$dateFormatField = new MemberDatetimeOptionsetField(
1462
					'DateFormat',
1463
					$self->fieldLabel('DateFormat'),
1464
					$dateFormatMap
1465
				)
1466
			);
1467
			$dateFormatField->setValue($self->DateFormat);
1468
1469
			$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
1470
			$timeFormatMap = array(
1471
				'h:mm a' => Zend_Date::now()->toString('h:mm a'),
1472
				'H:mm' => Zend_Date::now()->toString('H:mm'),
1473
			);
1474
			$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
1475
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1476
			$mainFields->push(
1477
				$timeFormatField = new MemberDatetimeOptionsetField(
1478
					'TimeFormat',
1479
					$self->fieldLabel('TimeFormat'),
1480
					$timeFormatMap
1481
				)
1482
			);
1483
			$timeFormatField->setValue($self->TimeFormat);
1484
		});
1485
1486
		return parent::getCMSFields();
1487
	}
1488
1489
	/**
1490
	 *
1491
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1492
	 *
1493
	 */
1494
	public function fieldLabels($includerelations = true) {
1495
		$labels = parent::fieldLabels($includerelations);
1496
1497
		$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1498
		$labels['Surname'] = _t('Member.SURNAME', 'Surname');
1499
		$labels['Email'] = _t('Member.EMAIL', 'Email');
1500
		$labels['Password'] = _t('Member.db_Password', 'Password');
1501
		$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1502
		$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1503
		$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1504
		$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
1505
		$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
1506
		if($includerelations){
1507
			$labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups',
1508
				'Security Groups this member belongs to');
1509
		}
1510
		return $labels;
1511
	}
1512
1513
    /**
1514
     * Users can view their own record.
1515
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1516
     * This is likely to be customized for social sites etc. with a looser permission model.
1517
     */
1518
    public function canView($member = null) {
1519
        //get member
1520
        if(!($member instanceof Member)) {
1521
            $member = Member::currentUser();
1522
        }
1523
        //check for extensions, we do this first as they can overrule everything
1524
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1525
        if($extended !== null) {
1526
            return $extended;
1527
        }
1528
1529
        //need to be logged in and/or most checks below rely on $member being a Member
1530
        if(!$member) {
1531
            return false;
1532
        }
1533
        // members can usually view their own record
1534
        if($this->ID == $member->ID) {
1535
            return true;
1536
        }
1537
        //standard check
1538
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1539
    }
1540
    /**
1541
     * Users can edit their own record.
1542
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1543
     */
1544 View Code Duplication
    public function canEdit($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1545
        //get member
1546
        if(!($member instanceof Member)) {
1547
            $member = Member::currentUser();
1548
        }
1549
        //check for extensions, we do this first as they can overrule everything
1550
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1551
        if($extended !== null) {
1552
            return $extended;
1553
        }
1554
1555
        //need to be logged in and/or most checks below rely on $member being a Member
1556
        if(!$member) {
1557
            return false;
1558
        }
1559
1560
        // HACK: we should not allow for an non-Admin to edit an Admin
1561
        if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
1562
            return false;
1563
        }
1564
        // members can usually edit their own record
1565
        if($this->ID == $member->ID) {
1566
            return true;
1567
        }
1568
        //standard check
1569
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1570
    }
1571
    /**
1572
     * Users can edit their own record.
1573
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1574
     */
1575 View Code Duplication
    public function canDelete($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1576
        if(!($member instanceof Member)) {
1577
            $member = Member::currentUser();
1578
        }
1579
        //check for extensions, we do this first as they can overrule everything
1580
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1581
        if($extended !== null) {
1582
            return $extended;
1583
        }
1584
1585
        //need to be logged in and/or most checks below rely on $member being a Member
1586
        if(!$member) {
1587
            return false;
1588
        }
1589
        // Members are not allowed to remove themselves,
1590
        // since it would create inconsistencies in the admin UIs.
1591
        if($this->ID && $member->ID == $this->ID) {
1592
            return false;
1593
        }
1594
1595
        // HACK: if you want to delete a member, you have to be a member yourself.
1596
        // this is a hack because what this should do is to stop a user
1597
        // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
1598
        if(Permission::checkMember($this, 'ADMIN')) {
1599
            if( ! Permission::checkMember($member, 'ADMIN')) {
1600
                return false;
1601
            }
1602
        }
1603
        //standard check
1604
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1605
    }
1606
1607
	/**
1608
	 * Validate this member object.
1609
	 */
1610
	public function validate() {
1611
		$valid = parent::validate();
1612
1613 View Code Duplication
		if(!$this->ID || $this->isChanged('Password')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1614
			if($this->Password && self::$password_validator) {
1615
				$valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1616
			}
1617
		}
1618
1619 View Code Duplication
		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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1620
			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...
1621
				$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...
1622
			}
1623
		}
1624
1625
		return $valid;
1626
	}
1627
1628
	/**
1629
	 * Change password. This will cause rehashing according to
1630
	 * the `PasswordEncryption` property.
1631
	 *
1632
	 * @param String $password Cleartext password
1633
	 */
1634
	public function changePassword($password) {
1635
		$this->Password = $password;
1636
		$valid = $this->validate();
1637
1638
		if($valid->valid()) {
1639
			$this->AutoLoginHash = null;
1640
			$this->write();
1641
		}
1642
1643
		return $valid;
1644
	}
1645
1646
	/**
1647
	 * Tell this member that someone made a failed attempt at logging in as them.
1648
	 * This can be used to lock the user out temporarily if too many failed attempts are made.
1649
	 */
1650
	public function registerFailedLogin() {
1651
		if(self::config()->lock_out_after_incorrect_logins) {
1652
			// Keep a tally of the number of failed log-ins so that we can lock people out
1653
			$this->FailedLoginCount = $this->FailedLoginCount + 1;
1654
1655
			if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1656
				$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...
1657
				$this->LockedOutUntil = date('Y-m-d H:i:s', SS_Datetime::now()->Format('U') + $lockoutMins*60);
1658
				$this->FailedLoginCount = 0;
1659
			}
1660
		}
1661
		$this->extend('registerFailedLogin');
1662
		$this->write();
1663
	}
1664
1665
	/**
1666
	 * Tell this member that a successful login has been made
1667
	 */
1668
	public function registerSuccessfulLogin() {
1669
		if(self::config()->lock_out_after_incorrect_logins) {
1670
			// Forgive all past login failures
1671
			$this->FailedLoginCount = 0;
1672
			$this->write();
1673
		}
1674
	}
1675
	/**
1676
	 * Get the HtmlEditorConfig for this user to be used in the CMS.
1677
	 * This is set by the group. If multiple configurations are set,
1678
	 * the one with the highest priority wins.
1679
	 *
1680
	 * @return string
1681
	 */
1682
	public function getHtmlEditorConfigForCMS() {
1683
		$currentName = '';
1684
		$currentPriority = 0;
1685
1686
		foreach($this->Groups() as $group) {
1687
			$configName = $group->HtmlEditorConfig;
1688
			if($configName) {
1689
				$config = HTMLEditorConfig::get($group->HtmlEditorConfig);
1690
				if($config && $config->getOption('priority') > $currentPriority) {
1691
					$currentName = $configName;
1692
					$currentPriority = $config->getOption('priority');
1693
				}
1694
			}
1695
		}
1696
1697
		// If can't find a suitable editor, just default to cms
1698
		return $currentName ? $currentName : 'cms';
1699
	}
1700
1701
	public static function get_template_global_variables() {
1702
		return array(
1703
			'CurrentMember' => 'currentUser',
1704
			'currentUser',
1705
		);
1706
	}
1707
}
1708
1709
/**
1710
 * Represents a set of Groups attached to a member.
1711
 * Handles the hierarchy logic.
1712
 * @package framework
1713
 * @subpackage security
1714
 */
1715
class Member_GroupSet extends ManyManyList {
1716
1717
	protected function linkJoinTable() {
1718
		// Do not join the table directly
1719
		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...
1720
			user_error('Member_GroupSet does not support many_many_extraFields', E_USER_ERROR);
1721
		}
1722
	}
1723
1724
	/**
1725
	 * Link this group set to a specific member.
1726
	 *
1727
	 * Recursively selects all groups applied to this member, as well as any
1728
	 * parent groups of any applied groups
1729
	 *
1730
	 * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
1731
	 * ids as per getForeignID
1732
	 * @return array Condition In array(SQL => parameters format)
1733
	 */
1734
	public function foreignIDFilter($id = null) {
1735
		if ($id === null) $id = $this->getForeignID();
1736
1737
		// Find directly applied groups
1738
		$manyManyFilter = parent::foreignIDFilter($id);
1739
		$query = new SQLSelect('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter);
0 ignored issues
show
Bug introduced by
It seems like $manyManyFilter defined by parent::foreignIDFilter($id) on line 1738 can also be of type null; however, SQLSelect::__construct() does only seem to accept array, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1740
		$groupIDs = $query->execute()->column();
1741
1742
		// Get all ancestors, iteratively merging these into the master set
1743
		$allGroupIDs = array();
1744
		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...
1745
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1746
			$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
1747
			$groupIDs = array_filter($groupIDs);
1748
		}
1749
1750
		// Add a filter to this DataList
1751
		if(!empty($allGroupIDs)) {
1752
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1753
			return array("\"Group\".\"ID\" IN ($allGroupIDsPlaceholders)" => $allGroupIDs);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array("\"Group\"....rs})" => $allGroupIDs); (array<*,array>) is incompatible with the return type of the parent method ManyManyList::foreignIDFilter of type array<string,array>|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1754
		} else {
1755
			return array('"Group"."ID"' => 0);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('"Group"."ID"' => 0); (array<string,integer>) is incompatible with the return type of the parent method ManyManyList::foreignIDFilter of type array<string,array>|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1756
		}
1757
	}
1758
1759
	public function foreignIDWriteFilter($id = null) {
1760
		// Use the ManyManyList::foreignIDFilter rather than the one
1761
		// in this class, otherwise we end up selecting all inherited groups
1762
		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...
1763
	}
1764
1765
	public function add($item, $extraFields = null) {
1766
		// Get Group.ID
1767
		$itemID = null;
1768
		if(is_numeric($item)) {
1769
			$itemID = $item;
1770
		} else if($item instanceof Group) {
1771
			$itemID = $item->ID;
1772
		}
1773
1774
		// Check if this group is allowed to be added
1775
		if($this->canAddGroups(array($itemID))) {
1776
			parent::add($item, $extraFields);
1777
		}
1778
	}
1779
1780
	/**
1781
	 * Determine if the following groups IDs can be added
1782
	 *
1783
	 * @param array $itemIDs
1784
	 * @return boolean
1785
	 */
1786
	protected function canAddGroups($itemIDs) {
1787
		if(empty($itemIDs)) {
1788
			return true;
1789
		}
1790
		$member = $this->getMember();
1791
		return empty($member) || $member->onChangeGroups($itemIDs);
1792
	}
1793
1794
	/**
1795
	 * Get foreign member record for this relation
1796
	 *
1797
	 * @return Member
1798
	 */
1799
	protected function getMember() {
1800
		$id = $this->getForeignID();
1801
		if($id) {
1802
			return DataObject::get_by_id('Member', $id);
1803
		}
1804
	}
1805
}
1806
1807
/**
1808
 * Member Validator
1809
 *
1810
 * Custom validation for the Member object can be achieved either through an
1811
 * {@link DataExtension} on the Member_Validator object or, by specifying a subclass of
1812
 * {@link Member_Validator} through the {@link Injector} API.
1813
 * The Validator can also be modified by adding an Extension to Member and implement the
1814
 * <code>updateValidator</code> hook.
1815
 * {@see Member::getValidator()}
1816
 *
1817
 * Additional required fields can also be set via config API, eg.
1818
 * <code>
1819
 * Member_Validator:
1820
 *   customRequired:
1821
 *     - Surname
1822
 * </code>
1823
 *
1824
 * @package framework
1825
 * @subpackage security
1826
 */
1827
class Member_Validator extends RequiredFields
1828
{
1829
	/**
1830
	 * Fields that are required by this validator
1831
	 * @config
1832
	 * @var array
1833
	 */
1834
	protected $customRequired = array(
1835
		'FirstName',
1836
		'Email'
1837
	);
1838
1839
	/**
1840
	 * Determine what member this validator is meant for
1841
	 * @var Member
1842
	 */
1843
	protected $forMember = null;
1844
1845
	/**
1846
	 * Constructor
1847
	 */
1848
	public function __construct() {
1849
		$required = func_get_args();
1850
1851 View Code Duplication
		if(isset($required[0]) && is_array($required[0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1852
			$required = $required[0];
1853
		}
1854
1855
		$required = array_merge($required, $this->customRequired);
1856
1857
		// check for config API values and merge them in
1858
		$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...
1859
		if(is_array($config)){
1860
			$required = array_merge($required, $config);
1861
		}
1862
1863
		parent::__construct(array_unique($required));
1864
	}
1865
1866
	/**
1867
	 * Get the member this validator applies to.
1868
	 * @return Member
1869
	 */
1870
	public function getForMember()
1871
	{
1872
		return $this->forMember;
1873
	}
1874
1875
	/**
1876
	 * Set the Member this validator applies to.
1877
	 * @param Member $value
1878
	 * @return $this
1879
	 */
1880
	public function setForMember(Member $value)
1881
	{
1882
		$this->forMember = $value;
1883
		return $this;
1884
	}
1885
1886
	/**
1887
	 * Check if the submitted member data is valid (server-side)
1888
	 *
1889
	 * Check if a member with that email doesn't already exist, or if it does
1890
	 * that it is this member.
1891
	 *
1892
	 * @param array $data Submitted data
1893
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
1894
	 *              FALSE.
1895
	 */
1896
	public function php($data)
1897
	{
1898
		$valid = parent::php($data);
1899
1900
		$identifierField = (string)Member::config()->unique_identifier_field;
1901
1902
		// Only validate identifier field if it's actually set. This could be the case if
1903
		// somebody removes `Email` from the list of required fields.
1904
		if(isset($data[$identifierField])){
1905
			$id = isset($data['ID']) ? (int)$data['ID'] : 0;
1906
			if(!$id && ($ctrl = $this->form->getController())){
1907
				// get the record when within GridField (Member editing page in CMS)
1908
				if($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()){
1909
					$id = $record->ID;
1910
				}
1911
			}
1912
1913
			// If there's no ID passed via controller or form-data, use the assigned member (if available)
1914
			if(!$id && ($member = $this->getForMember())){
1915
				$id = $member->exists() ? $member->ID : 0;
1916
			}
1917
1918
			// set the found ID to the data array, so that extensions can also use it
1919
			$data['ID'] = $id;
1920
1921
			$members = Member::get()->filter($identifierField, $data[$identifierField]);
1922
			if($id) {
1923
				$members = $members->exclude('ID', $id);
1924
			}
1925
1926
			if($members->count() > 0) {
1927
				$this->validationError(
1928
					$identifierField,
1929
					_t(
1930
						'Member.VALIDATIONMEMBEREXISTS',
1931
						'A member already exists with the same {identifier}',
1932
						array('identifier' => Member::singleton()->fieldLabel($identifierField))
0 ignored issues
show
Documentation introduced by
array('identifier' => \M...abel($identifierField)) is of type array<string,string,{"identifier":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1933
					),
1934
					'required'
1935
				);
1936
				$valid = false;
1937
			}
1938
		}
1939
1940
1941
		// Execute the validators on the extensions
1942
		$results = $this->extend('updatePHP', $data, $this->form);
1943
		$results[] = $valid;
1944
		return min($results);
1945
	}
1946
}
1947