Completed
Pull Request — master (#6498)
by Damian
09:19
created

Member::actAs()   A

Complexity

Conditions 3
Paths 8

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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