Completed
Push — create-endpoint-fetcher ( 23bf41 )
by Sam
07:40
created

Member_ChangePasswordEmail::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 5
Ratio 100 %
Metric Value
dl 5
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 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()) return $result;
301
302
		if(empty($this->Password) && $this->exists()) {
303
			$result->error(_t('Member.NoPassword','There is no password on this member.'));
304
			return $result;
305
		}
306
307
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
308
		if(!$e->check($this->Password, $password, $this->Salt, $this)) {
309
			$result->error(_t (
310
				'Member.ERRORWRONGCRED',
311
				'The provided details don\'t seem to be correct. Please try again.'
312
			));
313
		}
314
315
		return $result;
316
	}
317
318
	/**
319
	 * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
320
	 * one with error messages to display if the member is locked out.
321
	 *
322
	 * You can hook into this with a "canLogIn" method on an attached extension.
323
	 *
324
	 * @return ValidationResult
325
	 */
326
	public function canLogIn() {
327
		$result = ValidationResult::create();
328
329
		if($this->isLockedOut()) {
330
			$result->error(
331
				_t(
332
					'Member.ERRORLOCKEDOUT2',
333
					'Your account has been temporarily disabled because of too many failed attempts at ' .
334
					'logging in. Please try again in {count} minutes.',
335
					null,
336
					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...
337
				)
338
			);
339
		}
340
341
		$this->extend('canLogIn', $result);
342
		return $result;
343
	}
344
345
	/**
346
	 * Returns true if this user is locked out
347
	 */
348
	public function isLockedOut() {
349
		return $this->LockedOutUntil && time() < strtotime($this->LockedOutUntil);
350
	}
351
352
	/**
353
	 * Regenerate the session_id.
354
	 * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
355
	 * They have caused problems in certain
356
	 * quirky problems (such as using the Windmill 0.3.6 proxy).
357
	 */
358
	public static function session_regenerate_id() {
359
		if(!self::config()->session_regenerate_id) return;
360
361
		// This can be called via CLI during testing.
362
		if(Director::is_cli()) return;
363
364
		$file = '';
365
		$line = '';
366
367
		// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
368
		// There's nothing we can do about this, because it's an operating system function!
369
		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...
370
	}
371
372
	/**
373
	 * Get the field used for uniquely identifying a member
374
	 * in the database. {@see Member::$unique_identifier_field}
375
	 *
376
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
377
	 * @return string
378
	 */
379
	public static function get_unique_identifier_field() {
380
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
381
		return Member::config()->unique_identifier_field;
382
	}
383
384
	/**
385
	 * Set the field used for uniquely identifying a member
386
	 * in the database. {@see Member::$unique_identifier_field}
387
	 *
388
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
389
	 * @param $field The field name to set as the unique field
390
	 */
391
	public static function set_unique_identifier_field($field) {
392
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
393
		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...
394
	}
395
396
	/**
397
	 * Set a {@link PasswordValidator} object to use to validate member's passwords.
398
	 */
399
	public static function set_password_validator($pv) {
400
		self::$password_validator = $pv;
401
	}
402
403
	/**
404
	 * Returns the current {@link PasswordValidator}
405
	 */
406
	public static function password_validator() {
407
		return self::$password_validator;
408
	}
409
410
	/**
411
	 * Set the number of days that a password should be valid for.
412
	 * Set to null (the default) to have passwords never expire.
413
	 *
414
	 * @deprecated 4.0 Use the "Member.password_expiry_days" config setting instead
415
	 */
416
	public static function set_password_expiry($days) {
417
		Deprecation::notice('4.0', 'Use the "Member.password_expiry_days" config setting instead');
418
		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...
419
	}
420
421
	/**
422
	 * Configure the security system to lock users out after this many incorrect logins
423
	 *
424
	 * @deprecated 4.0 Use the "Member.lock_out_after_incorrect_logins" config setting instead
425
	 */
426
	public static function lock_out_after_incorrect_logins($numLogins) {
427
		Deprecation::notice('4.0', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead');
428
		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...
429
	}
430
431
432
	public function isPasswordExpired() {
433
		if(!$this->PasswordExpiry) return false;
434
		return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
435
	}
436
437
	/**
438
	 * Logs this member in
439
	 *
440
	 * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
441
	 */
442
	public function logIn($remember = false) {
443
		$this->extend('beforeMemberLoggedIn');
444
445
		self::session_regenerate_id();
446
447
		Session::set("loggedInAs", $this->ID);
448
		// This lets apache rules detect whether the user has logged in
449
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0);
450
451
		// Cleans up any potential previous hash for this member on this device
452
		if ($alcDevice = Cookie::get('alc_device')) {
453
			RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
454
		}
455
		if($remember) {
456
			$rememberLoginHash = RememberLoginHash::generate($this);
457
			$tokenExpiryDays = Config::inst()->get('RememberLoginHash', 'token_expiry_days');
458
			$deviceExpiryDays = Config::inst()->get('RememberLoginHash', 'device_expiry_days');
459
			Cookie::set('alc_enc', $this->ID . ':' . $rememberLoginHash->getToken(),
460
				$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...
461
			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...
462
		} else {
463
			Cookie::set('alc_enc', null);
464
			Cookie::set('alc_device', null);
465
			Cookie::force_expiry('alc_enc');
466
			Cookie::force_expiry('alc_device');
467
		}
468
469
		// Clear the incorrect log-in count
470
		$this->registerSuccessfulLogin();
471
472
		// Don't set column if its not built yet (the login might be precursor to a /dev/build...)
473
		if(array_key_exists('LockedOutUntil', DB::field_list('Member'))) {
474
			$this->LockedOutUntil = null;
475
		}
476
477
		$this->regenerateTempID();
478
479
		$this->write();
480
481
		// Audit logging hook
482
		$this->extend('memberLoggedIn');
483
	}
484
485
	/**
486
	 * Trigger regeneration of TempID.
487
	 *
488
	 * This should be performed any time the user presents their normal identification (normally Email)
489
	 * and is successfully authenticated.
490
	 */
491
	public function regenerateTempID() {
492
		$generator = new RandomGenerator();
493
		$this->TempIDHash = $generator->randomToken('sha1');
494
		$this->TempIDExpired = self::config()->temp_id_lifetime
495
			? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime)
496
			: null;
497
		$this->write();
498
	}
499
500
	/**
501
	 * Check if the member ID logged in session actually
502
	 * has a database record of the same ID. If there is
503
	 * no logged in user, FALSE is returned anyway.
504
	 *
505
	 * @return boolean TRUE record found FALSE no record found
506
	 */
507
	public static function logged_in_session_exists() {
508
		if($id = Member::currentUserID()) {
509
			if($member = DataObject::get_by_id('Member', $id)) {
510
				if($member->exists()) return true;
511
			}
512
		}
513
514
		return false;
515
	}
516
517
	/**
518
	 * Log the user in if the "remember login" cookie is set
519
	 *
520
	 * The <i>remember login token</i> will be changed on every successful
521
	 * auto-login.
522
	 */
523
	public static function autoLogin() {
524
		// Don't bother trying this multiple times
525
		if (!class_exists('SapphireTest', false) || !SapphireTest::is_running_test()) {
526
			self::$_already_tried_to_auto_log_in = true;
527
		}
528
529
		if(strpos(Cookie::get('alc_enc'), ':') === false
530
			|| Session::get("loggedInAs")
531
			|| !Security::database_is_ready()
532
		) {
533
			return;
534
		}
535
536
		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...
537
			list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
538
			$deviceID = Cookie::get('alc_device');
539
540
			$member = Member::get()->byId($uid);
541
542
			$rememberLoginHash = null;
543
544
			// check if autologin token matches
545
			if($member) {
546
				$hash = $member->encryptWithUserSettings($token);
547
				$rememberLoginHash = RememberLoginHash::get()
548
					->filter(array(
549
						'MemberID' => $member->ID,
550
						'DeviceID' => $deviceID,
551
						'Hash' => $hash
552
					))->First();
553
				if(!$rememberLoginHash) {
554
					$member = null;
555
				} else {
556
					// Check for expired token
557
					$expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
558
					$now = SS_Datetime::now();
559
					$now = new DateTime($now->Rfc2822());
560
					if ($now > $expiryDate) {
561
						$member = null;
562
					}
563
				}
564
			}
565
566
			if($member) {
567
				self::session_regenerate_id();
568
				Session::set("loggedInAs", $member->ID);
569
				// This lets apache rules detect whether the user has logged in
570
				if(Member::config()->login_marker_cookie) {
571
					Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
572
				}
573
574
				if ($rememberLoginHash) {
575
					$rememberLoginHash->renew();
576
					$tokenExpiryDays = Config::inst()->get('RememberLoginHash', 'token_expiry_days');
577
					Cookie::set('alc_enc', $member->ID . ':' . $rememberLoginHash->getToken(),
578
						$tokenExpiryDays, null, null, false, true);
579
				}
580
581
				$member->write();
582
583
				// Audit logging hook
584
				$member->extend('memberAutoLoggedIn');
585
			}
586
		}
587
	}
588
589
	/**
590
	 * Logs this member out.
591
	 */
592
	public function logOut() {
593
		$this->extend('beforeMemberLoggedOut');
594
595
		Session::clear("loggedInAs");
596
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
597
598
		Session::destroy();
599
600
		$this->extend('memberLoggedOut');
601
602
		// Clears any potential previous hashes for this member
603
		RememberLoginHash::clear($this, Cookie::get('alc_device'));
604
605
		Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
606
		Cookie::force_expiry('alc_enc');
607
		Cookie::set('alc_device', null);
608
		Cookie::force_expiry('alc_device');
609
610
		// Switch back to live in order to avoid infinite loops when
611
		// redirecting to the login screen (if this login screen is versioned)
612
		Session::clear('readingMode');
613
614
		$this->write();
615
616
		// Audit logging hook
617
		$this->extend('memberLoggedOut');
618
	}
619
620
	/**
621
	 * Utility for generating secure password hashes for this member.
622
	 */
623
	public function encryptWithUserSettings($string) {
624
		if (!$string) return null;
625
626
		// If the algorithm or salt is not available, it means we are operating
627
		// on legacy account with unhashed password. Do not hash the string.
628
		if (!$this->PasswordEncryption) {
629
			return $string;
630
		}
631
632
		// We assume we have PasswordEncryption and Salt available here.
633
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
634
		return $e->encrypt($string, $this->Salt);
635
636
	}
637
638
	/**
639
	 * Generate an auto login token which can be used to reset the password,
640
	 * at the same time hashing it and storing in the database.
641
	 *
642
	 * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
643
	 *
644
	 * @returns string Token that should be passed to the client (but NOT persisted).
645
	 *
646
	 * @todo Make it possible to handle database errors such as a "duplicate key" error
647
	 */
648
	public function generateAutologinTokenAndStoreHash($lifetime = 2) {
649
		do {
650
			$generator = new RandomGenerator();
651
			$token = $generator->randomToken();
652
			$hash = $this->encryptWithUserSettings($token);
653
		} while(DataObject::get_one('Member', array(
654
			'"Member"."AutoLoginHash"' => $hash
655
		)));
656
657
		$this->AutoLoginHash = $hash;
658
		$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
659
660
		$this->write();
661
662
		return $token;
663
	}
664
665
	/**
666
	 * Check the token against the member.
667
	 *
668
	 * @param string $autologinToken
669
	 *
670
	 * @returns bool Is token valid?
671
	 */
672
	public function validateAutoLoginToken($autologinToken) {
673
		$hash = $this->encryptWithUserSettings($autologinToken);
674
		$member = self::member_from_autologinhash($hash, false);
675
		return (bool)$member;
676
	}
677
678
	/**
679
	 * Return the member for the auto login hash
680
	 *
681
	 * @param string $hash The hash key
682
	 * @param bool $login Should the member be logged in?
683
	 *
684
	 * @return Member the matching member, if valid
685
	 * @return Member
686
	 */
687
	public static function member_from_autologinhash($hash, $login = false) {
688
689
		$nowExpression = DB::get_conn()->now();
690
		$member = DataObject::get_one('Member', array(
691
			"\"Member\".\"AutoLoginHash\"" => $hash,
692
			"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
693
		));
694
695
		if($login && $member) $member->logIn();
696
697
		return $member;
698
	}
699
700
	/**
701
	 * Find a member record with the given TempIDHash value
702
	 *
703
	 * @param string $tempid
704
	 * @return Member
705
	 */
706
	public static function member_from_tempid($tempid) {
707
		$members = Member::get()
708
			->filter('TempIDHash', $tempid);
709
710
		// Exclude expired
711
		if(static::config()->temp_id_lifetime) {
712
			$members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
713
		}
714
715
		return $members->first();
716
	}
717
718
	/**
719
	 * Returns the fields for the member form - used in the registration/profile module.
720
	 * It should return fields that are editable by the admin and the logged-in user.
721
	 *
722
	 * @return FieldList Returns a {@link FieldList} containing the fields for
723
	 *                   the member form.
724
	 */
725
	public function getMemberFormFields() {
726
		$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...
727
728
		$fields->replaceField('Password', $password = new ConfirmedPasswordField (
729
			'Password',
730
			$this->fieldLabel('Password'),
731
			null,
732
			null,
733
			(bool) $this->ID
734
		));
735
		$password->setCanBeEmpty(true);
736
737
		$fields->replaceField('Locale', new DropdownField (
738
			'Locale',
739
			$this->fieldLabel('Locale'),
740
			i18n::get_existing_translations()
741
		));
742
743
		$fields->removeByName(static::config()->hidden_fields);
744
		$fields->removeByName('FailedLoginCount');
745
746
747
		$this->extend('updateMemberFormFields', $fields);
748
		return $fields;
749
	}
750
751
	/**
752
	 * Returns the {@link RequiredFields} instance for the Member object. This
753
	 * Validator is used when saving a {@link CMSProfileController} or added to
754
	 * any form responsible for saving a users data.
755
	 *
756
	 * To customize the required fields, add a {@link DataExtension} to member
757
	 * calling the `updateValidator()` method.
758
	 *
759
	 * @return Member_Validator
760
	 */
761
	public function getValidator() {
762
		$validator = Injector::inst()->create('Member_Validator');
763
		$validator->setForMember($this);
764
		$this->extend('updateValidator', $validator);
765
766
		return $validator;
767
	}
768
769
770
	/**
771
	 * Returns the current logged in user
772
	 *
773
	 * @return Member|null
774
	 */
775
	public static function currentUser() {
776
		$id = Member::currentUserID();
777
778
		if($id) {
779
			return Member::get()->byId($id);
780
		}
781
	}
782
783
	/**
784
	 * Get the ID of the current logged in user
785
	 *
786
	 * @return int Returns the ID of the current logged in user or 0.
787
	 */
788
	public static function currentUserID() {
789
		$id = Session::get("loggedInAs");
790
		if(!$id && !self::$_already_tried_to_auto_log_in) {
791
			self::autoLogin();
792
			$id = Session::get("loggedInAs");
793
		}
794
795
		return is_numeric($id) ? $id : 0;
796
	}
797
	private static $_already_tried_to_auto_log_in = false;
798
799
800
	/*
801
	 * Generate a random password, with randomiser to kick in if there's no words file on the
802
	 * filesystem.
803
	 *
804
	 * @return string Returns a random password.
805
	 */
806
	public static function create_new_password() {
807
		$words = Config::inst()->get('Security', 'word_list');
808
809
		if($words && file_exists($words)) {
810
			$words = file($words);
811
812
			list($usec, $sec) = explode(' ', microtime());
813
			srand($sec + ((float) $usec * 100000));
814
815
			$word = trim($words[rand(0,sizeof($words)-1)]);
816
			$number = rand(10,999);
817
818
			return $word . $number;
819
		} else {
820
			$random = rand();
821
			$string = md5($random);
822
			$output = substr($string, 0, 6);
823
			return $output;
824
		}
825
	}
826
827
	/**
828
	 * Event handler called before writing to the database.
829
	 */
830
	public function onBeforeWrite() {
831
		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...
832
833
		// If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
834
		// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
835
		// but rather a last line of defense against data inconsistencies.
836
		$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...
837
		if($this->$identifierField) {
838
839
			// Note: Same logic as Member_Validator class
840
			$filter = array("\"$identifierField\"" => $this->$identifierField);
841
			if($this->ID) {
842
				$filter[] = array('"Member"."ID" <> ?' => $this->ID);
843
			}
844
			$existingRecord = DataObject::get_one('Member', $filter);
845
846
			if($existingRecord) {
847
				throw new ValidationException(ValidationResult::create(false, _t(
848
					'Member.ValidationIdentifierFailed',
849
					'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
850
					'Values in brackets show "fieldname = value", usually denoting an existing email address',
851
					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...
852
						'id' => $existingRecord->ID,
853
						'name' => $identifierField,
854
						'value' => $this->$identifierField
855
					)
856
				)));
857
			}
858
		}
859
860
		// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
861
		// However, if TestMailer is in use this isn't a risk.
862
		if(
863
			(Director::isLive() || Email::mailer() instanceof TestMailer)
864
			&& $this->isChanged('Password')
865
			&& $this->record['Password']
866
			&& $this->config()->notify_password_change
867
		) {
868
			/** @var Email $e */
869
			$e = Email::create();
870
			$e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'));
871
			$e->setTemplate('ChangePasswordEmail');
872
			$e->populateTemplate($this);
873
			$e->setTo($this->Email);
874
			$e->send();
875
		}
876
877
		// The test on $this->ID is used for when records are initially created.
878
		// Note that this only works with cleartext passwords, as we can't rehash
879
		// existing passwords.
880
		if((!$this->ID && $this->Password) || $this->isChanged('Password')) {
881
			// Password was changed: encrypt the password according the settings
882
			$encryption_details = Security::encrypt_password(
883
				$this->Password, // this is assumed to be cleartext
884
				$this->Salt,
885
				($this->PasswordEncryption) ?
886
					$this->PasswordEncryption : Security::config()->password_encryption_algorithm,
887
				$this
888
			);
889
890
			// Overwrite the Password property with the hashed value
891
			$this->Password = $encryption_details['password'];
892
			$this->Salt = $encryption_details['salt'];
893
			$this->PasswordEncryption = $encryption_details['algorithm'];
894
895
			// If we haven't manually set a password expiry
896
			if(!$this->isChanged('PasswordExpiry')) {
897
				// then set it for us
898
				if(self::config()->password_expiry_days) {
899
					$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
900
				} else {
901
					$this->PasswordExpiry = null;
902
				}
903
			}
904
		}
905
906
		// save locale
907
		if(!$this->Locale) {
908
			$this->Locale = i18n::get_locale();
909
		}
910
911
		parent::onBeforeWrite();
912
	}
913
914
	public function onAfterWrite() {
915
		parent::onAfterWrite();
916
917
		Permission::flush_permission_cache();
918
919
		if($this->isChanged('Password')) {
920
			MemberPassword::log($this);
921
		}
922
	}
923
924
	public function onAfterDelete() {
925
		parent::onAfterDelete();
926
927
		//prevent orphaned records remaining in the DB
928
		$this->deletePasswordLogs();
929
	}
930
931
	/**
932
	 * Delete the MemberPassword objects that are associated to this user
933
	 *
934
	 * @return self
935
	 */
936
	protected function deletePasswordLogs() {
937
		foreach ($this->LoggedPasswords() as $password) {
938
			$password->delete();
939
			$password->destroy();
940
		}
941
		return $this;
942
	}
943
944
	/**
945
	 * Filter out admin groups to avoid privilege escalation,
946
	 * If any admin groups are requested, deny the whole save operation.
947
	 *
948
	 * @param Array $ids Database IDs of Group records
949
	 * @return boolean True if the change can be accepted
950
	 */
951
	public function onChangeGroups($ids) {
952
		// unless the current user is an admin already OR the logged in user is an admin
953
		if(Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
954
			return true;
955
		}
956
957
		// If there are no admin groups in this set then it's ok
958
		$adminGroups = Permission::get_groups_by_permission('ADMIN');
959
		$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
960
		return count(array_intersect($ids, $adminGroupIDs)) == 0;
961
	}
962
963
964
	/**
965
	 * Check if the member is in one of the given groups.
966
	 *
967
	 * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
968
	 * @param boolean $strict Only determine direct group membership if set to true (Default: false)
969
	 * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
970
	 */
971
	public function inGroups($groups, $strict = false) {
972
		if($groups) foreach($groups as $group) {
973
			if($this->inGroup($group, $strict)) return true;
974
		}
975
976
		return false;
977
	}
978
979
980
	/**
981
	 * Check if the member is in the given group or any parent groups.
982
	 *
983
	 * @param int|Group|string $group Group instance, Group Code or ID
984
	 * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
985
	 * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
986
	 */
987
	public function inGroup($group, $strict = false) {
988
		if(is_numeric($group)) {
989
			$groupCheckObj = DataObject::get_by_id('Group', $group);
990
		} elseif(is_string($group)) {
991
			$groupCheckObj = DataObject::get_one('Group', array(
992
				'"Group"."Code"' => $group
993
			));
994
		} elseif($group instanceof Group) {
995
			$groupCheckObj = $group;
996
		} else {
997
			user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
998
		}
999
1000
		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...
1001
1002
		$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1003
		if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
1004
			if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
1005
		}
1006
1007
		return false;
1008
	}
1009
1010
	/**
1011
	 * Adds the member to a group. This will create the group if the given
1012
	 * group code does not return a valid group object.
1013
	 *
1014
	 * @param string $groupcode
1015
	 * @param string Title of the group
1016
	 */
1017
	public function addToGroupByCode($groupcode, $title = "") {
1018
		$group = DataObject::get_one('Group', array(
1019
			'"Group"."Code"' => $groupcode
1020
		));
1021
1022
		if($group) {
1023
			$this->Groups()->add($group);
1024
		} else {
1025
			if(!$title) $title = $groupcode;
1026
1027
			$group = new Group();
1028
			$group->Code = $groupcode;
1029
			$group->Title = $title;
1030
			$group->write();
1031
1032
			$this->Groups()->add($group);
1033
		}
1034
	}
1035
1036
	/**
1037
	 * Removes a member from a group.
1038
	 *
1039
	 * @param string $groupcode
1040
	 */
1041
	public function removeFromGroupByCode($groupcode) {
1042
		$group = Group::get()->filter(array('Code' => $groupcode))->first();
1043
1044
		if($group) {
1045
			$this->Groups()->remove($group);
1046
		}
1047
	}
1048
1049
	/**
1050
	 * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
1051
	 * @param String $sep Separator
1052
	 */
1053
	public static function set_title_columns($columns, $sep = ' ') {
1054
		if (!is_array($columns)) $columns = array($columns);
1055
		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...
1056
	}
1057
1058
	//------------------- HELPER METHODS -----------------------------------//
1059
1060
	/**
1061
	 * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1062
	 * Falls back to showing either field on its own.
1063
	 *
1064
	 * You can overload this getter with {@link set_title_format()}
1065
	 * and {@link set_title_sql()}.
1066
	 *
1067
	 * @return string Returns the first- and surname of the member. If the ID
1068
	 *  of the member is equal 0, only the surname is returned.
1069
	 */
1070
	public function getTitle() {
1071
		$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...
1072
		if ($format) {
1073
			$values = array();
1074
			foreach($format['columns'] as $col) {
1075
				$values[] = $this->getField($col);
1076
			}
1077
			return join($format['sep'], $values);
1078
		}
1079
		if($this->getField('ID') === 0)
1080
			return $this->getField('Surname');
1081
		else{
1082
			if($this->getField('Surname') && $this->getField('FirstName')){
1083
				return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1084
			}elseif($this->getField('Surname')){
1085
				return $this->getField('Surname');
1086
			}elseif($this->getField('FirstName')){
1087
				return $this->getField('FirstName');
1088
			}else{
1089
				return null;
1090
			}
1091
		}
1092
	}
1093
1094
	/**
1095
	 * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1096
	 * Useful for custom queries which assume a certain member title format.
1097
	 *
1098
	 * @param String $tableName
1099
	 * @return String SQL
1100
	 */
1101
	public static function get_title_sql($tableName = 'Member') {
1102
		// This should be abstracted to SSDatabase concatOperator or similar.
1103
		$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...
1104
1105
		$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...
1106
		if ($format) {
1107
			$columnsWithTablename = array();
1108
			foreach($format['columns'] as $column) {
1109
				$columnsWithTablename[] = "\"$tableName\".\"$column\"";
1110
			}
1111
1112
			return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")";
1113
		} else {
1114
			return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")";
1115
		}
1116
	}
1117
1118
1119
	/**
1120
	 * Get the complete name of the member
1121
	 *
1122
	 * @return string Returns the first- and surname of the member.
1123
	 */
1124
	public function getName() {
1125
		return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1126
	}
1127
1128
1129
	/**
1130
	 * Set first- and surname
1131
	 *
1132
	 * This method assumes that the last part of the name is the surname, e.g.
1133
	 * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1134
	 *
1135
	 * @param string $name The name
1136
	 */
1137
	public function setName($name) {
1138
		$nameParts = explode(' ', $name);
1139
		$this->Surname = array_pop($nameParts);
1140
		$this->FirstName = join(' ', $nameParts);
1141
	}
1142
1143
1144
	/**
1145
	 * Alias for {@link setName}
1146
	 *
1147
	 * @param string $name The name
1148
	 * @see setName()
1149
	 */
1150
	public function splitName($name) {
1151
		return $this->setName($name);
1152
	}
1153
1154
	/**
1155
	 * Override the default getter for DateFormat so the
1156
	 * default format for the user's locale is used
1157
	 * if the user has not defined their own.
1158
	 *
1159
	 * @return string ISO date format
1160
	 */
1161
	public function getDateFormat() {
1162
		if($this->getField('DateFormat')) {
1163
			return $this->getField('DateFormat');
1164
		} else {
1165
			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...
1166
		}
1167
	}
1168
1169
	/**
1170
	 * Override the default getter for TimeFormat so the
1171
	 * default format for the user's locale is used
1172
	 * if the user has not defined their own.
1173
	 *
1174
	 * @return string ISO date format
1175
	 */
1176
	public function getTimeFormat() {
1177
		if($this->getField('TimeFormat')) {
1178
			return $this->getField('TimeFormat');
1179
		} else {
1180
			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...
1181
		}
1182
	}
1183
1184
	//---------------------------------------------------------------------//
1185
1186
1187
	/**
1188
	 * Get a "many-to-many" map that holds for all members their group memberships,
1189
	 * including any parent groups where membership is implied.
1190
	 * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1191
	 *
1192
	 * @todo Push all this logic into Member_GroupSet's getIterator()?
1193
	 * @return Member_Groupset
1194
	 */
1195
	public function Groups() {
1196
		$groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID');
1197
		$groups = $groups->forForeignID($this->ID);
1198
1199
		$this->extend('updateGroups', $groups);
1200
1201
		return $groups;
1202
	}
1203
1204
	/**
1205
	 * @return ManyManyList
1206
	 */
1207
	public function DirectGroups() {
1208
		return $this->getManyManyComponents('Groups');
1209
	}
1210
1211
	/**
1212
	 * Get a member SQLMap of members in specific groups
1213
	 *
1214
	 * If no $groups is passed, all members will be returned
1215
	 *
1216
	 * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1217
	 * @return SS_Map Returns an SS_Map that returns all Member data.
1218
	 */
1219
	public static function map_in_groups($groups = null) {
1220
		$groupIDList = array();
1221
1222
		if($groups instanceof SS_List) {
1223
			foreach( $groups as $group ) {
1224
				$groupIDList[] = $group->ID;
1225
			}
1226
		} elseif(is_array($groups)) {
1227
			$groupIDList = $groups;
1228
		} elseif($groups) {
1229
			$groupIDList[] = $groups;
1230
		}
1231
1232
		// No groups, return all Members
1233
		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...
1234
			return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
1235
		}
1236
1237
		$membersList = new ArrayList();
1238
		// This is a bit ineffective, but follow the ORM style
1239
		foreach(Group::get()->byIDs($groupIDList) as $group) {
1240
			$membersList->merge($group->Members());
1241
		}
1242
1243
		$membersList->removeDuplicates('ID');
1244
		return $membersList->map();
1245
	}
1246
1247
1248
	/**
1249
	 * Get a map of all members in the groups given that have CMS permissions
1250
	 *
1251
	 * If no groups are passed, all groups with CMS permissions will be used.
1252
	 *
1253
	 * @param array $groups Groups to consider or NULL to use all groups with
1254
	 *                      CMS permissions.
1255
	 * @return SS_Map Returns a map of all members in the groups given that
1256
	 *                have CMS permissions.
1257
	 */
1258
	public static function mapInCMSGroups($groups = null) {
1259
		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...
1260
			$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1261
1262
			if(class_exists('CMSMain')) {
1263
				$cmsPerms = singleton('CMSMain')->providePermissions();
1264
			} else {
1265
				$cmsPerms = singleton('LeftAndMain')->providePermissions();
1266
			}
1267
1268
			if(!empty($cmsPerms)) {
1269
				$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1270
			}
1271
1272
			$permsClause = DB::placeholders($perms);
1273
			$groups = DataObject::get('Group')
1274
				->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1275
				->where(array(
1276
					"\"Permission\".\"Code\" IN ($permsClause)" => $perms
1277
				));
1278
		}
1279
1280
		$groupIDList = array();
1281
1282
		if(is_a($groups, 'SS_List')) {
1283
			foreach($groups as $group) {
1284
				$groupIDList[] = $group->ID;
1285
			}
1286
		} elseif(is_array($groups)) {
1287
			$groupIDList = $groups;
1288
		}
1289
1290
		$members = Member::get()
1291
			->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1292
			->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1293
		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...
1294
			$groupClause = DB::placeholders($groupIDList);
1295
			$members = $members->where(array(
1296
				"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1297
			));
1298
		}
1299
1300
		return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1301
	}
1302
1303
1304
	/**
1305
	 * Get the groups in which the member is NOT in
1306
	 *
1307
	 * When passed an array of groups, and a component set of groups, this
1308
	 * function will return the array of groups the member is NOT in.
1309
	 *
1310
	 * @param array $groupList An array of group code names.
1311
	 * @param array $memberGroups A component set of groups (if set to NULL,
1312
	 *                            $this->groups() will be used)
1313
	 * @return array Groups in which the member is NOT in.
1314
	 */
1315
	public function memberNotInGroups($groupList, $memberGroups = null){
1316
		if(!$memberGroups) $memberGroups = $this->Groups();
1317
1318
		foreach($memberGroups as $group) {
1319
			if(in_array($group->Code, $groupList)) {
1320
				$index = array_search($group->Code, $groupList);
1321
				unset($groupList[$index]);
1322
			}
1323
		}
1324
1325
		return $groupList;
1326
	}
1327
1328
1329
	/**
1330
	 * Return a {@link FieldList} of fields that would appropriate for editing
1331
	 * this member.
1332
	 *
1333
	 * @return FieldList Return a FieldList of fields that would appropriate for
1334
	 *                   editing this member.
1335
	 */
1336
	public function getCMSFields() {
1337
		require_once 'Zend/Date.php';
1338
1339
		$self = $this;
1340
		$this->beforeUpdateCMSFields(function($fields) use ($self) {
1341
			$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
1342
1343
			$password = new ConfirmedPasswordField(
1344
				'Password',
1345
				null,
1346
				null,
1347
				null,
1348
				true // showOnClick
1349
			);
1350
			$password->setCanBeEmpty(true);
1351
			if( ! $self->ID) $password->showOnClick = false;
0 ignored issues
show
Documentation introduced by
The property $showOnClick is declared protected in ConfirmedPasswordField. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

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...
1352
			$mainFields->replaceField('Password', $password);
1353
1354
			$mainFields->replaceField('Locale', new DropdownField(
1355
				"Locale",
1356
				_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1357
				i18n::get_existing_translations()
1358
			));
1359
			$mainFields->removeByName($self->config()->hidden_fields);
1360
1361
			if( ! $self->config()->lock_out_after_incorrect_logins) {
1362
				$mainFields->removeByName('FailedLoginCount');
1363
			}
1364
1365
1366
			// Groups relation will get us into logical conflicts because
1367
			// Members are displayed within  group edit form in SecurityAdmin
1368
			$fields->removeByName('Groups');
1369
1370
			// Members shouldn't be able to directly view/edit logged passwords
1371
			$fields->removeByName('LoggedPasswords');
1372
1373
			$fields->removeByName('RememberLoginHashes');
1374
1375
			if(Permission::check('EDIT_PERMISSIONS')) {
1376
				$groupsMap = array();
1377
				foreach(Group::get() as $group) {
1378
					// Listboxfield values are escaped, use ASCII char instead of &raquo;
1379
					$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1380
				}
1381
				asort($groupsMap);
1382
				$fields->addFieldToTab('Root.Main',
1383
					ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name())
1384
						->setSource($groupsMap)
1385
						->setAttribute(
1386
							'data-placeholder',
1387
							_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1388
						)
1389
				);
1390
1391
1392
				// Add permission field (readonly to avoid complicated group assignment logic).
1393
				// This should only be available for existing records, as new records start
1394
				// with no permissions until they have a group assignment anyway.
1395
				if($self->ID) {
1396
					$permissionsField = new PermissionCheckboxSetField_Readonly(
1397
						'Permissions',
1398
						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...
1399
						'Permission',
1400
						'GroupID',
1401
						// we don't want parent relationships, they're automatically resolved in the field
1402
						$self->getManyManyComponents('Groups')
1403
					);
1404
					$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1405
					$fields->addFieldToTab('Root.Permissions', $permissionsField);
1406
				}
1407
			}
1408
1409
			$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1410
			if($permissionsTab) $permissionsTab->addExtraClass('readonly');
1411
1412
			$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
1413
			$dateFormatMap = array(
1414
				'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
1415
				'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
1416
				'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
1417
				'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
1418
			);
1419
			$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
1420
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1421
			$mainFields->push(
1422
				$dateFormatField = new MemberDatetimeOptionsetField(
1423
					'DateFormat',
1424
					$self->fieldLabel('DateFormat'),
1425
					$dateFormatMap
1426
				)
1427
			);
1428
			$dateFormatField->setValue($self->DateFormat);
1429
1430
			$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
1431
			$timeFormatMap = array(
1432
				'h:mm a' => Zend_Date::now()->toString('h:mm a'),
1433
				'H:mm' => Zend_Date::now()->toString('H:mm'),
1434
			);
1435
			$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
1436
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1437
			$mainFields->push(
1438
				$timeFormatField = new MemberDatetimeOptionsetField(
1439
					'TimeFormat',
1440
					$self->fieldLabel('TimeFormat'),
1441
					$timeFormatMap
1442
				)
1443
			);
1444
			$timeFormatField->setValue($self->TimeFormat);
1445
		});
1446
1447
		return parent::getCMSFields();
1448
	}
1449
1450
	/**
1451
	 *
1452
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1453
	 *
1454
	 */
1455
	public function fieldLabels($includerelations = true) {
1456
		$labels = parent::fieldLabels($includerelations);
1457
1458
		$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1459
		$labels['Surname'] = _t('Member.SURNAME', 'Surname');
1460
		$labels['Email'] = _t('Member.EMAIL', 'Email');
1461
		$labels['Password'] = _t('Member.db_Password', 'Password');
1462
		$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1463
		$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1464
		$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1465
		$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
1466
		$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
1467
		if($includerelations){
1468
			$labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups',
1469
				'Security Groups this member belongs to');
1470
		}
1471
		return $labels;
1472
	}
1473
1474
    /**
1475
     * Users can view their own record.
1476
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1477
     * This is likely to be customized for social sites etc. with a looser permission model.
1478
     */
1479
    public function canView($member = null) {
1480
        //get member
1481
        if(!($member instanceof Member)) {
1482
            $member = Member::currentUser();
1483
        }
1484
        //check for extensions, we do this first as they can overrule everything
1485
        $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...
1486
        if($extended !== null) {
1487
            return $extended;
1488
        }
1489
1490
        //need to be logged in and/or most checks below rely on $member being a Member
1491
        if(!$member) {
1492
            return false;
1493
        }
1494
        // members can usually view their own record
1495
        if($this->ID == $member->ID) {
1496
            return true;
1497
        }
1498
        //standard check
1499
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1500
    }
1501
    /**
1502
     * Users can edit their own record.
1503
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1504
     */
1505 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...
1506
        //get member
1507
        if(!($member instanceof Member)) {
1508
            $member = Member::currentUser();
1509
        }
1510
        //check for extensions, we do this first as they can overrule everything
1511
        $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...
1512
        if($extended !== null) {
1513
            return $extended;
1514
        }
1515
1516
        //need to be logged in and/or most checks below rely on $member being a Member
1517
        if(!$member) {
1518
            return false;
1519
        }
1520
1521
        // HACK: we should not allow for an non-Admin to edit an Admin
1522
        if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
1523
            return false;
1524
        }
1525
        // members can usually edit their own record
1526
        if($this->ID == $member->ID) {
1527
            return true;
1528
        }
1529
        //standard check
1530
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1531
    }
1532
    /**
1533
     * Users can edit their own record.
1534
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1535
     */
1536 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...
1537
        if(!($member instanceof Member)) {
1538
            $member = Member::currentUser();
1539
        }
1540
        //check for extensions, we do this first as they can overrule everything
1541
        $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...
1542
        if($extended !== null) {
1543
            return $extended;
1544
        }
1545
1546
        //need to be logged in and/or most checks below rely on $member being a Member
1547
        if(!$member) {
1548
            return false;
1549
        }
1550
        // Members are not allowed to remove themselves,
1551
        // since it would create inconsistencies in the admin UIs.
1552
        if($this->ID && $member->ID == $this->ID) {
1553
            return false;
1554
        }
1555
1556
        // HACK: if you want to delete a member, you have to be a member yourself.
1557
        // this is a hack because what this should do is to stop a user
1558
        // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
1559
        if(Permission::checkMember($this, 'ADMIN')) {
1560
            if( ! Permission::checkMember($member, 'ADMIN')) {
1561
                return false;
1562
            }
1563
        }
1564
        //standard check
1565
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1566
    }
1567
1568
	/**
1569
	 * Validate this member object.
1570
	 */
1571
	public function validate() {
1572
		$valid = parent::validate();
1573
1574 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...
1575
			if($this->Password && self::$password_validator) {
1576
				$valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1577
			}
1578
		}
1579
1580 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...
1581
			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...
1582
				$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...
1583
			}
1584
		}
1585
1586
		return $valid;
1587
	}
1588
1589
	/**
1590
	 * Change password. This will cause rehashing according to
1591
	 * the `PasswordEncryption` property.
1592
	 *
1593
	 * @param String $password Cleartext password
1594
	 */
1595
	public function changePassword($password) {
1596
		$this->Password = $password;
1597
		$valid = $this->validate();
1598
1599
		if($valid->valid()) {
1600
			$this->AutoLoginHash = null;
1601
			$this->write();
1602
		}
1603
1604
		return $valid;
1605
	}
1606
1607
	/**
1608
	 * Tell this member that someone made a failed attempt at logging in as them.
1609
	 * This can be used to lock the user out temporarily if too many failed attempts are made.
1610
	 */
1611
	public function registerFailedLogin() {
1612
		if(self::config()->lock_out_after_incorrect_logins) {
1613
			// Keep a tally of the number of failed log-ins so that we can lock people out
1614
			$this->FailedLoginCount = $this->FailedLoginCount + 1;
1615
1616
			if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1617
				$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...
1618
				$this->LockedOutUntil = date('Y-m-d H:i:s', time() + $lockoutMins*60);
1619
				$this->FailedLoginCount = 0;
1620
			}
1621
		}
1622
		$this->extend('registerFailedLogin');
1623
		$this->write();
1624
	}
1625
1626
	/**
1627
	 * Tell this member that a successful login has been made
1628
	 */
1629
	public function registerSuccessfulLogin() {
1630
		if(self::config()->lock_out_after_incorrect_logins) {
1631
			// Forgive all past login failures
1632
			$this->FailedLoginCount = 0;
1633
			$this->write();
1634
		}
1635
	}
1636
	/**
1637
	 * Get the HtmlEditorConfig for this user to be used in the CMS.
1638
	 * This is set by the group. If multiple configurations are set,
1639
	 * the one with the highest priority wins.
1640
	 *
1641
	 * @return string
1642
	 */
1643
	public function getHtmlEditorConfigForCMS() {
1644
		$currentName = '';
1645
		$currentPriority = 0;
1646
1647
		foreach($this->Groups() as $group) {
1648
			$configName = $group->HtmlEditorConfig;
1649
			if($configName) {
1650
				$config = HtmlEditorConfig::get($group->HtmlEditorConfig);
1651
				if($config && $config->getOption('priority') > $currentPriority) {
1652
					$currentName = $configName;
1653
					$currentPriority = $config->getOption('priority');
1654
				}
1655
			}
1656
		}
1657
1658
		// If can't find a suitable editor, just default to cms
1659
		return $currentName ? $currentName : 'cms';
1660
	}
1661
1662
	public static function get_template_global_variables() {
1663
		return array(
1664
			'CurrentMember' => 'currentUser',
1665
			'currentUser',
1666
		);
1667
	}
1668
}
1669
1670
/**
1671
 * Represents a set of Groups attached to a member.
1672
 * Handles the hierarchy logic.
1673
 * @package framework
1674
 * @subpackage security
1675
 */
1676
class Member_GroupSet extends ManyManyList {
1677
1678
	protected function linkJoinTable() {
1679
		// Do not join the table directly
1680
		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...
1681
			user_error('Member_GroupSet does not support many_many_extraFields', E_USER_ERROR);
1682
		}
1683
	}
1684
1685
	/**
1686
	 * Link this group set to a specific member.
1687
	 *
1688
	 * Recursively selects all groups applied to this member, as well as any
1689
	 * parent groups of any applied groups
1690
	 *
1691
	 * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
1692
	 * ids as per getForeignID
1693
	 * @return array Condition In array(SQL => parameters format)
1694
	 */
1695
	public function foreignIDFilter($id = null) {
1696
		if ($id === null) $id = $this->getForeignID();
1697
1698
		// Find directly applied groups
1699
		$manyManyFilter = parent::foreignIDFilter($id);
1700
		$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 1699 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...
1701
		$groupIDs = $query->execute()->column();
1702
1703
		// Get all ancestors, iteratively merging these into the master set
1704
		$allGroupIDs = array();
1705
		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...
1706
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1707
			$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
1708
			$groupIDs = array_filter($groupIDs);
1709
		}
1710
1711
		// Add a filter to this DataList
1712
		if(!empty($allGroupIDs)) {
1713
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1714
			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...
1715
		} else {
1716
			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...
1717
		}
1718
	}
1719
1720
	public function foreignIDWriteFilter($id = null) {
1721
		// Use the ManyManyList::foreignIDFilter rather than the one
1722
		// in this class, otherwise we end up selecting all inherited groups
1723
		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...
1724
	}
1725
1726
	public function add($item, $extraFields = null) {
1727
		// Get Group.ID
1728
		$itemID = null;
1729
		if(is_numeric($item)) {
1730
			$itemID = $item;
1731
		} else if($item instanceof Group) {
1732
			$itemID = $item->ID;
1733
		}
1734
1735
		// Check if this group is allowed to be added
1736
		if($this->canAddGroups(array($itemID))) {
1737
			parent::add($item, $extraFields);
1738
		}
1739
	}
1740
1741
	/**
1742
	 * Determine if the following groups IDs can be added
1743
	 *
1744
	 * @param array $itemIDs
1745
	 * @return boolean
1746
	 */
1747
	protected function canAddGroups($itemIDs) {
1748
		if(empty($itemIDs)) {
1749
			return true;
1750
		}
1751
		$member = $this->getMember();
1752
		return empty($member) || $member->onChangeGroups($itemIDs);
1753
	}
1754
1755
	/**
1756
	 * Get foreign member record for this relation
1757
	 *
1758
	 * @return Member
1759
	 */
1760
	protected function getMember() {
1761
		$id = $this->getForeignID();
1762
		if($id) {
1763
			return DataObject::get_by_id('Member', $id);
1764
		}
1765
	}
1766
}
1767
1768
/**
1769
 * Member Validator
1770
 *
1771
 * Custom validation for the Member object can be achieved either through an
1772
 * {@link DataExtension} on the Member_Validator object or, by specifying a subclass of
1773
 * {@link Member_Validator} through the {@link Injector} API.
1774
 * The Validator can also be modified by adding an Extension to Member and implement the
1775
 * <code>updateValidator</code> hook.
1776
 * {@see Member::getValidator()}
1777
 *
1778
 * Additional required fields can also be set via config API, eg.
1779
 * <code>
1780
 * Member_Validator:
1781
 *   customRequired:
1782
 *     - Surname
1783
 * </code>
1784
 *
1785
 * @package framework
1786
 * @subpackage security
1787
 */
1788
class Member_Validator extends RequiredFields
1789
{
1790
	/**
1791
	 * Fields that are required by this validator
1792
	 * @config
1793
	 * @var array
1794
	 */
1795
	protected $customRequired = array(
1796
		'FirstName',
1797
		'Email'
1798
	);
1799
1800
	/**
1801
	 * Determine what member this validator is meant for
1802
	 * @var Member
1803
	 */
1804
	protected $forMember = null;
1805
1806
	/**
1807
	 * Constructor
1808
	 */
1809
	public function __construct() {
1810
		$required = func_get_args();
1811
1812 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...
1813
			$required = $required[0];
1814
		}
1815
1816
		$required = array_merge($required, $this->customRequired);
1817
1818
		// check for config API values and merge them in
1819
		$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...
1820
		if(is_array($config)){
1821
			$required = array_merge($required, $config);
1822
		}
1823
1824
		parent::__construct(array_unique($required));
1825
	}
1826
1827
	/**
1828
	 * Get the member this validator applies to.
1829
	 * @return Member
1830
	 */
1831
	public function getForMember()
1832
	{
1833
		return $this->forMember;
1834
	}
1835
1836
	/**
1837
	 * Set the Member this validator applies to.
1838
	 * @param Member $value
1839
	 * @return $this
1840
	 */
1841
	public function setForMember(Member $value)
1842
	{
1843
		$this->forMember = $value;
1844
		return $this;
1845
	}
1846
1847
	/**
1848
	 * Check if the submitted member data is valid (server-side)
1849
	 *
1850
	 * Check if a member with that email doesn't already exist, or if it does
1851
	 * that it is this member.
1852
	 *
1853
	 * @param array $data Submitted data
1854
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
1855
	 *              FALSE.
1856
	 */
1857
	public function php($data)
1858
	{
1859
		$valid = parent::php($data);
1860
1861
		$identifierField = (string)Member::config()->unique_identifier_field;
1862
1863
		// Only validate identifier field if it's actually set. This could be the case if
1864
		// somebody removes `Email` from the list of required fields.
1865
		if(isset($data[$identifierField])){
1866
			$id = isset($data['ID']) ? (int)$data['ID'] : 0;
1867
			if(!$id && ($ctrl = $this->form->getController())){
1868
				// get the record when within GridField (Member editing page in CMS)
1869
				if($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()){
1870
					$id = $record->ID;
1871
				}
1872
			}
1873
1874
			// If there's no ID passed via controller or form-data, use the assigned member (if available)
1875
			if(!$id && ($member = $this->getForMember())){
1876
				$id = $member->exists() ? $member->ID : 0;
1877
			}
1878
1879
			// set the found ID to the data array, so that extensions can also use it
1880
			$data['ID'] = $id;
1881
1882
			$members = Member::get()->filter($identifierField, $data[$identifierField]);
1883
			if($id) {
1884
				$members = $members->exclude('ID', $id);
1885
			}
1886
1887
			if($members->count() > 0) {
1888
				$this->validationError(
1889
					$identifierField,
1890
					_t(
1891
						'Member.VALIDATIONMEMBEREXISTS',
1892
						'A member already exists with the same {identifier}',
1893
						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...
1894
					),
1895
					'required'
1896
				);
1897
				$valid = false;
1898
			}
1899
		}
1900
1901
1902
		// Execute the validators on the extensions
1903
		$results = $this->extend('updatePHP', $data, $this->form);
1904
		$results[] = $valid;
1905
		return min($results);
1906
	}
1907
}
1908