Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

Member::onBeforeWrite()   C

Complexity

Conditions 17
Paths 100

Size

Total Lines 80
Code Lines 44

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 80
rs 5.033
cc 17
eloc 44
nc 100
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

Loading history...
474
475
		if($remember) {
476
			// Store the hash and give the client the cookie with the token.
477
			$generator = new RandomGenerator();
478
			$token = $generator->randomToken('sha1');
479
			$hash = $this->encryptWithUserSettings($token);
480
			$this->RememberLoginToken = $hash;
481
			Cookie::set('alc_enc', $this->ID . ':' . $token, 90, 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...
482
		} else {
483
			$this->RememberLoginToken = null;
484
			Cookie::force_expiry('alc_enc');
485
		}
486
487
		// Clear the incorrect log-in count
488
		$this->registerSuccessfulLogin();
489
490
		// Don't set column if its not built yet (the login might be precursor to a /dev/build...)
491
		if(array_key_exists('LockedOutUntil', DB::field_list('Member'))) {
492
			$this->LockedOutUntil = null;
493
		}
494
495
		$this->regenerateTempID();
496
497
		$this->write();
498
499
		// Audit logging hook
500
		$this->extend('memberLoggedIn');
501
	}
502
503
	/**
504
	 * @deprecated 4.0
505
	 */
506
	public function addVisit() {
507
		if($this->config()->log_num_visits) {
508
			Deprecation::notice(
509
				'4.0',
510
				'Member::$NumVisit is deprecated. From 4.0 onwards you should implement this as a custom extension'
511
			);
512
			$this->NumVisit++;
513
		}
514
	}
515
516
	/**
517
	 * Trigger regeneration of TempID.
518
	 *
519
	 * This should be performed any time the user presents their normal identification (normally Email)
520
	 * and is successfully authenticated.
521
	 */
522
	public function regenerateTempID() {
523
		$generator = new RandomGenerator();
524
		$this->TempIDHash = $generator->randomToken('sha1');
525
		$this->TempIDExpired = self::config()->temp_id_lifetime
526
			? date('Y-m-d H:i:s', strtotime(SS_Datetime::now()->getValue()) + self::config()->temp_id_lifetime)
527
			: null;
528
		$this->write();
529
	}
530
531
	/**
532
	 * Check if the member ID logged in session actually
533
	 * has a database record of the same ID. If there is
534
	 * no logged in user, FALSE is returned anyway.
535
	 *
536
	 * @return boolean TRUE record found FALSE no record found
537
	 */
538
	public static function logged_in_session_exists() {
539
		if($id = Member::currentUserID()) {
540
			if($member = DataObject::get_by_id('Member', $id)) {
541
				if($member->exists()) return true;
542
			}
543
		}
544
545
		return false;
546
	}
547
548
	/**
549
	 * Log the user in if the "remember login" cookie is set
550
	 *
551
	 * The <i>remember login token</i> will be changed on every successful
552
	 * auto-login.
553
	 */
554
	public static function autoLogin() {
555
		// Don't bother trying this multiple times
556
		self::$_already_tried_to_auto_log_in = true;
557
558
		if(strpos(Cookie::get('alc_enc'), ':') === false
559
			|| Session::get("loggedInAs")
560
			|| !Security::database_is_ready()
561
		) {
562
			return;
563
		}
564
565
		list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
566
567
		$member = DataObject::get_by_id("Member", $uid);
568
569
		// check if autologin token matches
570
		if($member) {
571
			$hash = $member->encryptWithUserSettings($token);
572
			if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
573
				$member = null;
574
			}
575
		}
576
577
		if($member) {
578
			self::session_regenerate_id();
579
			Session::set("loggedInAs", $member->ID);
580
			// This lets apache rules detect whether the user has logged in
581
			if(Member::config()->login_marker_cookie) {
582
				Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
583
			}
584
585
			$generator = new RandomGenerator();
586
			$token = $generator->randomToken('sha1');
587
			$hash = $member->encryptWithUserSettings($token);
588
			$member->RememberLoginToken = $hash;
0 ignored issues
show
Documentation introduced by
The property RememberLoginToken does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

Loading history...
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...
1492
			if(!min($results)) return false;
1493
			else return true;
1494
		}
1495
1496
		// members can usually edit their own record
1497
		if($member && $this->ID == $member->ID) return true;
1498
1499
		if(
1500
			Permission::checkMember($member, 'ADMIN')
1501
			|| Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin')
1502
		) {
1503
			return true;
1504
		}
1505
1506
		return false;
1507
	}
1508
1509
	/**
1510
	 * Users can edit their own record.
1511
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1512
	 */
1513
	public function canEdit($member = null) {
1514
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1515
1516
		// extended access checks
1517
		$results = $this->extend('canEdit', $member);
1518 View Code Duplication
		if($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
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...
1519
			if(!min($results)) return false;
1520
			else return true;
1521
		}
1522
1523
		// No member found
1524
		if(!($member && $member->exists())) return false;
1525
1526
		// If the requesting member is not an admin, but has access to manage members,
1527
		// they still can't edit other members with ADMIN permission.
1528
		// This is a bit weak, strictly speaking they shouldn't be allowed to
1529
		// perform any action that could change the password on a member
1530
		// with "higher" permissions than himself, but thats hard to determine.
1531
		if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) return false;
1532
1533
		return $this->canView($member);
1534
	}
1535
1536
	/**
1537
	 * Users can edit their own record.
1538
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1539
	 */
1540
	public function canDelete($member = null) {
1541
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
1542
1543
		// extended access checks
1544
		$results = $this->extend('canDelete', $member);
1545 View Code Duplication
		if($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
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...
1546
			if(!min($results)) return false;
1547
			else return true;
1548
		}
1549
1550
		// No member found
1551
		if(!($member && $member->exists())) return false;
1552
1553
		// Members are not allowed to remove themselves,
1554
		// since it would create inconsistencies in the admin UIs.
1555
		if($this->ID && $member->ID == $this->ID) return false;
1556
1557
		return $this->canEdit($member);
1558
	}
1559
1560
1561
	/**
1562
	 * Validate this member object.
1563
	 */
1564
	public function validate() {
1565
		$valid = parent::validate();
1566
1567 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...
1568
			if($this->Password && self::$password_validator) {
1569
				$valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1570
			}
1571
		}
1572
1573 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...
1574
			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...
1575
				$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...
1576
			}
1577
		}
1578
1579
		return $valid;
1580
	}
1581
1582
	/**
1583
	 * Change password. This will cause rehashing according to
1584
	 * the `PasswordEncryption` property.
1585
	 *
1586
	 * @param String $password Cleartext password
1587
	 */
1588
	public function changePassword($password) {
1589
		$this->Password = $password;
1590
		$valid = $this->validate();
1591
1592
		if($valid->valid()) {
1593
			$this->AutoLoginHash = null;
1594
			$this->write();
1595
		}
1596
1597
		return $valid;
1598
	}
1599
1600
	/**
1601
	 * Tell this member that someone made a failed attempt at logging in as them.
1602
	 * This can be used to lock the user out temporarily if too many failed attempts are made.
1603
	 */
1604
	public function registerFailedLogin() {
1605
		if(self::config()->lock_out_after_incorrect_logins) {
1606
			// Keep a tally of the number of failed log-ins so that we can lock people out
1607
			$this->FailedLoginCount = $this->FailedLoginCount + 1;
1608
1609
			if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1610
				$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...
1611
				$this->LockedOutUntil = date('Y-m-d H:i:s', time() + $lockoutMins*60);
1612
				$this->FailedLoginCount = 0;
1613
			}
1614
		}
1615
		$this->extend('registerFailedLogin');
1616
		$this->write();
1617
	}
1618
1619
	/**
1620
	 * Tell this member that a successful login has been made
1621
	 */
1622
	public function registerSuccessfulLogin() {
1623
		if(self::config()->lock_out_after_incorrect_logins) {
1624
			// Forgive all past login failures
1625
			$this->FailedLoginCount = 0;
1626
			$this->write();
1627
		}
1628
	}
1629
	/**
1630
	 * Get the HtmlEditorConfig for this user to be used in the CMS.
1631
	 * This is set by the group. If multiple configurations are set,
1632
	 * the one with the highest priority wins.
1633
	 *
1634
	 * @return string
1635
	 */
1636
	public function getHtmlEditorConfigForCMS() {
1637
		$currentName = '';
1638
		$currentPriority = 0;
1639
1640
		foreach($this->Groups() as $group) {
1641
			$configName = $group->HtmlEditorConfig;
1642
			if($configName) {
1643
				$config = HtmlEditorConfig::get($group->HtmlEditorConfig);
1644
				if($config && $config->getOption('priority') > $currentPriority) {
1645
					$currentName = $configName;
1646
					$currentPriority = $config->getOption('priority');
1647
				}
1648
			}
1649
		}
1650
1651
		// If can't find a suitable editor, just default to cms
1652
		return $currentName ? $currentName : 'cms';
1653
	}
1654
1655
	public static function get_template_global_variables() {
1656
		return array(
1657
			'CurrentMember' => 'currentUser',
1658
			'currentUser',
1659
		);
1660
	}
1661
}
1662
1663
/**
1664
 * Represents a set of Groups attached to a member.
1665
 * Handles the hierarchy logic.
1666
 * @package framework
1667
 * @subpackage security
1668
 */
1669
class Member_GroupSet extends ManyManyList {
1670
1671
	protected function linkJoinTable() {
1672
		// Do not join the table directly
1673
		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...
1674
			user_error('Member_GroupSet does not support many_many_extraFields', E_USER_ERROR);
1675
		}
1676
	}
1677
1678
	/**
1679
	 * Link this group set to a specific member.
1680
	 *
1681
	 * Recursively selects all groups applied to this member, as well as any
1682
	 * parent groups of any applied groups
1683
	 *
1684
	 * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
1685
	 * ids as per getForeignID
1686
	 * @return array Condition In array(SQL => parameters format)
1687
	 */
1688
	public function foreignIDFilter($id = null) {
1689
		if ($id === null) $id = $this->getForeignID();
1690
1691
		// Find directly applied groups
1692
		$manyManyFilter = parent::foreignIDFilter($id);
1693
		$query = new SQLQuery('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter);
0 ignored issues
show
Documentation introduced by
'"Group_Members"' is of type string, but the function expects a array.

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...
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

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

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

Loading history...
Bug introduced by
It seems like $manyManyFilter defined by parent::foreignIDFilter($id) on line 1692 can also be of type null; however, SQLQuery::__construct() does only seem to accept array, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1694
		$groupIDs = $query->execute()->column();
1695
1696
		// Get all ancestors, iteratively merging these into the master set
1697
		$allGroupIDs = array();
1698
		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...
1699
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1700
			$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
1701
			$groupIDs = array_filter($groupIDs);
1702
		}
1703
1704
		// Add a filter to this DataList
1705
		if(!empty($allGroupIDs)) {
1706
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1707
			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...
1708
		} else {
1709
			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...
1710
		}
1711
	}
1712
1713
	public function foreignIDWriteFilter($id = null) {
1714
		// Use the ManyManyList::foreignIDFilter rather than the one
1715
		// in this class, otherwise we end up selecting all inherited groups
1716
		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...
1717
	}
1718
1719
	public function add($item, $extraFields = null) {
1720
		// Get Group.ID
1721
		$itemID = null;
1722
		if(is_numeric($item)) {
1723
			$itemID = $item;
1724
		} else if($item instanceof Group) {
1725
			$itemID = $item->ID;
1726
		}
1727
1728
		// Check if this group is allowed to be added
1729
		if($this->canAddGroups(array($itemID))) {
1730
			parent::add($item, $extraFields);
1731
		}
1732
	}
1733
1734
	/**
1735
	 * Determine if the following groups IDs can be added
1736
	 *
1737
	 * @param array $itemIDs
1738
	 * @return boolean
1739
	 */
1740
	protected function canAddGroups($itemIDs) {
1741
		if(empty($itemIDs)) {
1742
			return true;
1743
		}
1744
		$member = $this->getMember();
1745
		return empty($member) || $member->onChangeGroups($itemIDs);
1746
	}
1747
1748
	/**
1749
	 * Get foreign member record for this relation
1750
	 *
1751
	 * @return Member
1752
	 */
1753
	protected function getMember() {
1754
		$id = $this->getForeignID();
1755
		if($id) {
1756
			return DataObject::get_by_id('Member', $id);
1757
		}
1758
	}
1759
}
1760
1761
/**
1762
 * Class used as template to send an email saying that the password has been
1763
 * changed.
1764
 *
1765
 * @package framework
1766
 * @subpackage security
1767
 */
1768 View Code Duplication
class Member_ChangePasswordEmail extends Email {
0 ignored issues
show
Duplication introduced by
This class 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...
1769
1770
	protected $from = '';   // setting a blank from address uses the site's default administrator email
1771
	protected $subject = '';
1772
	protected $ss_template = 'ChangePasswordEmail';
1773
1774
	public function __construct() {
1775
		parent::__construct();
1776
1777
		$this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject');
1778
	}
1779
}
1780
1781
1782
1783
/**
1784
 * Class used as template to send the forgot password email
1785
 *
1786
 * @package framework
1787
 * @subpackage security
1788
 */
1789 View Code Duplication
class Member_ForgotPasswordEmail extends Email {
0 ignored issues
show
Duplication introduced by
This class 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...
1790
	protected $from = '';  // setting a blank from address uses the site's default administrator email
1791
	protected $subject = '';
1792
	protected $ss_template = 'ForgotPasswordEmail';
1793
1794
	public function __construct() {
1795
		parent::__construct();
1796
1797
		$this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject');
1798
	}
1799
}
1800
1801
/**
1802
 * Member Validator
1803
 *
1804
 * Custom validation for the Member object can be achieved either through an
1805
 * {@link DataExtension} on the Member_Validator object or, by specifying a subclass of
1806
 * {@link Member_Validator} through the {@link Injector} API.
1807
 * The Validator can also be modified by adding an Extension to Member and implement the
1808
 * <code>updateValidator</code> hook.
1809
 * {@see Member::getValidator()}
1810
 *
1811
 * Additional required fields can also be set via config API, eg.
1812
 * <code>
1813
 * Member_Validator:
1814
 *   customRequired:
1815
 *     - Surname
1816
 * </code>
1817
 *
1818
 * @package framework
1819
 * @subpackage security
1820
 */
1821
class Member_Validator extends RequiredFields
1822
{
1823
	/**
1824
	 * Fields that are required by this validator
1825
	 * @config
1826
	 * @var array
1827
	 */
1828
	protected $customRequired = array(
1829
		'FirstName',
1830
		'Email'
1831
	);
1832
1833
	/**
1834
	 * Determine what member this validator is meant for
1835
	 * @var Member
1836
	 */
1837
	protected $forMember = null;
1838
1839
	/**
1840
	 * Constructor
1841
	 */
1842
	public function __construct() {
1843
		$required = func_get_args();
1844
1845 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...
1846
			$required = $required[0];
1847
		}
1848
1849
		$required = array_merge($required, $this->customRequired);
1850
1851
		// check for config API values and merge them in
1852
		$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...
1853
		if(is_array($config)){
1854
			$required = array_merge($required, $config);
1855
		}
1856
1857
		parent::__construct(array_unique($required));
1858
	}
1859
1860
	/**
1861
	 * Get the member this validator applies to.
1862
	 * @return Member
1863
	 */
1864
	public function getForMember()
1865
	{
1866
		return $this->forMember;
1867
	}
1868
1869
	/**
1870
	 * Set the Member this validator applies to.
1871
	 * @param Member $value
1872
	 * @return $this
1873
	 */
1874
	public function setForMember(Member $value)
1875
	{
1876
		$this->forMember = $value;
1877
		return $this;
1878
	}
1879
1880
	/**
1881
	 * Check if the submitted member data is valid (server-side)
1882
	 *
1883
	 * Check if a member with that email doesn't already exist, or if it does
1884
	 * that it is this member.
1885
	 *
1886
	 * @param array $data Submitted data
1887
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
1888
	 *              FALSE.
1889
	 */
1890
	public function php($data)
1891
	{
1892
		$valid = parent::php($data);
1893
1894
		$identifierField = (string)Member::config()->unique_identifier_field;
1895
1896
		// Only validate identifier field if it's actually set. This could be the case if
1897
		// somebody removes `Email` from the list of required fields.
1898
		if(isset($data[$identifierField])){
1899
			$id = isset($data['ID']) ? (int)$data['ID'] : 0;
1900
			if(!$id && ($ctrl = $this->form->getController())){
1901
				// get the record when within GridField (Member editing page in CMS)
1902
				if($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()){
1903
					$id = $record->ID;
1904
				}
1905
			}
1906
1907
			// If there's no ID passed via controller or form-data, use the assigned member (if available)
1908
			if(!$id && ($member = $this->getForMember())){
1909
				$id = $member->exists() ? $member->ID : 0;
1910
			}
1911
1912
			// set the found ID to the data array, so that extensions can also use it
1913
			$data['ID'] = $id;
1914
1915
			$members = Member::get()->filter($identifierField, $data[$identifierField]);
1916
			if($id) {
1917
				$members = $members->exclude('ID', $id);
1918
			}
1919
1920
			if($members->count() > 0) {
1921
				$this->validationError(
1922
					$identifierField,
1923
					_t(
1924
						'Member.VALIDATIONMEMBEREXISTS',
1925
						'A member already exists with the same {identifier}',
1926
						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...
1927
					),
1928
					'required'
1929
				);
1930
				$valid = false;
1931
			}
1932
		}
1933
1934
1935
		// Execute the validators on the extensions
1936
		$results = $this->extend('updatePHP', $data, $this->form);
1937
		$results[] = $valid;
1938
		return min($results);
1939
	}
1940
}
1941