Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

Member   F

Complexity

Total Complexity 198

Size/Duplication

Total Lines 1739
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1739
rs 0.6314
c 0
b 0
f 0
wmc 198

65 Methods

Rating   Name   Duplication   Size   Complexity  
A checkPassword() 0 14 3
A requireDefaultRecords() 0 6 1
A populateDefaults() 0 4 1
A canLogin() 0 3 1
A isDefaultAdmin() 0 3 1
A default_admin() 0 4 1
A validateCanLogin() 0 18 3
C canEdit() 0 28 7
A isPasswordExpired() 0 7 2
B getMemberPasswordField() 0 24 4
A removeFromGroupByCode() 0 6 2
A changePassword() 0 21 3
C mapInCMSGroups() 0 51 9
A currentUserID() 0 11 2
B validate() 0 12 5
A afterMemberLoggedIn() 0 13 1
A set_password_validator() 0 8 2
A getTimeFormat() 0 12 1
A beforeMemberLoggedOut() 0 3 1
C getTitle() 0 22 8
A splitName() 0 3 1
A validateAutoLoginToken() 0 6 1
A member_from_autologinhash() 0 13 3
A fieldLabels() 0 21 2
A onChangeGroups() 0 12 4
A encryptWithUserSettings() 0 16 3
A onAfterDelete() 0 6 1
A getLocale() 0 8 2
A logged_in_session_exists() 0 14 4
B getHtmlEditorConfigForCMS() 0 18 6
A deletePasswordLogs() 0 8 2
A getMemberFormFields() 0 19 1
A regenerateTempID() 0 9 2
B canView() 0 23 5
B encryptPassword() 0 30 4
A create_new_password() 0 20 3
A afterMemberLoggedOut() 0 3 1
A DirectGroups() 0 3 1
C canDelete() 0 32 8
A onAfterWrite() 0 8 3
A registerFailedLogin() 0 15 3
A inGroups() 0 11 4
C isLockedOut() 0 39 7
D inGroup() 0 28 9
A getDateFormat() 0 12 1
A logOut() 0 8 1
A get_title_sql() 0 20 3
A registerSuccessfulLogin() 0 7 2
A actAs() 0 14 2
B generateAutologinTokenAndStoreHash() 0 28 3
A beforeMemberLoggedIn() 0 4 1
A member_from_tempid() 0 12 2
A memberNotInGroups() 0 14 4
B getCMSFields() 0 75 6
C onBeforeWrite() 0 59 14
A getName() 0 3 2
A set_title_columns() 0 11 2
A addToGroupByCode() 0 19 3
A getValidator() 0 7 1
A logIn() 0 7 1
A currentUser() 0 8 1
C map_in_groups() 0 29 7
A setName() 0 5 1
A password_validator() 0 6 2
A Groups() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like Member often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Member, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Security;
4
5
use IntlDateFormatter;
6
use InvalidArgumentException;
7
use SilverStripe\Admin\LeftAndMain;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Admin\LeftAndMain was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use SilverStripe\CMS\Controllers\CMSMain;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\CMSMain was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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\Control\HTTPRequest;
14
use SilverStripe\Core\Config\Config;
15
use SilverStripe\Core\Convert;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\Dev\Deprecation;
18
use SilverStripe\Dev\TestMailer;
19
use SilverStripe\Forms\ConfirmedPasswordField;
20
use SilverStripe\Forms\DropdownField;
21
use SilverStripe\Forms\FieldList;
22
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
23
use SilverStripe\Forms\ListboxField;
24
use SilverStripe\Forms\Tab;
25
use SilverStripe\Forms\TabSet;
26
use SilverStripe\i18n\i18n;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\DB;
31
use SilverStripe\ORM\FieldType\DBDatetime;
32
use SilverStripe\ORM\HasManyList;
33
use SilverStripe\ORM\ManyManyList;
34
use SilverStripe\ORM\Map;
35
use SilverStripe\ORM\SS_List;
36
use SilverStripe\ORM\ValidationException;
37
use SilverStripe\ORM\ValidationResult;
38
39
/**
40
 * The member class which represents the users of the system
41
 *
42
 * @method HasManyList LoggedPasswords()
43
 * @method HasManyList RememberLoginHashes()
44
 * @property string $FirstName
45
 * @property string $Surname
46
 * @property string $Email
47
 * @property string $Password
48
 * @property string $TempIDHash
49
 * @property string $TempIDExpired
50
 * @property string $AutoLoginHash
51
 * @property string $AutoLoginExpired
52
 * @property string $PasswordEncryption
53
 * @property string $Salt
54
 * @property string $PasswordExpiry
55
 * @property string $LockedOutUntil
56
 * @property string $Locale
57
 * @property int $FailedLoginCount
58
 * @property string $DateFormat
59
 * @property string $TimeFormat
60
 */
61
class Member extends DataObject
62
{
63
64
    private static $db = array(
65
        'FirstName'          => 'Varchar',
66
        'Surname'            => 'Varchar',
67
        'Email'              => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
68
        'TempIDHash'         => 'Varchar(160)', // Temporary id used for cms re-authentication
69
        'TempIDExpired'      => 'Datetime', // Expiry of temp login
70
        'Password'           => 'Varchar(160)',
71
        'AutoLoginHash'      => 'Varchar(160)', // Used to auto-login the user on password reset
72
        'AutoLoginExpired'   => 'Datetime',
73
        // This is an arbitrary code pointing to a PasswordEncryptor instance,
74
        // not an actual encryption algorithm.
75
        // Warning: Never change this field after its the first password hashing without
76
        // providing a new cleartext password as well.
77
        'PasswordEncryption' => "Varchar(50)",
78
        'Salt'               => 'Varchar(50)',
79
        'PasswordExpiry'     => 'Date',
80
        'LockedOutUntil'     => 'Datetime',
81
        'Locale'             => 'Varchar(6)',
82
        // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
83
        'FailedLoginCount'   => 'Int',
84
    );
85
86
    private static $belongs_many_many = array(
87
        'Groups' => Group::class,
88
    );
89
90
    private static $has_many = array(
91
        'LoggedPasswords'     => MemberPassword::class,
92
        'RememberLoginHashes' => RememberLoginHash::class,
93
    );
94
95
    private static $table_name = "Member";
96
97
    private static $default_sort = '"Surname", "FirstName"';
98
99
    private static $indexes = array(
100
        'Email' => true,
101
        //Removed due to duplicate null values causing MSSQL problems
102
        //'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...
103
    );
104
105
    /**
106
     * @config
107
     * @var boolean
108
     */
109
    private static $notify_password_change = false;
110
111
    /**
112
     * All searchable database columns
113
     * in this object, currently queried
114
     * with a "column LIKE '%keywords%'
115
     * statement.
116
     *
117
     * @var array
118
     * @todo Generic implementation of $searchable_fields on DataObject,
119
     * with definition for different searching algorithms
120
     * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
121
     */
122
    private static $searchable_fields = array(
123
        'FirstName',
124
        'Surname',
125
        'Email',
126
    );
127
128
    /**
129
     * @config
130
     * @var array
131
     */
132
    private static $summary_fields = array(
133
        'FirstName',
134
        'Surname',
135
        'Email',
136
    );
137
138
    /**
139
     * @config
140
     * @var array
141
     */
142
    private static $casting = array(
143
        'Name' => 'Varchar',
144
    );
145
146
    /**
147
     * Internal-use only fields
148
     *
149
     * @config
150
     * @var array
151
     */
152
    private static $hidden_fields = array(
153
        'AutoLoginHash',
154
        'AutoLoginExpired',
155
        'PasswordEncryption',
156
        'PasswordExpiry',
157
        'LockedOutUntil',
158
        'TempIDHash',
159
        'TempIDExpired',
160
        'Salt',
161
    );
162
163
    /**
164
     * @config
165
     * @var array See {@link set_title_columns()}
166
     */
167
    private static $title_format = null;
168
169
    /**
170
     * The unique field used to identify this member.
171
     * By default, it's "Email", but another common
172
     * field could be Username.
173
     *
174
     * @config
175
     * @var string
176
     * @skipUpgrade
177
     */
178
    private static $unique_identifier_field = 'Email';
179
180
    /**
181
     * @config
182
     * The number of days that a password should be valid for.
183
     * By default, this is null, which means that passwords never expire
184
     */
185
    private static $password_expiry_days = null;
186
187
    /**
188
     * @config
189
     * @var bool enable or disable logging of previously used passwords. See {@link onAfterWrite}
190
     */
191
    private static $password_logging_enabled = true;
192
193
    /**
194
     * @config
195
     * @var Int Number of incorrect logins after which
196
     * the user is blocked from further attempts for the timespan
197
     * defined in {@link $lock_out_delay_mins}.
198
     */
199
    private static $lock_out_after_incorrect_logins = 10;
200
201
    /**
202
     * @config
203
     * @var integer Minutes of enforced lockout after incorrect password attempts.
204
     * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
205
     */
206
    private static $lock_out_delay_mins = 15;
207
208
    /**
209
     * @config
210
     * @var String If this is set, then a session cookie with the given name will be set on log-in,
211
     * and cleared on logout.
212
     */
213
    private static $login_marker_cookie = null;
214
215
    /**
216
     * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
217
     * should be called as a security precaution.
218
     *
219
     * This doesn't always work, especially if you're trying to set session cookies
220
     * across an entire site using the domain parameter to session_set_cookie_params()
221
     *
222
     * @config
223
     * @var boolean
224
     */
225
    private static $session_regenerate_id = true;
226
227
228
    /**
229
     * Default lifetime of temporary ids.
230
     *
231
     * This is the period within which a user can be re-authenticated within the CMS by entering only their password
232
     * and without losing their workspace.
233
     *
234
     * Any session expiration outside of this time will require them to login from the frontend using their full
235
     * username and password.
236
     *
237
     * Defaults to 72 hours. Set to zero to disable expiration.
238
     *
239
     * @config
240
     * @var int Lifetime in seconds
241
     */
242
    private static $temp_id_lifetime = 259200;
243
244
    /**
245
     * Default lifetime of auto login token.
246
     *
247
     * This is the maximum allowed period between a user requesting a password reset link and using it to reset
248
     * their password.
249
     *
250
     * Defaults to 2 days.
251
     *
252
     * @config
253
     * @var int Lifetime in seconds
254
     */
255
    private static $auto_login_token_lifetime = 172800;
256
257
    /**
258
     * Used to track whether {@link Member::changePassword} has made changed that need to be written. Used to prevent
259
     * the write from calling changePassword again.
260
     *
261
     * @var bool
262
     */
263
    protected $passwordChangesToWrite = false;
264
265
    /**
266
     * Ensure the locale is set to something sensible by default.
267
     */
268
    public function populateDefaults()
269
    {
270
        parent::populateDefaults();
271
        $this->Locale = i18n::get_locale();
272
    }
273
274
    public function requireDefaultRecords()
275
    {
276
        parent::requireDefaultRecords();
277
        // Default groups should've been built by Group->requireDefaultRecords() already
278
        $service = DefaultAdminService::singleton();
279
        $service->findOrCreateDefaultAdmin();
280
    }
281
282
    /**
283
     * Get the default admin record if it exists, or creates it otherwise if enabled
284
     *
285
     * @deprecated 4.0.0...5.0.0 Use DefaultAdminService::findOrCreateDefaultAdmin() instead
286
     * @return Member
287
     */
288
    public static function default_admin()
289
    {
290
        Deprecation::notice('5.0', 'Use DefaultAdminService::findOrCreateDefaultAdmin() instead');
291
        return DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
292
    }
293
294
    /**
295
     * Check if the passed password matches the stored one (if the member is not locked out).
296
     *
297
     * @deprecated 4.0.0...5.0.0 Use Authenticator::checkPassword() instead
298
     *
299
     * @param  string $password
300
     * @return ValidationResult
301
     */
302
    public function checkPassword($password)
303
    {
304
        Deprecation::notice('5.0', 'Use Authenticator::checkPassword() instead');
305
306
        // With a valid user and password, check the password is correct
307
        $result = ValidationResult::create();
308
        $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
309
        foreach ($authenticators as $authenticator) {
310
            $authenticator->checkPassword($this, $password, $result);
311
            if (!$result->isValid()) {
312
                break;
313
            }
314
        }
315
            return $result;
316
    }
317
318
    /**
319
     * Check if this user is the currently configured default admin
320
     *
321
     * @return bool
322
     */
323
    public function isDefaultAdmin()
324
    {
325
        return DefaultAdminService::isDefaultAdmin($this->Email);
326
    }
327
328
    /**
329
     * Check if this user can login
330
     *
331
     * @return bool
332
     */
333
    public function canLogin()
334
    {
335
        return $this->validateCanLogin()->isValid();
336
    }
337
338
    /**
339
     * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
340
     * one with error messages to display if the member is locked out.
341
     *
342
     * You can hook into this with a "canLogIn" method on an attached extension.
343
     *
344
     * @param ValidationResult $result Optional result to add errors to
345
     * @return ValidationResult
346
     */
347
    public function validateCanLogin(ValidationResult &$result = null)
348
    {
349
        $result = $result ?: ValidationResult::create();
350
        if ($this->isLockedOut()) {
351
            $result->addError(
352
                _t(
353
                    __CLASS__ . '.ERRORLOCKEDOUT2',
354
                    'Your account has been temporarily disabled because of too many failed attempts at ' .
355
                    'logging in. Please try again in {count} minutes.',
356
                    null,
357
                    array('count' => static::config()->get('lock_out_delay_mins'))
358
                )
359
            );
360
        }
361
362
        $this->extend('canLogIn', $result);
363
364
        return $result;
365
    }
366
367
    /**
368
     * Returns true if this user is locked out
369
     *
370
     * @skipUpgrade
371
     * @return bool
372
     */
373
    public function isLockedOut()
374
    {
375
        /** @var DBDatetime $lockedOutUntilObj */
376
        $lockedOutUntilObj = $this->dbObject('LockedOutUntil');
377
        if ($lockedOutUntilObj->InFuture()) {
378
            return true;
379
        }
380
381
        $maxAttempts = $this->config()->get('lock_out_after_incorrect_logins');
382
        if ($maxAttempts <= 0) {
383
            return false;
384
        }
385
386
        $idField = static::config()->get('unique_identifier_field');
387
        $attempts = LoginAttempt::getByEmail($this->{$idField})
388
            ->sort('Created', 'DESC')
389
            ->limit($maxAttempts);
390
391
        if ($attempts->count() < $maxAttempts) {
392
            return false;
393
        }
394
395
        foreach ($attempts as $attempt) {
396
            if ($attempt->Status === 'Success') {
397
                return false;
398
            }
399
        }
400
401
        // Calculate effective LockedOutUntil
402
        /** @var DBDatetime $firstFailureDate */
403
        $firstFailureDate = $attempts->first()->dbObject('Created');
404
        $maxAgeSeconds = $this->config()->get('lock_out_delay_mins') * 60;
405
        $lockedOutUntil = $firstFailureDate->getTimestamp() + $maxAgeSeconds;
406
        $now = DBDatetime::now()->getTimestamp();
407
        if ($now < $lockedOutUntil) {
408
            return true;
409
        }
410
411
        return false;
412
    }
413
414
    /**
415
     * Set a {@link PasswordValidator} object to use to validate member's passwords.
416
     *
417
     * @param PasswordValidator $validator
418
     */
419
    public static function set_password_validator(PasswordValidator $validator = null)
420
    {
421
        // Override existing config
422
        Config::modify()->remove(Injector::class, PasswordValidator::class);
423
        if ($validator) {
424
            Injector::inst()->registerService($validator, PasswordValidator::class);
425
        } else {
426
            Injector::inst()->unregisterNamedObject(PasswordValidator::class);
427
        }
428
    }
429
430
    /**
431
     * Returns the default {@link PasswordValidator}
432
     *
433
     * @return PasswordValidator
434
     */
435
    public static function password_validator()
436
    {
437
        if (Injector::inst()->has(PasswordValidator::class)) {
438
            return Injector::inst()->get(PasswordValidator::class);
439
        }
440
        return null;
441
    }
442
443
    public function isPasswordExpired()
444
    {
445
        if (!$this->PasswordExpiry) {
446
            return false;
447
        }
448
449
        return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
450
    }
451
452
    /**
453
     * @deprecated 5.0.0 Use Security::setCurrentUser() or IdentityStore::logIn()
454
     *
455
     */
456
    public function logIn()
457
    {
458
        Deprecation::notice(
459
            '5.0.0',
460
            'This method is deprecated and only logs in for the current request. Please use Security::setCurrentUser($user) or an IdentityStore'
461
        );
462
        Security::setCurrentUser($this);
463
    }
464
465
    /**
466
     * Called before a member is logged in via session/cookie/etc
467
     */
468
    public function beforeMemberLoggedIn()
469
    {
470
        // @todo Move to middleware on the AuthenticationMiddleware IdentityStore
471
        $this->extend('beforeMemberLoggedIn');
472
    }
473
474
    /**
475
     * Called after a member is logged in via session/cookie/etc
476
     */
477
    public function afterMemberLoggedIn()
478
    {
479
        // Clear the incorrect log-in count
480
        $this->registerSuccessfulLogin();
481
482
        $this->LockedOutUntil = null;
483
484
        $this->regenerateTempID();
485
486
        $this->write();
487
488
        // Audit logging hook
489
        $this->extend('afterMemberLoggedIn');
490
    }
491
492
    /**
493
     * Trigger regeneration of TempID.
494
     *
495
     * This should be performed any time the user presents their normal identification (normally Email)
496
     * and is successfully authenticated.
497
     */
498
    public function regenerateTempID()
499
    {
500
        $generator = new RandomGenerator();
501
        $lifetime = self::config()->get('temp_id_lifetime');
502
        $this->TempIDHash = $generator->randomToken('sha1');
503
        $this->TempIDExpired = $lifetime
504
            ? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + $lifetime)
505
            : null;
506
        $this->write();
507
    }
508
509
    /**
510
     * Check if the member ID logged in session actually
511
     * has a database record of the same ID. If there is
512
     * no logged in user, FALSE is returned anyway.
513
     *
514
     * @deprecated Not needed anymore, as it returns Security::getCurrentUser();
515
     *
516
     * @return boolean TRUE record found FALSE no record found
517
     */
518
    public static function logged_in_session_exists()
519
    {
520
        Deprecation::notice(
521
            '5.0.0',
522
            'This method is deprecated and now does not add value. Please use Security::getCurrentUser()'
523
        );
524
525
        if ($member = Security::getCurrentUser()) {
526
            if ($member && $member->exists()) {
527
                return true;
528
            }
529
        }
530
531
        return false;
532
    }
533
534
    /**
535
     * @deprecated Use Security::setCurrentUser(null) or an IdentityStore
536
     * Logs this member out.
537
     */
538
    public function logOut()
539
    {
540
        Deprecation::notice(
541
            '5.0.0',
542
            'This method is deprecated and now does not persist. Please use Security::setCurrentUser(null) or an IdentityStore'
543
        );
544
545
        Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest());
546
    }
547
548
    /**
549
     * Audit logging hook, called before a member is logged out
550
     *
551
     * @param HTTPRequest|null $request
552
     */
553
    public function beforeMemberLoggedOut(HTTPRequest $request = null)
554
    {
555
        $this->extend('beforeMemberLoggedOut', $request);
556
    }
557
558
    /**
559
     * Audit logging hook, called after a member is logged out
560
     *
561
     * @param HTTPRequest|null $request
562
     */
563
    public function afterMemberLoggedOut(HTTPRequest $request = null)
564
    {
565
        $this->extend('afterMemberLoggedOut', $request);
566
    }
567
568
    /**
569
     * Utility for generating secure password hashes for this member.
570
     *
571
     * @param string $string
572
     * @return string
573
     * @throws PasswordEncryptor_NotFoundException
574
     */
575
    public function encryptWithUserSettings($string)
576
    {
577
        if (!$string) {
578
            return null;
579
        }
580
581
        // If the algorithm or salt is not available, it means we are operating
582
        // on legacy account with unhashed password. Do not hash the string.
583
        if (!$this->PasswordEncryption) {
584
            return $string;
585
        }
586
587
        // We assume we have PasswordEncryption and Salt available here.
588
        $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
589
590
        return $e->encrypt($string, $this->Salt);
591
    }
592
593
    /**
594
     * Generate an auto login token which can be used to reset the password,
595
     * at the same time hashing it and storing in the database.
596
     *
597
     * @param int|null $lifetime DEPRECATED: The lifetime of the auto login hash in days. Overrides
598
     *                           the Member.auto_login_token_lifetime config value
599
     * @return string Token that should be passed to the client (but NOT persisted).
600
     */
601
    public function generateAutologinTokenAndStoreHash($lifetime = null)
602
    {
603
        if ($lifetime !== null) {
604
            Deprecation::notice(
605
                '5.0',
606
                'Passing a $lifetime to Member::generateAutologinTokenAndStoreHash() is deprecated,
607
                    use the Member.auto_login_token_lifetime config setting instead',
608
                Deprecation::SCOPE_GLOBAL
609
            );
610
            $lifetime = (86400 * $lifetime); // Method argument is days, convert to seconds
611
        } else {
612
            $lifetime = $this->config()->auto_login_token_lifetime;
0 ignored issues
show
Bug Best Practice introduced by
The property auto_login_token_lifetime does not exist on SilverStripe\Core\Config\Config_ForClass. Since you implemented __get, consider adding a @property annotation.
Loading history...
613
        }
614
615
        do {
616
            $generator = new RandomGenerator();
617
            $token = $generator->randomToken();
618
            $hash = $this->encryptWithUserSettings($token);
619
        } while (DataObject::get_one(Member::class, array(
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...
620
            '"Member"."AutoLoginHash"' => $hash
621
        )));
622
623
        $this->AutoLoginHash = $hash;
624
        $this->AutoLoginExpired = date('Y-m-d H:i:s', time() + $lifetime);
625
626
        $this->write();
627
628
        return $token;
629
    }
630
631
    /**
632
     * Check the token against the member.
633
     *
634
     * @param string $autologinToken
635
     *
636
     * @returns bool Is token valid?
637
     */
638
    public function validateAutoLoginToken($autologinToken)
639
    {
640
        $hash = $this->encryptWithUserSettings($autologinToken);
641
        $member = self::member_from_autologinhash($hash, false);
642
643
        return (bool)$member;
644
    }
645
646
    /**
647
     * Return the member for the auto login hash
648
     *
649
     * @param string $hash The hash key
650
     * @param bool $login Should the member be logged in?
651
     *
652
     * @return Member the matching member, if valid
653
     * @return Member
654
     */
655
    public static function member_from_autologinhash($hash, $login = false)
656
    {
657
        /** @var Member $member */
658
        $member = static::get()->filter([
659
            'AutoLoginHash'                => $hash,
660
            'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(),
661
        ])->first();
662
663
        if ($login && $member) {
664
            Injector::inst()->get(IdentityStore::class)->logIn($member);
665
        }
666
667
        return $member;
668
    }
669
670
    /**
671
     * Find a member record with the given TempIDHash value
672
     *
673
     * @param string $tempid
674
     * @return Member
675
     */
676
    public static function member_from_tempid($tempid)
677
    {
678
        $members = static::get()
679
            ->filter('TempIDHash', $tempid);
680
681
        // Exclude expired
682
        if (static::config()->get('temp_id_lifetime')) {
683
            /** @var DataList|Member[] $members */
684
            $members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
685
        }
686
687
        return $members->first();
688
    }
689
690
    /**
691
     * Returns the fields for the member form - used in the registration/profile module.
692
     * It should return fields that are editable by the admin and the logged-in user.
693
     *
694
     * @todo possibly move this to an extension
695
     *
696
     * @return FieldList Returns a {@link FieldList} containing the fields for
697
     *                   the member form.
698
     */
699
    public function getMemberFormFields()
700
    {
701
        $fields = parent::getFrontEndFields();
702
703
        $fields->replaceField('Password', $this->getMemberPasswordField());
704
705
        $fields->replaceField('Locale', new DropdownField(
706
            'Locale',
707
            $this->fieldLabel('Locale'),
708
            i18n::getSources()->getKnownLocales()
709
        ));
710
711
        $fields->removeByName(static::config()->get('hidden_fields'));
712
        $fields->removeByName('FailedLoginCount');
713
714
715
        $this->extend('updateMemberFormFields', $fields);
716
717
        return $fields;
718
    }
719
720
    /**
721
     * Builds "Change / Create Password" field for this member
722
     *
723
     * @return ConfirmedPasswordField
724
     */
725
    public function getMemberPasswordField()
726
    {
727
        $editingPassword = $this->isInDB();
728
        $label = $editingPassword
729
            ? _t(__CLASS__ . '.EDIT_PASSWORD', 'New Password')
730
            : $this->fieldLabel('Password');
731
        /** @var ConfirmedPasswordField $password */
732
        $password = ConfirmedPasswordField::create(
733
            'Password',
0 ignored issues
show
Bug introduced by
'Password' of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

733
            /** @scrutinizer ignore-type */ 'Password',
Loading history...
734
            $label,
735
            null,
736
            null,
737
            $editingPassword
0 ignored issues
show
Bug introduced by
$editingPassword of type boolean is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

737
            /** @scrutinizer ignore-type */ $editingPassword
Loading history...
738
        );
739
740
        // If editing own password, require confirmation of existing
741
        if ($editingPassword && $this->ID == Security::getCurrentUser()->ID) {
742
            $password->setRequireExistingPassword(true);
743
        }
744
745
        $password->setCanBeEmpty(true);
746
        $this->extend('updateMemberPasswordField', $password);
747
748
        return $password;
749
    }
750
751
752
    /**
753
     * Returns the {@link RequiredFields} instance for the Member object. This
754
     * Validator is used when saving a {@link CMSProfileController} or added to
755
     * any form responsible for saving a users data.
756
     *
757
     * To customize the required fields, add a {@link DataExtension} to member
758
     * calling the `updateValidator()` method.
759
     *
760
     * @return Member_Validator
761
     */
762
    public function getValidator()
763
    {
764
        $validator = Member_Validator::create();
765
        $validator->setForMember($this);
766
        $this->extend('updateValidator', $validator);
767
768
        return $validator;
769
    }
770
771
772
    /**
773
     * Returns the current logged in user
774
     *
775
     * @deprecated 5.0.0 use Security::getCurrentUser()
776
     *
777
     * @return Member
778
     */
779
    public static function currentUser()
780
    {
781
        Deprecation::notice(
782
            '5.0.0',
783
            'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore'
784
        );
785
786
        return Security::getCurrentUser();
787
    }
788
789
    /**
790
     * Temporarily act as the specified user, limited to a $callback, but
791
     * without logging in as that user.
792
     *
793
     * E.g.
794
     * <code>
795
     * Member::logInAs(Security::findAnAdministrator(), function() {
796
     *     $record->write();
797
     * });
798
     * </code>
799
     *
800
     * @param Member|null|int $member Member or member ID to log in as.
801
     * Set to null or 0 to act as a logged out user.
802
     * @param callable $callback
803
     */
804
    public static function actAs($member, $callback)
805
    {
806
        $previousUser = Security::getCurrentUser();
807
808
        // Transform ID to member
809
        if (is_numeric($member)) {
810
            $member = DataObject::get_by_id(Member::class, $member);
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...
811
        }
812
        Security::setCurrentUser($member);
813
814
        try {
815
            return $callback();
816
        } finally {
817
            Security::setCurrentUser($previousUser);
818
        }
819
    }
820
821
    /**
822
     * Get the ID of the current logged in user
823
     *
824
     * @deprecated 5.0.0 use Security::getCurrentUser()
825
     *
826
     * @return int Returns the ID of the current logged in user or 0.
827
     */
828
    public static function currentUserID()
829
    {
830
        Deprecation::notice(
831
            '5.0.0',
832
            'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore'
833
        );
834
835
        if ($member = Security::getCurrentUser()) {
836
            return $member->ID;
837
        } else {
838
            return 0;
839
        }
840
    }
841
842
    /**
843
     * Generate a random password, with randomiser to kick in if there's no words file on the
844
     * filesystem.
845
     *
846
     * @return string Returns a random password.
847
     */
848
    public static function create_new_password()
849
    {
850
        $words = Security::config()->uninherited('word_list');
851
852
        if ($words && file_exists($words)) {
853
            $words = file($words);
854
855
            list($usec, $sec) = explode(' ', microtime());
856
            mt_srand($sec + ((float)$usec * 100000));
0 ignored issues
show
Bug introduced by
$sec + (double)$usec * 100000 of type double is incompatible with the type integer expected by parameter $seed of mt_srand(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

856
            mt_srand(/** @scrutinizer ignore-type */ $sec + ((float)$usec * 100000));
Loading history...
857
858
            $word = trim($words[random_int(0, count($words) - 1)]);
0 ignored issues
show
Bug introduced by
It seems like $words can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

858
            $word = trim($words[random_int(0, count(/** @scrutinizer ignore-type */ $words) - 1)]);
Loading history...
859
            $number = random_int(10, 999);
860
861
            return $word . $number;
862
        } else {
863
            $random = mt_rand();
0 ignored issues
show
Bug introduced by
The call to mt_rand() has too few arguments starting with min. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

863
            $random = /** @scrutinizer ignore-call */ mt_rand();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
864
            $string = md5($random);
865
            $output = substr($string, 0, 8);
866
867
            return $output;
868
        }
869
    }
870
871
    /**
872
     * Event handler called before writing to the database.
873
     */
874
    public function onBeforeWrite()
875
    {
876
        // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
877
        // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
878
        // but rather a last line of defense against data inconsistencies.
879
        $identifierField = Member::config()->get('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...
880
        if ($this->$identifierField) {
881
            // Note: Same logic as Member_Validator class
882
            $filter = [
883
                "\"Member\".\"$identifierField\"" => $this->$identifierField
884
            ];
885
            if ($this->ID) {
886
                $filter[] = array('"Member"."ID" <> ?' => $this->ID);
887
            }
888
            $existingRecord = DataObject::get_one(Member::class, $filter);
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...
889
890
            if ($existingRecord) {
891
                throw new ValidationException(_t(
892
                    __CLASS__ . '.ValidationIdentifierFailed',
893
                    'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
894
                    'Values in brackets show "fieldname = value", usually denoting an existing email address',
895
                    array(
896
                        'id'    => $existingRecord->ID,
897
                        'name'  => $identifierField,
898
                        'value' => $this->$identifierField
899
                    )
900
                ));
901
            }
902
        }
903
904
        // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
905
        // However, if TestMailer is in use this isn't a risk.
906
        // @todo some developers use external tools, so emailing might be a good idea anyway
907
        if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer)
908
            && $this->isChanged('Password')
909
            && $this->record['Password']
910
            && static::config()->get('notify_password_change')
911
        ) {
912
            Email::create()
913
                ->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail')
914
                ->setData($this)
915
                ->setTo($this->Email)
916
                ->setSubject(_t(__CLASS__ . '.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'))
917
                ->send();
918
        }
919
920
        // The test on $this->ID is used for when records are initially created. Note that this only works with
921
        // cleartext passwords, as we can't rehash existing passwords. Checking passwordChangesToWrite prevents
922
        // recursion between changePassword and this method.
923
        if ((!$this->ID && $this->Password) || ($this->isChanged('Password') && !$this->passwordChangesToWrite)) {
924
            $this->changePassword($this->Password, false);
925
        }
926
927
        // save locale
928
        if (!$this->Locale) {
929
            $this->Locale = i18n::get_locale();
930
        }
931
932
        parent::onBeforeWrite();
933
    }
934
935
    public function onAfterWrite()
936
    {
937
        parent::onAfterWrite();
938
939
        Permission::reset();
940
941
        if ($this->isChanged('Password') && static::config()->get('password_logging_enabled')) {
942
            MemberPassword::log($this);
943
        }
944
    }
945
946
    public function onAfterDelete()
947
    {
948
        parent::onAfterDelete();
949
950
        //prevent orphaned records remaining in the DB
951
        $this->deletePasswordLogs();
952
    }
953
954
    /**
955
     * Delete the MemberPassword objects that are associated to this user
956
     *
957
     * @return $this
958
     */
959
    protected function deletePasswordLogs()
960
    {
961
        foreach ($this->LoggedPasswords() as $password) {
962
            $password->delete();
963
            $password->destroy();
964
        }
965
966
        return $this;
967
    }
968
969
    /**
970
     * Filter out admin groups to avoid privilege escalation,
971
     * If any admin groups are requested, deny the whole save operation.
972
     *
973
     * @param array $ids Database IDs of Group records
974
     * @return bool True if the change can be accepted
975
     */
976
    public function onChangeGroups($ids)
977
    {
978
        // unless the current user is an admin already OR the logged in user is an admin
979
        if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
980
            return true;
981
        }
982
983
        // If there are no admin groups in this set then it's ok
984
        $adminGroups = Permission::get_groups_by_permission('ADMIN');
985
        $adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
986
987
        return count(array_intersect($ids, $adminGroupIDs)) == 0;
988
    }
989
990
991
    /**
992
     * Check if the member is in one of the given groups.
993
     *
994
     * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
995
     * @param boolean $strict Only determine direct group membership if set to true (Default: false)
996
     * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
997
     */
998
    public function inGroups($groups, $strict = false)
999
    {
1000
        if ($groups) {
1001
            foreach ($groups as $group) {
1002
                if ($this->inGroup($group, $strict)) {
1003
                    return true;
1004
                }
1005
            }
1006
        }
1007
1008
        return false;
1009
    }
1010
1011
1012
    /**
1013
     * Check if the member is in the given group or any parent groups.
1014
     *
1015
     * @param int|Group|string $group Group instance, Group Code or ID
1016
     * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
1017
     * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
1018
     */
1019
    public function inGroup($group, $strict = false)
1020
    {
1021
        if (is_numeric($group)) {
1022
            $groupCheckObj = DataObject::get_by_id(Group::class, $group);
0 ignored issues
show
Bug introduced by
It seems like $group can also be of type string; however, parameter $id of SilverStripe\ORM\DataObject::get_by_id() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1022
            $groupCheckObj = DataObject::get_by_id(Group::class, /** @scrutinizer ignore-type */ $group);
Loading history...
1023
        } elseif (is_string($group)) {
1024
            $groupCheckObj = DataObject::get_one(Group::class, array(
1025
                '"Group"."Code"' => $group
1026
            ));
1027
        } elseif ($group instanceof Group) {
1028
            $groupCheckObj = $group;
1029
        } else {
1030
            throw new InvalidArgumentException('Member::inGroup(): Wrong format for $group parameter');
1031
        }
1032
1033
        if (!$groupCheckObj) {
1034
            return false;
1035
        }
1036
1037
        $groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1038
        if ($groupCandidateObjs) {
1039
            foreach ($groupCandidateObjs as $groupCandidateObj) {
1040
                if ($groupCandidateObj->ID == $groupCheckObj->ID) {
1041
                    return true;
1042
                }
1043
            }
1044
        }
1045
1046
        return false;
1047
    }
1048
1049
    /**
1050
     * Adds the member to a group. This will create the group if the given
1051
     * group code does not return a valid group object.
1052
     *
1053
     * @param string $groupcode
1054
     * @param string $title Title of the group
1055
     */
1056
    public function addToGroupByCode($groupcode, $title = "")
1057
    {
1058
        $group = DataObject::get_one(Group::class, array(
1059
            '"Group"."Code"' => $groupcode
1060
        ));
1061
1062
        if ($group) {
1063
            $this->Groups()->add($group);
1064
        } else {
1065
            if (!$title) {
1066
                $title = $groupcode;
1067
            }
1068
1069
            $group = new Group();
1070
            $group->Code = $groupcode;
1071
            $group->Title = $title;
1072
            $group->write();
1073
1074
            $this->Groups()->add($group);
1075
        }
1076
    }
1077
1078
    /**
1079
     * Removes a member from a group.
1080
     *
1081
     * @param string $groupcode
1082
     */
1083
    public function removeFromGroupByCode($groupcode)
1084
    {
1085
        $group = Group::get()->filter(array('Code' => $groupcode))->first();
1086
1087
        if ($group) {
1088
            $this->Groups()->remove($group);
1089
        }
1090
    }
1091
1092
    /**
1093
     * @param array $columns Column names on the Member record to show in {@link getTitle()}.
1094
     * @param String $sep Separator
1095
     */
1096
    public static function set_title_columns($columns, $sep = ' ')
1097
    {
1098
        Deprecation::notice('5.0', 'Use Member.title_format config instead');
1099
        if (!is_array($columns)) {
1100
            $columns = array($columns);
1101
        }
1102
        self::config()->set(
1103
            'title_format',
1104
            [
1105
                'columns' => $columns,
1106
                'sep' => $sep
1107
            ]
1108
        );
1109
    }
1110
1111
    //------------------- HELPER METHODS -----------------------------------//
1112
1113
    /**
1114
     * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1115
     * Falls back to showing either field on its own.
1116
     *
1117
     * You can overload this getter with {@link set_title_format()}
1118
     * and {@link set_title_sql()}.
1119
     *
1120
     * @return string Returns the first- and surname of the member. If the ID
1121
     *  of the member is equal 0, only the surname is returned.
1122
     */
1123
    public function getTitle()
1124
    {
1125
        $format = static::config()->get('title_format');
1126
        if ($format) {
1127
            $values = array();
1128
            foreach ($format['columns'] as $col) {
1129
                $values[] = $this->getField($col);
1130
            }
1131
1132
            return implode($format['sep'], $values);
1133
        }
1134
        if ($this->getField('ID') === 0) {
1135
            return $this->getField('Surname');
1136
        } else {
1137
            if ($this->getField('Surname') && $this->getField('FirstName')) {
1138
                return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1139
            } elseif ($this->getField('Surname')) {
1140
                return $this->getField('Surname');
1141
            } elseif ($this->getField('FirstName')) {
1142
                return $this->getField('FirstName');
1143
            } else {
1144
                return null;
1145
            }
1146
        }
1147
    }
1148
1149
    /**
1150
     * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1151
     * Useful for custom queries which assume a certain member title format.
1152
     *
1153
     * @return String SQL
1154
     */
1155
    public static function get_title_sql()
1156
    {
1157
1158
        // Get title_format with fallback to default
1159
        $format = static::config()->get('title_format');
1160
        if (!$format) {
1161
            $format = [
1162
                'columns' => ['Surname', 'FirstName'],
1163
                'sep'     => ' ',
1164
            ];
1165
        }
1166
1167
        $columnsWithTablename = array();
1168
        foreach ($format['columns'] as $column) {
1169
            $columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column);
1170
        }
1171
1172
        $sepSQL = Convert::raw2sql($format['sep'], true);
1173
        $op = DB::get_conn()->concatOperator();
1174
        return "(" . join(" $op $sepSQL $op ", $columnsWithTablename) . ")";
1175
    }
1176
1177
1178
    /**
1179
     * Get the complete name of the member
1180
     *
1181
     * @return string Returns the first- and surname of the member.
1182
     */
1183
    public function getName()
1184
    {
1185
        return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1186
    }
1187
1188
1189
    /**
1190
     * Set first- and surname
1191
     *
1192
     * This method assumes that the last part of the name is the surname, e.g.
1193
     * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1194
     *
1195
     * @param string $name The name
1196
     */
1197
    public function setName($name)
1198
    {
1199
        $nameParts = explode(' ', $name);
1200
        $this->Surname = array_pop($nameParts);
1201
        $this->FirstName = join(' ', $nameParts);
1202
    }
1203
1204
1205
    /**
1206
     * Alias for {@link setName}
1207
     *
1208
     * @param string $name The name
1209
     * @see setName()
1210
     */
1211
    public function splitName($name)
1212
    {
1213
        return $this->setName($name);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->setName($name) targeting SilverStripe\Security\Member::setName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1214
    }
1215
1216
    /**
1217
     * Return the date format based on the user's chosen locale,
1218
     * falling back to the default format defined by the {@link i18n.get_locale()} setting.
1219
     *
1220
     * @return string ISO date format
1221
     */
1222
    public function getDateFormat()
1223
    {
1224
        $formatter = new IntlDateFormatter(
0 ignored issues
show
Bug introduced by
The call to IntlDateFormatter::__construct() has too few arguments starting with timezone. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1224
        $formatter = /** @scrutinizer ignore-call */ new IntlDateFormatter(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1225
            $this->getLocale(),
1226
            IntlDateFormatter::MEDIUM,
1227
            IntlDateFormatter::NONE
1228
        );
1229
        $format = $formatter->getPattern();
1230
1231
        $this->extend('updateDateFormat', $format);
1232
1233
        return $format;
1234
    }
1235
1236
    /**
1237
     * Get user locale
1238
     */
1239
    public function getLocale()
1240
    {
1241
        $locale = $this->getField('Locale');
1242
        if ($locale) {
1243
            return $locale;
1244
        }
1245
1246
        return i18n::get_locale();
1247
    }
1248
1249
    /**
1250
     * Return the time format based on the user's chosen locale,
1251
     * falling back to the default format defined by the {@link i18n.get_locale()} setting.
1252
     *
1253
     * @return string ISO date format
1254
     */
1255
    public function getTimeFormat()
1256
    {
1257
        $formatter = new IntlDateFormatter(
0 ignored issues
show
Bug introduced by
The call to IntlDateFormatter::__construct() has too few arguments starting with timezone. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1257
        $formatter = /** @scrutinizer ignore-call */ new IntlDateFormatter(

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1258
            $this->getLocale(),
1259
            IntlDateFormatter::NONE,
1260
            IntlDateFormatter::MEDIUM
1261
        );
1262
        $format = $formatter->getPattern();
1263
1264
        $this->extend('updateTimeFormat', $format);
1265
1266
        return $format;
1267
    }
1268
1269
    //---------------------------------------------------------------------//
1270
1271
1272
    /**
1273
     * Get a "many-to-many" map that holds for all members their group memberships,
1274
     * including any parent groups where membership is implied.
1275
     * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1276
     *
1277
     * @todo Push all this logic into Member_GroupSet's getIterator()?
1278
     * @return Member_Groupset
1279
     */
1280
    public function Groups()
1281
    {
1282
        $groups = Member_GroupSet::create(Group::class, 'Group_Members', 'GroupID', 'MemberID');
0 ignored issues
show
Bug introduced by
SilverStripe\Security\Group::class of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1282
        $groups = Member_GroupSet::create(/** @scrutinizer ignore-type */ Group::class, 'Group_Members', 'GroupID', 'MemberID');
Loading history...
1283
        $groups = $groups->forForeignID($this->ID);
1284
1285
        $this->extend('updateGroups', $groups);
1286
1287
        return $groups;
1288
    }
1289
1290
    /**
1291
     * @return ManyManyList
1292
     */
1293
    public function DirectGroups()
1294
    {
1295
        return $this->getManyManyComponents('Groups');
1296
    }
1297
1298
    /**
1299
     * Get a member SQLMap of members in specific groups
1300
     *
1301
     * If no $groups is passed, all members will be returned
1302
     *
1303
     * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1304
     * @return Map Returns an Map that returns all Member data.
1305
     */
1306
    public static function map_in_groups($groups = null)
1307
    {
1308
        $groupIDList = array();
1309
1310
        if ($groups instanceof SS_List) {
1311
            foreach ($groups as $group) {
1312
                $groupIDList[] = $group->ID;
1313
            }
1314
        } elseif (is_array($groups)) {
1315
            $groupIDList = $groups;
1316
        } elseif ($groups) {
1317
            $groupIDList[] = $groups;
1318
        }
1319
1320
        // No groups, return all Members
1321
        if (!$groupIDList) {
1322
            return static::get()->sort(array('Surname' => 'ASC', 'FirstName' => 'ASC'))->map();
1323
        }
1324
1325
        $membersList = new ArrayList();
1326
        // This is a bit ineffective, but follow the ORM style
1327
        /** @var Group $group */
1328
        foreach (Group::get()->byIDs($groupIDList) as $group) {
1329
            $membersList->merge($group->Members());
1330
        }
1331
1332
        $membersList->removeDuplicates('ID');
1333
1334
        return $membersList->map();
1335
    }
1336
1337
1338
    /**
1339
     * Get a map of all members in the groups given that have CMS permissions
1340
     *
1341
     * If no groups are passed, all groups with CMS permissions will be used.
1342
     *
1343
     * @param array $groups Groups to consider or NULL to use all groups with
1344
     *                      CMS permissions.
1345
     * @return Map Returns a map of all members in the groups given that
1346
     *                have CMS permissions.
1347
     */
1348
    public static function mapInCMSGroups($groups = null)
1349
    {
1350
        // Check CMS module exists
1351
        if (!class_exists(LeftAndMain::class)) {
1352
            return ArrayList::create()->map();
1353
        }
1354
1355
        if (count($groups) == 0) {
1356
            $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1357
1358
            if (class_exists(CMSMain::class)) {
1359
                $cmsPerms = CMSMain::singleton()->providePermissions();
1360
            } else {
1361
                $cmsPerms = LeftAndMain::singleton()->providePermissions();
1362
            }
1363
1364
            if (!empty($cmsPerms)) {
1365
                $perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1366
            }
1367
1368
            $permsClause = DB::placeholders($perms);
1369
            /** @skipUpgrade */
1370
            $groups = Group::get()
1371
                ->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1372
                ->where(array(
1373
                    "\"Permission\".\"Code\" IN ($permsClause)" => $perms
1374
                ));
1375
        }
1376
1377
        $groupIDList = array();
1378
1379
        if ($groups instanceof SS_List) {
1380
            foreach ($groups as $group) {
1381
                $groupIDList[] = $group->ID;
1382
            }
1383
        } elseif (is_array($groups)) {
1384
            $groupIDList = $groups;
1385
        }
1386
1387
        /** @skipUpgrade */
1388
        $members = static::get()
1389
            ->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1390
            ->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1391
        if ($groupIDList) {
1392
            $groupClause = DB::placeholders($groupIDList);
1393
            $members = $members->where(array(
1394
                "\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1395
            ));
1396
        }
1397
1398
        return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1399
    }
1400
1401
1402
    /**
1403
     * Get the groups in which the member is NOT in
1404
     *
1405
     * When passed an array of groups, and a component set of groups, this
1406
     * function will return the array of groups the member is NOT in.
1407
     *
1408
     * @param array $groupList An array of group code names.
1409
     * @param array $memberGroups A component set of groups (if set to NULL,
1410
     *                            $this->groups() will be used)
1411
     * @return array Groups in which the member is NOT in.
1412
     */
1413
    public function memberNotInGroups($groupList, $memberGroups = null)
1414
    {
1415
        if (!$memberGroups) {
1416
            $memberGroups = $this->Groups();
1417
        }
1418
1419
        foreach ($memberGroups as $group) {
1420
            if (in_array($group->Code, $groupList)) {
1421
                $index = array_search($group->Code, $groupList);
1422
                unset($groupList[$index]);
1423
            }
1424
        }
1425
1426
        return $groupList;
1427
    }
1428
1429
1430
    /**
1431
     * Return a {@link FieldList} of fields that would appropriate for editing
1432
     * this member.
1433
     *
1434
     * @skipUpgrade
1435
     * @return FieldList Return a FieldList of fields that would appropriate for
1436
     *                   editing this member.
1437
     */
1438
    public function getCMSFields()
1439
    {
1440
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
1441
            /** @var TabSet $rootTabSet */
1442
            $rootTabSet = $fields->fieldByName("Root");
1443
            /** @var Tab $mainTab */
1444
            $mainTab = $rootTabSet->fieldByName("Main");
1445
            /** @var FieldList $mainFields */
1446
            $mainFields = $mainTab->getChildren();
1447
1448
            // Build change password field
1449
            $mainFields->replaceField('Password', $this->getMemberPasswordField());
1450
1451
            $mainFields->replaceField('Locale', new DropdownField(
1452
                "Locale",
1453
                _t(__CLASS__ . '.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1454
                i18n::getSources()->getKnownLocales()
1455
            ));
1456
            $mainFields->removeByName(static::config()->get('hidden_fields'));
1457
1458
            if (!static::config()->get('lock_out_after_incorrect_logins')) {
1459
                $mainFields->removeByName('FailedLoginCount');
1460
            }
1461
1462
            // Groups relation will get us into logical conflicts because
1463
            // Members are displayed within  group edit form in SecurityAdmin
1464
            $fields->removeByName('Groups');
1465
1466
            // Members shouldn't be able to directly view/edit logged passwords
1467
            $fields->removeByName('LoggedPasswords');
1468
1469
            $fields->removeByName('RememberLoginHashes');
1470
1471
            if (Permission::check('EDIT_PERMISSIONS')) {
1472
                $groupsMap = array();
1473
                foreach (Group::get() as $group) {
1474
                    // Listboxfield values are escaped, use ASCII char instead of &raquo;
1475
                    $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1476
                }
1477
                asort($groupsMap);
1478
                $fields->addFieldToTab(
1479
                    'Root.Main',
1480
                    ListboxField::create('DirectGroups', Group::singleton()->i18n_plural_name())
0 ignored issues
show
Bug introduced by
'DirectGroups' of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1480
                    ListboxField::create(/** @scrutinizer ignore-type */ 'DirectGroups', Group::singleton()->i18n_plural_name())
Loading history...
1481
                        ->setSource($groupsMap)
1482
                        ->setAttribute(
1483
                            'data-placeholder',
1484
                            _t(__CLASS__ . '.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1485
                        )
1486
                );
1487
1488
1489
                // Add permission field (readonly to avoid complicated group assignment logic).
1490
                // This should only be available for existing records, as new records start
1491
                // with no permissions until they have a group assignment anyway.
1492
                if ($this->ID) {
1493
                    $permissionsField = new PermissionCheckboxSetField_Readonly(
1494
                        'Permissions',
1495
                        false,
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $title of SilverStripe\Security\Pe...Readonly::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1495
                        /** @scrutinizer ignore-type */ false,
Loading history...
1496
                        Permission::class,
1497
                        'GroupID',
1498
                        // we don't want parent relationships, they're automatically resolved in the field
1499
                        $this->getManyManyComponents('Groups')
1500
                    );
1501
                    $fields->findOrMakeTab('Root.Permissions', Permission::singleton()->i18n_plural_name());
1502
                    $fields->addFieldToTab('Root.Permissions', $permissionsField);
1503
                }
1504
            }
1505
1506
            $permissionsTab = $rootTabSet->fieldByName('Permissions');
1507
            if ($permissionsTab) {
1508
                $permissionsTab->addExtraClass('readonly');
1509
            }
1510
        });
1511
1512
        return parent::getCMSFields();
1513
    }
1514
1515
    /**
1516
     * @param bool $includerelations Indicate if the labels returned include relation fields
1517
     * @return array
1518
     */
1519
    public function fieldLabels($includerelations = true)
1520
    {
1521
        $labels = parent::fieldLabels($includerelations);
1522
1523
        $labels['FirstName'] = _t(__CLASS__ . '.FIRSTNAME', 'First Name');
1524
        $labels['Surname'] = _t(__CLASS__ . '.SURNAME', 'Surname');
1525
        /** @skipUpgrade */
1526
        $labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email');
1527
        $labels['Password'] = _t(__CLASS__ . '.db_Password', 'Password');
1528
        $labels['PasswordExpiry'] = _t(__CLASS__ . '.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1529
        $labels['LockedOutUntil'] = _t(__CLASS__ . '.db_LockedOutUntil', 'Locked out until', 'Security related date');
1530
        $labels['Locale'] = _t(__CLASS__ . '.db_Locale', 'Interface Locale');
1531
        if ($includerelations) {
1532
            $labels['Groups'] = _t(
1533
                __CLASS__ . '.belongs_many_many_Groups',
1534
                'Groups',
1535
                'Security Groups this member belongs to'
1536
            );
1537
        }
1538
1539
        return $labels;
1540
    }
1541
1542
    /**
1543
     * Users can view their own record.
1544
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1545
     * This is likely to be customized for social sites etc. with a looser permission model.
1546
     *
1547
     * @param Member $member
1548
     * @return bool
1549
     */
1550
    public function canView($member = null)
1551
    {
1552
        //get member
1553
        if (!$member) {
1554
            $member = Security::getCurrentUser();
1555
        }
1556
        //check for extensions, we do this first as they can overrule everything
1557
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1558
        if ($extended !== null) {
1559
            return $extended;
1560
        }
1561
1562
        //need to be logged in and/or most checks below rely on $member being a Member
1563
        if (!$member) {
1564
            return false;
1565
        }
1566
        // members can usually view their own record
1567
        if ($this->ID == $member->ID) {
1568
            return true;
1569
        }
1570
1571
        //standard check
1572
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1573
    }
1574
1575
    /**
1576
     * Users can edit their own record.
1577
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1578
     *
1579
     * @param Member $member
1580
     * @return bool
1581
     */
1582
    public function canEdit($member = null)
1583
    {
1584
        //get member
1585
        if (!$member) {
1586
            $member = Security::getCurrentUser();
1587
        }
1588
        //check for extensions, we do this first as they can overrule everything
1589
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1590
        if ($extended !== null) {
1591
            return $extended;
1592
        }
1593
1594
        //need to be logged in and/or most checks below rely on $member being a Member
1595
        if (!$member) {
1596
            return false;
1597
        }
1598
1599
        // HACK: we should not allow for an non-Admin to edit an Admin
1600
        if (!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
1601
            return false;
1602
        }
1603
        // members can usually edit their own record
1604
        if ($this->ID == $member->ID) {
1605
            return true;
1606
        }
1607
1608
        //standard check
1609
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1610
    }
1611
1612
    /**
1613
     * Users can edit their own record.
1614
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1615
     *
1616
     * @param Member $member
1617
     * @return bool
1618
     */
1619
    public function canDelete($member = null)
1620
    {
1621
        if (!$member) {
1622
            $member = Security::getCurrentUser();
1623
        }
1624
        //check for extensions, we do this first as they can overrule everything
1625
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1626
        if ($extended !== null) {
1627
            return $extended;
1628
        }
1629
1630
        //need to be logged in and/or most checks below rely on $member being a Member
1631
        if (!$member) {
1632
            return false;
1633
        }
1634
        // Members are not allowed to remove themselves,
1635
        // since it would create inconsistencies in the admin UIs.
1636
        if ($this->ID && $member->ID == $this->ID) {
1637
            return false;
1638
        }
1639
1640
        // HACK: if you want to delete a member, you have to be a member yourself.
1641
        // this is a hack because what this should do is to stop a user
1642
        // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
1643
        if (Permission::checkMember($this, 'ADMIN')) {
1644
            if (!Permission::checkMember($member, 'ADMIN')) {
1645
                return false;
1646
            }
1647
        }
1648
1649
        //standard check
1650
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1651
    }
1652
1653
    /**
1654
     * Validate this member object.
1655
     */
1656
    public function validate()
1657
    {
1658
        $valid = parent::validate();
1659
        $validator = static::password_validator();
1660
1661
        if (!$this->ID || $this->isChanged('Password')) {
1662
            if ($this->Password && $validator) {
1663
                $valid->combineAnd($validator->validate($this->Password, $this));
1664
            }
1665
        }
1666
1667
        return $valid;
1668
    }
1669
1670
    /**
1671
     * Change password. This will cause rehashing according to the `PasswordEncryption` property. This method will
1672
     * allow extensions to perform actions and augment the validation result if required before the password is written
1673
     * and can check it after the write also.
1674
     *
1675
     * This method will encrypt the password prior to writing.
1676
     *
1677
     * @param string $password  Cleartext password
1678
     * @param bool   $write     Whether to write the member afterwards
1679
     * @return ValidationResult
1680
     */
1681
    public function changePassword($password, $write = true)
1682
    {
1683
        $this->Password = $password;
1684
        $valid = $this->validate();
1685
1686
        $this->extend('onBeforeChangePassword', $password, $valid);
1687
1688
        if ($valid->isValid()) {
1689
            $this->AutoLoginHash = null;
1690
1691
            $this->encryptPassword();
1692
1693
            if ($write) {
1694
                $this->passwordChangesToWrite = true;
1695
                $this->write();
1696
            }
1697
        }
1698
1699
        $this->extend('onAfterChangePassword', $password, $valid);
1700
1701
        return $valid;
1702
    }
1703
1704
    /**
1705
     * Takes a plaintext password (on the Member object) and encrypts it
1706
     *
1707
     * @return $this
1708
     */
1709
    protected function encryptPassword()
1710
    {
1711
        // reset salt so that it gets regenerated - this will invalidate any persistent login cookies
1712
        // or other information encrypted with this Member's settings (see self::encryptWithUserSettings)
1713
        $this->Salt = '';
1714
1715
        // Password was changed: encrypt the password according the settings
1716
        $encryption_details = Security::encrypt_password(
1717
            $this->Password,
1718
            $this->Salt,
1719
            $this->isChanged('PasswordEncryption') ? $this->PasswordEncryption : null,
1720
            $this
1721
        );
1722
1723
        // Overwrite the Password property with the hashed value
1724
        $this->Password = $encryption_details['password'];
1725
        $this->Salt = $encryption_details['salt'];
1726
        $this->PasswordEncryption = $encryption_details['algorithm'];
1727
1728
        // If we haven't manually set a password expiry
1729
        if (!$this->isChanged('PasswordExpiry')) {
1730
            // then set it for us
1731
            if (static::config()->get('password_expiry_days')) {
1732
                $this->PasswordExpiry = date('Y-m-d', time() + 86400 * static::config()->get('password_expiry_days'));
1733
            } else {
1734
                $this->PasswordExpiry = null;
1735
            }
1736
        }
1737
1738
        return $this;
1739
    }
1740
1741
    /**
1742
     * Tell this member that someone made a failed attempt at logging in as them.
1743
     * This can be used to lock the user out temporarily if too many failed attempts are made.
1744
     */
1745
    public function registerFailedLogin()
1746
    {
1747
        $lockOutAfterCount = self::config()->get('lock_out_after_incorrect_logins');
1748
        if ($lockOutAfterCount) {
1749
            // Keep a tally of the number of failed log-ins so that we can lock people out
1750
            ++$this->FailedLoginCount;
1751
1752
            if ($this->FailedLoginCount >= $lockOutAfterCount) {
1753
                $lockoutMins = self::config()->get('lock_out_delay_mins');
1754
                $this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins * 60);
1755
                $this->FailedLoginCount = 0;
1756
            }
1757
        }
1758
        $this->extend('registerFailedLogin');
1759
        $this->write();
1760
    }
1761
1762
    /**
1763
     * Tell this member that a successful login has been made
1764
     */
1765
    public function registerSuccessfulLogin()
1766
    {
1767
        if (self::config()->get('lock_out_after_incorrect_logins')) {
1768
            // Forgive all past login failures
1769
            $this->FailedLoginCount = 0;
1770
            $this->LockedOutUntil = null;
1771
            $this->write();
1772
        }
1773
    }
1774
1775
    /**
1776
     * Get the HtmlEditorConfig for this user to be used in the CMS.
1777
     * This is set by the group. If multiple configurations are set,
1778
     * the one with the highest priority wins.
1779
     *
1780
     * @return string
1781
     */
1782
    public function getHtmlEditorConfigForCMS()
1783
    {
1784
        $currentName = '';
1785
        $currentPriority = 0;
1786
1787
        foreach ($this->Groups() as $group) {
1788
            $configName = $group->HtmlEditorConfig;
1789
            if ($configName) {
1790
                $config = HTMLEditorConfig::get($group->HtmlEditorConfig);
1791
                if ($config && $config->getOption('priority') > $currentPriority) {
1792
                    $currentName = $configName;
1793
                    $currentPriority = $config->getOption('priority');
1794
                }
1795
            }
1796
        }
1797
1798
        // If can't find a suitable editor, just default to cms
1799
        return $currentName ? $currentName : 'cms';
1800
    }
1801
}
1802