1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Security; |
4
|
|
|
|
5
|
|
|
use IntlDateFormatter; |
6
|
|
|
use InvalidArgumentException; |
7
|
|
|
use SilverStripe\Admin\LeftAndMain; |
8
|
|
|
use SilverStripe\CMS\Controllers\CMSMain; |
9
|
|
|
use SilverStripe\Control\Controller; |
10
|
|
|
use SilverStripe\Control\Director; |
11
|
|
|
use SilverStripe\Control\Email\Email; |
12
|
|
|
use SilverStripe\Control\Email\Mailer; |
13
|
|
|
use SilverStripe\Core\Convert; |
14
|
|
|
use SilverStripe\Core\Injector\Injector; |
15
|
|
|
use SilverStripe\Dev\Deprecation; |
16
|
|
|
use SilverStripe\Dev\TestMailer; |
17
|
|
|
use SilverStripe\Forms\ConfirmedPasswordField; |
18
|
|
|
use SilverStripe\Forms\DropdownField; |
19
|
|
|
use SilverStripe\Forms\FieldList; |
20
|
|
|
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; |
21
|
|
|
use SilverStripe\Forms\ListboxField; |
22
|
|
|
use SilverStripe\i18n\i18n; |
23
|
|
|
use SilverStripe\ORM\ArrayList; |
24
|
|
|
use SilverStripe\ORM\DataList; |
25
|
|
|
use SilverStripe\ORM\DataObject; |
26
|
|
|
use SilverStripe\ORM\DB; |
27
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime; |
28
|
|
|
use SilverStripe\ORM\HasManyList; |
29
|
|
|
use SilverStripe\ORM\ManyManyList; |
30
|
|
|
use SilverStripe\ORM\Map; |
31
|
|
|
use SilverStripe\ORM\SS_List; |
32
|
|
|
use SilverStripe\ORM\ValidationException; |
33
|
|
|
use SilverStripe\ORM\ValidationResult; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* The member class which represents the users of the system |
37
|
|
|
* |
38
|
|
|
* @method HasManyList LoggedPasswords() |
39
|
|
|
* @method HasManyList RememberLoginHashes() |
40
|
|
|
* @property string $FirstName |
41
|
|
|
* @property string $Surname |
42
|
|
|
* @property string $Email |
43
|
|
|
* @property string $Password |
44
|
|
|
* @property string $TempIDHash |
45
|
|
|
* @property string $TempIDExpired |
46
|
|
|
* @property string $AutoLoginHash |
47
|
|
|
* @property string $AutoLoginExpired |
48
|
|
|
* @property string $PasswordEncryption |
49
|
|
|
* @property string $Salt |
50
|
|
|
* @property string $PasswordExpiry |
51
|
|
|
* @property string $LockedOutUntil |
52
|
|
|
* @property string $Locale |
53
|
|
|
* @property int $FailedLoginCount |
54
|
|
|
* @property string $DateFormat |
55
|
|
|
* @property string $TimeFormat |
56
|
|
|
* @property string $SetPassword Pseudo-DB field for temp storage. Not emitted to DB |
57
|
|
|
*/ |
58
|
|
|
class Member extends DataObject |
59
|
|
|
{ |
60
|
|
|
|
61
|
|
|
private static $db = array( |
62
|
|
|
'FirstName' => 'Varchar', |
63
|
|
|
'Surname' => 'Varchar', |
64
|
|
|
'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character) |
65
|
|
|
'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication |
66
|
|
|
'TempIDExpired' => 'Datetime', // Expiry of temp login |
67
|
|
|
'Password' => 'Varchar(160)', |
68
|
|
|
'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset |
69
|
|
|
'AutoLoginExpired' => 'Datetime', |
70
|
|
|
// This is an arbitrary code pointing to a PasswordEncryptor instance, |
71
|
|
|
// not an actual encryption algorithm. |
72
|
|
|
// Warning: Never change this field after its the first password hashing without |
73
|
|
|
// providing a new cleartext password as well. |
74
|
|
|
'PasswordEncryption' => "Varchar(50)", |
75
|
|
|
'Salt' => 'Varchar(50)', |
76
|
|
|
'PasswordExpiry' => 'Date', |
77
|
|
|
'LockedOutUntil' => 'Datetime', |
78
|
|
|
'Locale' => 'Varchar(6)', |
79
|
|
|
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set |
80
|
|
|
'FailedLoginCount' => 'Int', |
81
|
|
|
); |
82
|
|
|
|
83
|
|
|
private static $belongs_many_many = array( |
84
|
|
|
'Groups' => Group::class, |
85
|
|
|
); |
86
|
|
|
|
87
|
|
|
private static $has_many = array( |
88
|
|
|
'LoggedPasswords' => MemberPassword::class, |
89
|
|
|
'RememberLoginHashes' => RememberLoginHash::class, |
90
|
|
|
); |
91
|
|
|
|
92
|
|
|
private static $table_name = "Member"; |
93
|
|
|
|
94
|
|
|
private static $default_sort = '"Surname", "FirstName"'; |
95
|
|
|
|
96
|
|
|
private static $indexes = array( |
97
|
|
|
'Email' => true, |
98
|
|
|
//Removed due to duplicate null values causing MSSQL problems |
99
|
|
|
//'AutoLoginHash' => Array('type'=>'unique', 'value'=>'AutoLoginHash', 'ignoreNulls'=>true) |
|
|
|
|
100
|
|
|
); |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* @config |
104
|
|
|
* @var boolean |
105
|
|
|
*/ |
106
|
|
|
private static $notify_password_change = false; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* All searchable database columns |
110
|
|
|
* in this object, currently queried |
111
|
|
|
* with a "column LIKE '%keywords%' |
112
|
|
|
* statement. |
113
|
|
|
* |
114
|
|
|
* @var array |
115
|
|
|
* @todo Generic implementation of $searchable_fields on DataObject, |
116
|
|
|
* with definition for different searching algorithms |
117
|
|
|
* (LIKE, FULLTEXT) and default FormFields to construct a searchform. |
118
|
|
|
*/ |
119
|
|
|
private static $searchable_fields = array( |
120
|
|
|
'FirstName', |
121
|
|
|
'Surname', |
122
|
|
|
'Email', |
123
|
|
|
); |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* @config |
127
|
|
|
* @var array |
128
|
|
|
*/ |
129
|
|
|
private static $summary_fields = array( |
130
|
|
|
'FirstName', |
131
|
|
|
'Surname', |
132
|
|
|
'Email', |
133
|
|
|
); |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* @config |
137
|
|
|
* @var array |
138
|
|
|
*/ |
139
|
|
|
private static $casting = array( |
140
|
|
|
'Name' => 'Varchar', |
141
|
|
|
); |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Internal-use only fields |
145
|
|
|
* |
146
|
|
|
* @config |
147
|
|
|
* @var array |
148
|
|
|
*/ |
149
|
|
|
private static $hidden_fields = array( |
150
|
|
|
'AutoLoginHash', |
151
|
|
|
'AutoLoginExpired', |
152
|
|
|
'PasswordEncryption', |
153
|
|
|
'PasswordExpiry', |
154
|
|
|
'LockedOutUntil', |
155
|
|
|
'TempIDHash', |
156
|
|
|
'TempIDExpired', |
157
|
|
|
'Salt', |
158
|
|
|
); |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* @config |
162
|
|
|
* @var array See {@link set_title_columns()} |
163
|
|
|
*/ |
164
|
|
|
private static $title_format = null; |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* The unique field used to identify this member. |
168
|
|
|
* By default, it's "Email", but another common |
169
|
|
|
* field could be Username. |
170
|
|
|
* |
171
|
|
|
* @config |
172
|
|
|
* @var string |
173
|
|
|
* @skipUpgrade |
174
|
|
|
*/ |
175
|
|
|
private static $unique_identifier_field = 'Email'; |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Object for validating user's password |
179
|
|
|
* |
180
|
|
|
* @config |
181
|
|
|
* @var PasswordValidator |
182
|
|
|
*/ |
183
|
|
|
private static $password_validator = null; |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @config |
187
|
|
|
* The number of days that a password should be valid for. |
188
|
|
|
* By default, this is null, which means that passwords never expire |
189
|
|
|
*/ |
190
|
|
|
private static $password_expiry_days = null; |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* @config |
194
|
|
|
* @var bool enable or disable logging of previously used passwords. See {@link onAfterWrite} |
195
|
|
|
*/ |
196
|
|
|
private static $password_logging_enabled = true; |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @config |
200
|
|
|
* @var Int Number of incorrect logins after which |
201
|
|
|
* the user is blocked from further attempts for the timespan |
202
|
|
|
* defined in {@link $lock_out_delay_mins}. |
203
|
|
|
*/ |
204
|
|
|
private static $lock_out_after_incorrect_logins = 10; |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @config |
208
|
|
|
* @var integer Minutes of enforced lockout after incorrect password attempts. |
209
|
|
|
* Only applies if {@link $lock_out_after_incorrect_logins} greater than 0. |
210
|
|
|
*/ |
211
|
|
|
private static $lock_out_delay_mins = 15; |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* @config |
215
|
|
|
* @var String If this is set, then a session cookie with the given name will be set on log-in, |
216
|
|
|
* and cleared on logout. |
217
|
|
|
*/ |
218
|
|
|
private static $login_marker_cookie = null; |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Indicates that when a {@link Member} logs in, Member:session_regenerate_id() |
222
|
|
|
* should be called as a security precaution. |
223
|
|
|
* |
224
|
|
|
* This doesn't always work, especially if you're trying to set session cookies |
225
|
|
|
* across an entire site using the domain parameter to session_set_cookie_params() |
226
|
|
|
* |
227
|
|
|
* @config |
228
|
|
|
* @var boolean |
229
|
|
|
*/ |
230
|
|
|
private static $session_regenerate_id = true; |
231
|
|
|
|
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Default lifetime of temporary ids. |
235
|
|
|
* |
236
|
|
|
* This is the period within which a user can be re-authenticated within the CMS by entering only their password |
237
|
|
|
* and without losing their workspace. |
238
|
|
|
* |
239
|
|
|
* Any session expiration outside of this time will require them to login from the frontend using their full |
240
|
|
|
* username and password. |
241
|
|
|
* |
242
|
|
|
* Defaults to 72 hours. Set to zero to disable expiration. |
243
|
|
|
* |
244
|
|
|
* @config |
245
|
|
|
* @var int Lifetime in seconds |
246
|
|
|
*/ |
247
|
|
|
private static $temp_id_lifetime = 259200; |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Ensure the locale is set to something sensible by default. |
251
|
|
|
*/ |
252
|
|
|
public function populateDefaults() |
253
|
|
|
{ |
254
|
|
|
parent::populateDefaults(); |
255
|
|
|
$this->Locale = i18n::get_closest_translation(i18n::get_locale()); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
public function requireDefaultRecords() |
259
|
|
|
{ |
260
|
|
|
parent::requireDefaultRecords(); |
261
|
|
|
// Default groups should've been built by Group->requireDefaultRecords() already |
262
|
|
|
static::default_admin(); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Get the default admin record if it exists, or creates it otherwise if enabled |
267
|
|
|
* |
268
|
|
|
* @return Member |
269
|
|
|
*/ |
270
|
|
|
public static function default_admin() |
271
|
|
|
{ |
272
|
|
|
// Check if set |
273
|
|
|
if (!Security::has_default_admin()) { |
274
|
|
|
return null; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
// Find or create ADMIN group |
278
|
|
|
Group::singleton()->requireDefaultRecords(); |
279
|
|
|
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first(); |
280
|
|
|
|
281
|
|
|
// Find member |
282
|
|
|
/** @skipUpgrade */ |
283
|
|
|
$admin = static::get() |
284
|
|
|
->filter('Email', Security::default_admin_username()) |
285
|
|
|
->first(); |
286
|
|
|
if (!$admin) { |
287
|
|
|
// 'Password' is not set to avoid creating |
288
|
|
|
// persistent logins in the database. See Security::setDefaultAdmin(). |
289
|
|
|
// Set 'Email' to identify this as the default admin |
290
|
|
|
$admin = Member::create(); |
|
|
|
|
291
|
|
|
$admin->FirstName = _t(__CLASS__ . '.DefaultAdminFirstname', 'Default Admin'); |
292
|
|
|
$admin->Email = Security::default_admin_username(); |
293
|
|
|
$admin->write(); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// Ensure this user is in the admin group |
297
|
|
|
if (!$admin->inGroup($adminGroup)) { |
298
|
|
|
// Add member to group instead of adding group to member |
299
|
|
|
// This bypasses the privilege escallation code in Member_GroupSet |
300
|
|
|
$adminGroup |
301
|
|
|
->DirectMembers() |
302
|
|
|
->add($admin); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return $admin; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Check if the passed password matches the stored one (if the member is not locked out). |
310
|
|
|
* |
311
|
|
|
* @param string $password |
312
|
|
|
* @return ValidationResult |
313
|
|
|
*/ |
314
|
|
|
public function checkPassword($password) |
315
|
|
|
{ |
316
|
|
|
$result = $this->canLogIn(); |
317
|
|
|
|
318
|
|
|
// Short-circuit the result upon failure, no further checks needed. |
319
|
|
|
if (!$result->isValid()) { |
320
|
|
|
return $result; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
// Allow default admin to login as self |
324
|
|
|
if ($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) { |
325
|
|
|
return $result; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
// Check a password is set on this member |
329
|
|
|
if (empty($this->Password) && $this->exists()) { |
330
|
|
|
$result->addError(_t(__CLASS__ . '.NoPassword', 'There is no password on this member.')); |
331
|
|
|
|
332
|
|
|
return $result; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); |
336
|
|
|
if (!$e->check($this->Password, $password, $this->Salt, $this)) { |
337
|
|
|
$result->addError(_t( |
338
|
|
|
__CLASS__ . '.ERRORWRONGCRED', |
339
|
|
|
'The provided details don\'t seem to be correct. Please try again.' |
340
|
|
|
)); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
return $result; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Check if this user is the currently configured default admin |
348
|
|
|
* |
349
|
|
|
* @return bool |
350
|
|
|
*/ |
351
|
|
|
public function isDefaultAdmin() |
352
|
|
|
{ |
353
|
|
|
return Security::has_default_admin() |
354
|
|
|
&& $this->Email === Security::default_admin_username(); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid |
359
|
|
|
* one with error messages to display if the member is locked out. |
360
|
|
|
* |
361
|
|
|
* You can hook into this with a "canLogIn" method on an attached extension. |
362
|
|
|
* |
363
|
|
|
* @return ValidationResult |
364
|
|
|
*/ |
365
|
|
|
public function canLogIn() |
366
|
|
|
{ |
367
|
|
|
$result = ValidationResult::create(); |
368
|
|
|
|
369
|
|
|
if ($this->isLockedOut()) { |
370
|
|
|
$result->addError( |
371
|
|
|
_t( |
372
|
|
|
__CLASS__ . '.ERRORLOCKEDOUT2', |
373
|
|
|
'Your account has been temporarily disabled because of too many failed attempts at ' . |
374
|
|
|
'logging in. Please try again in {count} minutes.', |
375
|
|
|
null, |
376
|
|
|
array('count' => static::config()->get('lock_out_delay_mins')) |
377
|
|
|
) |
378
|
|
|
); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
$this->extend('canLogIn', $result); |
382
|
|
|
|
383
|
|
|
return $result; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Returns true if this user is locked out |
388
|
|
|
* |
389
|
|
|
* @return bool |
390
|
|
|
*/ |
391
|
|
|
public function isLockedOut() |
392
|
|
|
{ |
393
|
|
|
if (!$this->LockedOutUntil) { |
394
|
|
|
return false; |
395
|
|
|
} |
396
|
|
|
|
397
|
|
|
return DBDatetime::now()->getTimestamp() < $this->dbObject('LockedOutUntil')->getTimestamp(); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* Set a {@link PasswordValidator} object to use to validate member's passwords. |
402
|
|
|
* |
403
|
|
|
* @param PasswordValidator $pv |
404
|
|
|
*/ |
405
|
|
|
public static function set_password_validator($pv) |
406
|
|
|
{ |
407
|
|
|
self::$password_validator = $pv; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* Returns the current {@link PasswordValidator} |
412
|
|
|
* |
413
|
|
|
* @return PasswordValidator |
414
|
|
|
*/ |
415
|
|
|
public static function password_validator() |
416
|
|
|
{ |
417
|
|
|
return self::$password_validator; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
|
421
|
|
|
public function isPasswordExpired() |
422
|
|
|
{ |
423
|
|
|
if (!$this->PasswordExpiry) { |
424
|
|
|
return false; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* @deprecated 5.0.0 Use Security::setCurrentUser() or IdentityStore::logIn() |
432
|
|
|
* |
433
|
|
|
*/ |
434
|
|
|
public function logIn() |
435
|
|
|
{ |
436
|
|
|
Deprecation::notice( |
437
|
|
|
'5.0.0', |
438
|
|
|
'This method is deprecated and only logs in for the current request. Please use Security::setCurrentUser($user) or an IdentityStore' |
439
|
|
|
); |
440
|
|
|
Security::setCurrentUser($this); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Called before a member is logged in via session/cookie/etc |
445
|
|
|
*/ |
446
|
|
|
public function beforeMemberLoggedIn() |
447
|
|
|
{ |
448
|
|
|
// @todo Move to middleware on the AuthenticationRequestFilter IdentityStore |
449
|
|
|
$this->extend('beforeMemberLoggedIn'); |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
/** |
453
|
|
|
* Called after a member is logged in via session/cookie/etc |
454
|
|
|
*/ |
455
|
|
|
public function afterMemberLoggedIn() |
456
|
|
|
{ |
457
|
|
|
// Clear the incorrect log-in count |
458
|
|
|
$this->registerSuccessfulLogin(); |
459
|
|
|
|
460
|
|
|
$this->LockedOutUntil = null; |
461
|
|
|
|
462
|
|
|
$this->regenerateTempID(); |
463
|
|
|
|
464
|
|
|
$this->write(); |
465
|
|
|
|
466
|
|
|
// Audit logging hook |
467
|
|
|
$this->extend('afterMemberLoggedIn'); |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
/** |
471
|
|
|
* Trigger regeneration of TempID. |
472
|
|
|
* |
473
|
|
|
* This should be performed any time the user presents their normal identification (normally Email) |
474
|
|
|
* and is successfully authenticated. |
475
|
|
|
*/ |
476
|
|
|
public function regenerateTempID() |
477
|
|
|
{ |
478
|
|
|
$generator = new RandomGenerator(); |
479
|
|
|
$lifetime = self::config()->get('temp_id_lifetime'); |
480
|
|
|
$this->TempIDHash = $generator->randomToken('sha1'); |
481
|
|
|
$this->TempIDExpired = $lifetime |
482
|
|
|
? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + $lifetime) |
483
|
|
|
: null; |
484
|
|
|
$this->write(); |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Check if the member ID logged in session actually |
489
|
|
|
* has a database record of the same ID. If there is |
490
|
|
|
* no logged in user, FALSE is returned anyway. |
491
|
|
|
* |
492
|
|
|
* @deprecated Not needed anymore, as it returns Security::getCurrentUser(); |
493
|
|
|
* |
494
|
|
|
* @return boolean TRUE record found FALSE no record found |
495
|
|
|
*/ |
496
|
|
|
public static function logged_in_session_exists() |
497
|
|
|
{ |
498
|
|
|
Deprecation::notice( |
499
|
|
|
'5.0.0', |
500
|
|
|
'This method is deprecated and now does not add value. Please use Security::getCurrentUser()' |
501
|
|
|
); |
502
|
|
|
|
503
|
|
|
if ($member = Security::getCurrentUser()) { |
504
|
|
|
if ($member && $member->exists()) { |
505
|
|
|
return true; |
506
|
|
|
} |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
return false; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
/** |
513
|
|
|
* @deprecated Use Security::setCurrentUser(null) or an IdentityStore |
514
|
|
|
* Logs this member out. |
515
|
|
|
*/ |
516
|
|
|
public function logOut() |
517
|
|
|
{ |
518
|
|
|
Deprecation::notice( |
519
|
|
|
'5.0.0', |
520
|
|
|
'This method is deprecated and now does not persist. Please use Security::setCurrentUser(null) or an IdenityStore' |
521
|
|
|
); |
522
|
|
|
|
523
|
|
|
$this->extend('beforeMemberLoggedOut'); |
524
|
|
|
|
525
|
|
|
Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest()); |
526
|
|
|
// Audit logging hook |
527
|
|
|
$this->extend('afterMemberLoggedOut'); |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
|
|
* Utility for generating secure password hashes for this member. |
532
|
|
|
* |
533
|
|
|
* @param string $string |
534
|
|
|
* @return string |
535
|
|
|
* @throws PasswordEncryptor_NotFoundException |
536
|
|
|
*/ |
537
|
|
|
public function encryptWithUserSettings($string) |
538
|
|
|
{ |
539
|
|
|
if (!$string) { |
540
|
|
|
return null; |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
// If the algorithm or salt is not available, it means we are operating |
544
|
|
|
// on legacy account with unhashed password. Do not hash the string. |
545
|
|
|
if (!$this->PasswordEncryption) { |
546
|
|
|
return $string; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
// We assume we have PasswordEncryption and Salt available here. |
550
|
|
|
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); |
551
|
|
|
|
552
|
|
|
return $e->encrypt($string, $this->Salt); |
553
|
|
|
} |
554
|
|
|
|
555
|
|
|
/** |
556
|
|
|
* Generate an auto login token which can be used to reset the password, |
557
|
|
|
* at the same time hashing it and storing in the database. |
558
|
|
|
* |
559
|
|
|
* @param int $lifetime The lifetime of the auto login hash in days (by default 2 days) |
560
|
|
|
* |
561
|
|
|
* @returns string Token that should be passed to the client (but NOT persisted). |
562
|
|
|
* |
563
|
|
|
* @todo Make it possible to handle database errors such as a "duplicate key" error |
564
|
|
|
*/ |
565
|
|
|
public function generateAutologinTokenAndStoreHash($lifetime = 2) |
566
|
|
|
{ |
567
|
|
|
do { |
568
|
|
|
$generator = new RandomGenerator(); |
569
|
|
|
$token = $generator->randomToken(); |
570
|
|
|
$hash = $this->encryptWithUserSettings($token); |
571
|
|
|
} while (DataObject::get_one(Member::class, array( |
|
|
|
|
572
|
|
|
'"Member"."AutoLoginHash"' => $hash |
573
|
|
|
))); |
574
|
|
|
|
575
|
|
|
$this->AutoLoginHash = $hash; |
576
|
|
|
$this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime)); |
577
|
|
|
|
578
|
|
|
$this->write(); |
579
|
|
|
|
580
|
|
|
return $token; |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
/** |
584
|
|
|
* Check the token against the member. |
585
|
|
|
* |
586
|
|
|
* @param string $autologinToken |
587
|
|
|
* |
588
|
|
|
* @returns bool Is token valid? |
589
|
|
|
*/ |
590
|
|
|
public function validateAutoLoginToken($autologinToken) |
591
|
|
|
{ |
592
|
|
|
$hash = $this->encryptWithUserSettings($autologinToken); |
593
|
|
|
$member = self::member_from_autologinhash($hash, false); |
594
|
|
|
|
595
|
|
|
return (bool)$member; |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* Return the member for the auto login hash |
600
|
|
|
* |
601
|
|
|
* @param string $hash The hash key |
602
|
|
|
* @param bool $login Should the member be logged in? |
603
|
|
|
* |
604
|
|
|
* @return Member the matching member, if valid |
605
|
|
|
* @return Member |
606
|
|
|
*/ |
607
|
|
|
public static function member_from_autologinhash($hash, $login = false) |
608
|
|
|
{ |
609
|
|
|
/** @var Member $member */ |
610
|
|
|
$member = static::get()->filter([ |
611
|
|
|
'AutoLoginHash' => $hash, |
612
|
|
|
'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(), |
613
|
|
|
])->first(); |
614
|
|
|
|
615
|
|
|
if ($login && $member) { |
616
|
|
|
Injector::inst()->get(IdentityStore::class)->logIn($member); |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
return $member; |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
/** |
623
|
|
|
* Find a member record with the given TempIDHash value |
624
|
|
|
* |
625
|
|
|
* @param string $tempid |
626
|
|
|
* @return Member |
627
|
|
|
*/ |
628
|
|
|
public static function member_from_tempid($tempid) |
629
|
|
|
{ |
630
|
|
|
$members = static::get() |
631
|
|
|
->filter('TempIDHash', $tempid); |
632
|
|
|
|
633
|
|
|
// Exclude expired |
634
|
|
|
if (static::config()->get('temp_id_lifetime')) { |
635
|
|
|
/** @var DataList|Member[] $members */ |
636
|
|
|
$members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue()); |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
return $members->first(); |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
/** |
643
|
|
|
* Returns the fields for the member form - used in the registration/profile module. |
644
|
|
|
* It should return fields that are editable by the admin and the logged-in user. |
645
|
|
|
* |
646
|
|
|
* @todo possibly move this to an extension |
647
|
|
|
* |
648
|
|
|
* @return FieldList Returns a {@link FieldList} containing the fields for |
649
|
|
|
* the member form. |
650
|
|
|
*/ |
651
|
|
|
public function getMemberFormFields() |
652
|
|
|
{ |
653
|
|
|
$fields = parent::getFrontEndFields(); |
|
|
|
|
654
|
|
|
|
655
|
|
|
$fields->replaceField('Password', $this->getMemberPasswordField()); |
656
|
|
|
|
657
|
|
|
$fields->replaceField('Locale', new DropdownField( |
658
|
|
|
'Locale', |
659
|
|
|
$this->fieldLabel('Locale'), |
660
|
|
|
i18n::getSources()->getKnownLocales() |
661
|
|
|
)); |
662
|
|
|
|
663
|
|
|
$fields->removeByName(static::config()->get('hidden_fields')); |
664
|
|
|
$fields->removeByName('FailedLoginCount'); |
665
|
|
|
|
666
|
|
|
|
667
|
|
|
$this->extend('updateMemberFormFields', $fields); |
668
|
|
|
|
669
|
|
|
return $fields; |
670
|
|
|
} |
671
|
|
|
|
672
|
|
|
/** |
673
|
|
|
* Builds "Change / Create Password" field for this member |
674
|
|
|
* |
675
|
|
|
* @return ConfirmedPasswordField |
676
|
|
|
*/ |
677
|
|
|
public function getMemberPasswordField() |
678
|
|
|
{ |
679
|
|
|
$editingPassword = $this->isInDB(); |
680
|
|
|
$label = $editingPassword |
681
|
|
|
? _t(__CLASS__ . '.EDIT_PASSWORD', 'New Password') |
682
|
|
|
: $this->fieldLabel('Password'); |
683
|
|
|
/** @var ConfirmedPasswordField $password */ |
684
|
|
|
$password = ConfirmedPasswordField::create( |
685
|
|
|
'Password', |
686
|
|
|
$label, |
687
|
|
|
null, |
688
|
|
|
null, |
689
|
|
|
$editingPassword |
690
|
|
|
); |
691
|
|
|
|
692
|
|
|
// If editing own password, require confirmation of existing |
693
|
|
|
if ($editingPassword && $this->ID == Security::getCurrentUser()->ID) { |
694
|
|
|
$password->setRequireExistingPassword(true); |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
$password->setCanBeEmpty(true); |
698
|
|
|
$this->extend('updateMemberPasswordField', $password); |
699
|
|
|
|
700
|
|
|
return $password; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
|
704
|
|
|
/** |
705
|
|
|
* Returns the {@link RequiredFields} instance for the Member object. This |
706
|
|
|
* Validator is used when saving a {@link CMSProfileController} or added to |
707
|
|
|
* any form responsible for saving a users data. |
708
|
|
|
* |
709
|
|
|
* To customize the required fields, add a {@link DataExtension} to member |
710
|
|
|
* calling the `updateValidator()` method. |
711
|
|
|
* |
712
|
|
|
* @return Member_Validator |
713
|
|
|
*/ |
714
|
|
|
public function getValidator() |
715
|
|
|
{ |
716
|
|
|
$validator = Member_Validator::create(); |
717
|
|
|
$validator->setForMember($this); |
718
|
|
|
$this->extend('updateValidator', $validator); |
719
|
|
|
|
720
|
|
|
return $validator; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
|
724
|
|
|
/** |
725
|
|
|
* Returns the current logged in user |
726
|
|
|
* |
727
|
|
|
* @deprecated 5.0.0 use Security::getCurrentUser() |
728
|
|
|
* |
729
|
|
|
* @return Member |
730
|
|
|
*/ |
731
|
|
|
public static function currentUser() |
732
|
|
|
{ |
733
|
|
|
Deprecation::notice( |
734
|
|
|
'5.0.0', |
735
|
|
|
'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore' |
736
|
|
|
); |
737
|
|
|
|
738
|
|
|
return Security::getCurrentUser(); |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
/** |
742
|
|
|
* Temporarily act as the specified user, limited to a $callback, but |
743
|
|
|
* without logging in as that user. |
744
|
|
|
* |
745
|
|
|
* E.g. |
746
|
|
|
* <code> |
747
|
|
|
* Member::logInAs(Security::findAnAdministrator(), function() { |
748
|
|
|
* $record->write(); |
749
|
|
|
* }); |
750
|
|
|
* </code> |
751
|
|
|
* |
752
|
|
|
* @param Member|null|int $member Member or member ID to log in as. |
753
|
|
|
* Set to null or 0 to act as a logged out user. |
754
|
|
|
* @param callable $callback |
755
|
|
|
*/ |
756
|
|
|
public static function actAs($member, $callback) |
757
|
|
|
{ |
758
|
|
|
$previousUser = Security::getCurrentUser(); |
759
|
|
|
|
760
|
|
|
// Transform ID to member |
761
|
|
|
if (is_numeric($member)) { |
762
|
|
|
$member = DataObject::get_by_id(Member::class, $member); |
|
|
|
|
763
|
|
|
} |
764
|
|
|
Security::setCurrentUser($member); |
|
|
|
|
765
|
|
|
|
766
|
|
|
try { |
767
|
|
|
return $callback(); |
768
|
|
|
} finally { |
769
|
|
|
Security::setCurrentUser($previousUser); |
770
|
|
|
} |
771
|
|
|
} |
772
|
|
|
|
773
|
|
|
/** |
774
|
|
|
* Get the ID of the current logged in user |
775
|
|
|
* |
776
|
|
|
* @deprecated 5.0.0 use Security::getCurrentUser() |
777
|
|
|
* |
778
|
|
|
* @return int Returns the ID of the current logged in user or 0. |
779
|
|
|
*/ |
780
|
|
|
public static function currentUserID() |
781
|
|
|
{ |
782
|
|
|
Deprecation::notice( |
783
|
|
|
'5.0.0', |
784
|
|
|
'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore' |
785
|
|
|
); |
786
|
|
|
|
787
|
|
|
if ($member = Security::getCurrentUser()) { |
788
|
|
|
return $member->ID; |
789
|
|
|
} else { |
790
|
|
|
return 0; |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
/** |
795
|
|
|
* Generate a random password, with randomiser to kick in if there's no words file on the |
796
|
|
|
* filesystem. |
797
|
|
|
* |
798
|
|
|
* @return string Returns a random password. |
799
|
|
|
*/ |
800
|
|
|
public static function create_new_password() |
801
|
|
|
{ |
802
|
|
|
$words = Security::config()->uninherited('word_list'); |
803
|
|
|
|
804
|
|
|
if ($words && file_exists($words)) { |
805
|
|
|
$words = file($words); |
806
|
|
|
|
807
|
|
|
list($usec, $sec) = explode(' ', microtime()); |
808
|
|
|
mt_srand($sec + ((float)$usec * 100000)); |
809
|
|
|
|
810
|
|
|
$word = trim($words[random_int(0, count($words) - 1)]); |
811
|
|
|
$number = random_int(10, 999); |
812
|
|
|
|
813
|
|
|
return $word . $number; |
814
|
|
|
} else { |
815
|
|
|
$random = mt_rand(); |
816
|
|
|
$string = md5($random); |
817
|
|
|
$output = substr($string, 0, 8); |
818
|
|
|
|
819
|
|
|
return $output; |
820
|
|
|
} |
821
|
|
|
} |
822
|
|
|
|
823
|
|
|
/** |
824
|
|
|
* Event handler called before writing to the database. |
825
|
|
|
*/ |
826
|
|
|
public function onBeforeWrite() |
827
|
|
|
{ |
828
|
|
|
if ($this->SetPassword) { |
829
|
|
|
$this->Password = $this->SetPassword; |
830
|
|
|
} |
831
|
|
|
|
832
|
|
|
// If a member with the same "unique identifier" already exists with a different ID, don't allow merging. |
833
|
|
|
// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), |
834
|
|
|
// but rather a last line of defense against data inconsistencies. |
835
|
|
|
$identifierField = Member::config()->get('unique_identifier_field'); |
|
|
|
|
836
|
|
|
if ($this->$identifierField) { |
837
|
|
|
// Note: Same logic as Member_Validator class |
838
|
|
|
$filter = [ |
839
|
|
|
"\"Member\".\"$identifierField\"" => $this->$identifierField |
840
|
|
|
]; |
841
|
|
|
if ($this->ID) { |
842
|
|
|
$filter[] = array('"Member"."ID" <> ?' => $this->ID); |
843
|
|
|
} |
844
|
|
|
$existingRecord = DataObject::get_one(Member::class, $filter); |
|
|
|
|
845
|
|
|
|
846
|
|
|
if ($existingRecord) { |
847
|
|
|
throw new ValidationException(_t( |
848
|
|
|
__CLASS__ . '.ValidationIdentifierFailed', |
849
|
|
|
'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', |
850
|
|
|
'Values in brackets show "fieldname = value", usually denoting an existing email address', |
851
|
|
|
array( |
852
|
|
|
'id' => $existingRecord->ID, |
853
|
|
|
'name' => $identifierField, |
854
|
|
|
'value' => $this->$identifierField |
855
|
|
|
) |
856
|
|
|
)); |
857
|
|
|
} |
858
|
|
|
} |
859
|
|
|
|
860
|
|
|
// We don't send emails out on dev/tests sites to prevent accidentally spamming users. |
861
|
|
|
// However, if TestMailer is in use this isn't a risk. |
862
|
|
|
// @todo some developers use external tools, so emailing might be a good idea anyway |
863
|
|
|
if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer) |
864
|
|
|
&& $this->isChanged('Password') |
865
|
|
|
&& $this->record['Password'] |
866
|
|
|
&& static::config()->get('notify_password_change') |
867
|
|
|
) { |
868
|
|
|
Email::create() |
869
|
|
|
->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail') |
870
|
|
|
->setData($this) |
871
|
|
|
->setTo($this->Email) |
872
|
|
|
->setSubject(_t(__CLASS__ . '.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')) |
873
|
|
|
->send(); |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
// The test on $this->ID is used for when records are initially created. |
877
|
|
|
// Note that this only works with cleartext passwords, as we can't rehash |
878
|
|
|
// existing passwords. |
879
|
|
|
if ((!$this->ID && $this->Password) || $this->isChanged('Password')) { |
880
|
|
|
//reset salt so that it gets regenerated - this will invalidate any persistent login cookies |
881
|
|
|
// or other information encrypted with this Member's settings (see self::encryptWithUserSettings) |
882
|
|
|
$this->Salt = ''; |
883
|
|
|
// Password was changed: encrypt the password according the settings |
884
|
|
|
$encryption_details = Security::encrypt_password( |
885
|
|
|
$this->Password, // this is assumed to be cleartext |
886
|
|
|
$this->Salt, |
887
|
|
|
($this->PasswordEncryption) ? |
888
|
|
|
$this->PasswordEncryption : Security::config()->get('password_encryption_algorithm'), |
889
|
|
|
$this |
890
|
|
|
); |
891
|
|
|
|
892
|
|
|
// Overwrite the Password property with the hashed value |
893
|
|
|
$this->Password = $encryption_details['password']; |
894
|
|
|
$this->Salt = $encryption_details['salt']; |
895
|
|
|
$this->PasswordEncryption = $encryption_details['algorithm']; |
896
|
|
|
|
897
|
|
|
// If we haven't manually set a password expiry |
898
|
|
|
if (!$this->isChanged('PasswordExpiry')) { |
899
|
|
|
// then set it for us |
900
|
|
|
if (static::config()->get('password_expiry_days')) { |
901
|
|
|
$this->PasswordExpiry = date('Y-m-d', time() + 86400 * static::config()->get('password_expiry_days')); |
902
|
|
|
} else { |
903
|
|
|
$this->PasswordExpiry = null; |
904
|
|
|
} |
905
|
|
|
} |
906
|
|
|
} |
907
|
|
|
|
908
|
|
|
// save locale |
909
|
|
|
if (!$this->Locale) { |
910
|
|
|
$this->Locale = i18n::get_locale(); |
911
|
|
|
} |
912
|
|
|
|
913
|
|
|
parent::onBeforeWrite(); |
914
|
|
|
} |
915
|
|
|
|
916
|
|
|
public function onAfterWrite() |
917
|
|
|
{ |
918
|
|
|
parent::onAfterWrite(); |
919
|
|
|
|
920
|
|
|
Permission::reset(); |
921
|
|
|
|
922
|
|
|
if ($this->isChanged('Password') && static::config()->get('password_logging_enabled')) { |
923
|
|
|
MemberPassword::log($this); |
924
|
|
|
} |
925
|
|
|
} |
926
|
|
|
|
927
|
|
|
public function onAfterDelete() |
928
|
|
|
{ |
929
|
|
|
parent::onAfterDelete(); |
930
|
|
|
|
931
|
|
|
//prevent orphaned records remaining in the DB |
932
|
|
|
$this->deletePasswordLogs(); |
933
|
|
|
} |
934
|
|
|
|
935
|
|
|
/** |
936
|
|
|
* Delete the MemberPassword objects that are associated to this user |
937
|
|
|
* |
938
|
|
|
* @return $this |
939
|
|
|
*/ |
940
|
|
|
protected function deletePasswordLogs() |
941
|
|
|
{ |
942
|
|
|
foreach ($this->LoggedPasswords() as $password) { |
943
|
|
|
$password->delete(); |
944
|
|
|
$password->destroy(); |
945
|
|
|
} |
946
|
|
|
|
947
|
|
|
return $this; |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
/** |
951
|
|
|
* Filter out admin groups to avoid privilege escalation, |
952
|
|
|
* If any admin groups are requested, deny the whole save operation. |
953
|
|
|
* |
954
|
|
|
* @param array $ids Database IDs of Group records |
955
|
|
|
* @return bool True if the change can be accepted |
956
|
|
|
*/ |
957
|
|
|
public function onChangeGroups($ids) |
958
|
|
|
{ |
959
|
|
|
// unless the current user is an admin already OR the logged in user is an admin |
960
|
|
|
if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) { |
961
|
|
|
return true; |
962
|
|
|
} |
963
|
|
|
|
964
|
|
|
// If there are no admin groups in this set then it's ok |
965
|
|
|
$adminGroups = Permission::get_groups_by_permission('ADMIN'); |
966
|
|
|
$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array(); |
967
|
|
|
|
968
|
|
|
return count(array_intersect($ids, $adminGroupIDs)) == 0; |
969
|
|
|
} |
970
|
|
|
|
971
|
|
|
|
972
|
|
|
/** |
973
|
|
|
* Check if the member is in one of the given groups. |
974
|
|
|
* |
975
|
|
|
* @param array|SS_List $groups Collection of {@link Group} DataObjects to check |
976
|
|
|
* @param boolean $strict Only determine direct group membership if set to true (Default: false) |
977
|
|
|
* @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE. |
978
|
|
|
*/ |
979
|
|
|
public function inGroups($groups, $strict = false) |
980
|
|
|
{ |
981
|
|
|
if ($groups) { |
982
|
|
|
foreach ($groups as $group) { |
983
|
|
|
if ($this->inGroup($group, $strict)) { |
984
|
|
|
return true; |
985
|
|
|
} |
986
|
|
|
} |
987
|
|
|
} |
988
|
|
|
|
989
|
|
|
return false; |
990
|
|
|
} |
991
|
|
|
|
992
|
|
|
|
993
|
|
|
/** |
994
|
|
|
* Check if the member is in the given group or any parent groups. |
995
|
|
|
* |
996
|
|
|
* @param int|Group|string $group Group instance, Group Code or ID |
997
|
|
|
* @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE) |
998
|
|
|
* @return bool Returns TRUE if the member is in the given group, otherwise FALSE. |
999
|
|
|
*/ |
1000
|
|
|
public function inGroup($group, $strict = false) |
1001
|
|
|
{ |
1002
|
|
|
if (is_numeric($group)) { |
1003
|
|
|
$groupCheckObj = DataObject::get_by_id(Group::class, $group); |
1004
|
|
|
} elseif (is_string($group)) { |
1005
|
|
|
$groupCheckObj = DataObject::get_one(Group::class, array( |
1006
|
|
|
'"Group"."Code"' => $group |
1007
|
|
|
)); |
1008
|
|
|
} elseif ($group instanceof Group) { |
1009
|
|
|
$groupCheckObj = $group; |
1010
|
|
|
} else { |
1011
|
|
|
throw new InvalidArgumentException('Member::inGroup(): Wrong format for $group parameter'); |
1012
|
|
|
} |
1013
|
|
|
|
1014
|
|
|
if (!$groupCheckObj) { |
1015
|
|
|
return false; |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
|
|
$groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups(); |
1019
|
|
|
if ($groupCandidateObjs) { |
1020
|
|
|
foreach ($groupCandidateObjs as $groupCandidateObj) { |
1021
|
|
|
if ($groupCandidateObj->ID == $groupCheckObj->ID) { |
1022
|
|
|
return true; |
1023
|
|
|
} |
1024
|
|
|
} |
1025
|
|
|
} |
1026
|
|
|
|
1027
|
|
|
return false; |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
|
|
/** |
1031
|
|
|
* Adds the member to a group. This will create the group if the given |
1032
|
|
|
* group code does not return a valid group object. |
1033
|
|
|
* |
1034
|
|
|
* @param string $groupcode |
1035
|
|
|
* @param string $title Title of the group |
1036
|
|
|
*/ |
1037
|
|
|
public function addToGroupByCode($groupcode, $title = "") |
1038
|
|
|
{ |
1039
|
|
|
$group = DataObject::get_one(Group::class, array( |
1040
|
|
|
'"Group"."Code"' => $groupcode |
1041
|
|
|
)); |
1042
|
|
|
|
1043
|
|
|
if ($group) { |
1044
|
|
|
$this->Groups()->add($group); |
1045
|
|
|
} else { |
1046
|
|
|
if (!$title) { |
1047
|
|
|
$title = $groupcode; |
1048
|
|
|
} |
1049
|
|
|
|
1050
|
|
|
$group = new Group(); |
1051
|
|
|
$group->Code = $groupcode; |
1052
|
|
|
$group->Title = $title; |
1053
|
|
|
$group->write(); |
1054
|
|
|
|
1055
|
|
|
$this->Groups()->add($group); |
1056
|
|
|
} |
1057
|
|
|
} |
1058
|
|
|
|
1059
|
|
|
/** |
1060
|
|
|
* Removes a member from a group. |
1061
|
|
|
* |
1062
|
|
|
* @param string $groupcode |
1063
|
|
|
*/ |
1064
|
|
|
public function removeFromGroupByCode($groupcode) |
1065
|
|
|
{ |
1066
|
|
|
$group = Group::get()->filter(array('Code' => $groupcode))->first(); |
1067
|
|
|
|
1068
|
|
|
if ($group) { |
1069
|
|
|
$this->Groups()->remove($group); |
1070
|
|
|
} |
1071
|
|
|
} |
1072
|
|
|
|
1073
|
|
|
/** |
1074
|
|
|
* @param array $columns Column names on the Member record to show in {@link getTitle()}. |
1075
|
|
|
* @param String $sep Separator |
1076
|
|
|
*/ |
1077
|
|
|
public static function set_title_columns($columns, $sep = ' ') |
1078
|
|
|
{ |
1079
|
|
|
Deprecation::notice('5.0', 'Use Member.title_format config instead'); |
1080
|
|
|
if (!is_array($columns)) { |
1081
|
|
|
$columns = array($columns); |
1082
|
|
|
} |
1083
|
|
|
self::config()->set( |
1084
|
|
|
'title_format', |
1085
|
|
|
[ |
1086
|
|
|
'columns' => $columns, |
1087
|
|
|
'sep' => $sep |
1088
|
|
|
] |
1089
|
|
|
); |
1090
|
|
|
} |
1091
|
|
|
|
1092
|
|
|
//------------------- HELPER METHODS -----------------------------------// |
1093
|
|
|
|
1094
|
|
|
/** |
1095
|
|
|
* Get the complete name of the member, by default in the format "<Surname>, <FirstName>". |
1096
|
|
|
* Falls back to showing either field on its own. |
1097
|
|
|
* |
1098
|
|
|
* You can overload this getter with {@link set_title_format()} |
1099
|
|
|
* and {@link set_title_sql()}. |
1100
|
|
|
* |
1101
|
|
|
* @return string Returns the first- and surname of the member. If the ID |
1102
|
|
|
* of the member is equal 0, only the surname is returned. |
1103
|
|
|
*/ |
1104
|
|
|
public function getTitle() |
1105
|
|
|
{ |
1106
|
|
|
$format = static::config()->get('title_format'); |
1107
|
|
|
if ($format) { |
1108
|
|
|
$values = array(); |
1109
|
|
|
foreach ($format['columns'] as $col) { |
1110
|
|
|
$values[] = $this->getField($col); |
1111
|
|
|
} |
1112
|
|
|
|
1113
|
|
|
return implode($format['sep'], $values); |
1114
|
|
|
} |
1115
|
|
|
if ($this->getField('ID') === 0) { |
1116
|
|
|
return $this->getField('Surname'); |
1117
|
|
|
} else { |
1118
|
|
|
if ($this->getField('Surname') && $this->getField('FirstName')) { |
1119
|
|
|
return $this->getField('Surname') . ', ' . $this->getField('FirstName'); |
1120
|
|
|
} elseif ($this->getField('Surname')) { |
1121
|
|
|
return $this->getField('Surname'); |
1122
|
|
|
} elseif ($this->getField('FirstName')) { |
1123
|
|
|
return $this->getField('FirstName'); |
1124
|
|
|
} else { |
1125
|
|
|
return null; |
1126
|
|
|
} |
1127
|
|
|
} |
1128
|
|
|
} |
1129
|
|
|
|
1130
|
|
|
/** |
1131
|
|
|
* Return a SQL CONCAT() fragment suitable for a SELECT statement. |
1132
|
|
|
* Useful for custom queries which assume a certain member title format. |
1133
|
|
|
* |
1134
|
|
|
* @return String SQL |
1135
|
|
|
*/ |
1136
|
|
|
public static function get_title_sql() |
1137
|
|
|
{ |
1138
|
|
|
|
1139
|
|
|
// Get title_format with fallback to default |
1140
|
|
|
$format = static::config()->get('title_format'); |
1141
|
|
|
if (!$format) { |
1142
|
|
|
$format = [ |
1143
|
|
|
'columns' => ['Surname', 'FirstName'], |
1144
|
|
|
'sep' => ' ', |
1145
|
|
|
]; |
1146
|
|
|
} |
1147
|
|
|
|
1148
|
|
|
$columnsWithTablename = array(); |
1149
|
|
|
foreach ($format['columns'] as $column) { |
1150
|
|
|
$columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column); |
1151
|
|
|
} |
1152
|
|
|
|
1153
|
|
|
$sepSQL = Convert::raw2sql($format['sep'], true); |
1154
|
|
|
$op = DB::get_conn()->concatOperator(); |
1155
|
|
|
return "(" . join(" $op $sepSQL $op ", $columnsWithTablename) . ")"; |
1156
|
|
|
} |
1157
|
|
|
|
1158
|
|
|
|
1159
|
|
|
/** |
1160
|
|
|
* Get the complete name of the member |
1161
|
|
|
* |
1162
|
|
|
* @return string Returns the first- and surname of the member. |
1163
|
|
|
*/ |
1164
|
|
|
public function getName() |
1165
|
|
|
{ |
1166
|
|
|
return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName; |
1167
|
|
|
} |
1168
|
|
|
|
1169
|
|
|
|
1170
|
|
|
/** |
1171
|
|
|
* Set first- and surname |
1172
|
|
|
* |
1173
|
|
|
* This method assumes that the last part of the name is the surname, e.g. |
1174
|
|
|
* <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i> |
1175
|
|
|
* |
1176
|
|
|
* @param string $name The name |
1177
|
|
|
*/ |
1178
|
|
|
public function setName($name) |
1179
|
|
|
{ |
1180
|
|
|
$nameParts = explode(' ', $name); |
1181
|
|
|
$this->Surname = array_pop($nameParts); |
1182
|
|
|
$this->FirstName = join(' ', $nameParts); |
1183
|
|
|
} |
1184
|
|
|
|
1185
|
|
|
|
1186
|
|
|
/** |
1187
|
|
|
* Alias for {@link setName} |
1188
|
|
|
* |
1189
|
|
|
* @param string $name The name |
1190
|
|
|
* @see setName() |
1191
|
|
|
*/ |
1192
|
|
|
public function splitName($name) |
1193
|
|
|
{ |
1194
|
|
|
return $this->setName($name); |
1195
|
|
|
} |
1196
|
|
|
|
1197
|
|
|
/** |
1198
|
|
|
* Return the date format based on the user's chosen locale, |
1199
|
|
|
* falling back to the default format defined by the {@link i18n.get_locale()} setting. |
1200
|
|
|
* |
1201
|
|
|
* @return string ISO date format |
1202
|
|
|
*/ |
1203
|
|
|
public function getDateFormat() |
1204
|
|
|
{ |
1205
|
|
|
$formatter = new IntlDateFormatter( |
1206
|
|
|
$this->getLocale(), |
1207
|
|
|
IntlDateFormatter::MEDIUM, |
1208
|
|
|
IntlDateFormatter::NONE |
1209
|
|
|
); |
1210
|
|
|
$format = $formatter->getPattern(); |
1211
|
|
|
|
1212
|
|
|
$this->extend('updateDateFormat', $format); |
1213
|
|
|
|
1214
|
|
|
return $format; |
1215
|
|
|
} |
1216
|
|
|
|
1217
|
|
|
/** |
1218
|
|
|
* Get user locale |
1219
|
|
|
*/ |
1220
|
|
|
public function getLocale() |
1221
|
|
|
{ |
1222
|
|
|
$locale = $this->getField('Locale'); |
1223
|
|
|
if ($locale) { |
1224
|
|
|
return $locale; |
1225
|
|
|
} |
1226
|
|
|
|
1227
|
|
|
return i18n::get_locale(); |
1228
|
|
|
} |
1229
|
|
|
|
1230
|
|
|
/** |
1231
|
|
|
* Return the time format based on the user's chosen locale, |
1232
|
|
|
* falling back to the default format defined by the {@link i18n.get_locale()} setting. |
1233
|
|
|
* |
1234
|
|
|
* @return string ISO date format |
1235
|
|
|
*/ |
1236
|
|
|
public function getTimeFormat() |
1237
|
|
|
{ |
1238
|
|
|
$formatter = new IntlDateFormatter( |
1239
|
|
|
$this->getLocale(), |
1240
|
|
|
IntlDateFormatter::NONE, |
1241
|
|
|
IntlDateFormatter::MEDIUM |
1242
|
|
|
); |
1243
|
|
|
$format = $formatter->getPattern(); |
1244
|
|
|
|
1245
|
|
|
$this->extend('updateTimeFormat', $format); |
1246
|
|
|
|
1247
|
|
|
return $format; |
1248
|
|
|
} |
1249
|
|
|
|
1250
|
|
|
//---------------------------------------------------------------------// |
1251
|
|
|
|
1252
|
|
|
|
1253
|
|
|
/** |
1254
|
|
|
* Get a "many-to-many" map that holds for all members their group memberships, |
1255
|
|
|
* including any parent groups where membership is implied. |
1256
|
|
|
* Use {@link DirectGroups()} to only retrieve the group relations without inheritance. |
1257
|
|
|
* |
1258
|
|
|
* @todo Push all this logic into Member_GroupSet's getIterator()? |
1259
|
|
|
* @return Member_Groupset |
1260
|
|
|
*/ |
1261
|
|
|
public function Groups() |
1262
|
|
|
{ |
1263
|
|
|
$groups = Member_GroupSet::create(Group::class, 'Group_Members', 'GroupID', 'MemberID'); |
1264
|
|
|
$groups = $groups->forForeignID($this->ID); |
1265
|
|
|
|
1266
|
|
|
$this->extend('updateGroups', $groups); |
1267
|
|
|
|
1268
|
|
|
return $groups; |
1269
|
|
|
} |
1270
|
|
|
|
1271
|
|
|
/** |
1272
|
|
|
* @return ManyManyList |
1273
|
|
|
*/ |
1274
|
|
|
public function DirectGroups() |
1275
|
|
|
{ |
1276
|
|
|
return $this->getManyManyComponents('Groups'); |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
/** |
1280
|
|
|
* Get a member SQLMap of members in specific groups |
1281
|
|
|
* |
1282
|
|
|
* If no $groups is passed, all members will be returned |
1283
|
|
|
* |
1284
|
|
|
* @param mixed $groups - takes a SS_List, an array or a single Group.ID |
1285
|
|
|
* @return Map Returns an Map that returns all Member data. |
1286
|
|
|
*/ |
1287
|
|
|
public static function map_in_groups($groups = null) |
1288
|
|
|
{ |
1289
|
|
|
$groupIDList = array(); |
1290
|
|
|
|
1291
|
|
|
if ($groups instanceof SS_List) { |
1292
|
|
|
foreach ($groups as $group) { |
1293
|
|
|
$groupIDList[] = $group->ID; |
1294
|
|
|
} |
1295
|
|
|
} elseif (is_array($groups)) { |
1296
|
|
|
$groupIDList = $groups; |
1297
|
|
|
} elseif ($groups) { |
1298
|
|
|
$groupIDList[] = $groups; |
1299
|
|
|
} |
1300
|
|
|
|
1301
|
|
|
// No groups, return all Members |
1302
|
|
|
if (!$groupIDList) { |
|
|
|
|
1303
|
|
|
return static::get()->sort(array('Surname' => 'ASC', 'FirstName' => 'ASC'))->map(); |
1304
|
|
|
} |
1305
|
|
|
|
1306
|
|
|
$membersList = new ArrayList(); |
1307
|
|
|
// This is a bit ineffective, but follow the ORM style |
1308
|
|
|
/** @var Group $group */ |
1309
|
|
|
foreach (Group::get()->byIDs($groupIDList) as $group) { |
1310
|
|
|
$membersList->merge($group->Members()); |
1311
|
|
|
} |
1312
|
|
|
|
1313
|
|
|
$membersList->removeDuplicates('ID'); |
1314
|
|
|
|
1315
|
|
|
return $membersList->map(); |
1316
|
|
|
} |
1317
|
|
|
|
1318
|
|
|
|
1319
|
|
|
/** |
1320
|
|
|
* Get a map of all members in the groups given that have CMS permissions |
1321
|
|
|
* |
1322
|
|
|
* If no groups are passed, all groups with CMS permissions will be used. |
1323
|
|
|
* |
1324
|
|
|
* @param array $groups Groups to consider or NULL to use all groups with |
1325
|
|
|
* CMS permissions. |
1326
|
|
|
* @return Map Returns a map of all members in the groups given that |
1327
|
|
|
* have CMS permissions. |
1328
|
|
|
*/ |
1329
|
|
|
public static function mapInCMSGroups($groups = null) |
1330
|
|
|
{ |
1331
|
|
|
// Check CMS module exists |
1332
|
|
|
if (!class_exists(LeftAndMain::class)) { |
1333
|
|
|
return ArrayList::create()->map(); |
1334
|
|
|
} |
1335
|
|
|
|
1336
|
|
|
if (count($groups) == 0) { |
1337
|
|
|
$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin'); |
1338
|
|
|
|
1339
|
|
|
if (class_exists(CMSMain::class)) { |
1340
|
|
|
$cmsPerms = CMSMain::singleton()->providePermissions(); |
1341
|
|
|
} else { |
1342
|
|
|
$cmsPerms = LeftAndMain::singleton()->providePermissions(); |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
if (!empty($cmsPerms)) { |
1346
|
|
|
$perms = array_unique(array_merge($perms, array_keys($cmsPerms))); |
1347
|
|
|
} |
1348
|
|
|
|
1349
|
|
|
$permsClause = DB::placeholders($perms); |
1350
|
|
|
/** @skipUpgrade */ |
1351
|
|
|
$groups = Group::get() |
1352
|
|
|
->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"') |
1353
|
|
|
->where(array( |
1354
|
|
|
"\"Permission\".\"Code\" IN ($permsClause)" => $perms |
1355
|
|
|
)); |
1356
|
|
|
} |
1357
|
|
|
|
1358
|
|
|
$groupIDList = array(); |
1359
|
|
|
|
1360
|
|
|
if ($groups instanceof SS_List) { |
1361
|
|
|
foreach ($groups as $group) { |
1362
|
|
|
$groupIDList[] = $group->ID; |
1363
|
|
|
} |
1364
|
|
|
} elseif (is_array($groups)) { |
1365
|
|
|
$groupIDList = $groups; |
1366
|
|
|
} |
1367
|
|
|
|
1368
|
|
|
/** @skipUpgrade */ |
1369
|
|
|
$members = static::get() |
1370
|
|
|
->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"') |
1371
|
|
|
->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"'); |
1372
|
|
|
if ($groupIDList) { |
|
|
|
|
1373
|
|
|
$groupClause = DB::placeholders($groupIDList); |
1374
|
|
|
$members = $members->where(array( |
1375
|
|
|
"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList |
1376
|
|
|
)); |
1377
|
|
|
} |
1378
|
|
|
|
1379
|
|
|
return $members->sort('"Member"."Surname", "Member"."FirstName"')->map(); |
1380
|
|
|
} |
1381
|
|
|
|
1382
|
|
|
|
1383
|
|
|
/** |
1384
|
|
|
* Get the groups in which the member is NOT in |
1385
|
|
|
* |
1386
|
|
|
* When passed an array of groups, and a component set of groups, this |
1387
|
|
|
* function will return the array of groups the member is NOT in. |
1388
|
|
|
* |
1389
|
|
|
* @param array $groupList An array of group code names. |
1390
|
|
|
* @param array $memberGroups A component set of groups (if set to NULL, |
1391
|
|
|
* $this->groups() will be used) |
1392
|
|
|
* @return array Groups in which the member is NOT in. |
1393
|
|
|
*/ |
1394
|
|
|
public function memberNotInGroups($groupList, $memberGroups = null) |
1395
|
|
|
{ |
1396
|
|
|
if (!$memberGroups) { |
1397
|
|
|
$memberGroups = $this->Groups(); |
1398
|
|
|
} |
1399
|
|
|
|
1400
|
|
|
foreach ($memberGroups as $group) { |
1401
|
|
|
if (in_array($group->Code, $groupList)) { |
1402
|
|
|
$index = array_search($group->Code, $groupList); |
1403
|
|
|
unset($groupList[$index]); |
1404
|
|
|
} |
1405
|
|
|
} |
1406
|
|
|
|
1407
|
|
|
return $groupList; |
1408
|
|
|
} |
1409
|
|
|
|
1410
|
|
|
|
1411
|
|
|
/** |
1412
|
|
|
* Return a {@link FieldList} of fields that would appropriate for editing |
1413
|
|
|
* this member. |
1414
|
|
|
* |
1415
|
|
|
* @return FieldList Return a FieldList of fields that would appropriate for |
1416
|
|
|
* editing this member. |
1417
|
|
|
*/ |
1418
|
|
|
public function getCMSFields() |
1419
|
|
|
{ |
1420
|
|
|
$this->beforeUpdateCMSFields(function (FieldList $fields) { |
1421
|
|
|
/** @var FieldList $mainFields */ |
1422
|
|
|
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren(); |
1423
|
|
|
|
1424
|
|
|
// Build change password field |
1425
|
|
|
$mainFields->replaceField('Password', $this->getMemberPasswordField()); |
1426
|
|
|
|
1427
|
|
|
$mainFields->replaceField('Locale', new DropdownField( |
1428
|
|
|
"Locale", |
1429
|
|
|
_t(__CLASS__ . '.INTERFACELANG', "Interface Language", 'Language of the CMS'), |
1430
|
|
|
i18n::getSources()->getKnownLocales() |
1431
|
|
|
)); |
1432
|
|
|
$mainFields->removeByName(static::config()->get('hidden_fields')); |
1433
|
|
|
|
1434
|
|
|
if (!static::config()->get('lock_out_after_incorrect_logins')) { |
1435
|
|
|
$mainFields->removeByName('FailedLoginCount'); |
1436
|
|
|
} |
1437
|
|
|
|
1438
|
|
|
// Groups relation will get us into logical conflicts because |
1439
|
|
|
// Members are displayed within group edit form in SecurityAdmin |
1440
|
|
|
$fields->removeByName('Groups'); |
1441
|
|
|
|
1442
|
|
|
// Members shouldn't be able to directly view/edit logged passwords |
1443
|
|
|
$fields->removeByName('LoggedPasswords'); |
1444
|
|
|
|
1445
|
|
|
$fields->removeByName('RememberLoginHashes'); |
1446
|
|
|
|
1447
|
|
|
if (Permission::check('EDIT_PERMISSIONS')) { |
1448
|
|
|
$groupsMap = array(); |
1449
|
|
|
foreach (Group::get() as $group) { |
1450
|
|
|
// Listboxfield values are escaped, use ASCII char instead of » |
1451
|
|
|
$groupsMap[$group->ID] = $group->getBreadcrumbs(' > '); |
1452
|
|
|
} |
1453
|
|
|
asort($groupsMap); |
1454
|
|
|
$fields->addFieldToTab( |
1455
|
|
|
'Root.Main', |
1456
|
|
|
ListboxField::create('DirectGroups', Group::singleton()->i18n_plural_name()) |
1457
|
|
|
->setSource($groupsMap) |
1458
|
|
|
->setAttribute( |
1459
|
|
|
'data-placeholder', |
1460
|
|
|
_t(__CLASS__ . '.ADDGROUP', 'Add group', 'Placeholder text for a dropdown') |
1461
|
|
|
) |
1462
|
|
|
); |
1463
|
|
|
|
1464
|
|
|
|
1465
|
|
|
// Add permission field (readonly to avoid complicated group assignment logic). |
1466
|
|
|
// This should only be available for existing records, as new records start |
1467
|
|
|
// with no permissions until they have a group assignment anyway. |
1468
|
|
|
if ($this->ID) { |
1469
|
|
|
$permissionsField = new PermissionCheckboxSetField_Readonly( |
1470
|
|
|
'Permissions', |
1471
|
|
|
false, |
1472
|
|
|
Permission::class, |
1473
|
|
|
'GroupID', |
1474
|
|
|
// we don't want parent relationships, they're automatically resolved in the field |
1475
|
|
|
$this->getManyManyComponents('Groups') |
1476
|
|
|
); |
1477
|
|
|
$fields->findOrMakeTab('Root.Permissions', Permission::singleton()->i18n_plural_name()); |
1478
|
|
|
$fields->addFieldToTab('Root.Permissions', $permissionsField); |
1479
|
|
|
} |
1480
|
|
|
} |
1481
|
|
|
|
1482
|
|
|
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions'); |
1483
|
|
|
if ($permissionsTab) { |
1484
|
|
|
$permissionsTab->addExtraClass('readonly'); |
1485
|
|
|
} |
1486
|
|
|
}); |
1487
|
|
|
|
1488
|
|
|
return parent::getCMSFields(); |
1489
|
|
|
} |
1490
|
|
|
|
1491
|
|
|
/** |
1492
|
|
|
* @param bool $includerelations Indicate if the labels returned include relation fields |
1493
|
|
|
* @return array |
1494
|
|
|
*/ |
1495
|
|
|
public function fieldLabels($includerelations = true) |
1496
|
|
|
{ |
1497
|
|
|
$labels = parent::fieldLabels($includerelations); |
1498
|
|
|
|
1499
|
|
|
$labels['FirstName'] = _t(__CLASS__ . '.FIRSTNAME', 'First Name'); |
1500
|
|
|
$labels['Surname'] = _t(__CLASS__ . '.SURNAME', 'Surname'); |
1501
|
|
|
/** @skipUpgrade */ |
1502
|
|
|
$labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email'); |
1503
|
|
|
$labels['Password'] = _t(__CLASS__ . '.db_Password', 'Password'); |
1504
|
|
|
$labels['PasswordExpiry'] = _t(__CLASS__ . '.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); |
1505
|
|
|
$labels['LockedOutUntil'] = _t(__CLASS__ . '.db_LockedOutUntil', 'Locked out until', 'Security related date'); |
1506
|
|
|
$labels['Locale'] = _t(__CLASS__ . '.db_Locale', 'Interface Locale'); |
1507
|
|
|
if ($includerelations) { |
1508
|
|
|
$labels['Groups'] = _t( |
1509
|
|
|
__CLASS__ . '.belongs_many_many_Groups', |
1510
|
|
|
'Groups', |
1511
|
|
|
'Security Groups this member belongs to' |
1512
|
|
|
); |
1513
|
|
|
} |
1514
|
|
|
|
1515
|
|
|
return $labels; |
1516
|
|
|
} |
1517
|
|
|
|
1518
|
|
|
/** |
1519
|
|
|
* Users can view their own record. |
1520
|
|
|
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions. |
1521
|
|
|
* This is likely to be customized for social sites etc. with a looser permission model. |
1522
|
|
|
* |
1523
|
|
|
* @param Member $member |
1524
|
|
|
* @return bool |
1525
|
|
|
*/ |
1526
|
|
|
public function canView($member = null) |
1527
|
|
|
{ |
1528
|
|
|
//get member |
1529
|
|
|
if (!$member) { |
1530
|
|
|
$member = Security::getCurrentUser(); |
1531
|
|
|
} |
1532
|
|
|
//check for extensions, we do this first as they can overrule everything |
1533
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member); |
|
|
|
|
1534
|
|
|
if ($extended !== null) { |
1535
|
|
|
return $extended; |
1536
|
|
|
} |
1537
|
|
|
|
1538
|
|
|
//need to be logged in and/or most checks below rely on $member being a Member |
1539
|
|
|
if (!$member) { |
1540
|
|
|
return false; |
1541
|
|
|
} |
1542
|
|
|
// members can usually view their own record |
1543
|
|
|
if ($this->ID == $member->ID) { |
1544
|
|
|
return true; |
1545
|
|
|
} |
1546
|
|
|
|
1547
|
|
|
//standard check |
1548
|
|
|
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); |
1549
|
|
|
} |
1550
|
|
|
|
1551
|
|
|
/** |
1552
|
|
|
* Users can edit their own record. |
1553
|
|
|
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions |
1554
|
|
|
* |
1555
|
|
|
* @param Member $member |
1556
|
|
|
* @return bool |
1557
|
|
|
*/ |
1558
|
|
|
public function canEdit($member = null) |
1559
|
|
|
{ |
1560
|
|
|
//get member |
1561
|
|
|
if (!$member) { |
1562
|
|
|
$member = Security::getCurrentUser(); |
1563
|
|
|
} |
1564
|
|
|
//check for extensions, we do this first as they can overrule everything |
1565
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member); |
|
|
|
|
1566
|
|
|
if ($extended !== null) { |
1567
|
|
|
return $extended; |
1568
|
|
|
} |
1569
|
|
|
|
1570
|
|
|
//need to be logged in and/or most checks below rely on $member being a Member |
1571
|
|
|
if (!$member) { |
1572
|
|
|
return false; |
1573
|
|
|
} |
1574
|
|
|
|
1575
|
|
|
// HACK: we should not allow for an non-Admin to edit an Admin |
1576
|
|
|
if (!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) { |
1577
|
|
|
return false; |
1578
|
|
|
} |
1579
|
|
|
// members can usually edit their own record |
1580
|
|
|
if ($this->ID == $member->ID) { |
1581
|
|
|
return true; |
1582
|
|
|
} |
1583
|
|
|
|
1584
|
|
|
//standard check |
1585
|
|
|
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); |
1586
|
|
|
} |
1587
|
|
|
|
1588
|
|
|
/** |
1589
|
|
|
* Users can edit their own record. |
1590
|
|
|
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions |
1591
|
|
|
* |
1592
|
|
|
* @param Member $member |
1593
|
|
|
* @return bool |
1594
|
|
|
*/ |
1595
|
|
|
public function canDelete($member = null) |
1596
|
|
|
{ |
1597
|
|
|
if (!$member) { |
1598
|
|
|
$member = Security::getCurrentUser(); |
1599
|
|
|
} |
1600
|
|
|
//check for extensions, we do this first as they can overrule everything |
1601
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member); |
|
|
|
|
1602
|
|
|
if ($extended !== null) { |
1603
|
|
|
return $extended; |
1604
|
|
|
} |
1605
|
|
|
|
1606
|
|
|
//need to be logged in and/or most checks below rely on $member being a Member |
1607
|
|
|
if (!$member) { |
1608
|
|
|
return false; |
1609
|
|
|
} |
1610
|
|
|
// Members are not allowed to remove themselves, |
1611
|
|
|
// since it would create inconsistencies in the admin UIs. |
1612
|
|
|
if ($this->ID && $member->ID == $this->ID) { |
1613
|
|
|
return false; |
1614
|
|
|
} |
1615
|
|
|
|
1616
|
|
|
// HACK: if you want to delete a member, you have to be a member yourself. |
1617
|
|
|
// this is a hack because what this should do is to stop a user |
1618
|
|
|
// deleting a member who has more privileges (e.g. a non-Admin deleting an Admin) |
1619
|
|
|
if (Permission::checkMember($this, 'ADMIN')) { |
1620
|
|
|
if (!Permission::checkMember($member, 'ADMIN')) { |
1621
|
|
|
return false; |
1622
|
|
|
} |
1623
|
|
|
} |
1624
|
|
|
|
1625
|
|
|
//standard check |
1626
|
|
|
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); |
1627
|
|
|
} |
1628
|
|
|
|
1629
|
|
|
/** |
1630
|
|
|
* Validate this member object. |
1631
|
|
|
*/ |
1632
|
|
|
public function validate() |
1633
|
|
|
{ |
1634
|
|
|
$valid = parent::validate(); |
1635
|
|
|
|
1636
|
|
|
if (!$this->ID || $this->isChanged('Password')) { |
1637
|
|
|
if ($this->Password && self::$password_validator) { |
1638
|
|
|
$valid->combineAnd(self::$password_validator->validate($this->Password, $this)); |
1639
|
|
|
} |
1640
|
|
|
} |
1641
|
|
|
|
1642
|
|
|
if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) { |
1643
|
|
|
if ($this->SetPassword && self::$password_validator) { |
1644
|
|
|
$valid->combineAnd(self::$password_validator->validate($this->SetPassword, $this)); |
1645
|
|
|
} |
1646
|
|
|
} |
1647
|
|
|
|
1648
|
|
|
return $valid; |
1649
|
|
|
} |
1650
|
|
|
|
1651
|
|
|
/** |
1652
|
|
|
* Change password. This will cause rehashing according to |
1653
|
|
|
* the `PasswordEncryption` property. |
1654
|
|
|
* |
1655
|
|
|
* @param string $password Cleartext password |
1656
|
|
|
* @return ValidationResult |
1657
|
|
|
*/ |
1658
|
|
|
public function changePassword($password) |
1659
|
|
|
{ |
1660
|
|
|
$this->Password = $password; |
1661
|
|
|
$valid = $this->validate(); |
1662
|
|
|
|
1663
|
|
|
if ($valid->isValid()) { |
1664
|
|
|
$this->AutoLoginHash = null; |
1665
|
|
|
$this->write(); |
1666
|
|
|
} |
1667
|
|
|
|
1668
|
|
|
return $valid; |
1669
|
|
|
} |
1670
|
|
|
|
1671
|
|
|
/** |
1672
|
|
|
* Tell this member that someone made a failed attempt at logging in as them. |
1673
|
|
|
* This can be used to lock the user out temporarily if too many failed attempts are made. |
1674
|
|
|
*/ |
1675
|
|
|
public function registerFailedLogin() |
1676
|
|
|
{ |
1677
|
|
|
$lockOutAfterCount = self::config()->get('lock_out_after_incorrect_logins'); |
1678
|
|
|
if ($lockOutAfterCount) { |
1679
|
|
|
// Keep a tally of the number of failed log-ins so that we can lock people out |
1680
|
|
|
$this->FailedLoginCount = $this->FailedLoginCount + 1; |
1681
|
|
|
|
1682
|
|
|
if ($this->FailedLoginCount >= $lockOutAfterCount) { |
1683
|
|
|
$lockoutMins = self::config()->get('lock_out_delay_mins'); |
1684
|
|
|
$this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins * 60); |
1685
|
|
|
$this->FailedLoginCount = 0; |
1686
|
|
|
} |
1687
|
|
|
} |
1688
|
|
|
$this->extend('registerFailedLogin'); |
1689
|
|
|
$this->write(); |
1690
|
|
|
} |
1691
|
|
|
|
1692
|
|
|
/** |
1693
|
|
|
* Tell this member that a successful login has been made |
1694
|
|
|
*/ |
1695
|
|
|
public function registerSuccessfulLogin() |
1696
|
|
|
{ |
1697
|
|
|
if (self::config()->get('lock_out_after_incorrect_logins')) { |
1698
|
|
|
// Forgive all past login failures |
1699
|
|
|
$this->FailedLoginCount = 0; |
1700
|
|
|
$this->write(); |
1701
|
|
|
} |
1702
|
|
|
} |
1703
|
|
|
|
1704
|
|
|
/** |
1705
|
|
|
* Get the HtmlEditorConfig for this user to be used in the CMS. |
1706
|
|
|
* This is set by the group. If multiple configurations are set, |
1707
|
|
|
* the one with the highest priority wins. |
1708
|
|
|
* |
1709
|
|
|
* @return string |
1710
|
|
|
*/ |
1711
|
|
|
public function getHtmlEditorConfigForCMS() |
1712
|
|
|
{ |
1713
|
|
|
$currentName = ''; |
1714
|
|
|
$currentPriority = 0; |
1715
|
|
|
|
1716
|
|
|
foreach ($this->Groups() as $group) { |
1717
|
|
|
$configName = $group->HtmlEditorConfig; |
1718
|
|
|
if ($configName) { |
1719
|
|
|
$config = HTMLEditorConfig::get($group->HtmlEditorConfig); |
1720
|
|
|
if ($config && $config->getOption('priority') > $currentPriority) { |
1721
|
|
|
$currentName = $configName; |
1722
|
|
|
$currentPriority = $config->getOption('priority'); |
1723
|
|
|
} |
1724
|
|
|
} |
1725
|
|
|
} |
1726
|
|
|
|
1727
|
|
|
// If can't find a suitable editor, just default to cms |
1728
|
|
|
return $currentName ? $currentName : 'cms'; |
1729
|
|
|
} |
1730
|
|
|
} |
1731
|
|
|
|
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.