Completed
Pull Request — master (#5136)
by Nicolaas
11:40
created

Member::canView()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
534
			list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
535
			$deviceID = Cookie::get('alc_device');
536
537
			$member = Member::get()->byId($uid);
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...
538
539
			$rememberLoginHash = null;
540
541
			// check if autologin token matches
542
			if($member) {
543
				$hash = $member->encryptWithUserSettings($token);
544
				$rememberLoginHash = RememberLoginHash::get()
545
					->filter(array(
546
						'MemberID' => $member->ID,
547
						'DeviceID' => $deviceID,
548
						'Hash' => $hash
549
					))->First();
550
				if(!$rememberLoginHash) {
551
					$member = null;
552
				} else {
553
					// Check for expired token
554
					$expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
555
					$now = SS_Datetime::now();
556
					$now = new DateTime($now->Rfc2822());
557
					if ($now > $expiryDate) {
558
						$member = null;
559
					}
560
				}
561
			}
562
563
			if($member) {
564
				self::session_regenerate_id();
565
				Session::set("loggedInAs", $member->ID);
566
				// This lets apache rules detect whether the user has logged in
567
				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...
568
					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...
569
				}
570
571
				if ($rememberLoginHash) {
572
					$rememberLoginHash->renew();
573
					$tokenExpiryDays = Config::inst()->get('RememberLoginHash', 'token_expiry_days');
574
					Cookie::set('alc_enc', $member->ID . ':' . $rememberLoginHash->getToken(), 
575
						$tokenExpiryDays, null, null, false, true);
576
				}
577
578
				$member->write();
579
580
				// Audit logging hook
581
				$member->extend('memberAutoLoggedIn');
582
			}
583
		}
584
	}
585
586
	/**
587
	 * Logs this member out.
588
	 */
589
	public function logOut() {
590
		$this->extend('beforeMemberLoggedOut');
591
592
		Session::clear("loggedInAs");
593
		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...
594
595
		Session::destroy();
596
597
		$this->extend('memberLoggedOut');
598
599
		// Clears any potential previous hashes for this member
600
		RememberLoginHash::clear($this, Cookie::get('alc_device'));
601
602
		Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
603
		Cookie::force_expiry('alc_enc');
604
		Cookie::set('alc_device', null);
605
		Cookie::force_expiry('alc_device');
606
607
		// Switch back to live in order to avoid infinite loops when
608
		// redirecting to the login screen (if this login screen is versioned)
609
		Session::clear('readingMode');
610
611
		$this->write();
612
613
		// Audit logging hook
614
		$this->extend('memberLoggedOut');
615
	}
616
617
	/**
618
	 * Utility for generating secure password hashes for this member.
619
	 */
620
	public function encryptWithUserSettings($string) {
621
		if (!$string) return null;
622
623
		// If the algorithm or salt is not available, it means we are operating
624
		// on legacy account with unhashed password. Do not hash the string.
625
		if (!$this->PasswordEncryption) {
626
			return $string;
627
		}
628
629
		// We assume we have PasswordEncryption and Salt available here.
630
		$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
631
		return $e->encrypt($string, $this->Salt);
632
633
	}
634
635
	/**
636
	 * Generate an auto login token which can be used to reset the password,
637
	 * at the same time hashing it and storing in the database.
638
	 *
639
	 * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
640
	 *
641
	 * @returns string Token that should be passed to the client (but NOT persisted).
642
	 *
643
	 * @todo Make it possible to handle database errors such as a "duplicate key" error
644
	 */
645
	public function generateAutologinTokenAndStoreHash($lifetime = 2) {
646
		do {
647
			$generator = new RandomGenerator();
648
			$token = $generator->randomToken();
649
			$hash = $this->encryptWithUserSettings($token);
650
		} while(DataObject::get_one('Member', array(
651
			'"Member"."AutoLoginHash"' => $hash
652
		)));
653
654
		$this->AutoLoginHash = $hash;
655
		$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
656
657
		$this->write();
658
659
		return $token;
660
	}
661
662
	/**
663
	 * Check the token against the member.
664
	 *
665
	 * @param string $autologinToken
666
	 *
667
	 * @returns bool Is token valid?
668
	 */
669
	public function validateAutoLoginToken($autologinToken) {
670
		$hash = $this->encryptWithUserSettings($autologinToken);
671
		$member = self::member_from_autologinhash($hash, false);
672
		return (bool)$member;
673
	}
674
675
	/**
676
	 * Return the member for the auto login hash
677
	 *
678
	 * @param string $hash The hash key
679
	 * @param bool $login Should the member be logged in?
680
	 *
681
	 * @return Member the matching member, if valid
682
	 * @return Member
683
	 */
684
	public static function member_from_autologinhash($hash, $login = false) {
685
686
		$nowExpression = DB::get_conn()->now();
687
		$member = DataObject::get_one('Member', array(
688
			"\"Member\".\"AutoLoginHash\"" => $hash,
689
			"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
690
		));
691
692
		if($login && $member) $member->logIn();
693
694
		return $member;
695
	}
696
697
	/**
698
	 * Find a member record with the given TempIDHash value
699
	 *
700
	 * @param string $tempid
701
	 * @return Member
702
	 */
703
	public static function member_from_tempid($tempid) {
704
		$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...
705
			->filter('TempIDHash', $tempid);
706
707
		// Exclude expired
708
		if(static::config()->temp_id_lifetime) {
709
			$members = $members->filter('TempIDExpired:GreaterThan', SS_Datetime::now()->getValue());
710
		}
711
712
		return $members->first();
713
	}
714
715
	/**
716
	 * Returns the fields for the member form - used in the registration/profile module.
717
	 * It should return fields that are editable by the admin and the logged-in user.
718
	 *
719
	 * @return FieldList Returns a {@link FieldList} containing the fields for
720
	 *                   the member form.
721
	 */
722
	public function getMemberFormFields() {
723
		$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...
724
725
		$fields->replaceField('Password', $password = new ConfirmedPasswordField (
726
			'Password',
727
			$this->fieldLabel('Password'),
728
			null,
729
			null,
730
			(bool) $this->ID
731
		));
732
		$password->setCanBeEmpty(true);
733
734
		$fields->replaceField('Locale', new DropdownField (
735
			'Locale',
736
			$this->fieldLabel('Locale'),
737
			i18n::get_existing_translations()
738
		));
739
740
		$fields->removeByName(static::config()->hidden_fields);
741
		$fields->removeByName('FailedLoginCount');
742
743
744
		$this->extend('updateMemberFormFields', $fields);
745
		return $fields;
746
	}
747
748
	/**
749
	 * Returns the {@link RequiredFields} instance for the Member object. This
750
	 * Validator is used when saving a {@link CMSProfileController} or added to
751
	 * any form responsible for saving a users data.
752
	 *
753
	 * To customize the required fields, add a {@link DataExtension} to member
754
	 * calling the `updateValidator()` method.
755
	 *
756
	 * @return Member_Validator
757
	 */
758
	public function getValidator() {
759
		$validator = Injector::inst()->create('Member_Validator');
760
		$this->extend('updateValidator', $validator);
761
762
		return $validator;
763
	}
764
765
766
	/**
767
	 * Returns the current logged in user
768
	 *
769
	 * @return Member|null
770
	 */
771
	public static function currentUser() {
772
		$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...
773
774
		if($id) {
775
			return Member::get()->byId($id);
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...
776
		}
777
	}
778
779
	/**
780
	 * Get the ID of the current logged in user
781
	 *
782
	 * @return int Returns the ID of the current logged in user or 0.
783
	 */
784
	public static function currentUserID() {
785
		$id = Session::get("loggedInAs");
786
		if(!$id && !self::$_already_tried_to_auto_log_in) {
787
			self::autoLogin();
788
			$id = Session::get("loggedInAs");
789
		}
790
791
		return is_numeric($id) ? $id : 0;
792
	}
793
	private static $_already_tried_to_auto_log_in = false;
794
795
796
	/*
797
	 * Generate a random password, with randomiser to kick in if there's no words file on the
798
	 * filesystem.
799
	 *
800
	 * @return string Returns a random password.
801
	 */
802
	public static function create_new_password() {
803
		$words = Config::inst()->get('Security', 'word_list');
804
805
		if($words && file_exists($words)) {
806
			$words = file($words);
807
808
			list($usec, $sec) = explode(' ', microtime());
809
			srand($sec + ((float) $usec * 100000));
810
811
			$word = trim($words[rand(0,sizeof($words)-1)]);
812
			$number = rand(10,999);
813
814
			return $word . $number;
815
		} else {
816
			$random = rand();
817
			$string = md5($random);
818
			$output = substr($string, 0, 6);
819
			return $output;
820
		}
821
	}
822
823
	/**
824
	 * Event handler called before writing to the database.
825
	 */
826
	public function onBeforeWrite() {
827
		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...
828
829
		// If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
830
		// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
831
		// but rather a last line of defense against data inconsistencies.
832
		$identifierField = 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...
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...
833
		if($this->$identifierField) {
834
835
			// Note: Same logic as Member_Validator class
836
			$filter = array("\"$identifierField\"" => $this->$identifierField);
837
			if($this->ID) {
838
				$filter[] = array('"Member"."ID" <> ?' => $this->ID);
839
			}
840
			$existingRecord = DataObject::get_one('Member', $filter);
841
842
			if($existingRecord) {
843
				throw new ValidationException(ValidationResult::create(false, _t(
844
					'Member.ValidationIdentifierFailed',
845
					'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
846
					'Values in brackets show "fieldname = value", usually denoting an existing email address',
847
					array(
848
						'id' => $existingRecord->ID,
849
						'name' => $identifierField,
850
						'value' => $this->$identifierField
851
					)
852
				)));
853
			}
854
		}
855
856
		// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
857
		// However, if TestMailer is in use this isn't a risk.
858
		if(
859
			(Director::isLive() || Email::mailer() instanceof TestMailer)
860
			&& $this->isChanged('Password')
861
			&& $this->record['Password']
862
			&& $this->config()->notify_password_change
863
		) {
864
			$e = Member_ChangePasswordEmail::create();
865
			$e->populateTemplate($this);
866
			$e->setTo($this->Email);
867
			$e->send();
868
		}
869
870
		// The test on $this->ID is used for when records are initially created.
871
		// Note that this only works with cleartext passwords, as we can't rehash
872
		// existing passwords.
873
		if((!$this->ID && $this->Password) || $this->isChanged('Password')) {
874
			// Password was changed: encrypt the password according the settings
875
			$encryption_details = Security::encrypt_password(
876
				$this->Password, // this is assumed to be cleartext
877
				$this->Salt,
878
				($this->PasswordEncryption) ?
879
					$this->PasswordEncryption : Security::config()->password_encryption_algorithm,
880
				$this
881
			);
882
883
			// Overwrite the Password property with the hashed value
884
			$this->Password = $encryption_details['password'];
885
			$this->Salt = $encryption_details['salt'];
886
			$this->PasswordEncryption = $encryption_details['algorithm'];
887
888
			// If we haven't manually set a password expiry
889
			if(!$this->isChanged('PasswordExpiry')) {
890
				// then set it for us
891
				if(self::config()->password_expiry_days) {
892
					$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
893
				} else {
894
					$this->PasswordExpiry = null;
895
				}
896
			}
897
		}
898
899
		// save locale
900
		if(!$this->Locale) {
901
			$this->Locale = i18n::get_locale();
902
		}
903
904
		parent::onBeforeWrite();
905
	}
906
907
	public function onAfterWrite() {
908
		parent::onAfterWrite();
909
910
		Permission::flush_permission_cache();
911
912
		if($this->isChanged('Password')) {
913
			MemberPassword::log($this);
914
		}
915
	}
916
917
	public function onAfterDelete() {
918
		parent::onAfterDelete();
919
920
		//prevent orphaned records remaining in the DB
921
		$this->deletePasswordLogs();
922
	}
923
924
	/**
925
	 * Delete the MemberPassword objects that are associated to this user
926
	 *
927
	 * @return self
928
	 */
929
	protected function deletePasswordLogs() {
930
		foreach ($this->LoggedPasswords() as $password) {
931
			$password->delete();
932
			$password->destroy();
933
		}
934
		return $this;
935
	}
936
937
	/**
938
	 * Filter out admin groups to avoid privilege escalation,
939
	 * If any admin groups are requested, deny the whole save operation.
940
	 *
941
	 * @param Array $ids Database IDs of Group records
942
	 * @return boolean True if the change can be accepted
943
	 */
944
	public function onChangeGroups($ids) {
945
		// unless the current user is an admin already OR the logged in user is an admin
946
		if(Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
947
			return true;
948
		}
949
950
		// If there are no admin groups in this set then it's ok
951
		$adminGroups = Permission::get_groups_by_permission('ADMIN');
952
		$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
953
		return count(array_intersect($ids, $adminGroupIDs)) == 0;
954
	}
955
956
957
	/**
958
	 * Check if the member is in one of the given groups.
959
	 *
960
	 * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
961
	 * @param boolean $strict Only determine direct group membership if set to true (Default: false)
962
	 * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
963
	 */
964
	public function inGroups($groups, $strict = false) {
965
		if($groups) foreach($groups as $group) {
966
			if($this->inGroup($group, $strict)) return true;
967
		}
968
969
		return false;
970
	}
971
972
973
	/**
974
	 * Check if the member is in the given group or any parent groups.
975
	 *
976
	 * @param int|Group|string $group Group instance, Group Code or ID
977
	 * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
978
	 * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
979
	 */
980
	public function inGroup($group, $strict = false) {
981
		if(is_numeric($group)) {
982
			$groupCheckObj = DataObject::get_by_id('Group', $group);
983
		} elseif(is_string($group)) {
984
			$groupCheckObj = DataObject::get_one('Group', array(
985
				'"Group"."Code"' => $group
986
			));
987
		} elseif($group instanceof Group) {
988
			$groupCheckObj = $group;
989
		} else {
990
			user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
991
		}
992
993
		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...
994
995
		$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
996
		if($groupCandidateObjs) foreach($groupCandidateObjs as $groupCandidateObj) {
997
			if($groupCandidateObj->ID == $groupCheckObj->ID) return true;
998
		}
999
1000
		return false;
1001
	}
1002
1003
	/**
1004
	 * Adds the member to a group. This will create the group if the given
1005
	 * group code does not return a valid group object.
1006
	 *
1007
	 * @param string $groupcode
1008
	 * @param string Title of the group
1009
	 */
1010
	public function addToGroupByCode($groupcode, $title = "") {
1011
		$group = DataObject::get_one('Group', array(
1012
			'"Group"."Code"' => $groupcode
1013
		));
1014
1015
		if($group) {
1016
			$this->Groups()->add($group);
1017
		} else {
1018
			if(!$title) $title = $groupcode;
1019
1020
			$group = new Group();
1021
			$group->Code = $groupcode;
1022
			$group->Title = $title;
1023
			$group->write();
1024
1025
			$this->Groups()->add($group);
1026
		}
1027
	}
1028
1029
	/**
1030
	 * Removes a member from a group.
1031
	 *
1032
	 * @param string $groupcode
1033
	 */
1034
	public function removeFromGroupByCode($groupcode) {
1035
		$group = Group::get()->filter(array('Code' => $groupcode))->first();
1036
1037
		if($group) {
1038
			$this->Groups()->remove($group);
1039
		}
1040
	}
1041
1042
	/**
1043
	 * @param Array $columns Column names on the Member record to show in {@link getTitle()}.
1044
	 * @param String $sep Separator
1045
	 */
1046
	public static function set_title_columns($columns, $sep = ' ') {
1047
		if (!is_array($columns)) $columns = array($columns);
1048
		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...
1049
	}
1050
1051
	//------------------- HELPER METHODS -----------------------------------//
1052
1053
	/**
1054
	 * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1055
	 * Falls back to showing either field on its own.
1056
	 *
1057
	 * You can overload this getter with {@link set_title_format()}
1058
	 * and {@link set_title_sql()}.
1059
	 *
1060
	 * @return string Returns the first- and surname of the member. If the ID
1061
	 *  of the member is equal 0, only the surname is returned.
1062
	 */
1063
	public function getTitle() {
1064
		$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...
1065
		if ($format) {
1066
			$values = array();
1067
			foreach($format['columns'] as $col) {
1068
				$values[] = $this->getField($col);
1069
			}
1070
			return join($format['sep'], $values);
1071
		}
1072
		if($this->getField('ID') === 0)
1073
			return $this->getField('Surname');
1074
		else{
1075
			if($this->getField('Surname') && $this->getField('FirstName')){
1076
				return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1077
			}elseif($this->getField('Surname')){
1078
				return $this->getField('Surname');
1079
			}elseif($this->getField('FirstName')){
1080
				return $this->getField('FirstName');
1081
			}else{
1082
				return null;
1083
			}
1084
		}
1085
	}
1086
1087
	/**
1088
	 * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1089
	 * Useful for custom queries which assume a certain member title format.
1090
	 *
1091
	 * @param String $tableName
1092
	 * @return String SQL
1093
	 */
1094
	public static function get_title_sql($tableName = 'Member') {
1095
		// This should be abstracted to SSDatabase concatOperator or similar.
1096
		$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...
1097
1098
		$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...
1099
		if ($format) {
1100
			$columnsWithTablename = array();
1101
			foreach($format['columns'] as $column) {
1102
				$columnsWithTablename[] = "\"$tableName\".\"$column\"";
1103
			}
1104
1105
			return "(".join(" $op '".$format['sep']."' $op ", $columnsWithTablename).")";
1106
		} else {
1107
			return "(\"$tableName\".\"Surname\" $op ' ' $op \"$tableName\".\"FirstName\")";
1108
		}
1109
	}
1110
1111
1112
	/**
1113
	 * Get the complete name of the member
1114
	 *
1115
	 * @return string Returns the first- and surname of the member.
1116
	 */
1117
	public function getName() {
1118
		return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1119
	}
1120
1121
1122
	/**
1123
	 * Set first- and surname
1124
	 *
1125
	 * This method assumes that the last part of the name is the surname, e.g.
1126
	 * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1127
	 *
1128
	 * @param string $name The name
1129
	 */
1130
	public function setName($name) {
1131
		$nameParts = explode(' ', $name);
1132
		$this->Surname = array_pop($nameParts);
1133
		$this->FirstName = join(' ', $nameParts);
1134
	}
1135
1136
1137
	/**
1138
	 * Alias for {@link setName}
1139
	 *
1140
	 * @param string $name The name
1141
	 * @see setName()
1142
	 */
1143
	public function splitName($name) {
1144
		return $this->setName($name);
1145
	}
1146
1147
	/**
1148
	 * Override the default getter for DateFormat so the
1149
	 * default format for the user's locale is used
1150
	 * if the user has not defined their own.
1151
	 *
1152
	 * @return string ISO date format
1153
	 */
1154
	public function getDateFormat() {
1155
		if($this->getField('DateFormat')) {
1156
			return $this->getField('DateFormat');
1157
		} else {
1158
			return Config::inst()->get('i18n', 'date_format');
1159
		}
1160
	}
1161
1162
	/**
1163
	 * Override the default getter for TimeFormat so the
1164
	 * default format for the user's locale is used
1165
	 * if the user has not defined their own.
1166
	 *
1167
	 * @return string ISO date format
1168
	 */
1169
	public function getTimeFormat() {
1170
		if($this->getField('TimeFormat')) {
1171
			return $this->getField('TimeFormat');
1172
		} else {
1173
			return Config::inst()->get('i18n', 'time_format');
1174
		}
1175
	}
1176
1177
	//---------------------------------------------------------------------//
1178
1179
1180
	/**
1181
	 * Get a "many-to-many" map that holds for all members their group memberships,
1182
	 * including any parent groups where membership is implied.
1183
	 * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1184
	 *
1185
	 * @todo Push all this logic into Member_GroupSet's getIterator()?
1186
	 * @return Member_Groupset
1187
	 */
1188
	public function Groups() {
1189
		$groups = Member_GroupSet::create('Group', 'Group_Members', 'GroupID', 'MemberID');
1190
		$groups = $groups->forForeignID($this->ID);
1191
1192
		$this->extend('updateGroups', $groups);
1193
1194
		return $groups;
1195
	}
1196
1197
	/**
1198
	 * @return ManyManyList
1199
	 */
1200
	public function DirectGroups() {
1201
		return $this->getManyManyComponents('Groups');
1202
	}
1203
1204
	/**
1205
	 * Get a member SQLMap of members in specific groups
1206
	 *
1207
	 * If no $groups is passed, all members will be returned
1208
	 *
1209
	 * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1210
	 * @return SS_Map Returns an SS_Map that returns all Member data.
1211
	 */
1212
	public static function map_in_groups($groups = null) {
1213
		$groupIDList = array();
1214
1215
		if($groups instanceof SS_List) {
1216
			foreach( $groups as $group ) {
1217
				$groupIDList[] = $group->ID;
1218
			}
1219
		} elseif(is_array($groups)) {
1220
			$groupIDList = $groups;
1221
		} elseif($groups) {
1222
			$groupIDList[] = $groups;
1223
		}
1224
1225
		// No groups, return all Members
1226
		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...
1227
			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...
1228
		}
1229
1230
		$membersList = new ArrayList();
1231
		// This is a bit ineffective, but follow the ORM style
1232
		foreach(Group::get()->byIDs($groupIDList) as $group) {
1233
			$membersList->merge($group->Members());
1234
		}
1235
1236
		$membersList->removeDuplicates('ID');
1237
		return $membersList->map();
1238
	}
1239
1240
1241
	/**
1242
	 * Get a map of all members in the groups given that have CMS permissions
1243
	 *
1244
	 * If no groups are passed, all groups with CMS permissions will be used.
1245
	 *
1246
	 * @param array $groups Groups to consider or NULL to use all groups with
1247
	 *                      CMS permissions.
1248
	 * @return SS_Map Returns a map of all members in the groups given that
1249
	 *                have CMS permissions.
1250
	 */
1251
	public static function mapInCMSGroups($groups = null) {
1252
		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...
1253
			$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1254
1255
			if(class_exists('CMSMain')) {
1256
				$cmsPerms = singleton('CMSMain')->providePermissions();
1257
			} else {
1258
				$cmsPerms = singleton('LeftAndMain')->providePermissions();
1259
			}
1260
1261
			if(!empty($cmsPerms)) {
1262
				$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1263
			}
1264
1265
			$permsClause = DB::placeholders($perms);
1266
			$groups = DataObject::get('Group')
1267
				->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1268
				->where(array(
1269
					"\"Permission\".\"Code\" IN ($permsClause)" => $perms
1270
				));
1271
		}
1272
1273
		$groupIDList = array();
1274
1275
		if(is_a($groups, 'SS_List')) {
1276
			foreach($groups as $group) {
1277
				$groupIDList[] = $group->ID;
1278
			}
1279
		} elseif(is_array($groups)) {
1280
			$groupIDList = $groups;
1281
		}
1282
1283
		$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...
1284
			->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1285
			->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1286
		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...
1287
			$groupClause = DB::placeholders($groupIDList);
1288
			$members = $members->where(array(
1289
				"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1290
			));
1291
		}
1292
1293
		return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1294
	}
1295
1296
1297
	/**
1298
	 * Get the groups in which the member is NOT in
1299
	 *
1300
	 * When passed an array of groups, and a component set of groups, this
1301
	 * function will return the array of groups the member is NOT in.
1302
	 *
1303
	 * @param array $groupList An array of group code names.
1304
	 * @param array $memberGroups A component set of groups (if set to NULL,
1305
	 *                            $this->groups() will be used)
1306
	 * @return array Groups in which the member is NOT in.
1307
	 */
1308
	public function memberNotInGroups($groupList, $memberGroups = null){
1309
		if(!$memberGroups) $memberGroups = $this->Groups();
1310
1311
		foreach($memberGroups as $group) {
1312
			if(in_array($group->Code, $groupList)) {
1313
				$index = array_search($group->Code, $groupList);
1314
				unset($groupList[$index]);
1315
			}
1316
		}
1317
1318
		return $groupList;
1319
	}
1320
1321
1322
	/**
1323
	 * Return a {@link FieldList} of fields that would appropriate for editing
1324
	 * this member.
1325
	 *
1326
	 * @return FieldList Return a FieldList of fields that would appropriate for
1327
	 *                   editing this member.
1328
	 */
1329
	public function getCMSFields() {
1330
		require_once 'Zend/Date.php';
1331
1332
		$self = $this;
1333
		$this->beforeUpdateCMSFields(function($fields) use ($self) {
1334
			$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->Children;
1335
1336
			$password = new ConfirmedPasswordField(
1337
				'Password',
1338
				null,
1339
				null,
1340
				null,
1341
				true // showOnClick
1342
			);
1343
			$password->setCanBeEmpty(true);
1344
			if( ! $self->ID) $password->showOnClick = false;
0 ignored issues
show
Documentation introduced by
The property $showOnClick is declared protected in ConfirmedPasswordField. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1345
			$mainFields->replaceField('Password', $password);
1346
1347
			$mainFields->replaceField('Locale', new DropdownField(
1348
				"Locale",
1349
				_t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1350
				i18n::get_existing_translations()
1351
			));
1352
			$mainFields->removeByName($self->config()->hidden_fields);
1353
1354
			if( ! $self->config()->lock_out_after_incorrect_logins) {
1355
				$mainFields->removeByName('FailedLoginCount');
1356
			}
1357
1358
1359
			// Groups relation will get us into logical conflicts because
1360
			// Members are displayed within  group edit form in SecurityAdmin
1361
			$fields->removeByName('Groups');
1362
1363
			// Members shouldn't be able to directly view/edit logged passwords
1364
			$fields->removeByName('LoggedPasswords');
1365
1366
			$fields->removeByName('RememberLoginHashes');
1367
1368
			if(Permission::check('EDIT_PERMISSIONS')) {
1369
				$groupsMap = array();
1370
				foreach(Group::get() as $group) {
1371
					// Listboxfield values are escaped, use ASCII char instead of &raquo;
1372
					$groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1373
				}
1374
				asort($groupsMap);
1375
				$fields->addFieldToTab('Root.Main',
1376
					ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name())
1377
						->setSource($groupsMap)
1378
						->setAttribute(
1379
							'data-placeholder',
1380
							_t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1381
						)
1382
				);
1383
1384
1385
				// Add permission field (readonly to avoid complicated group assignment logic).
1386
				// This should only be available for existing records, as new records start
1387
				// with no permissions until they have a group assignment anyway.
1388
				if($self->ID) {
1389
					$permissionsField = new PermissionCheckboxSetField_Readonly(
1390
						'Permissions',
1391
						false,
1392
						'Permission',
1393
						'GroupID',
1394
						// we don't want parent relationships, they're automatically resolved in the field
1395
						$self->getManyManyComponents('Groups')
1396
					);
1397
					$fields->findOrMakeTab('Root.Permissions', singleton('Permission')->i18n_plural_name());
1398
					$fields->addFieldToTab('Root.Permissions', $permissionsField);
1399
				}
1400
			}
1401
1402
			$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1403
			if($permissionsTab) $permissionsTab->addExtraClass('readonly');
1404
1405
			$defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($self->Locale));
1406
			$dateFormatMap = array(
1407
				'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'),
1408
				'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'),
1409
				'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'),
1410
				'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'),
1411
			);
1412
			$dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat)
1413
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1414
			$mainFields->push(
1415
				$dateFormatField = new MemberDatetimeOptionsetField(
1416
					'DateFormat',
1417
					$self->fieldLabel('DateFormat'),
1418
					$dateFormatMap
1419
				)
1420
			);
1421
			$dateFormatField->setValue($self->DateFormat);
1422
1423
			$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
1424
			$timeFormatMap = array(
1425
				'h:mm a' => Zend_Date::now()->toString('h:mm a'),
1426
				'H:mm' => Zend_Date::now()->toString('H:mm'),
1427
			);
1428
			$timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat)
1429
				. sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
1430
			$mainFields->push(
1431
				$timeFormatField = new MemberDatetimeOptionsetField(
1432
					'TimeFormat',
1433
					$self->fieldLabel('TimeFormat'),
1434
					$timeFormatMap
1435
				)
1436
			);
1437
			$timeFormatField->setValue($self->TimeFormat);
1438
		});
1439
1440
		return parent::getCMSFields();
1441
	}
1442
1443
	/**
1444
	 *
1445
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
1446
	 *
1447
	 */
1448
	public function fieldLabels($includerelations = true) {
1449
		$labels = parent::fieldLabels($includerelations);
1450
1451
		$labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1452
		$labels['Surname'] = _t('Member.SURNAME', 'Surname');
1453
		$labels['Email'] = _t('Member.EMAIL', 'Email');
1454
		$labels['Password'] = _t('Member.db_Password', 'Password');
1455
		$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1456
		$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1457
		$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1458
		$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
1459
		$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
1460
		if($includerelations){
1461
			$labels['Groups'] = _t('Member.belongs_many_many_Groups', 'Groups',
1462
				'Security Groups this member belongs to');
1463
		}
1464
		return $labels;
1465
	}
1466
1467
    /**
1468
     * Users can view their own record.
1469
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1470
     * This is likely to be customized for social sites etc. with a looser permission model.
1471
     */
1472
    public function canView($member = null) {
1473
        //get member
1474
        if(!($member instanceof Member)) {
1475
            $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...
1476
        }
1477
        //check for extensions, we do this first as they can overrule everything
1478
        $extended = $this->extendedCan(__FUNCTION__, $member);
1479
        if($extended !== null) {
1480
            return $extended;
1481
        }
1482
1483
        //need to be logged in and/or most checks below rely on $member being a Member
1484
        if(!$member) {
1485
            return false;       
1486
        }
1487
        // members can usually view their own record
1488
        if($this->ID == $member->ID) {
1489
            return true;
1490
        }
1491
        //standard check
1492
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1493
    }
1494
    /**
1495
     * Users can edit their own record.
1496
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1497
     */
1498
    public function canEdit($member = null) {
1499
        //get member
1500
        if(!($member instanceof Member)) {
1501
            $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...
1502
        }
1503
        //check for extensions, we do this first as they can overrule everything
1504
        $extended = $this->extendedCan(__FUNCTION__, $member);
1505
        if($extended !== null) {
1506
            return $extended;
1507
        }
1508
1509
        //need to be logged in and/or most checks below rely on $member being a Member
1510
        if(!$member) {
1511
            return false;       
1512
        }
1513
1514
        // HACK: we should not allow for an non-Admin to edit an Admin
1515
        if(!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
1516
            return false;
1517
        }
1518
        // members can usually edit their own record
1519
        if($this->ID == $member->ID) {
1520
            return true;
1521
        }
1522
        //standard check
1523
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1524
    }
1525
    /**
1526
     * Users can edit their own record.
1527
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1528
     */
1529
    public function canDelete($member = null) {
1530
        if(!($member instanceof Member)) {
1531
            $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...
1532
        }
1533
        //check for extensions, we do this first as they can overrule everything
1534
        $extended = $this->extendedCan(__FUNCTION__, $member);
1535
        if($extended !== null) {
1536
            return $extended;
1537
        }
1538
1539
        //need to be logged in and/or most checks below rely on $member being a Member
1540
        if(!$member) {
1541
            return false;       
1542
        }
1543
        // Members are not allowed to remove themselves,
1544
        // since it would create inconsistencies in the admin UIs.
1545
        if($this->ID && $member->ID == $this->ID) {
1546
            return false;           
1547
        }
1548
1549
        // HACK: if you want to delete a member, you have to be a member yourself.
1550
        // this is a hack because what this should do is to stop a user
1551
        // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
1552
        if(Permission::checkMember($this, 'ADMIN')) {
1553
            if( ! Permission::checkMember($member, 'ADMIN')) {
1554
                return false;               
1555
            }
1556
        }
1557
        //standard check
1558
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1559
    }
1560
1561
	/**
1562
	 * Validate this member object.
1563
	 */
1564
	public function validate() {
1565
		$valid = parent::validate();
1566
1567
		if(!$this->ID || $this->isChanged('Password')) {
1568
			if($this->Password && self::$password_validator) {
1569
				$valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1570
			}
1571
		}
1572
1573
		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...
1574
			if($this->SetPassword && self::$password_validator) {
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1575
				$valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this));
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1576
			}
1577
		}
1578
1579
		return $valid;
1580
	}
1581
1582
	/**
1583
	 * Change password. This will cause rehashing according to
1584
	 * the `PasswordEncryption` property.
1585
	 *
1586
	 * @param String $password Cleartext password
1587
	 */
1588
	public function changePassword($password) {
1589
		$this->Password = $password;
1590
		$valid = $this->validate();
1591
1592
		if($valid->valid()) {
1593
			$this->AutoLoginHash = null;
1594
			$this->write();
1595
		}
1596
1597
		return $valid;
1598
	}
1599
1600
	/**
1601
	 * Tell this member that someone made a failed attempt at logging in as them.
1602
	 * This can be used to lock the user out temporarily if too many failed attempts are made.
1603
	 */
1604
	public function registerFailedLogin() {
1605
		if(self::config()->lock_out_after_incorrect_logins) {
1606
			// Keep a tally of the number of failed log-ins so that we can lock people out
1607
			$this->FailedLoginCount = $this->FailedLoginCount + 1;
1608
1609
			if($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1610
				$lockoutMins = self::config()->lock_out_delay_mins;
0 ignored issues
show
Documentation introduced by
The property lock_out_delay_mins does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1611
				$this->LockedOutUntil = date('Y-m-d H:i:s', time() + $lockoutMins*60);
1612
				$this->FailedLoginCount = 0;
1613
			}
1614
		}
1615
		$this->extend('registerFailedLogin');
1616
		$this->write();
1617
	}
1618
1619
	/**
1620
	 * Tell this member that a successful login has been made
1621
	 */
1622
	public function registerSuccessfulLogin() {
1623
		if(self::config()->lock_out_after_incorrect_logins) {
1624
			// Forgive all past login failures
1625
			$this->FailedLoginCount = 0;
1626
			$this->write();
1627
		}
1628
	}
1629
	/**
1630
	 * Get the HtmlEditorConfig for this user to be used in the CMS.
1631
	 * This is set by the group. If multiple configurations are set,
1632
	 * the one with the highest priority wins.
1633
	 *
1634
	 * @return string
1635
	 */
1636
	public function getHtmlEditorConfigForCMS() {
1637
		$currentName = '';
1638
		$currentPriority = 0;
1639
1640
		foreach($this->Groups() as $group) {
1641
			$configName = $group->HtmlEditorConfig;
1642
			if($configName) {
1643
				$config = HtmlEditorConfig::get($group->HtmlEditorConfig);
1644
				if($config && $config->getOption('priority') > $currentPriority) {
1645
					$currentName = $configName;
1646
					$currentPriority = $config->getOption('priority');
1647
				}
1648
			}
1649
		}
1650
1651
		// If can't find a suitable editor, just default to cms
1652
		return $currentName ? $currentName : 'cms';
1653
	}
1654
1655
	public static function get_template_global_variables() {
1656
		return array(
1657
			'CurrentMember' => 'currentUser',
1658
			'currentUser',
1659
		);
1660
	}
1661
}
1662
1663
/**
1664
 * Represents a set of Groups attached to a member.
1665
 * Handles the hierarchy logic.
1666
 * @package framework
1667
 * @subpackage security
1668
 */
1669
class Member_GroupSet extends ManyManyList {
1670
1671
	protected function linkJoinTable() {
1672
		// Do not join the table directly
1673
		if($this->extraFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1674
			user_error('Member_GroupSet does not support many_many_extraFields', E_USER_ERROR);
1675
		}
1676
	}
1677
1678
	/**
1679
	 * Link this group set to a specific member.
1680
	 *
1681
	 * Recursively selects all groups applied to this member, as well as any
1682
	 * parent groups of any applied groups
1683
	 *
1684
	 * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
1685
	 * ids as per getForeignID
1686
	 * @return array Condition In array(SQL => parameters format)
1687
	 */
1688
	public function foreignIDFilter($id = null) {
1689
		if ($id === null) $id = $this->getForeignID();
1690
1691
		// Find directly applied groups
1692
		$manyManyFilter = parent::foreignIDFilter($id);
1693
		$query = new SQLSelect('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter);
0 ignored issues
show
Bug introduced by
It seems like $manyManyFilter defined by parent::foreignIDFilter($id) on line 1692 can also be of type null; however, SQLSelect::__construct() does only seem to accept array, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1694
		$groupIDs = $query->execute()->column();
1695
1696
		// Get all ancestors, iteratively merging these into the master set
1697
		$allGroupIDs = array();
1698
		while($groupIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIDs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1699
			$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
1700
			$groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID");
1701
			$groupIDs = array_filter($groupIDs);
1702
		}
1703
1704
		// Add a filter to this DataList
1705
		if(!empty($allGroupIDs)) {
1706
			$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
1707
			return array("\"Group\".\"ID\" IN ($allGroupIDsPlaceholders)" => $allGroupIDs);
1708
		} else {
1709
			return array('"Group"."ID"' => 0);
1710
		}
1711
	}
1712
1713
	public function foreignIDWriteFilter($id = null) {
1714
		// Use the ManyManyList::foreignIDFilter rather than the one
1715
		// in this class, otherwise we end up selecting all inherited groups
1716
		return parent::foreignIDFilter($id);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (foreignIDFilter() instead of foreignIDWriteFilter()). Are you sure this is correct? If so, you might want to change this to $this->foreignIDFilter().

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

Consider the following code:

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

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

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

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

Loading history...
1717
	}
1718
1719
	public function add($item, $extraFields = null) {
1720
		// Get Group.ID
1721
		$itemID = null;
1722
		if(is_numeric($item)) {
1723
			$itemID = $item;
1724
		} else if($item instanceof Group) {
1725
			$itemID = $item->ID;
1726
		}
1727
1728
		// Check if this group is allowed to be added
1729
		if($this->canAddGroups(array($itemID))) {
1730
			parent::add($item, $extraFields);
1731
		}
1732
	}
1733
1734
	/**
1735
	 * Determine if the following groups IDs can be added
1736
	 *
1737
	 * @param array $itemIDs
1738
	 * @return boolean
1739
	 */
1740
	protected function canAddGroups($itemIDs) {
1741
		if(empty($itemIDs)) {
1742
			return true;
1743
		}
1744
		$member = $this->getMember();
1745
		return empty($member) || $member->onChangeGroups($itemIDs);
1746
	}
1747
1748
	/**
1749
	 * Get foreign member record for this relation
1750
	 *
1751
	 * @return Member
1752
	 */
1753
	protected function getMember() {
1754
		$id = $this->getForeignID();
1755
		if($id) {
1756
			return DataObject::get_by_id('Member', $id);
1757
		}
1758
	}
1759
}
1760
1761
/**
1762
 * Class used as template to send an email saying that the password has been
1763
 * changed.
1764
 *
1765
 * @package framework
1766
 * @subpackage security
1767
 */
1768
class Member_ChangePasswordEmail extends Email {
1769
1770
	protected $from = '';   // setting a blank from address uses the site's default administrator email
1771
	protected $subject = '';
1772
	protected $ss_template = 'ChangePasswordEmail';
1773
1774
	public function __construct() {
1775
		parent::__construct();
1776
1777
		$this->subject = _t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject');
1778
	}
1779
}
1780
1781
1782
1783
/**
1784
 * Class used as template to send the forgot password email
1785
 *
1786
 * @package framework
1787
 * @subpackage security
1788
 */
1789
class Member_ForgotPasswordEmail extends Email {
1790
	protected $from = '';  // setting a blank from address uses the site's default administrator email
1791
	protected $subject = '';
1792
	protected $ss_template = 'ForgotPasswordEmail';
1793
1794
	public function __construct() {
1795
		parent::__construct();
1796
1797
		$this->subject = _t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject');
1798
	}
1799
}
1800
1801
/**
1802
 * Member Validator
1803
 *
1804
 * Custom validation for the Member object can be achieved either through an
1805
 * {@link DataExtension} on the Member object or, by specifying a subclass of
1806
 * {@link Member_Validator} through the {@link Injector} API.
1807
 *
1808
 * {@see Member::getValidator()}
1809
 *
1810
 * @package framework
1811
 * @subpackage security
1812
 */
1813
class Member_Validator extends RequiredFields {
1814
1815
	protected $customRequired = array(
1816
		'FirstName',
1817
		'Email'
1818
	);
1819
1820
1821
	/**
1822
	 * Constructor
1823
	 */
1824
	public function __construct() {
1825
		$required = func_get_args();
1826
1827
		if(isset($required[0]) && is_array($required[0])) {
1828
			$required = $required[0];
1829
		}
1830
1831
		$required = array_merge($required, $this->customRequired);
1832
1833
		parent::__construct($required);
1834
	}
1835
1836
	/**
1837
	 * Check if the submitted member data is valid (server-side)
1838
	 *
1839
	 * Check if a member with that email doesn't already exist, or if it does
1840
	 * that it is this member.
1841
	 *
1842
	 * @param array $data Submitted data
1843
	 * @return bool Returns TRUE if the submitted data is valid, otherwise
1844
	 *              FALSE.
1845
	 */
1846
	public function php($data) {
0 ignored issues
show
Coding Style introduced by
php uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1847
		$valid = parent::php($data);
1848
1849
		$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...
1850
		$member = DataObject::get_one('Member', array(
1851
			"\"$identifierField\"" => $data[$identifierField]
1852
		));
1853
1854
		// if we are in a complex table field popup, use ctf[childID], else use ID
1855
		if(isset($_REQUEST['ctf']['childID'])) {
1856
			$id = $_REQUEST['ctf']['childID'];
1857
		} elseif(isset($_REQUEST['ID'])) {
1858
			$id = $_REQUEST['ID'];
1859
		} else {
1860
			$id = null;
1861
		}
1862
1863
		if($id && is_object($member) && $member->ID != $id) {
1864
			$uniqueField = $this->form->Fields()->dataFieldByName($identifierField);
1865
			$this->validationError(
1866
				$uniqueField->id(),
1867
				_t(
1868
					'Member.VALIDATIONMEMBEREXISTS',
1869
					'A member already exists with the same %s',
1870
					array('identifier' => strtolower($identifierField))
1871
				),
1872
				'required'
1873
			);
1874
			$valid = false;
1875
		}
1876
1877
		// Execute the validators on the extensions
1878
		if($this->extension_instances) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type Extension[] 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...
1879
			foreach($this->extension_instances as $extension) {
1880
				if(method_exists($extension, 'hasMethod') && $extension->hasMethod('updatePHP')) {
1881
					$valid &= $extension->updatePHP($data, $this->form);
1882
				}
1883
			}
1884
		}
1885
1886
		return $valid;
1887
	}
1888
1889
}
1890