Completed
Pull Request — master (#6766)
by Ingo
08:55
created

Member::getDateFormats()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 0
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use IntlDateFormatter;
6
use SilverStripe\Admin\LeftAndMain;
7
use SilverStripe\CMS\Controllers\CMSMain;
8
use SilverStripe\Control\Cookie;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Control\Email\Email;
11
use SilverStripe\Control\Email\Mailer;
12
use SilverStripe\Control\Session;
13
use SilverStripe\Core\Convert;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Dev\SapphireTest;
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\MSSQL\MSSQLDatabase;
24
use SilverStripe\ORM\ArrayList;
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\SS_List;
31
use SilverStripe\ORM\Map;
32
use SilverStripe\ORM\ValidationException;
33
use SilverStripe\ORM\ValidationResult;
34
use SilverStripe\View\SSViewer;
35
use SilverStripe\View\TemplateGlobalProvider;
36
use DateTime;
37
38
/**
39
 * The member class which represents the users of the system
40
 *
41
 * @method HasManyList LoggedPasswords()
42
 * @method HasManyList RememberLoginHashes()
43
 * @property string $FirstName
44
 * @property string $Surname
45
 * @property string $Email
46
 * @property string $Password
47
 * @property string $TempIDHash
48
 * @property string $TempIDExpired
49
 * @property string $AutoLoginHash
50
 * @property string $AutoLoginExpired
51
 * @property string $PasswordEncryption
52
 * @property string $Salt
53
 * @property string $PasswordExpiry
54
 * @property string $LockedOutUntil
55
 * @property string $Locale
56
 * @property int $FailedLoginCount
57
 * @property string $DateFormat
58
 * @property string $TimeFormat
59
 */
60
class Member extends DataObject implements TemplateGlobalProvider
61
{
62
63
    private static $db = array(
64
        'FirstName' => 'Varchar',
65
        'Surname' => 'Varchar',
66
        'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
67
        'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
68
        'TempIDExpired' => 'Datetime', // Expiry of temp login
69
        'Password' => 'Varchar(160)',
70
        'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
71
        'AutoLoginExpired' => 'Datetime',
72
        // This is an arbitrary code pointing to a PasswordEncryptor instance,
73
        // not an actual encryption algorithm.
74
        // Warning: Never change this field after its the first password hashing without
75
        // providing a new cleartext password as well.
76
        'PasswordEncryption' => "Varchar(50)",
77
        'Salt' => 'Varchar(50)',
78
        'PasswordExpiry' => 'Date',
79
        'LockedOutUntil' => 'Datetime',
80
        'Locale' => 'Varchar(6)',
81
        // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
82
        'FailedLoginCount' => 'Int',
83
    );
84
85
    private static $belongs_many_many = array(
86
        'Groups' => Group::class,
87
    );
88
89
    private static $has_many = array(
90
        'LoggedPasswords' => MemberPassword::class,
91
        'RememberLoginHashes' => RememberLoginHash::class,
92
    );
93
94
    private static $table_name = "Member";
95
96
    private static $default_sort = '"Surname", "FirstName"';
97
98
    private static $indexes = array(
99
        'Email' => true,
100
        //Removed due to duplicate null values causing MSSQL problems
101
        //'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...
102
    );
103
104
    /**
105
     * @config
106
     * @var boolean
107
     */
108
    private static $notify_password_change = false;
109
110
    /**
111
     * All searchable database columns
112
     * in this object, currently queried
113
     * with a "column LIKE '%keywords%'
114
     * statement.
115
     *
116
     * @var array
117
     * @todo Generic implementation of $searchable_fields on DataObject,
118
     * with definition for different searching algorithms
119
     * (LIKE, FULLTEXT) and default FormFields to construct a searchform.
120
     */
121
    private static $searchable_fields = array(
122
        'FirstName',
123
        'Surname',
124
        'Email',
125
    );
126
127
    /**
128
     * @config
129
     * @var array
130
     */
131
    private static $summary_fields = array(
132
        'FirstName',
133
        'Surname',
134
        'Email',
135
    );
136
137
    /**
138
     * @config
139
     * @var array
140
     */
141
    private static $casting = array(
142
        'Name' => 'Varchar',
143
    );
144
145
    /**
146
     * Internal-use only fields
147
     *
148
     * @config
149
     * @var array
150
     */
151
    private static $hidden_fields = array(
152
        'AutoLoginHash',
153
        'AutoLoginExpired',
154
        'PasswordEncryption',
155
        'PasswordExpiry',
156
        'LockedOutUntil',
157
        'TempIDHash',
158
        'TempIDExpired',
159
        'Salt',
160
    );
161
162
    /**
163
     * @config
164
     * @var array See {@link set_title_columns()}
165
     */
166
    private static $title_format = null;
167
168
    /**
169
     * The unique field used to identify this member.
170
     * By default, it's "Email", but another common
171
     * field could be Username.
172
     *
173
     * @config
174
     * @var string
175
     * @skipUpgrade
176
     */
177
    private static $unique_identifier_field = 'Email';
178
179
    /**
180
     * Object for validating user's password
181
     *
182
     * @config
183
     * @var PasswordValidator
184
     */
185
    private static $password_validator = null;
186
187
    /**
188
     * @config
189
     * The number of days that a password should be valid for.
190
     * By default, this is null, which means that passwords never expire
191
     */
192
    private static $password_expiry_days = null;
193
194
    /**
195
     * @config
196
     * @var Int Number of incorrect logins after which
197
     * the user is blocked from further attempts for the timespan
198
     * defined in {@link $lock_out_delay_mins}.
199
     */
200
    private static $lock_out_after_incorrect_logins = 10;
201
202
    /**
203
     * @config
204
     * @var integer Minutes of enforced lockout after incorrect password attempts.
205
     * Only applies if {@link $lock_out_after_incorrect_logins} greater than 0.
206
     */
207
    private static $lock_out_delay_mins = 15;
208
209
    /**
210
     * @config
211
     * @var String If this is set, then a session cookie with the given name will be set on log-in,
212
     * and cleared on logout.
213
     */
214
    private static $login_marker_cookie = null;
215
216
    /**
217
     * Indicates that when a {@link Member} logs in, Member:session_regenerate_id()
218
     * should be called as a security precaution.
219
     *
220
     * This doesn't always work, especially if you're trying to set session cookies
221
     * across an entire site using the domain parameter to session_set_cookie_params()
222
     *
223
     * @config
224
     * @var boolean
225
     */
226
    private static $session_regenerate_id = true;
227
228
229
    /**
230
     * Default lifetime of temporary ids.
231
     *
232
     * This is the period within which a user can be re-authenticated within the CMS by entering only their password
233
     * and without losing their workspace.
234
     *
235
     * Any session expiration outside of this time will require them to login from the frontend using their full
236
     * username and password.
237
     *
238
     * Defaults to 72 hours. Set to zero to disable expiration.
239
     *
240
     * @config
241
     * @var int Lifetime in seconds
242
     */
243
    private static $temp_id_lifetime = 259200;
244
245
    /**
246
     * Ensure the locale is set to something sensible by default.
247
     */
248
    public function populateDefaults()
249
    {
250
        parent::populateDefaults();
251
        $this->Locale = i18n::get_closest_translation(i18n::get_locale());
252
    }
253
254
    public function requireDefaultRecords()
255
    {
256
        parent::requireDefaultRecords();
257
        // Default groups should've been built by Group->requireDefaultRecords() already
258
        static::default_admin();
259
    }
260
261
    /**
262
     * Get the default admin record if it exists, or creates it otherwise if enabled
263
     *
264
     * @return Member
265
     */
266
    public static function default_admin()
267
    {
268
        // Check if set
269
        if (!Security::has_default_admin()) {
270
            return null;
271
        }
272
273
        // Find or create ADMIN group
274
        Group::singleton()->requireDefaultRecords();
275
        $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
276
277
        // Find member
278
        /** @skipUpgrade */
279
        $admin = Member::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
287
            $admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
288
            $admin->Email = Security::default_admin_username();
289
            $admin->write();
290
        }
291
292
        // Ensure this user is in the admin group
293
        if (!$admin->inGroup($adminGroup)) {
294
            // Add member to group instead of adding group to member
295
            // This bypasses the privilege escallation code in Member_GroupSet
296
            $adminGroup
297
                ->DirectMembers()
298
                ->add($admin);
299
        }
300
301
        return $admin;
302
    }
303
304
    /**
305
     * Check if the passed password matches the stored one (if the member is not locked out).
306
     *
307
     * @param  string $password
308
     * @return ValidationResult
309
     */
310
    public function checkPassword($password)
311
    {
312
        $result = $this->canLogIn();
313
314
        // Short-circuit the result upon failure, no further checks needed.
315
        if (!$result->isValid()) {
316
            return $result;
317
        }
318
319
        // Allow default admin to login as self
320
        if ($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
321
            return $result;
322
        }
323
324
        // Check a password is set on this member
325
        if (empty($this->Password) && $this->exists()) {
326
            $result->addError(_t('Member.NoPassword', 'There is no password on this member.'));
327
            return $result;
328
        }
329
330
        $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
331
        if (!$e->check($this->Password, $password, $this->Salt, $this)) {
332
            $result->addError(_t(
333
                'Member.ERRORWRONGCRED',
334
                'The provided details don\'t seem to be correct. Please try again.'
335
            ));
336
        }
337
338
        return $result;
339
    }
340
341
    /**
342
     * Check if this user is the currently configured default admin
343
     *
344
     * @return bool
345
     */
346
    public function isDefaultAdmin()
347
    {
348
        return Security::has_default_admin()
349
            && $this->Email === Security::default_admin_username();
350
    }
351
352
    /**
353
     * Returns a valid {@link ValidationResult} if this member can currently log in, or an invalid
354
     * one with error messages to display if the member is locked out.
355
     *
356
     * You can hook into this with a "canLogIn" method on an attached extension.
357
     *
358
     * @return ValidationResult
359
     */
360
    public function canLogIn()
361
    {
362
        $result = ValidationResult::create();
363
364
        if ($this->isLockedOut()) {
365
            $result->addError(
366
                _t(
367
                    'Member.ERRORLOCKEDOUT2',
368
                    'Your account has been temporarily disabled because of too many failed attempts at ' .
369
                    'logging in. Please try again in {count} minutes.',
370
                    null,
371
                    array('count' => $this->config()->lock_out_delay_mins)
372
                )
373
            );
374
        }
375
376
        $this->extend('canLogIn', $result);
377
        return $result;
378
    }
379
380
    /**
381
     * Returns true if this user is locked out
382
     *
383
     * @return bool
384
     */
385
    public function isLockedOut()
386
    {
387
        if (!$this->LockedOutUntil) {
388
            return false;
389
        }
390
        return DBDatetime::now()->getTimestamp() < $this->dbObject('LockedOutUntil')->getTimestamp();
391
    }
392
393
    /**
394
     * Regenerate the session_id.
395
     * This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
396
     * They have caused problems in certain
397
     * quirky problems (such as using the Windmill 0.3.6 proxy).
398
     */
399
    public static function session_regenerate_id()
400
    {
401
        if (!self::config()->session_regenerate_id) {
402
            return;
403
        }
404
405
        // This can be called via CLI during testing.
406
        if (Director::is_cli()) {
407
            return;
408
        }
409
410
        $file = '';
411
        $line = '';
412
413
        // @ is to supress win32 warnings/notices when session wasn't cleaned up properly
414
        // There's nothing we can do about this, because it's an operating system function!
415
        if (!headers_sent($file, $line)) {
416
            @session_regenerate_id(true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
417
        }
418
    }
419
420
    /**
421
     * Set a {@link PasswordValidator} object to use to validate member's passwords.
422
     *
423
     * @param PasswordValidator $pv
424
     */
425
    public static function set_password_validator($pv)
426
    {
427
        self::$password_validator = $pv;
428
    }
429
430
    /**
431
     * Returns the current {@link PasswordValidator}
432
     *
433
     * @return PasswordValidator
434
     */
435
    public static function password_validator()
436
    {
437
        return self::$password_validator;
438
    }
439
440
441
    public function isPasswordExpired()
442
    {
443
        if (!$this->PasswordExpiry) {
444
            return false;
445
        }
446
        return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
447
    }
448
449
    /**
450
     * Logs this member in
451
     *
452
     * @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
453
     */
454
    public function logIn($remember = false)
455
    {
456
        $this->extend('beforeMemberLoggedIn');
457
458
        self::session_regenerate_id();
459
460
        Session::set("loggedInAs", $this->ID);
461
        // This lets apache rules detect whether the user has logged in
462
        if (Member::config()->login_marker_cookie) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
464
        }
465
466
        if (Security::config()->autologin_enabled) {
467
        // Cleans up any potential previous hash for this member on this device
468
            if ($alcDevice = Cookie::get('alc_device')) {
469
                RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
470
            }
471
            if ($remember) {
472
                $rememberLoginHash = RememberLoginHash::generate($this);
473
                $tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
474
                $deviceExpiryDays = RememberLoginHash::config()->uninherited('device_expiry_days');
475
                Cookie::set(
476
                    'alc_enc',
477
                    $this->ID . ':' . $rememberLoginHash->getToken(),
478
                    $tokenExpiryDays,
479
                    null,
480
                    null,
481
                    null,
482
                    true
483
                );
484
                Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true);
485
            } else {
486
                Cookie::set('alc_enc', null);
487
                Cookie::set('alc_device', null);
488
                Cookie::force_expiry('alc_enc');
489
                Cookie::force_expiry('alc_device');
490
            }
491
        }
492
        // Clear the incorrect log-in count
493
        $this->registerSuccessfulLogin();
494
495
            $this->LockedOutUntil = null;
496
497
        $this->regenerateTempID();
498
499
        $this->write();
500
501
        // Audit logging hook
502
        $this->extend('memberLoggedIn');
503
    }
504
505
    /**
506
     * Trigger regeneration of TempID.
507
     *
508
     * This should be performed any time the user presents their normal identification (normally Email)
509
     * and is successfully authenticated.
510
     */
511
    public function regenerateTempID()
512
    {
513
        $generator = new RandomGenerator();
514
        $this->TempIDHash = $generator->randomToken('sha1');
515
        $this->TempIDExpired = self::config()->temp_id_lifetime
516
            ? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime)
517
            : null;
518
        $this->write();
519
    }
520
521
    /**
522
     * Check if the member ID logged in session actually
523
     * has a database record of the same ID. If there is
524
     * no logged in user, FALSE is returned anyway.
525
     *
526
     * @return boolean TRUE record found FALSE no record found
527
     */
528
    public static function logged_in_session_exists()
529
    {
530
        if ($id = Member::currentUserID()) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
531
            if ($member = DataObject::get_by_id(Member::class, $id)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
532
                if ($member->exists()) {
533
                    return true;
534
                }
535
            }
536
        }
537
538
        return false;
539
    }
540
541
    /**
542
     * Log the user in if the "remember login" cookie is set
543
     *
544
     * The <i>remember login token</i> will be changed on every successful
545
     * auto-login.
546
     */
547
    public static function autoLogin()
548
    {
549
        // Don't bother trying this multiple times
550
        if (!class_exists(SapphireTest::class, false) || !SapphireTest::is_running_test()) {
551
            self::$_already_tried_to_auto_log_in = true;
552
        }
553
554
        if (!Security::config()->autologin_enabled
555
            || strpos(Cookie::get('alc_enc'), ':') === false
556
            || Session::get("loggedInAs")
557
            || !Security::database_is_ready()
558
        ) {
559
            return;
560
        }
561
562
        if (strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SilverStripe\Control\Cookie::get('alc_device') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
563
            list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
564
565
            if (!$uid || !$token) {
566
                return;
567
            }
568
569
            $deviceID = Cookie::get('alc_device');
570
571
            /** @var Member $member */
572
            $member = Member::get()->byID($uid);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
573
574
            /** @var RememberLoginHash $rememberLoginHash */
575
            $rememberLoginHash = null;
576
577
            // check if autologin token matches
578
            if ($member) {
579
                $hash = $member->encryptWithUserSettings($token);
580
                $rememberLoginHash = RememberLoginHash::get()
581
                    ->filter(array(
582
                        'MemberID' => $member->ID,
583
                        'DeviceID' => $deviceID,
584
                        'Hash' => $hash
585
                    ))->first();
586
                if (!$rememberLoginHash) {
587
                    $member = null;
588
                } else {
589
                    // Check for expired token
590
                    $expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
591
                    $now = DBDatetime::now();
592
                    $now = new DateTime($now->Rfc2822());
593
                    if ($now > $expiryDate) {
594
                        $member = null;
595
                    }
596
                }
597
            }
598
599
            if ($member) {
600
                self::session_regenerate_id();
601
                Session::set("loggedInAs", $member->ID);
602
                // This lets apache rules detect whether the user has logged in
603
                if (Member::config()->login_marker_cookie) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
605
                }
606
607
                if ($rememberLoginHash) {
608
                    $rememberLoginHash->renew();
609
                    $tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
610
                    Cookie::set(
611
                        'alc_enc',
612
                        $member->ID . ':' . $rememberLoginHash->getToken(),
613
                        $tokenExpiryDays,
614
                        null,
615
                        null,
616
                        false,
617
                        true
618
                    );
619
                }
620
621
                $member->write();
622
623
                // Audit logging hook
624
                $member->extend('memberAutoLoggedIn');
625
            }
626
        }
627
    }
628
629
    /**
630
     * Logs this member out.
631
     */
632
    public function logOut()
633
    {
634
        $this->extend('beforeMemberLoggedOut');
635
636
        Session::clear("loggedInAs");
637
        if (Member::config()->login_marker_cookie) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
639
        }
640
641
        Session::destroy();
642
643
        $this->extend('memberLoggedOut');
644
645
        // Clears any potential previous hashes for this member
646
        RememberLoginHash::clear($this, Cookie::get('alc_device'));
647
648
        Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
649
        Cookie::force_expiry('alc_enc');
650
        Cookie::set('alc_device', null);
651
        Cookie::force_expiry('alc_device');
652
653
        // Switch back to live in order to avoid infinite loops when
654
        // redirecting to the login screen (if this login screen is versioned)
655
        Session::clear('readingMode');
656
657
        $this->write();
658
659
        // Audit logging hook
660
        $this->extend('memberLoggedOut');
661
    }
662
663
    /**
664
     * Utility for generating secure password hashes for this member.
665
     *
666
     * @param string $string
667
     * @return string
668
     * @throws PasswordEncryptor_NotFoundException
669
     */
670
    public function encryptWithUserSettings($string)
671
    {
672
        if (!$string) {
673
            return null;
674
        }
675
676
        // If the algorithm or salt is not available, it means we are operating
677
        // on legacy account with unhashed password. Do not hash the string.
678
        if (!$this->PasswordEncryption) {
679
            return $string;
680
        }
681
682
        // We assume we have PasswordEncryption and Salt available here.
683
        $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
684
        return $e->encrypt($string, $this->Salt);
685
    }
686
687
    /**
688
     * Generate an auto login token which can be used to reset the password,
689
     * at the same time hashing it and storing in the database.
690
     *
691
     * @param int $lifetime The lifetime of the auto login hash in days (by default 2 days)
692
     *
693
     * @returns string Token that should be passed to the client (but NOT persisted).
694
     *
695
     * @todo Make it possible to handle database errors such as a "duplicate key" error
696
     */
697
    public function generateAutologinTokenAndStoreHash($lifetime = 2)
698
    {
699
        do {
700
            $generator = new RandomGenerator();
701
            $token = $generator->randomToken();
702
            $hash = $this->encryptWithUserSettings($token);
703
        } 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...
704
            '"Member"."AutoLoginHash"' => $hash
705
        )));
706
707
        $this->AutoLoginHash = $hash;
708
        $this->AutoLoginExpired = date('Y-m-d H:i:s', time() + (86400 * $lifetime));
709
710
        $this->write();
711
712
        return $token;
713
    }
714
715
    /**
716
     * Check the token against the member.
717
     *
718
     * @param string $autologinToken
719
     *
720
     * @returns bool Is token valid?
721
     */
722
    public function validateAutoLoginToken($autologinToken)
723
    {
724
        $hash = $this->encryptWithUserSettings($autologinToken);
725
        $member = self::member_from_autologinhash($hash, false);
726
        return (bool)$member;
727
    }
728
729
    /**
730
     * Return the member for the auto login hash
731
     *
732
     * @param string $hash The hash key
733
     * @param bool $login Should the member be logged in?
734
     *
735
     * @return Member the matching member, if valid
736
     * @return Member
737
     */
738
    public static function member_from_autologinhash($hash, $login = false)
739
    {
740
        /** @var Member $member */
741
        $member = Member::get()->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...
742
            'AutoLoginHash' => $hash,
743
            'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(),
744
        ])->first();
745
746
        if ($login && $member) {
747
            $member->logIn();
748
        }
749
750
        return $member;
751
    }
752
753
    /**
754
     * Find a member record with the given TempIDHash value
755
     *
756
     * @param string $tempid
757
     * @return Member
758
     */
759
    public static function member_from_tempid($tempid)
760
    {
761
        $members = Member::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
762
            ->filter('TempIDHash', $tempid);
763
764
        // Exclude expired
765
        if (static::config()->temp_id_lifetime) {
766
            $members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
767
        }
768
769
        return $members->first();
770
    }
771
772
    /**
773
     * Returns the fields for the member form - used in the registration/profile module.
774
     * It should return fields that are editable by the admin and the logged-in user.
775
     *
776
     * @return FieldList Returns a {@link FieldList} containing the fields for
777
     *                   the member form.
778
     */
779
    public function getMemberFormFields()
780
    {
781
        $fields = parent::getFrontEndFields();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (getFrontEndFields() instead of getMemberFormFields()). Are you sure this is correct? If so, you might want to change this to $this->getFrontEndFields().

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

Consider the following code:

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

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

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

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

Loading history...
782
783
        $fields->replaceField('Password', $this->getMemberPasswordField());
784
785
        $fields->replaceField('Locale', new DropdownField(
786
            'Locale',
787
            $this->fieldLabel('Locale'),
788
            i18n::getSources()->getKnownLocales()
789
        ));
790
791
        $fields->removeByName(static::config()->hidden_fields);
792
        $fields->removeByName('FailedLoginCount');
793
794
795
        $this->extend('updateMemberFormFields', $fields);
796
        return $fields;
797
    }
798
799
    /**
800
     * Builds "Change / Create Password" field for this member
801
     *
802
     * @return ConfirmedPasswordField
803
     */
804
    public function getMemberPasswordField()
805
    {
806
        $editingPassword = $this->isInDB();
807
        $label = $editingPassword
808
            ? _t('Member.EDIT_PASSWORD', 'New Password')
809
            : $this->fieldLabel('Password');
810
        /** @var ConfirmedPasswordField $password */
811
        $password = ConfirmedPasswordField::create(
812
            'Password',
813
            $label,
814
            null,
815
            null,
816
            $editingPassword
817
        );
818
819
        // If editing own password, require confirmation of existing
820
        if ($editingPassword && $this->ID == Member::currentUserID()) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
821
            $password->setRequireExistingPassword(true);
822
        }
823
824
        $password->setCanBeEmpty(true);
825
        $this->extend('updateMemberPasswordField', $password);
826
        return $password;
827
    }
828
829
830
    /**
831
     * Returns the {@link RequiredFields} instance for the Member object. This
832
     * Validator is used when saving a {@link CMSProfileController} or added to
833
     * any form responsible for saving a users data.
834
     *
835
     * To customize the required fields, add a {@link DataExtension} to member
836
     * calling the `updateValidator()` method.
837
     *
838
     * @return Member_Validator
839
     */
840
    public function getValidator()
841
    {
842
        $validator = Member_Validator::create();
843
        $validator->setForMember($this);
844
        $this->extend('updateValidator', $validator);
845
846
        return $validator;
847
    }
848
849
850
    /**
851
     * Returns the current logged in user
852
     *
853
     * @return Member
854
     */
855
    public static function currentUser()
856
    {
857
        $id = Member::currentUserID();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
858
859
        if ($id) {
860
            return DataObject::get_by_id(Member::class, $id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
861
        }
862
    }
863
864
    /**
865
     * Allow override of the current user ID
866
     *
867
     * @var int|null Set to null to fallback to session, or an explicit ID
868
     */
869
    protected static $overrideID = null;
870
871
    /**
872
     * Temporarily act as the specified user, limited to a $callback, but
873
     * without logging in as that user.
874
     *
875
     * E.g.
876
     * <code>
877
     * Member::logInAs(Security::findAnAdministrator(), function() {
878
     *     $record->write();
879
     * });
880
     * </code>
881
     *
882
     * @param Member|null|int $member Member or member ID to log in as.
883
     * Set to null or 0 to act as a logged out user.
884
     * @param $callback
885
     */
886
    public static function actAs($member, $callback)
887
    {
888
        $id = ($member instanceof Member ? $member->ID : $member) ?: 0;
889
        $previousID = static::$overrideID;
890
        static::$overrideID = $id;
891
        try {
892
            return $callback();
893
        } finally {
894
            static::$overrideID = $previousID;
895
        }
896
    }
897
898
    /**
899
     * Get the ID of the current logged in user
900
     *
901
     * @return int Returns the ID of the current logged in user or 0.
902
     */
903
    public static function currentUserID()
904
    {
905
        if (isset(static::$overrideID)) {
906
            return static::$overrideID;
907
        }
908
909
        $id = Session::get("loggedInAs");
910
        if (!$id && !self::$_already_tried_to_auto_log_in) {
911
            self::autoLogin();
912
            $id = Session::get("loggedInAs");
913
        }
914
915
        return is_numeric($id) ? $id : 0;
916
    }
917
918
    private static $_already_tried_to_auto_log_in = false;
919
920
921
    /*
922
	 * Generate a random password, with randomiser to kick in if there's no words file on the
923
	 * filesystem.
924
	 *
925
	 * @return string Returns a random password.
926
	 */
927
    public static function create_new_password()
928
    {
929
        $words = Security::config()->uninherited('word_list');
930
931
        if ($words && file_exists($words)) {
932
            $words = file($words);
933
934
            list($usec, $sec) = explode(' ', microtime());
935
            srand($sec + ((float) $usec * 100000));
936
937
            $word = trim($words[rand(0, sizeof($words)-1)]);
938
            $number = rand(10, 999);
939
940
            return $word . $number;
941
        } else {
942
            $random = rand();
943
            $string = md5($random);
944
            $output = substr($string, 0, 8);
945
            return $output;
946
        }
947
    }
948
949
    /**
950
     * Event handler called before writing to the database.
951
     */
952
    public function onBeforeWrite()
953
    {
954
        if ($this->SetPassword) {
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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

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

Loading history...
955
            $this->Password = $this->SetPassword;
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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

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

Loading history...
956
        }
957
958
        // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
959
        // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
960
        // but rather a last line of defense against data inconsistencies.
961
        $identifierField = Member::config()->unique_identifier_field;
0 ignored issues
show
Documentation introduced by
The property unique_identifier_field does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
962
        if ($this->$identifierField) {
963
            // Note: Same logic as Member_Validator class
964
            $filter = [
965
                "\"Member\".\"$identifierField\"" => $this->$identifierField
966
            ];
967
            if ($this->ID) {
968
                $filter[] = array('"Member"."ID" <> ?' => $this->ID);
969
            }
970
            $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...
971
972
            if ($existingRecord) {
973
                throw new ValidationException(_t(
974
                    'Member.ValidationIdentifierFailed',
975
                    'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
976
                    'Values in brackets show "fieldname = value", usually denoting an existing email address',
977
                    array(
978
                        'id' => $existingRecord->ID,
979
                        'name' => $identifierField,
980
                        'value' => $this->$identifierField
981
                    )
982
                ));
983
            }
984
        }
985
986
        // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
987
        // However, if TestMailer is in use this isn't a risk.
988
        if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer)
989
            && $this->isChanged('Password')
990
            && $this->record['Password']
991
            && $this->config()->notify_password_change
992
        ) {
993
            Email::create()
994
                ->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail')
995
                ->setData($this)
996
                ->setTo($this->Email)
997
                ->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'))
998
                ->send();
999
        }
1000
1001
        // The test on $this->ID is used for when records are initially created.
1002
        // Note that this only works with cleartext passwords, as we can't rehash
1003
        // existing passwords.
1004
        if ((!$this->ID && $this->Password) || $this->isChanged('Password')) {
1005
            //reset salt so that it gets regenerated - this will invalidate any persistant login cookies
1006
            // or other information encrypted with this Member's settings (see self::encryptWithUserSettings)
1007
            $this->Salt = '';
1008
            // Password was changed: encrypt the password according the settings
1009
            $encryption_details = Security::encrypt_password(
1010
                $this->Password, // this is assumed to be cleartext
1011
                $this->Salt,
1012
                ($this->PasswordEncryption) ?
1013
                    $this->PasswordEncryption : Security::config()->password_encryption_algorithm,
1014
                $this
1015
            );
1016
1017
            // Overwrite the Password property with the hashed value
1018
            $this->Password = $encryption_details['password'];
1019
            $this->Salt = $encryption_details['salt'];
1020
            $this->PasswordEncryption = $encryption_details['algorithm'];
1021
1022
            // If we haven't manually set a password expiry
1023
            if (!$this->isChanged('PasswordExpiry')) {
1024
                // then set it for us
1025
                if (self::config()->password_expiry_days) {
1026
                    $this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
1027
                } else {
1028
                    $this->PasswordExpiry = null;
1029
                }
1030
            }
1031
        }
1032
1033
        // save locale
1034
        if (!$this->Locale) {
1035
            $this->Locale = i18n::get_locale();
1036
        }
1037
1038
        parent::onBeforeWrite();
1039
    }
1040
1041
    public function onAfterWrite()
1042
    {
1043
        parent::onAfterWrite();
1044
1045
        Permission::flush_permission_cache();
1046
1047
        if ($this->isChanged('Password')) {
1048
            MemberPassword::log($this);
1049
        }
1050
    }
1051
1052
    public function onAfterDelete()
1053
    {
1054
        parent::onAfterDelete();
1055
1056
        //prevent orphaned records remaining in the DB
1057
        $this->deletePasswordLogs();
1058
    }
1059
1060
    /**
1061
     * Delete the MemberPassword objects that are associated to this user
1062
     *
1063
     * @return $this
1064
     */
1065
    protected function deletePasswordLogs()
1066
    {
1067
        foreach ($this->LoggedPasswords() as $password) {
1068
            $password->delete();
1069
            $password->destroy();
1070
        }
1071
        return $this;
1072
    }
1073
1074
    /**
1075
     * Filter out admin groups to avoid privilege escalation,
1076
     * If any admin groups are requested, deny the whole save operation.
1077
     *
1078
     * @param array $ids Database IDs of Group records
1079
     * @return bool True if the change can be accepted
1080
     */
1081
    public function onChangeGroups($ids)
1082
    {
1083
        // unless the current user is an admin already OR the logged in user is an admin
1084
        if (Permission::check('ADMIN') || Permission::checkMember($this, 'ADMIN')) {
1085
            return true;
1086
        }
1087
1088
        // If there are no admin groups in this set then it's ok
1089
            $adminGroups = Permission::get_groups_by_permission('ADMIN');
1090
            $adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
1091
            return count(array_intersect($ids, $adminGroupIDs)) == 0;
1092
    }
1093
1094
1095
    /**
1096
     * Check if the member is in one of the given groups.
1097
     *
1098
     * @param array|SS_List $groups Collection of {@link Group} DataObjects to check
1099
     * @param boolean $strict Only determine direct group membership if set to true (Default: false)
1100
     * @return bool Returns TRUE if the member is in one of the given groups, otherwise FALSE.
1101
     */
1102
    public function inGroups($groups, $strict = false)
1103
    {
1104
        if ($groups) {
1105
            foreach ($groups as $group) {
1106
                if ($this->inGroup($group, $strict)) {
1107
                    return true;
1108
                }
1109
            }
1110
        }
1111
1112
        return false;
1113
    }
1114
1115
1116
    /**
1117
     * Check if the member is in the given group or any parent groups.
1118
     *
1119
     * @param int|Group|string $group Group instance, Group Code or ID
1120
     * @param boolean $strict Only determine direct group membership if set to TRUE (Default: FALSE)
1121
     * @return bool Returns TRUE if the member is in the given group, otherwise FALSE.
1122
     */
1123
    public function inGroup($group, $strict = false)
1124
    {
1125
        if (is_numeric($group)) {
1126
            $groupCheckObj = DataObject::get_by_id(Group::class, $group);
1127
        } elseif (is_string($group)) {
1128
            $groupCheckObj = DataObject::get_one(Group::class, array(
1129
                '"Group"."Code"' => $group
1130
            ));
1131
        } elseif ($group instanceof Group) {
1132
            $groupCheckObj = $group;
1133
        } else {
1134
            user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR);
1135
        }
1136
1137
        if (!$groupCheckObj) {
0 ignored issues
show
Bug introduced by
The variable $groupCheckObj does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1138
            return false;
1139
        }
1140
1141
        $groupCandidateObjs = ($strict) ? $this->getManyManyComponents("Groups") : $this->Groups();
1142
        if ($groupCandidateObjs) {
1143
            foreach ($groupCandidateObjs as $groupCandidateObj) {
1144
                if ($groupCandidateObj->ID == $groupCheckObj->ID) {
1145
                    return true;
1146
                }
1147
            }
1148
        }
1149
1150
        return false;
1151
    }
1152
1153
    /**
1154
     * Adds the member to a group. This will create the group if the given
1155
     * group code does not return a valid group object.
1156
     *
1157
     * @param string $groupcode
1158
     * @param string $title Title of the group
1159
     */
1160
    public function addToGroupByCode($groupcode, $title = "")
1161
    {
1162
        $group = DataObject::get_one(Group::class, array(
1163
            '"Group"."Code"' => $groupcode
1164
        ));
1165
1166
        if ($group) {
1167
            $this->Groups()->add($group);
1168
        } else {
1169
            if (!$title) {
1170
                $title = $groupcode;
1171
            }
1172
1173
            $group = new Group();
1174
            $group->Code = $groupcode;
1175
            $group->Title = $title;
1176
            $group->write();
1177
1178
            $this->Groups()->add($group);
1179
        }
1180
    }
1181
1182
    /**
1183
     * Removes a member from a group.
1184
     *
1185
     * @param string $groupcode
1186
     */
1187
    public function removeFromGroupByCode($groupcode)
1188
    {
1189
        $group = Group::get()->filter(array('Code' => $groupcode))->first();
1190
1191
        if ($group) {
1192
            $this->Groups()->remove($group);
1193
        }
1194
    }
1195
1196
    /**
1197
     * @param array $columns Column names on the Member record to show in {@link getTitle()}.
1198
     * @param String $sep Separator
1199
     */
1200
    public static function set_title_columns($columns, $sep = ' ')
1201
    {
1202
        if (!is_array($columns)) {
1203
            $columns = array($columns);
1204
        }
1205
        self::config()->title_format = array('columns' => $columns, 'sep' => $sep);
0 ignored issues
show
Documentation introduced by
The property title_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1206
    }
1207
1208
    //------------------- HELPER METHODS -----------------------------------//
1209
1210
    /**
1211
     * Get the complete name of the member, by default in the format "<Surname>, <FirstName>".
1212
     * Falls back to showing either field on its own.
1213
     *
1214
     * You can overload this getter with {@link set_title_format()}
1215
     * and {@link set_title_sql()}.
1216
     *
1217
     * @return string Returns the first- and surname of the member. If the ID
1218
     *  of the member is equal 0, only the surname is returned.
1219
     */
1220
    public function getTitle()
1221
    {
1222
        $format = $this->config()->title_format;
0 ignored issues
show
Documentation introduced by
The property title_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1223
        if ($format) {
1224
            $values = array();
1225
            foreach ($format['columns'] as $col) {
1226
                $values[] = $this->getField($col);
1227
            }
1228
            return join($format['sep'], $values);
1229
        }
1230
        if ($this->getField('ID') === 0) {
1231
            return $this->getField('Surname');
1232
        } else {
1233
            if ($this->getField('Surname') && $this->getField('FirstName')) {
1234
                return $this->getField('Surname') . ', ' . $this->getField('FirstName');
1235
            } elseif ($this->getField('Surname')) {
1236
                return $this->getField('Surname');
1237
            } elseif ($this->getField('FirstName')) {
1238
                return $this->getField('FirstName');
1239
            } else {
1240
                return null;
1241
            }
1242
        }
1243
    }
1244
1245
    /**
1246
     * Return a SQL CONCAT() fragment suitable for a SELECT statement.
1247
     * Useful for custom queries which assume a certain member title format.
1248
     *
1249
     * @return String SQL
1250
     */
1251
    public static function get_title_sql()
1252
    {
1253
        // This should be abstracted to SSDatabase concatOperator or similar.
1254
        $op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || ";
0 ignored issues
show
Bug introduced by
The class SilverStripe\MSSQL\MSSQLDatabase does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1255
1256
        // Get title_format with fallback to default
1257
        $format = static::config()->title_format;
0 ignored issues
show
Documentation introduced by
The property title_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1258
        if (!$format) {
1259
            $format = [
1260
                'columns' => ['Surname', 'FirstName'],
1261
                'sep' => ' ',
1262
            ];
1263
        }
1264
1265
            $columnsWithTablename = array();
1266
        foreach ($format['columns'] as $column) {
1267
            $columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column);
1268
        }
1269
1270
        $sepSQL = Convert::raw2sql($format['sep'], true);
1271
        return "(".join(" $op $sepSQL $op ", $columnsWithTablename).")";
1272
    }
1273
1274
1275
    /**
1276
     * Get the complete name of the member
1277
     *
1278
     * @return string Returns the first- and surname of the member.
1279
     */
1280
    public function getName()
1281
    {
1282
        return ($this->Surname) ? trim($this->FirstName . ' ' . $this->Surname) : $this->FirstName;
1283
    }
1284
1285
1286
    /**
1287
     * Set first- and surname
1288
     *
1289
     * This method assumes that the last part of the name is the surname, e.g.
1290
     * <i>A B C</i> will result in firstname <i>A B</i> and surname <i>C</i>
1291
     *
1292
     * @param string $name The name
1293
     */
1294
    public function setName($name)
1295
    {
1296
        $nameParts = explode(' ', $name);
1297
        $this->Surname = array_pop($nameParts);
1298
        $this->FirstName = join(' ', $nameParts);
1299
    }
1300
1301
1302
    /**
1303
     * Alias for {@link setName}
1304
     *
1305
     * @param string $name The name
1306
     * @see setName()
1307
     */
1308
    public function splitName($name)
1309
    {
1310
        return $this->setName($name);
1311
    }
1312
1313
    /**
1314
     * Return the date format based on the user's chosen locale,
1315
     * falling back to the default format defined by the {@link i18n.get_locale()} setting.
1316
     *
1317
     * @return string ISO date format
1318
     */
1319
    public function getDateFormat()
1320
    {
1321
        $formatter = new IntlDateFormatter(
1322
            $this->getLocale(),
1323
            IntlDateFormatter::MEDIUM,
1324
            IntlDateFormatter::NONE
1325
        );
1326
        $format = $formatter->getPattern();
1327
1328
        $this->extend('updateDateFormat', $format);
1329
1330
        return $format;
1331
    }
1332
1333
    /**
1334
     * Get user locale
1335
     */
1336
    public function getLocale()
1337
    {
1338
        $locale = $this->getField('Locale');
1339
        if ($locale) {
1340
            return $locale;
1341
        }
1342
        return i18n::get_locale();
1343
    }
1344
1345
    /**
1346
     * Return the time format based on the user's chosen locale,
1347
     * falling back to the default format defined by the {@link i18n.get_locale()} setting.
1348
     *
1349
     * @return string ISO date format
1350
     */
1351
    public function getTimeFormat()
1352
    {
1353
        $formatter = new IntlDateFormatter(
1354
            $this->getLocale(),
1355
            IntlDateFormatter::NONE,
1356
            IntlDateFormatter::MEDIUM
1357
        );
1358
        $format = $formatter->getPattern();
1359
1360
        $this->extend('updateTimeFormat', $format);
1361
1362
        return $format;
1363
    }
1364
1365
    //---------------------------------------------------------------------//
1366
1367
1368
    /**
1369
     * Get a "many-to-many" map that holds for all members their group memberships,
1370
     * including any parent groups where membership is implied.
1371
     * Use {@link DirectGroups()} to only retrieve the group relations without inheritance.
1372
     *
1373
     * @todo Push all this logic into Member_GroupSet's getIterator()?
1374
     * @return Member_Groupset
1375
     */
1376
    public function Groups()
1377
    {
1378
        $groups = Member_GroupSet::create(Group::class, 'Group_Members', 'GroupID', 'MemberID');
1379
        $groups = $groups->forForeignID($this->ID);
1380
1381
        $this->extend('updateGroups', $groups);
1382
1383
        return $groups;
1384
    }
1385
1386
    /**
1387
     * @return ManyManyList
1388
     */
1389
    public function DirectGroups()
1390
    {
1391
        return $this->getManyManyComponents('Groups');
1392
    }
1393
1394
    /**
1395
     * Get a member SQLMap of members in specific groups
1396
     *
1397
     * If no $groups is passed, all members will be returned
1398
     *
1399
     * @param mixed $groups - takes a SS_List, an array or a single Group.ID
1400
     * @return Map Returns an Map that returns all Member data.
1401
     */
1402
    public static function map_in_groups($groups = null)
1403
    {
1404
        $groupIDList = array();
1405
1406
        if ($groups instanceof SS_List) {
1407
            foreach ($groups as $group) {
1408
                $groupIDList[] = $group->ID;
1409
            }
1410
        } elseif (is_array($groups)) {
1411
            $groupIDList = $groups;
1412
        } elseif ($groups) {
1413
            $groupIDList[] = $groups;
1414
        }
1415
1416
        // No groups, return all Members
1417
        if (!$groupIDList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIDList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1418
            return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1419
        }
1420
1421
        $membersList = new ArrayList();
1422
        // This is a bit ineffective, but follow the ORM style
1423
        foreach (Group::get()->byIDs($groupIDList) as $group) {
1424
            $membersList->merge($group->Members());
1425
        }
1426
1427
        $membersList->removeDuplicates('ID');
1428
        return $membersList->map();
1429
    }
1430
1431
1432
    /**
1433
     * Get a map of all members in the groups given that have CMS permissions
1434
     *
1435
     * If no groups are passed, all groups with CMS permissions will be used.
1436
     *
1437
     * @param array $groups Groups to consider or NULL to use all groups with
1438
     *                      CMS permissions.
1439
     * @return Map Returns a map of all members in the groups given that
1440
     *                have CMS permissions.
1441
     */
1442
    public static function mapInCMSGroups($groups = null)
1443
    {
1444
        // Check CMS module exists
1445
        if (!class_exists(LeftAndMain::class)) {
1446
            return ArrayList::create()->map();
1447
        }
1448
1449
        if (!$groups || $groups->Count() == 0) {
0 ignored issues
show
Bug introduced by
The method Count cannot be called on $groups (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1450
            $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
1451
1452
            if (class_exists(CMSMain::class)) {
1453
                $cmsPerms = CMSMain::singleton()->providePermissions();
1454
            } else {
1455
                $cmsPerms = LeftAndMain::singleton()->providePermissions();
1456
            }
1457
1458
            if (!empty($cmsPerms)) {
1459
                $perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
1460
            }
1461
1462
            $permsClause = DB::placeholders($perms);
1463
            /** @skipUpgrade */
1464
            $groups = Group::get()
1465
                ->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
1466
                ->where(array(
1467
                    "\"Permission\".\"Code\" IN ($permsClause)" => $perms
1468
                ));
1469
        }
1470
1471
        $groupIDList = array();
1472
1473
        if ($groups instanceof SS_List) {
1474
            foreach ($groups as $group) {
1475
                $groupIDList[] = $group->ID;
1476
            }
1477
        } elseif (is_array($groups)) {
1478
            $groupIDList = $groups;
1479
        }
1480
1481
        /** @skipUpgrade */
1482
        $members = Member::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1483
            ->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
1484
            ->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
1485
        if ($groupIDList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupIDList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1486
            $groupClause = DB::placeholders($groupIDList);
1487
            $members = $members->where(array(
1488
                "\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
1489
            ));
1490
        }
1491
1492
        return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
1493
    }
1494
1495
1496
    /**
1497
     * Get the groups in which the member is NOT in
1498
     *
1499
     * When passed an array of groups, and a component set of groups, this
1500
     * function will return the array of groups the member is NOT in.
1501
     *
1502
     * @param array $groupList An array of group code names.
1503
     * @param array $memberGroups A component set of groups (if set to NULL,
1504
     *                            $this->groups() will be used)
1505
     * @return array Groups in which the member is NOT in.
1506
     */
1507
    public function memberNotInGroups($groupList, $memberGroups = null)
1508
    {
1509
        if (!$memberGroups) {
1510
            $memberGroups = $this->Groups();
1511
        }
1512
1513
        foreach ($memberGroups as $group) {
1514
            if (in_array($group->Code, $groupList)) {
1515
                $index = array_search($group->Code, $groupList);
1516
                unset($groupList[$index]);
1517
            }
1518
        }
1519
1520
        return $groupList;
1521
    }
1522
1523
1524
    /**
1525
     * Return a {@link FieldList} of fields that would appropriate for editing
1526
     * this member.
1527
     *
1528
     * @return FieldList Return a FieldList of fields that would appropriate for
1529
     *                   editing this member.
1530
     */
1531
    public function getCMSFields()
1532
    {
1533
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
1534
            /** @var FieldList $mainFields */
1535
            $mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren();
1536
1537
            // Build change password field
1538
            $mainFields->replaceField('Password', $this->getMemberPasswordField());
1539
1540
            $mainFields->replaceField('Locale', new DropdownField(
1541
                "Locale",
1542
                _t('Member.INTERFACELANG', "Interface Language", 'Language of the CMS'),
1543
                i18n::getSources()->getKnownLocales()
1544
            ));
1545
            $mainFields->removeByName($this->config()->hidden_fields);
1546
1547
            if (! $this->config()->lock_out_after_incorrect_logins) {
1548
                $mainFields->removeByName('FailedLoginCount');
1549
            }
1550
1551
            // Groups relation will get us into logical conflicts because
1552
            // Members are displayed within  group edit form in SecurityAdmin
1553
            $fields->removeByName('Groups');
1554
1555
            // Members shouldn't be able to directly view/edit logged passwords
1556
            $fields->removeByName('LoggedPasswords');
1557
1558
            $fields->removeByName('RememberLoginHashes');
1559
1560
            if (Permission::check('EDIT_PERMISSIONS')) {
1561
                $groupsMap = array();
1562
                foreach (Group::get() as $group) {
1563
                    // Listboxfield values are escaped, use ASCII char instead of &raquo;
1564
                    $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
1565
                }
1566
                asort($groupsMap);
1567
                $fields->addFieldToTab(
1568
                    'Root.Main',
1569
                    ListboxField::create('DirectGroups', Group::singleton()->i18n_plural_name())
1570
                        ->setSource($groupsMap)
1571
                        ->setAttribute(
1572
                            'data-placeholder',
1573
                            _t('Member.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
1574
                        )
1575
                );
1576
1577
1578
                // Add permission field (readonly to avoid complicated group assignment logic).
1579
                // This should only be available for existing records, as new records start
1580
                // with no permissions until they have a group assignment anyway.
1581
                if ($this->ID) {
1582
                    $permissionsField = new PermissionCheckboxSetField_Readonly(
1583
                        'Permissions',
1584
                        false,
1585
                        Permission::class,
1586
                        'GroupID',
1587
                        // we don't want parent relationships, they're automatically resolved in the field
1588
                        $this->getManyManyComponents('Groups')
1589
                    );
1590
                    $fields->findOrMakeTab('Root.Permissions', Permission::singleton()->i18n_plural_name());
1591
                    $fields->addFieldToTab('Root.Permissions', $permissionsField);
1592
                }
1593
            }
1594
1595
            $permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions');
1596
            if ($permissionsTab) {
1597
                $permissionsTab->addExtraClass('readonly');
1598
            }
1599
        });
1600
1601
        return parent::getCMSFields();
1602
    }
1603
1604
    /**
1605
     * @param bool $includerelations Indicate if the labels returned include relation fields
1606
     * @return array
1607
     */
1608
    public function fieldLabels($includerelations = true)
1609
    {
1610
        $labels = parent::fieldLabels($includerelations);
1611
1612
        $labels['FirstName'] = _t('Member.FIRSTNAME', 'First Name');
1613
        $labels['Surname'] = _t('Member.SURNAME', 'Surname');
1614
        /** @skipUpgrade */
1615
        $labels['Email'] = _t('Member.EMAIL', 'Email');
1616
        $labels['Password'] = _t('Member.db_Password', 'Password');
1617
        $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
1618
        $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
1619
        $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
1620
        if ($includerelations) {
1621
            $labels['Groups'] = _t(
1622
                'Member.belongs_many_many_Groups',
1623
                'Groups',
1624
                'Security Groups this member belongs to'
1625
            );
1626
        }
1627
        return $labels;
1628
    }
1629
1630
    /**
1631
     * Users can view their own record.
1632
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions.
1633
     * This is likely to be customized for social sites etc. with a looser permission model.
1634
     *
1635
     * @param Member $member
1636
     * @return bool
1637
     */
1638
    public function canView($member = null)
1639
    {
1640
        //get member
1641
        if (!($member instanceof Member)) {
1642
            $member = Member::currentUser();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1643
        }
1644
        //check for extensions, we do this first as they can overrule everything
1645
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1646
        if ($extended !== null) {
1647
            return $extended;
1648
        }
1649
1650
        //need to be logged in and/or most checks below rely on $member being a Member
1651
        if (!$member) {
1652
            return false;
1653
        }
1654
        // members can usually view their own record
1655
        if ($this->ID == $member->ID) {
1656
            return true;
1657
        }
1658
        //standard check
1659
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1660
    }
1661
1662
    /**
1663
     * Users can edit their own record.
1664
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1665
     *
1666
     * @param Member $member
1667
     * @return bool
1668
     */
1669
    public function canEdit($member = null)
1670
    {
1671
        //get member
1672
        if (!($member instanceof Member)) {
1673
            $member = Member::currentUser();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1674
        }
1675
        //check for extensions, we do this first as they can overrule everything
1676
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1677
        if ($extended !== null) {
1678
            return $extended;
1679
        }
1680
1681
        //need to be logged in and/or most checks below rely on $member being a Member
1682
        if (!$member) {
1683
            return false;
1684
        }
1685
1686
        // HACK: we should not allow for an non-Admin to edit an Admin
1687
        if (!Permission::checkMember($member, 'ADMIN') && Permission::checkMember($this, 'ADMIN')) {
1688
            return false;
1689
        }
1690
        // members can usually edit their own record
1691
        if ($this->ID == $member->ID) {
1692
            return true;
1693
        }
1694
        //standard check
1695
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1696
    }
1697
    /**
1698
     * Users can edit their own record.
1699
     * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
1700
     *
1701
     * @param Member $member
1702
     * @return bool
1703
     */
1704
    public function canDelete($member = null)
1705
    {
1706
        if (!($member instanceof Member)) {
1707
            $member = Member::currentUser();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1708
        }
1709
        //check for extensions, we do this first as they can overrule everything
1710
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1711
        if ($extended !== null) {
1712
            return $extended;
1713
        }
1714
1715
        //need to be logged in and/or most checks below rely on $member being a Member
1716
        if (!$member) {
1717
            return false;
1718
        }
1719
        // Members are not allowed to remove themselves,
1720
        // since it would create inconsistencies in the admin UIs.
1721
        if ($this->ID && $member->ID == $this->ID) {
1722
            return false;
1723
        }
1724
1725
        // HACK: if you want to delete a member, you have to be a member yourself.
1726
        // this is a hack because what this should do is to stop a user
1727
        // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
1728
        if (Permission::checkMember($this, 'ADMIN')) {
1729
            if (! Permission::checkMember($member, 'ADMIN')) {
1730
                return false;
1731
            }
1732
        }
1733
        //standard check
1734
        return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
1735
    }
1736
1737
    /**
1738
     * Validate this member object.
1739
     */
1740
    public function validate()
1741
    {
1742
        $valid = parent::validate();
1743
1744
        if (!$this->ID || $this->isChanged('Password')) {
1745
            if ($this->Password && self::$password_validator) {
1746
                $valid->combineAnd(self::$password_validator->validate($this->Password, $this));
1747
            }
1748
        }
1749
1750
        if ((!$this->ID && $this->SetPassword) || $this->isChanged('SetPassword')) {
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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

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

Loading history...
1751
            if ($this->SetPassword && self::$password_validator) {
0 ignored issues
show
Bug introduced by
The property SetPassword does not seem to exist. Did you mean Password?

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

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

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

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

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

Loading history...
1753
            }
1754
        }
1755
1756
        return $valid;
1757
    }
1758
1759
    /**
1760
     * Change password. This will cause rehashing according to
1761
     * the `PasswordEncryption` property.
1762
     *
1763
     * @param string $password Cleartext password
1764
     * @return ValidationResult
1765
     */
1766
    public function changePassword($password)
1767
    {
1768
        $this->Password = $password;
1769
        $valid = $this->validate();
1770
1771
        if ($valid->isValid()) {
1772
            $this->AutoLoginHash = null;
1773
            $this->write();
1774
        }
1775
1776
        return $valid;
1777
    }
1778
1779
    /**
1780
     * Tell this member that someone made a failed attempt at logging in as them.
1781
     * This can be used to lock the user out temporarily if too many failed attempts are made.
1782
     */
1783
    public function registerFailedLogin()
1784
    {
1785
        if (self::config()->lock_out_after_incorrect_logins) {
1786
            // Keep a tally of the number of failed log-ins so that we can lock people out
1787
            $this->FailedLoginCount = $this->FailedLoginCount + 1;
1788
1789
            if ($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
1790
                $lockoutMins = self::config()->lock_out_delay_mins;
0 ignored issues
show
Documentation introduced by
The property lock_out_delay_mins does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1791
                $this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins*60);
1792
                $this->FailedLoginCount = 0;
1793
            }
1794
        }
1795
        $this->extend('registerFailedLogin');
1796
        $this->write();
1797
    }
1798
1799
    /**
1800
     * Tell this member that a successful login has been made
1801
     */
1802
    public function registerSuccessfulLogin()
1803
    {
1804
        if (self::config()->lock_out_after_incorrect_logins) {
1805
            // Forgive all past login failures
1806
            $this->FailedLoginCount = 0;
1807
            $this->write();
1808
        }
1809
    }
1810
1811
    /**
1812
     * Get the HtmlEditorConfig for this user to be used in the CMS.
1813
     * This is set by the group. If multiple configurations are set,
1814
     * the one with the highest priority wins.
1815
     *
1816
     * @return string
1817
     */
1818
    public function getHtmlEditorConfigForCMS()
1819
    {
1820
        $currentName = '';
1821
        $currentPriority = 0;
1822
1823
        foreach ($this->Groups() as $group) {
1824
            $configName = $group->HtmlEditorConfig;
1825
            if ($configName) {
1826
                $config = HTMLEditorConfig::get($group->HtmlEditorConfig);
1827
                if ($config && $config->getOption('priority') > $currentPriority) {
1828
                    $currentName = $configName;
1829
                    $currentPriority = $config->getOption('priority');
1830
                }
1831
            }
1832
        }
1833
1834
        // If can't find a suitable editor, just default to cms
1835
        return $currentName ? $currentName : 'cms';
1836
    }
1837
1838
    public static function get_template_global_variables()
1839
    {
1840
        return array(
1841
            'CurrentMember' => 'currentUser',
1842
            'currentUser',
1843
        );
1844
    }
1845
}
1846