Completed
Push — 3.5.8 ( 912dc6 )
by Robbie
10:34
created

Member::disallowedGroups()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * The member class which represents the users of the system
4
 *
5
 * @package framework
6
 * @subpackage security
7
 *
8
 * @property string $FirstName
9
 * @property string $Surname
10
 * @property string $Email
11
 * @property string $Password
12
 * @property string $RememberLoginToken
13
 * @property string $TempIDHash
14
 * @property string $TempIDExpired
15
 * @property int $NumVisit @deprecated 4.0
16
 * @property string $LastVisited @deprecated 4.0
17
 * @property string $AutoLoginHash
18
 * @property string $AutoLoginExpired
19
 * @property string $PasswordEncryption
20
 * @property string $Salt
21
 * @property string $PasswordExpiry
22
 * @property string $LockedOutUntil
23
 * @property string $Locale
24
 * @property int $FailedLoginCount
25
 * @property string $DateFormat
26
 * @property string $TimeFormat
27
 */
28
class Member extends DataObject implements TemplateGlobalProvider {
29
30
	private static $db = array(
31
		'FirstName' => 'Varchar',
32
		'Surname' => 'Varchar',
33
		'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
34
		'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
35
		'TempIDExpired' => 'SS_Datetime', // Expiry of temp login
36
		'Password' => 'Varchar(160)',
37
		'RememberLoginToken' => 'Varchar(160)', // Note: this currently holds a hash, not a token.
38
		'NumVisit' => 'Int', // @deprecated 4.0
39
		'LastVisited' => 'SS_Datetime', // @deprecated 4.0
40
		'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
41
		'AutoLoginExpired' => 'SS_Datetime',
42
		// This is an arbitrary code pointing to a PasswordEncryptor instance,
43
		// not an actual encryption algorithm.
44
		// Warning: Never change this field after its the first password hashing without
45
		// providing a new cleartext password as well.
46
		'PasswordEncryption' => "Varchar(50)",
47
		'Salt' => 'Varchar(50)',
48
		'PasswordExpiry' => 'Date',
49
		'LockedOutUntil' => 'SS_Datetime',
50
		'Locale' => 'Varchar(6)',
51
		// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
52
		'FailedLoginCount' => 'Int',
53
		// In ISO format
54
		'DateFormat' => 'Varchar(30)',
55
		'TimeFormat' => 'Varchar(30)',
56
	);
57
58
	private static $belongs_many_many = array(
59
		'Groups' => 'Group',
60
	);
61
62
	private static $has_one = array();
63
64
	private static $has_many = array(
65
		'LoggedPasswords' => 'MemberPassword',
66
	);
67
68
	private static $many_many = array();
69
70
	private static $many_many_extraFields = array();
71
72
	private static $default_sort = '"Surname", "FirstName"';
73
74
	private static $indexes = array(
75
		'Email' => true,
76
		//Removed due to duplicate null values causing MSSQL problems
77
		//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true)
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
78
	);
79
80
	/**
81
	 * @config
82
	 * @var boolean
83
	 */
84
	private static $notify_password_change = false;
85
86
	/**
87
	 * Flag whether or not member visits should be logged (count only)
88
	 *
89
	 * @deprecated 4.0
90
	 * @var bool
91
	 * @config
92
	 */
93
	private static $log_last_visited = true;
94
95
	/**
96
	 * Flag whether we should count number of visits
97
	 *
98
	 * @deprecated 4.0
99
	 * @var bool
100
	 * @config
101
	 */
102
	private static $log_num_visits = true;
103
104
	/**
105
	 * All searchable database columns
106
	 * in this object, currently queried
107
	 * with a "column LIKE '%keywords%'
108
	 * statement.
109
	 *
110
	 * @var array
111
	 * @todo Generic implementation of $searchable_fields on DataObject,
112
	 * with definition for different searching algorithms
113
	 * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
114
	 */
115
	private static $searchable_fields = array(
116
		'FirstName',
117
		'Surname',
118
		'Email',
119
	);
120
121
	/**
122
	 * @config
123
	 * @var array
124
	 */
125
	private static $summary_fields = array(
126
		'FirstName',
127
		'Surname',
128
		'Email',
129
	);
130
131
	/**
132
	 * @config
133
	 * @var array
134
	 */
135
	private static $casting = array(
136
		'Name' => 'Varchar',
137
	);
138
139
	/**
140
	 * Internal-use only fields
141
	 *
142
	 * @config
143
	 * @var array
144
	 */
145
	private static $hidden_fields = array(
146
		'RememberLoginToken',
147
		'AutoLoginHash',
148
		'AutoLoginExpired',
149
		'PasswordEncryption',
150
		'PasswordExpiry',
151
		'LockedOutUntil',
152
		'TempIDHash',
153
		'TempIDExpired',
154
		'Salt',
155
		'NumVisit', // @deprecated 4.0
156
	);
157
158
	/**
159
	 * @config
160
	 * @var Array See {@link set_title_columns()}
161
	 */
162
	private static $title_format = null;
163
164
	/**
165
	 * The unique field used to identify this member.
166
	 * By default, it's "Email", but another common
167
	 * field could be Username.
168
	 *
169
	 * @config
170
	 * @var string
171
	 */
172
	private static $unique_identifier_field = 'Email';
173
174
	/**
175
	 * @config
176
	 * {@link PasswordValidator} object for validating user's password
177
	 */
178
	private static $password_validator = null;
179
180
	/**
181
	 * @config
182
	 * The number of days that a password should be valid for.
183
	 * By default, this is null, which means that passwords never expire
184
	 */
185
	private static $password_expiry_days = null;
186
187
	/**
188
	 * @config
189
	 * @var Int Number of incorrect logins after which
190
	 * the user is blocked from further attempts for the timespan
191
	 * defined in {@link $lock_out_delay_mins}.
192
	 */
193
	private static $lock_out_after_incorrect_logins = 10;
194
195
	/**
196
	 * @config
197
	 * @var integer Minutes of enforced lockout after incorrect password attempts.
198
	 * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
199
	 */
200
	private static $lock_out_delay_mins = 15;
201
202
	/**
203
	 * @config
204
	 * @var String If this is set, then a session cookie with the given name will be set on log-in,
205
	 * and cleared on logout.
206
	 */
207
	private static $login_marker_cookie = null;
208
209
	/**
210
	 * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
211
	 * should be called as a security precaution.
212
	 *
213
	 * This doesn't always work, especially if you're trying to set session cookies
214
	 * across an entire site using the domain parameter to session_set_cookie_params()
215
	 *
216
	 * @config
217
	 * @var boolean
218
	 */
219
	private static $session_regenerate_id = true;
220
221
222
	/**
223
	 * Default lifetime of temporary ids.
224
	 *
225
	 * This is the period within which a user can be re-authenticated within the CMS by entering only their password
226
	 * and without losing their workspace.
227
	 *
228
	 * Any session expiration outside of this time will require them to login from the frontend using their full
229
	 * username and password.
230
	 *
231
	 * Defaults to 72 hours. Set to zero to disable expiration.
232
	 *
233
	 * @config
234
	 * @var int Lifetime in seconds
235
	 */
236
	private static $temp_id_lifetime = 259200;
237
238
	/**
239
	 * @deprecated 4.0 Use the "Member.session_regenerate_id" config setting instead
240
	 */
241
	public static function set_session_regenerate_id($bool) {
242
		Deprecation::notice('4.0', 'Use the "Member.session_regenerate_id" config setting instead');
243
		self::config()->session_regenerate_id = $bool;
0 ignored issues
show
Documentation introduced by
The property session_regenerate_id does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
244
	}
245
246
	/**
247
	 * Ensure the locale is set to something sensible by default.
248
	 */
249
	public function populateDefaults() {
250
		parent::populateDefaults();
251
		$this->Locale = i18n::get_closest_translation(i18n::get_locale());
252
	}
253
254
	public function requireDefaultRecords() {
255
		parent::requireDefaultRecords();
256
		// Default groups should've been built by Group->requireDefaultRecords() already
257
		static::default_admin();
258
	}
259
260
	/**
261
	 * Get the default admin record if it exists, or creates it otherwise if enabled
262
	 *
263
	 * @return Member
264
	 */
265
	public static function default_admin() {
266
		// Check if set
267
		if(!Security::has_default_admin()) return null;
268
269
		// Find or create ADMIN group
270
		singleton('Group')->requireDefaultRecords();
271
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
272
273
		// Find member
274
		$admin = Member::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
275
			->filter('Email', Security::default_admin_username())
276
			->first();
277
		if(!$admin) {
278
			// 'Password' is not set to avoid creating
279
			// persistent logins in the database. See Security::setDefaultAdmin().
280
			// Set 'Email' to identify this as the default admin
281
			$admin = Member::create();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
282
			$admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
283
			$admin->Email = Security::default_admin_username();
284
			$admin->write();
285
		}
286
287
		// Ensure this user is in the admin group
288
		if(!$admin->inGroup($adminGroup)) {
289
			// Add member to group instead of adding group to member
290
			// This bypasses the privilege escallation code in Member_GroupSet
291
			$adminGroup
292
				->DirectMembers()
293
				->add($admin);
294
		}
295
296
		return $admin;
297
	}
298
299
	/**
300
	 * If this is called, then a session cookie will be set to "1" whenever a user
301
	 * logs in.  This lets 3rd party tools, such as apache's mod_rewrite, detect
302
	 * whether a user is logged in or not and alter behaviour accordingly.
303
	 *
304
	 * One known use of this is to bypass static caching for logged in users.  This is
305
	 * done by putting this into _config.php
306
	 * <pre>
307
	 * Member::set_login_marker_cookie("SS_LOGGED_IN");
308
	 * </pre>
309
	 *
310
	 * And then adding this condition to each of the rewrite rules that make use of
311
	 * the static cache.
312
	 * <pre>
313
	 * RewriteCond %{HTTP_COOKIE} !SS_LOGGED_IN=1
314
	 * </pre>
315
	 *
316
	 * @deprecated 4.0 Use the "Member.login_marker_cookie" config setting instead
317
	 * @param $cookieName string The name of the cookie to set.
318
	 */
319
	public static function set_login_marker_cookie($cookieName) {
320
		Deprecation::notice('4.0', 'Use the "Member.login_marker_cookie" config setting instead');
321
		self::config()->login_marker_cookie = $cookieName;
0 ignored issues
show
Documentation introduced by
The property login_marker_cookie does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
322
	}
323
324
	/**
325
	 * Check if the passed password matches the stored one (if the member is not locked out).
326
	 *
327
	 * @param string $password
328
	 * @return ValidationResult
329
	 */
330
	public function checkPassword($password) {
331
		$result = $this->canLogIn();
332
333
		// Short-circuit the result upon failure, no further checks needed.
334
		if (!$result->valid()) {
335
			return $result;
336
		}
337
338
		// Allow default admin to login as self
339
		if($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
340
			return $result;
341
		}
342
343
		// Check a password is set on this member
344
		if(empty($this->Password) && $this->exists()) {
345
			$result->error(_t('Member.NoPassword','There is no password on this member.'));
346
			return $result;
347
		}
348
349
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
350
		if(!$e->check($this->Password, $password, $this->Salt, $this)) {
351
			$result->error(_t (
352
				'Member.ERRORWRONGCRED',
353
				'The provided details don\'t seem to be correct. Please try again.'
354
			));
355
		}
356
357
		return $result;
358
	}
359
360
	/**
361
	 * Check if this user is the currently configured default admin
362
	 *
363
	 * @return bool
364
	 */
365
	public function isDefaultAdmin() {
366
		return Security::has_default_admin()
367
			&& $this->Email === Security::default_admin_username();
368
	}
369
370
	/**
371
	 * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
372
	 * one with error messages to display if the member is locked out.
373
	 *
374
	 * You can hook into this with a "canLogIn" method on an attached extension.
375
	 *
376
	 * @return ValidationResult
377
	 */
378
	public function canLogIn() {
379
		$result = ValidationResult::create();
380
381
		if($this->isLockedOut()) {
382
			$result->error(
383
				_t(
384
					'Member.ERRORLOCKEDOUT2',
385
					'Your account has been temporarily disabled because of too many failed attempts at ' .
386
					'logging in. Please try again in {count} minutes.',
387
					null,
388
					array('count' => $this->config()->lock_out_delay_mins)
389
				)
390
			);
391
		}
392
393
		$this->extend('canLogIn', $result);
394
		return $result;
395
	}
396
397
	/**
398
	 * Returns true if this user is locked out
399
	 */
400
	public function isLockedOut() {
401
		global $debug;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
402
		if ($this->LockedOutUntil && $this->dbObject('LockedOutUntil')->InFuture()) {
403
			return true;
404
		}
405
406
		if ($this->config()->lock_out_after_incorrect_logins <= 0) {
407
			return false;
408
		}
409
410
		$email = $this->{static::config()->unique_identifier_field};
411
		$attempts = LoginAttempt::getByEmail($email)
412
			->sort('Created', 'DESC')
413
			->limit($this->config()->lock_out_after_incorrect_logins);
414
415
		if ($attempts->count() < $this->config()->lock_out_after_incorrect_logins) {
416
			return false;
417
		}
418
419
		foreach ($attempts as $attempt) {
420
			if ($attempt->Status === 'Success') {
421
				return false;
422
			}
423
		}
424
425
		$lockedOutUntil = $attempts->first()->dbObject('Created')->Format('U') + ($this->config()->lock_out_delay_mins * 60);
426
		if (SS_Datetime::now()->Format('U') < $lockedOutUntil) {
427
			return true;
428
		}
429
430
		return false;
431
	}
432
433
	/**
434
	 * Regenerate the session_id.
435
	 * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
436
	 * They have caused problems in certain
437
	 * quirky problems (such as using the Windmill 0.3.6 proxy).
438
	 */
439
	public static function session_regenerate_id() {
440
		if(!self::config()->session_regenerate_id) return;
441
442
		// This can be called via CLI during testing.
443
		if(Director::is_cli()) return;
444
445
		$file = '';
446
		$line = '';
447
448
		// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
449
		// There's nothing we can do about this, because it's an operating system function!
450
		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...
451
	}
452
453
	/**
454
	 * Get the field used for uniquely identifying a member
455
	 * in the database. {@see Member::$unique_identifier_field}
456
	 *
457
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
458
	 * @return string
459
	 */
460
	public static function get_unique_identifier_field() {
461
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
462
		return Member::config()->unique_identifier_field;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
463
	}
464
465
	/**
466
	 * Set the field used for uniquely identifying a member
467
	 * in the database. {@see Member::$unique_identifier_field}
468
	 *
469
	 * @deprecated 4.0 Use the "Member.unique_identifier_field" config setting instead
470
	 * @param $field The field name to set as the unique field
471
	 */
472
	public static function set_unique_identifier_field($field) {
473
		Deprecation::notice('4.0', 'Use the "Member.unique_identifier_field" config setting instead');
474
		Member::config()->unique_identifier_field = $field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
475
	}
476
477
	/**
478
	 * Set a {@link PasswordValidator} object to use to validate member's passwords.
479
	 */
480
	public static function set_password_validator($pv) {
481
		self::$password_validator = $pv;
482
	}
483
484
	/**
485
	 * Returns the current {@link PasswordValidator}
486
	 */
487
	public static function password_validator() {
488
		return self::$password_validator;
489
	}
490
491
	/**
492
	 * Set the number of days that a password should be valid for.
493
	 * Set to null (the default) to have passwords never expire.
494
	 *
495
	 * @deprecated 4.0 Use the "Member.password_expiry_days" config setting instead
496
	 */
497
	public static function set_password_expiry($days) {
498
		Deprecation::notice('4.0', 'Use the "Member.password_expiry_days" config setting instead');
499
		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...
500
	}
501
502
	/**
503
	 * Configure the security system to lock users out after this many incorrect logins
504
	 *
505
	 * @deprecated 4.0 Use the "Member.lock_out_after_incorrect_logins" config setting instead
506
	 */
507
	public static function lock_out_after_incorrect_logins($numLogins) {
508
		Deprecation::notice('4.0', 'Use the "Member.lock_out_after_incorrect_logins" config setting instead');
509
		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...
510
	}
511
512
513
	public function isPasswordExpired() {
514
		if(!$this->PasswordExpiry) return false;
515
		return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
516
	}
517
518
	/**
519
	 * Logs this member in
520
	 *
521
	 * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
522
	 */
523
	public function logIn($remember = false) {
524
		$this->extend('beforeMemberLoggedIn');
525
526
		self::session_regenerate_id();
527
528
		Session::set("loggedInAs", $this->ID);
529
		// This lets apache rules detect whether the user has logged in
530
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, 1, 0);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
531
532
		$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...
533
534
		// Only set the cookie if autologin is enabled
535
		if($remember && Security::config()->autologin_enabled) {
536
			// Store the hash and give the client the cookie with the token.
537
			$generator = new RandomGenerator();
538
			$token = $generator->randomToken('sha1');
539
			$hash = $this->encryptWithUserSettings($token);
540
			$this->RememberLoginToken = $hash;
541
			Cookie::set('alc_enc', $this->ID . ':' . $token, 90, null, null, null, true);
542
		} else {
543
			$this->RememberLoginToken = null;
544
			Cookie::force_expiry('alc_enc');
545
		}
546
547
		// Clear the incorrect log-in count
548
		$this->registerSuccessfulLogin();
549
550
		// Don't set column if its not built yet (the login might be precursor to a /dev/build...)
551
		if(array_key_exists('LockedOutUntil', DB::field_list('Member'))) {
552
			$this->LockedOutUntil = null;
553
		}
554
555
		$this->regenerateTempID();
556
557
		$this->write();
558
559
		// Audit logging hook
560
		$this->extend('memberLoggedIn');
561
	}
562
563
	/**
564
	 * @deprecated 4.0
565
	 */
566
	public function addVisit() {
567
		if($this->config()->log_num_visits) {
568
			Deprecation::notice(
569
				'4.0',
570
				'Member::$NumVisit is deprecated. From 4.0 onwards you should implement this as a custom extension'
571
			);
572
			$this->NumVisit++;
573
		}
574
	}
575
576
	/**
577
	 * Trigger regeneration of TempID.
578
	 *
579
	 * This should be performed any time the user presents their normal identification (normally Email)
580
	 * and is successfully authenticated.
581
	 */
582
	public function regenerateTempID() {
583
		$generator = new RandomGenerator();
584
		$this->TempIDHash = $generator->randomToken('sha1');
585
		$this->TempIDExpired = self::config()->temp_id_lifetime
586
			? date('Y-m-d H:i:s', strtotime(SS_Datetime::now()->getValue()) + self::config()->temp_id_lifetime)
587
			: null;
588
		$this->write();
589
	}
590
591
	/**
592
	 * Check if the member ID logged in session actually
593
	 * has a database record of the same ID. If there is
594
	 * no logged in user, FALSE is returned anyway.
595
	 *
596
	 * @return boolean TRUE record found FALSE no record found
597
	 */
598
	public static function logged_in_session_exists() {
599
		if($id = Member::currentUserID()) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
600
			if($member = DataObject::get_by_id('Member', $id)) {
601
				if($member->exists()) return true;
602
			}
603
		}
604
605
		return false;
606
	}
607
608
	/**
609
	 * Log the user in if the "remember login" cookie is set
610
	 *
611
	 * The <i>remember login token</i> will be changed on every successful
612
	 * auto-login.
613
	 */
614
	public static function autoLogin() {
615
		// Don't bother trying this multiple times
616
		self::$_already_tried_to_auto_log_in = true;
617
618
		if(!Security::config()->autologin_enabled
619
			|| strpos(Cookie::get('alc_enc'), ':') === false
620
			|| Session::get("loggedInAs")
621
			|| !Security::database_is_ready()
622
		) {
623
			return;
624
		}
625
626
		list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
627
628
		if (!$uid || !$token) {
629
			return;
630
		}
631
632
		$member = DataObject::get_by_id("Member", $uid);
633
634
		// check if autologin token matches
635
		if($member) {
636
			$hash = $member->encryptWithUserSettings($token);
637
			if(!$member->RememberLoginToken || $member->RememberLoginToken !== $hash) {
638
				$member = null;
639
			}
640
		}
641
642
		if($member) {
643
			self::session_regenerate_id();
644
			Session::set("loggedInAs", $member->ID);
645
			// This lets apache rules detect whether the user has logged in
646
			if(Member::config()->login_marker_cookie) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
647
				Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
648
			}
649
650
			$generator = new RandomGenerator();
651
			$token = $generator->randomToken('sha1');
652
			$hash = $member->encryptWithUserSettings($token);
653
			$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...
654
			Cookie::set('alc_enc', $member->ID . ':' . $token, 90, null, null, false, true);
655
656
			$member->addVisit();
657
			$member->write();
658
659
			// Audit logging hook
660
			$member->extend('memberAutoLoggedIn');
661
		}
662
	}
663
664
	/**
665
	 * Logs this member out.
666
	 */
667
	public function logOut() {
668
		$this->extend('beforeMemberLoggedOut');
669
670
		Session::clear("loggedInAs");
671
		if(Member::config()->login_marker_cookie) Cookie::set(Member::config()->login_marker_cookie, null, 0);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
672
673
		Session::destroy();
674
675
		$this->extend('memberLoggedOut');
676
677
		$this->RememberLoginToken = null;
678
		Cookie::force_expiry('alc_enc');
679
680
		// Switch back to live in order to avoid infinite loops when
681
		// redirecting to the login screen (if this login screen is versioned)
682
		Session::clear('readingMode');
683
684
		$this->write();
685
686
		// Audit logging hook
687
		$this->extend('memberLoggedOut');
688
	}
689
690
	/**
691
	 * Utility for generating secure password hashes for this member.
692
	 */
693
	public function encryptWithUserSettings($string) {
694
		if (!$string) return null;
695
696
		// If the algorithm or salt is not available, it means we are operating
697
		// on legacy account with unhashed password. Do not hash the string.
698
		if (!$this->PasswordEncryption) {
699
			return $string;
700
		}
701
702
		// We assume we have PasswordEncryption and Salt available here.
703
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
704
		return $e->encrypt($string, $this->Salt);
705
706
	}
707
708
	/**
709
	 * Generate an auto login token which can be used to reset the password,
710
	 * at the same time hashing it and storing in the database.
711
	 *
712
	 * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
713
	 *
714
	 * @returns string Token that should be passed to the client (but NOT persisted).
715
	 *
716
	 * @todo Make it possible to handle database errors such as a "duplicate key" error
717
	 */
718
	public function generateAutologinTokenAndStoreHash($lifetime = 2) {
719
		do {
720
			$generator = new RandomGenerator();
721
			$token = $generator->randomToken();
722
			$hash = $this->encryptWithUserSettings($token);
723
		} while(DataObject::get_one('Member', array(
724
			'"Member"."AutoLoginHash"' => $hash
725
		)));
726
727
		$this->AutoLoginHash = $hash;
728
		$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
729
730
		$this->write();
731
732
		return $token;
733
	}
734
735
	/**
736
	 * Check the token against the member.
737
	 *
738
	 * @param string $autologinToken
739
	 *
740
	 * @returns bool Is token valid?
741
	 */
742
	public function validateAutoLoginToken($autologinToken) {
743
		$hash = $this->encryptWithUserSettings($autologinToken);
744
		$member = self::member_from_autologinhash($hash, false);
745
		return (bool)$member;
746
	}
747
748
	/**
749
	 * Return the member for the auto login hash
750
	 *
751
	 * @param string $hash The hash key
752
	 * @param bool $login Should the member be logged in?
753
	 *
754
	 * @return Member the matching member, if valid
755
	 * @return Member
756
	 */
757
	public static function member_from_autologinhash($hash, $login = false) {
758
759
		$nowExpression = DB::get_conn()->now();
760
		$member = DataObject::get_one('Member', array(
761
			"\"Member\".\"AutoLoginHash\"" => $hash,
762
			"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
763
		));
764
765
		if($login && $member) $member->logIn();
766
767
		return $member;
768
	}
769
770
	/**
771
	 * Find a member record with the given TempIDHash value
772
	 *
773
	 * @param string $tempid
774
	 * @return Member
775
	 */
776
	public static function member_from_tempid($tempid) {
777
		$members = Member::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
778
			->filter('TempIDHash', $tempid);
779
780
		// Exclude expired
781
		if(static::config()->temp_id_lifetime) {
782
			$members = $members->filter('TempIDExpired:GreaterThan', SS_Datetime::now()->getValue());
783
		}
784
785
		return $members->first();
786
	}
787
788
	/**
789
	 * Returns the fields for the member form - used in the registration/profile module.
790
	 * It should return fields that are editable by the admin and the logged-in user.
791
	 *
792
	 * @return FieldList Returns a {@link FieldList} containing the fields for
793
	 *                   the member form.
794
	 */
795
	public function getMemberFormFields() {
796
		$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...
797
798
		$fields->replaceField('Password', $this->getMemberPasswordField());
799
800
		$fields->replaceField('Locale', new DropdownField (
801
			'Locale',
802
			$this->fieldLabel('Locale'),
803
			i18n::get_existing_translations()
804
		));
805
806
		$fields->removeByName(static::config()->hidden_fields);
807
		$fields->removeByName('LastVisited');
808
		$fields->removeByName('FailedLoginCount');
809
810
811
		$this->extend('updateMemberFormFields', $fields);
812
		return $fields;
813
	}
814
815
	/**
816
	 * Builds "Change / Create Password" field for this member
817
	 *
818
	 * @return ConfirmedPasswordField
819
	 */
820
	public function getMemberPasswordField() {
821
		$editingPassword = $this->isInDB();
822
		$label = $editingPassword
823
			? _t('Member.EDIT_PASSWORD', 'New Password')
824
			: $this->fieldLabel('Password');
825
		/** @var ConfirmedPasswordField $password */
826
		$password = ConfirmedPasswordField::create(
827
			'Password',
828
			$label,
829
			null,
830
			null,
831
			$editingPassword
832
		);
833
834
		// If editing own password, require confirmation of existing
835
		if($editingPassword && $this->ID == Member::currentUserID()) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
836
			$password->setRequireExistingPassword(true);
837
		}
838
839
		$password->setCanBeEmpty(true);
840
		$this->extend('updateMemberPasswordField', $password);
841
		return $password;
842
	}
843
844
845
	/**
846
	 * Returns the {@link RequiredFields} instance for the Member object. This
847
	 * Validator is used when saving a {@link CMSProfileController} or added to
848
	 * any form responsible for saving a users data.
849
	 *
850
	 * To customize the required fields, add a {@link DataExtension} to member
851
	 * calling the `updateValidator()` method.
852
	 *
853
	 * @return Member_Validator
854
	 */
855
	public function getValidator() {
856
		$validator = Injector::inst()->create('Member_Validator');
857
		$validator->setForMember($this);
858
		$this->extend('updateValidator', $validator);
859
860
		return $validator;
861
	}
862
863
864
	/**
865
	 * Returns the current logged in user
866
	 *
867
	 * @return Member|null
868
	 */
869
	public static function currentUser() {
870
		$id = Member::currentUserID();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
871
872
		if($id) {
873
			return DataObject::get_by_id('Member', $id) ?: null;
874
		}
875
	}
876
877
	/**
878
	 * Get the ID of the current logged in user
879
	 *
880
	 * @return int Returns the ID of the current logged in user or 0.
881
	 */
882
	public static function currentUserID() {
883
		$id = Session::get("loggedInAs");
884
		if(!$id && !self::$_already_tried_to_auto_log_in) {
885
			self::autoLogin();
886
			$id = Session::get("loggedInAs");
887
		}
888
889
		return is_numeric($id) ? $id : 0;
890
	}
891
	private static $_already_tried_to_auto_log_in = false;
892
893
894
	/*
895
	 * Generate a random password, with randomiser to kick in if there's no words file on the
896
	 * filesystem.
897
	 *
898
	 * @return string Returns a random password.
899
	 */
900
	public static function create_new_password() {
901
		$words = Config::inst()->get('Security', 'word_list');
902
903
		if($words && file_exists($words)) {
904
			$words = file($words);
905
906
			list($usec, $sec) = explode(' ', microtime());
907
			srand($sec + ((float) $usec * 100000));
908
909
			$word = trim($words[rand(0,sizeof($words)-1)]);
910
			$number = rand(10,999);
911
912
			return $word . $number;
913
		} else {
914
			$random = rand();
915
			$string = md5($random);
916
			$output = substr($string, 0, 8);
917
			return $output;
918
		}
919
	}
920
921
	/**
922
	 * Event handler called before writing to the database.
923
	 */
924
	public function onBeforeWrite() {
925
		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...
926
927
		// If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
928
		// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
929
		// but rather a last line of defense against data inconsistencies.
930
		$identifierField = Member::config()->unique_identifier_field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
931
		if($this->$identifierField) {
932
933
			// Note: Same logic as Member_Validator class
934
			$filter = array("\"$identifierField\"" => $this->$identifierField);
935
			if($this->ID) {
936
				$filter[] = array('"Member"."ID" <> ?' => $this->ID);
937
			}
938
			$existingRecord = DataObject::get_one('Member', $filter);
939
940
			if($existingRecord) {
941
				throw new ValidationException(ValidationResult::create(false, _t(
942
					'Member.ValidationIdentifierFailed',
943
					'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
944
					'Values in brackets show "fieldname = value", usually denoting an existing email address',
945
					array(
946
						'id' => $existingRecord->ID,
947
						'name' => $identifierField,
948
						'value' => $this->$identifierField
949
					)
950
				)));
951
			}
952
		}
953
954
		// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
955
		// However, if TestMailer is in use this isn't a risk.
956
		if(
957
			(Director::isLive() || Email::mailer() instanceof TestMailer)
958
			&& $this->isChanged('Password')
959
			&& $this->record['Password']
960
			&& $this->config()->notify_password_change
961
		) {
962
			$e = Member_ChangePasswordEmail::create();
963
			$e->populateTemplate($this);
964
			$e->setTo($this->Email);
965
			$e->send();
966
		}
967
968
		// The test on $this->ID is used for when records are initially created.
969
		// Note that this only works with cleartext passwords, as we can't rehash
970
		// existing passwords.
971
		if((!$this->ID && $this->Password) || $this->isChanged('Password')) {
972
			//reset salt so that it gets regenerated - this will invalidate any persistant login cookies
973
			// or other information encrypted with this Member's settings (see self::encryptWithUserSettings)
974
			$this->Salt = '';
975
			// Password was changed: encrypt the password according the settings
976
			$encryption_details = Security::encrypt_password(
977
				$this->Password, // this is assumed to be cleartext
978
				$this->Salt,
979
				$this->isChanged('PasswordEncryption') ? $this->PasswordEncryption : null,
980
				$this
981
			);
982
983
			// Overwrite the Password property with the hashed value
984
			$this->Password = $encryption_details['password'];
985
			$this->Salt = $encryption_details['salt'];
986
			$this->PasswordEncryption = $encryption_details['algorithm'];
987
988
			// If we haven't manually set a password expiry
989
			if(!$this->isChanged('PasswordExpiry')) {
990
				// then set it for us
991
				if(self::config()->password_expiry_days) {
992
					$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
993
				} else {
994
					$this->PasswordExpiry = null;
995
				}
996
			}
997
		}
998
999
		// save locale
1000
		if(!$this->Locale) {
1001
			$this->Locale = i18n::get_locale();
1002
		}
1003
1004
		parent::onBeforeWrite();
1005
	}
1006
1007
	public function onAfterWrite() {
1008
		parent::onAfterWrite();
1009
1010
		Permission::flush_permission_cache();
1011
1012
		if($this->isChanged('Password')) {
1013
			MemberPassword::log($this);
1014
		}
1015
	}
1016
1017
	public function onAfterDelete() {
1018
		parent::onAfterDelete();
1019
1020
		//prevent orphaned records remaining in the DB
1021
		$this->deletePasswordLogs();
1022
	}
1023
1024
	/**
1025
	 * Delete the MemberPassword objects that are associated to this user
1026
	 *
1027
	 * @return self
1028
	 */
1029
	protected function deletePasswordLogs() {
1030
		foreach ($this->LoggedPasswords() as $password) {
1031
			$password->delete();
1032
			$password->destroy();
1033
		}
1034
		return $this;
1035
	}
1036
1037
	/**
1038
	 * Filter out admin groups to avoid privilege escalation,
1039
	 * If any admin groups are requested, deny the whole save operation.
1040
	 *
1041
	 * @param Array $ids Database IDs of Group records
1042
	 * @return boolean True if the change can be accepted
1043
	 */
1044
	public function onChangeGroups($ids) {
1045
		// Ensure none of these match disallowed list
1046
		$disallowedGroupIDs = $this->disallowedGroups();
1047
		return count(array_intersect($ids, $disallowedGroupIDs)) == 0;
1048
	}
1049
1050
	/**
1051
	 * List of group IDs this user is disallowed from
1052
	 *
1053
	 * @return int[] List of group IDs
1054
	 */
1055
	protected function disallowedGroups() {
1056
		// unless the current user is an admin already OR the logged in user is an admin
1057
		if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
1058
			return array();
1059
		}
1060
1061
		// Non-admins may not belong to admin groups
1062
		return Permission::get_groups_by_permission('ADMIN')->column('ID');
1063
	}
1064
1065
1066
	/**
1067
	 * Check if the member is in one of the given groups.
1068
	 *
1069
	 * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
1070
	 * @param boolean $strict Only determine direct group membership if set to true (Default: false)
1071
	 * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
1072
	 */
1073
	public function inGroups($groups, $strict = false) {
1074
		if($groups) foreach($groups as $group) {
1075
			if($this->inGroup($group, $strict)) return true;
1076
		}
1077
1078
		return false;
1079
	}
1080
1081
1082
	/**
1083
	 * Check if the member is in the given group or any parent groups.
1084
	 *
1085
	 * @param int|Group|string $group Group instance, Group Code or ID
1086
	 * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
1087
	 * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
1088
	 */
1089
	public function inGroup($group, $strict = false) {
1090
		if(is_numeric($group)) {
1091
			$groupCheckObj = DataObject::get_by_id('Group', $group);
1092
		} elseif(is_string($group)) {
1093
			$groupCheckObj = DataObject::get_one('Group', array(
1094
				'"Group"."Code"' => $group
1095
			));
1096
		} elseif($group instanceof Group) {
1097
			$groupCheckObj = $group;
1098
		} else {
1099
			user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
1100
		}
1101
1102
		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...
1103
1104
		$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1105
		if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
1106
			if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
1107
		}
1108
1109
		return false;
1110
	}
1111
1112
	/**
1113
	 * Adds the member to a group. This will create the group if the given
1114
	 * group code does not return a valid group object.
1115
	 *
1116
	 * @param string $groupcode
1117
	 * @param string Title of the group
1118
	 */
1119
	public function addToGroupByCode($groupcode, $title = "") {
1120
		$group = DataObject::get_one('Group', array(
1121
			'"Group"."Code"' => $groupcode
1122
		));
1123
1124
		if($group) {
1125
			$this->Groups()->add($group);
1126
		} else {
1127
			if(!$title) $title = $groupcode;
1128
1129
			$group = new Group();
1130
			$group->Code = $groupcode;
1131
			$group->Title = $title;
1132
			$group->write();
1133
1134
			$this->Groups()->add($group);
1135
		}
1136
	}
1137
1138
	/**
1139
	 * Removes a member from a group.
1140
	 *
1141
	 * @param string $groupcode
1142
	 */
1143
	public function removeFromGroupByCode($groupcode) {
1144
		$group = Group::get()->filter(array('Code' => $groupcode))->first();
1145
1146
		if($group) {
1147
			$this->Groups()->remove($group);
1148
		}
1149
	}
1150
1151
	/**
1152
	 * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
1153
	 * @param String $sep Separator
1154
	 */
1155
	public static function set_title_columns($columns, $sep = ' ') {
1156
		if (!is_array($columns)) $columns = array($columns);
1157
		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...
1158
	}
1159
1160
	//------------------- HELPER METHODS -----------------------------------//
1161
1162
	/**
1163
	 * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1164
	 * Falls back to showing either field on its own.
1165
	 *
1166
	 * You can overload this getter with {@link set_title_format()}
1167
	 * and {@link set_title_sql()}.
1168
	 *
1169
	 * @return string Returns the first- and surname of the member. If the ID
1170
	 *  of the member is equal 0, only the surname is returned.
1171
	 */
1172
	public function getTitle() {
1173
		$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...
1174
		if ($format) {
1175
			$values = array();
1176
			foreach($format['columns'] as $col) {
1177
				$values[] = $this->getField($col);
1178
			}
1179
			return join($format['sep'], $values);
1180
		}
1181
		if($this->getField('ID') === 0)
1182
			return $this->getField('Surname');
1183
		else{
1184
			if($this->getField('Surname') && $this->getField('FirstName')){
1185
				return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1186
			}elseif($this->getField('Surname')){
1187
				return $this->getField('Surname');
1188
			}elseif($this->getField('FirstName')){
1189
				return $this->getField('FirstName');
1190
			}else{
1191
				return null;
1192
			}
1193
		}
1194
	}
1195
1196
	/**
1197
	 * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1198
	 * Useful for custom queries which assume a certain member title format.
1199
	 *
1200
	 * @param String $tableName
1201
	 * @return String SQL
1202
	 */
1203
	public static function get_title_sql($tableName = 'Member') {
1204
		// This should be abstracted to SSDatabase concatOperator or similar.
1205
		$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...
1206
1207
		$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...
1208
		if ($format) {
1209
			$columnsWithTablename = array();
1210
			foreach($format['columns'] as $column) {
1211
				$columnsWithTablename[] = "\"$tableName\".\"$column\"";
1212
			}
1213
1214
			return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")";
1215
		} else {
1216
			return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")";
1217
		}
1218
	}
1219
1220
1221
	/**
1222
	 * Get the complete name of the member
1223
	 *
1224
	 * @return string Returns the first- and surname of the member.
1225
	 */
1226
	public function getName() {
1227
		return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1228
	}
1229
1230
1231
	/**
1232
	 * Set first- and surname
1233
	 *
1234
	 * This method assumes that the last part of the name is the surname, e.g.
1235
	 * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1236
	 *
1237
	 * @param string $name The name
1238
	 */
1239
	public function setName($name) {
1240
		$nameParts = explode(' ', $name);
1241
		$this->Surname = array_pop($nameParts);
1242
		$this->FirstName = join(' ', $nameParts);
1243
	}
1244
1245
1246
	/**
1247
	 * Alias for {@link setName}
1248
	 *
1249
	 * @param string $name The name
1250
	 * @see setName()
1251
	 */
1252
	public function splitName($name) {
1253
		return $this->setName($name);
1254
	}
1255
1256
	/**
1257
	 * Override the default getter for DateFormat so the
1258
	 * default format for the user's locale is used
1259
	 * if the user has not defined their own.
1260
	 *
1261
	 * @return string ISO date format
1262
	 */
1263
	public function getDateFormat() {
1264
		if($this->getField('DateFormat')) {
1265
			return $this->getField('DateFormat');
1266
		} else {
1267
			return Config::inst()->get('i18n', 'date_format');
1268
		}
1269
	}
1270
1271
	/**
1272
	 * Override the default getter for TimeFormat so the
1273
	 * default format for the user's locale is used
1274
	 * if the user has not defined their own.
1275
	 *
1276
	 * @return string ISO date format
1277
	 */
1278
	public function getTimeFormat() {
1279
		if($this->getField('TimeFormat')) {
1280
			return $this->getField('TimeFormat');
1281
		} else {
1282
			return Config::inst()->get('i18n', 'time_format');
1283
		}
1284
	}
1285
1286
	//---------------------------------------------------------------------//
1287
1288
1289
	/**
1290
	 * Get a "many-to-many" map that holds for all members their group memberships,
1291
	 * including any parent groups where membership is implied.
1292
	 * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1293
	 *
1294
	 * @todo Push all this logic into Member_GroupSet's getIterator()?
1295
	 * @return Member_Groupset
1296
	 */
1297
	public function Groups() {
1298
		$groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID');
1299
		$groups = $groups->forForeignID($this->ID);
1300
1301
		$this->extend('updateGroups', $groups);
1302
1303
		return $groups;
1304
	}
1305
1306
	/**
1307
	 * @return ManyManyList
1308
	 */
1309
	public function DirectGroups() {
1310
		return $this->getManyManyComponents('Groups');
1311
	}
1312
1313
	/**
1314
	 * Get a member SQLMap of members in specific groups
1315
	 *
1316
	 * If no $groups is passed, all members will be returned
1317
	 *
1318
	 * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1319
	 * @return SQLMap Returns an SQLMap that returns all Member data.
1320
	 * @see map()
1321
	 */
1322
	public static function map_in_groups($groups = null) {
1323
		$groupIDList = array();
1324
1325
		if($groups instanceof SS_List) {
1326
			foreach( $groups as $group ) {
1327
				$groupIDList[] = $group->ID;
1328
			}
1329
		} elseif(is_array($groups)) {
1330
			$groupIDList = $groups;
1331
		} elseif($groups) {
1332
			$groupIDList[] = $groups;
1333
		}
1334
1335
		// No groups, return all Members
1336
		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...
1337
			return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1338
		}
1339
1340
		$membersList = new ArrayList();
1341
		// This is a bit ineffective, but follow the ORM style
1342
		foreach(Group::get()->byIDs($groupIDList) as $group) {
1343
			$membersList->merge($group->Members());
1344
		}
1345
1346
		$membersList->removeDuplicates('ID');
1347
		return $membersList->map();
1348
	}
1349
1350
1351
	/**
1352
	 * Get a map of all members in the groups given that have CMS permissions
1353
	 *
1354
	 * If no groups are passed, all groups with CMS permissions will be used.
1355
	 *
1356
	 * @param array $groups Groups to consider or NULL to use all groups with
1357
	 *                      CMS permissions.
1358
	 * @return SS_Map Returns a map of all members in the groups given that
1359
	 *                have CMS permissions.
1360
	 */
1361
	public static function mapInCMSGroups($groups = null) {
1362
		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...
1363
			$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1364
1365
			if(class_exists('CMSMain')) {
1366
				$cmsPerms = singleton('CMSMain')->providePermissions();
1367
			} else {
1368
				$cmsPerms = singleton('LeftAndMain')->providePermissions();
1369
			}
1370
1371
			if(!empty($cmsPerms)) {
1372
				$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1373
			}
1374
1375
			$permsClause = DB::placeholders($perms);
1376
			$groups = DataObject::get('Group')
1377
				->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1378
				->where(array(
1379
					"\"Permission\".\"Code\" IN ($permsClause)" => $perms
1380
				));
1381
		}
1382
1383
		$groupIDList = array();
1384
1385
		if(is_a($groups, 'SS_List')) {
1386
			foreach($groups as $group) {
1387
				$groupIDList[] = $group->ID;
1388
			}
1389
		} elseif(is_array($groups)) {
1390
			$groupIDList = $groups;
1391
		}
1392
1393
		$members = Member::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1394
			->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1395
			->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1396
		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...
1397
			$groupClause = DB::placeholders($groupIDList);
1398
			$members = $members->where(array(
1399
				"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1400
			));
1401
		}
1402
1403
		return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1404
	}
1405
1406
1407
	/**
1408
	 * Get the groups in which the member is NOT in
1409
	 *
1410
	 * When passed an array of groups, and a component set of groups, this
1411
	 * function will return the array of groups the member is NOT in.
1412
	 *
1413
	 * @param array $groupList An array of group code names.
1414
	 * @param array $memberGroups A component set of groups (if set to NULL,
1415
	 *                            $this->groups() will be used)
1416
	 * @return array Groups in which the member is NOT in.
1417
	 */
1418
	public function memberNotInGroups($groupList, $memberGroups = null){
1419
		if(!$memberGroups) $memberGroups = $this->Groups();
1420
1421
		foreach($memberGroups as $group) {
1422
			if(in_array($group->Code, $groupList)) {
1423
				$index = array_search($group->Code, $groupList);
1424
				unset($groupList[$index]);
1425
			}
1426
		}
1427
1428
		return $groupList;
1429
	}
1430
1431
1432
	/**
1433
	 * Return a {@link FieldList} of fields that would appropriate for editing
1434
	 * this member.
1435
	 *
1436
	 * @return FieldList Return a FieldList of fields that would appropriate for
1437
	 *                   editing this member.
1438
	 */
1439
	public function getCMSFields() {
1440
		require_once 'Zend/Date.php';
1441
1442
		$self = $this;
1443
		$this->beforeUpdateCMSFields(function(FieldList $fields) use ($self) {
1444
			/** @var FieldList $mainFields */
1445
			$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
1446
1447
			// Build change password field
1448
			$mainFields->replaceField('Password', $self->getMemberPasswordField());
1449
1450
			$mainFields->replaceField('Locale', new DropdownField(
1451
				"Locale",
1452
				_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1453
				i18n::get_existing_translations()
1454
			));
1455
1456
			$mainFields->removeByName($self->config()->hidden_fields);
1457
1458
			// make sure that the "LastVisited" field exists
1459
			// it may have been removed using $self->config()->hidden_fields
1460
			if($mainFields->fieldByName("LastVisited")){
1461
			$mainFields->makeFieldReadonly('LastVisited');
1462
			}
1463
1464
			if( ! $self->config()->lock_out_after_incorrect_logins) {
1465
				$mainFields->removeByName('FailedLoginCount');
1466
			}
1467
1468
1469
			// Groups relation will get us into logical conflicts because
1470
			// Members are displayed within  group edit form in SecurityAdmin
1471
			$fields->removeByName('Groups');
1472
1473
			// Members shouldn't be able to directly view/edit logged passwords
1474
			$fields->removeByName('LoggedPasswords');
1475
1476
			if(Permission::check('EDIT_PERMISSIONS')) {
1477
                // Filter allowed groups
1478
                $groups = Group::get();
1479
                $disallowedGroupIDs = $this->disallowedGroups();
1480
                if ($disallowedGroupIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $disallowedGroupIDs of type integer[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1481
                    $groups = $groups->exclude('ID', $disallowedGroupIDs);
1482
                }
1483
                $groupsMap = array();
1484
                foreach ($groups as $group) {
1485
                    // Listboxfield values are escaped, use ASCII char instead of &raquo;
1486
                    $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1487
                }
1488
                asort($groupsMap);
1489
				$fields->addFieldToTab('Root.Main',
1490
					ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name())
1491
						->setMultiple(true)
1492
						->setSource($groupsMap)
1493
						->setAttribute(
1494
							'data-placeholder',
1495
							_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1496
						)
1497
				);
1498
1499
1500
				// Add permission field (readonly to avoid complicated group assignment logic).
1501
				// This should only be available for existing records, as new records start
1502
				// with no permissions until they have a group assignment anyway.
1503
				if($self->ID) {
1504
					$permissionsField = new PermissionCheckboxSetField_Readonly(
1505
						'Permissions',
1506
						false,
1507
						'Permission',
1508
						'GroupID',
1509
						// we don't want parent relationships, they're automatically resolved in the field
1510
						$self->getManyManyComponents('Groups')
1511
					);
1512
					$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1513
					$fields->addFieldToTab('Root.Permissions', $permissionsField);
1514
				}
1515
			}
1516
1517
			$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1518
			if($permissionsTab) $permissionsTab->addExtraClass('readonly');
1519
1520
			$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
1521
			$dateFormatMap = array(
1522
				'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
1523
				'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
1524
				'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
1525
				'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
1526
			);
1527
			$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
1528
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1529
			$mainFields->push(
1530
				$dateFormatField = new MemberDatetimeOptionsetField(
1531
					'DateFormat',
1532
					$self->fieldLabel('DateFormat'),
1533
					$dateFormatMap
1534
				)
1535
			);
1536
			$dateFormatField->setValue($self->DateFormat);
1537
1538
			$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
1539
			$timeFormatMap = array(
1540
				'h:mm a' => Zend_Date::now()->toString('h:mm a'),
1541
				'H:mm' => Zend_Date::now()->toString('H:mm'),
1542
			);
1543
			$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
1544
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1545
			$mainFields->push(
1546
				$timeFormatField = new MemberDatetimeOptionsetField(
1547
					'TimeFormat',
1548
					$self->fieldLabel('TimeFormat'),
1549
					$timeFormatMap
1550
				)
1551
			);
1552
			$timeFormatField->setValue($self->TimeFormat);
1553
		});
1554
1555
		return parent::getCMSFields();
1556
	}
1557
1558
	/**
1559
	 *
1560
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1561
	 *
1562
	 */
1563
	public function fieldLabels($includerelations = true) {
1564
		$labels = parent::fieldLabels($includerelations);
1565
1566
		$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1567
		$labels['Surname'] = _t('Member.SURNAME', 'Surname');
1568
		$labels['Email'] = _t('Member.EMAIL', 'Email');
1569
		$labels['Password'] = _t('Member.db_Password', 'Password');
1570
		$labels['NumVisit'] = _t('Member.db_NumVisit', 'Number of Visits');
1571
		$labels['LastVisited'] = _t('Member.db_LastVisited', 'Last Visited Date');
1572
		$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1573
		$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1574
		$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1575
		$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
1576
		$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
1577
		if($includerelations){
1578
			$labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups',
1579
				'Security Groups this member belongs to');
1580
		}
1581
		return $labels;
1582
	}
1583
1584
	/**
1585
	 * Users can view their own record.
1586
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1587
	 * This is likely to be customized for social sites etc. with a looser permission model.
1588
	 */
1589
	public function canView($member = null) {
1590
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1591
1592
		// extended access checks
1593
		$results = $this->extend('canView', $member);
1594
		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...
1595
			if(!min($results)) return false;
1596
			else return true;
1597
		}
1598
1599
		// members can usually edit their own record
1600
		if($member && $this->ID == $member->ID) return true;
1601
1602
		if(
1603
			Permission::checkMember($member, 'ADMIN')
1604
			|| Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin')
1605
		) {
1606
			return true;
1607
		}
1608
1609
		return false;
1610
	}
1611
1612
	/**
1613
	 * Users can edit their own record.
1614
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1615
	 */
1616
	public function canEdit($member = null) {
1617
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1618
1619
		// extended access checks
1620
		$results = $this->extend('canEdit', $member);
1621
		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...
1622
			if(!min($results)) return false;
1623
			else return true;
1624
		}
1625
1626
		// No member found
1627
		if(!($member && $member->exists())) return false;
1628
1629
		// If the requesting member is not an admin, but has access to manage members,
1630
		// they still can't edit other members with ADMIN permission.
1631
		// This is a bit weak, strictly speaking they shouldn't be allowed to
1632
		// perform any action that could change the password on a member
1633
		// with "higher" permissions than himself, but thats hard to determine.
1634
		if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) return false;
1635
1636
		return $this->canView($member);
1637
	}
1638
1639
	/**
1640
	 * Users can edit their own record.
1641
	 * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1642
	 */
1643
	public function canDelete($member = null) {
1644
		if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

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

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

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

    return array();
}

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

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

Loading history...
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

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

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

Loading history...
1798
		$groupIDs = $query->execute()->column();
1799
1800
		// Get all ancestors, iteratively merging these into the master set
1801
		$allGroupIDs = array();
1802
		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...
1803
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1804
			$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
1805
			$groupIDs = array_filter($groupIDs);
1806
		}
1807
1808
		// Add a filter to this DataList
1809
		if(!empty($allGroupIDs)) {
1810
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1811
			return array("\"Group\".\"ID\" IN ($allGroupIDsPlaceholders)" => $allGroupIDs);
1812
		} else {
1813
			return array('"Group"."ID"' => 0);
1814
		}
1815
	}
1816
1817
	public function foreignIDWriteFilter($id = null) {
1818
		// Use the ManyManyList::foreignIDFilter rather than the one
1819
		// in this class, otherwise we end up selecting all inherited groups
1820
		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...
1821
	}
1822
1823
	public function add($item, $extraFields = null) {
1824
		// Get Group.ID
1825
		$itemID = null;
1826
		if(is_numeric($item)) {
1827
			$itemID = $item;
1828
		} else if($item instanceof Group) {
1829
			$itemID = $item->ID;
1830
		}
1831
1832
		// Check if this group is allowed to be added
1833
		if($this->canAddGroups(array($itemID))) {
1834
			parent::add($item, $extraFields);
1835
		}
1836
	}
1837
1838
	public function removeAll() {
1839
		$base = ClassInfo::baseDataClass($this->dataClass());
1840
1841
		// Remove the join to the join table to avoid MySQL row locking issues.
1842
		$query = $this->dataQuery();
1843
		$foreignFilter = $query->getQueryParam('Foreign.Filter');
1844
		$query->removeFilterOn($foreignFilter);
1845
1846
		$selectQuery = $query->query();
1847
		$selectQuery->setSelect("\"{$base}\".\"ID\"");
1848
1849
		$from = $selectQuery->getFrom();
1850
		unset($from[$this->joinTable]);
1851
		$selectQuery->setFrom($from);
1852
		$selectQuery->setOrderBy(); // ORDER BY in subselects breaks MS SQL Server and is not necessary here
1853
		$selectQuery->setDistinct(false);
1854
1855
		// Use a sub-query as SQLite does not support setting delete targets in
1856
		// joined queries.
1857
		$delete = new SQLDelete();
1858
		$delete->setFrom("\"{$this->joinTable}\"");
1859
		// Use ManyManyList::foreignIDFilter() rather than the one in this class
1860
		// otherwise we end up selecting the wrong columns
1861
		$delete->addWhere(parent::foreignIDFilter());
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (foreignIDFilter() instead of removeAll()). Are you sure this is correct? If so, you might want to change this to $this->foreignIDFilter().

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

Consider the following code:

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

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

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

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

Loading history...
1862
		$subSelect = $selectQuery->sql($parameters);
1863
		$delete->addWhere(array(
1864
			"\"{$this->joinTable}\".\"{$this->localKey}\" IN ($subSelect)" => $parameters
1865
		));
1866
		$delete->execute();
1867
	}
1868
1869
	/**
1870
	 * Determine if the following groups IDs can be added
1871
	 *
1872
	 * @param array $itemIDs
1873
	 * @return boolean
1874
	 */
1875
	protected function canAddGroups($itemIDs) {
1876
		if(empty($itemIDs)) {
1877
			return true;
1878
		}
1879
		$member = $this->getMember();
1880
		return empty($member) || $member->onChangeGroups($itemIDs);
1881
	}
1882
1883
	/**
1884
	 * Get foreign member record for this relation
1885
	 *
1886
	 * @return Member
1887
	 */
1888
	protected function getMember() {
1889
		$id = $this->getForeignID();
1890
		if($id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1891
			return DataObject::get_by_id('Member', $id);
1892
		}
1893
	}
1894
}
1895
1896
/**
1897
 * Class used as template to send an email saying that the password has been
1898
 * changed.
1899
 *
1900
 * @package framework
1901
 * @subpackage security
1902
 */
1903
class Member_ChangePasswordEmail extends Email {
1904
1905
	protected $from = '';   // setting a blank from address uses the site's default administrator email
1906
	protected $subject = '';
1907
	protected $ss_template = 'ChangePasswordEmail';
1908
1909
	public function __construct() {
1910
		parent::__construct();
1911
1912
		$this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject');
1913
	}
1914
}
1915
1916
1917
1918
/**
1919
 * Class used as template to send the forgot password email
1920
 *
1921
 * @package framework
1922
 * @subpackage security
1923
 */
1924
class Member_ForgotPasswordEmail extends Email {
1925
	protected $from = '';  // setting a blank from address uses the site's default administrator email
1926
	protected $subject = '';
1927
	protected $ss_template = 'ForgotPasswordEmail';
1928
1929
	public function __construct() {
1930
		parent::__construct();
1931
1932
		$this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject');
1933
	}
1934
}
1935
1936
/**
1937
 * Member Validator
1938
 *
1939
 * Custom validation for the Member object can be achieved either through an
1940
 * {@link DataExtension} on the Member_Validator object or, by specifying a subclass of
1941
 * {@link Member_Validator} through the {@link Injector} API.
1942
 * The Validator can also be modified by adding an Extension to Member and implement the
1943
 * <code>updateValidator</code> hook.
1944
 * {@see Member::getValidator()}
1945
 *
1946
 * Additional required fields can also be set via config API, eg.
1947
 * <code>
1948
 * Member_Validator:
1949
 *   customRequired:
1950
 *     - Surname
1951
 * </code>
1952
 *
1953
 * @package framework
1954
 * @subpackage security
1955
 */
1956
class Member_Validator extends RequiredFields
1957
{
1958
	/**
1959
	 * Fields that are required by this validator
1960
	 * @config
1961
	 * @var array
1962
	 */
1963
	protected $customRequired = array(
1964
		'FirstName',
1965
		'Email'
1966
	);
1967
1968
	/**
1969
	 * Determine what member this validator is meant for
1970
	 * @var Member
1971
	 */
1972
	protected $forMember = null;
1973
1974
	/**
1975
	 * Constructor
1976
	 */
1977
	public function __construct() {
1978
		$required = func_get_args();
1979
1980
		if(isset($required[0]) && is_array($required[0])) {
1981
			$required = $required[0];
1982
		}
1983
1984
		$required = array_merge($required, $this->customRequired);
1985
1986
		// check for config API values and merge them in
1987
		$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...
1988
		if(is_array($config)){
1989
			$required = array_merge($required, $config);
1990
		}
1991
1992
		parent::__construct(array_unique($required));
1993
	}
1994
1995
	/**
1996
	 * Get the member this validator applies to.
1997
	 * @return Member
1998
	 */
1999
	public function getForMember()
2000
	{
2001
		return $this->forMember;
2002
	}
2003
2004
	/**
2005
	 * Set the Member this validator applies to.
2006
	 * @param Member $value
2007
	 * @return $this
2008
	 */
2009
	public function setForMember(Member $value)
2010
	{
2011
		$this->forMember = $value;
2012
		return $this;
2013
	}
2014
2015
	/**
2016
	 * Check if the submitted member data is valid (server-side)
2017
	 *
2018
	 * Check if a member with that email doesn't already exist, or if it does
2019
	 * that it is this member.
2020
	 *
2021
	 * @param array $data Submitted data
2022
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
2023
	 *              FALSE.
2024
	 */
2025
	public function php($data)
2026
	{
2027
		$valid = parent::php($data);
2028
2029
		$identifierField = (string)Member::config()->unique_identifier_field;
2030
2031
		// Only validate identifier field if it's actually set. This could be the case if
2032
		// somebody removes `Email` from the list of required fields.
2033
		if(isset($data[$identifierField])){
2034
			$id = isset($data['ID']) ? (int)$data['ID'] : 0;
2035
			if(!$id && ($ctrl = $this->form->getController())){
2036
				// get the record when within GridField (Member editing page in CMS)
2037
				if($ctrl instanceof GridFieldDetailForm_ItemRequest && $record = $ctrl->getRecord()){
2038
					$id = $record->ID;
2039
				}
2040
			}
2041
2042
			// If there's no ID passed via controller or form-data, use the assigned member (if available)
2043
			if(!$id && ($member = $this->getForMember())){
2044
				$id = $member->exists() ? $member->ID : 0;
2045
			}
2046
2047
			// set the found ID to the data array, so that extensions can also use it
2048
			$data['ID'] = $id;
2049
2050
			$members = Member::get()->filter($identifierField, $data[$identifierField]);
2051
			if($id) {
2052
				$members = $members->exclude('ID', $id);
2053
			}
2054
2055
			if($members->count() > 0) {
2056
				$this->validationError(
2057
					$identifierField,
2058
					_t(
2059
						'Member.VALIDATIONMEMBEREXISTS',
2060
						'A member already exists with the same {identifier}',
2061
						array('identifier' => Member::singleton()->fieldLabel($identifierField))
2062
					),
2063
					'required'
2064
				);
2065
				$valid = false;
2066
			}
2067
		}
2068
2069
2070
		// Execute the validators on the extensions
2071
		$results = $this->extend('updatePHP', $data, $this->form);
2072
		$results[] = $valid;
2073
		return min($results);
2074
	}
2075
}
2076