ConfirmedPasswordField   F
last analyzed

Complexity

Total Complexity 86

Size/Duplication

Total Lines 681
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 86
eloc 232
dl 0
loc 681
rs 2
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A setTitle() 0 4 1
A getShowOnClickTitle() 0 3 1
A getChildren() 0 3 1
A Title() 0 4 1
A setCanBeEmpty() 0 5 1
A __construct() 0 41 5
A setRightTitle() 0 8 2
A setShowOnClickTitle() 0 5 1
A setChildrenTitles() 0 15 6
B Field() 0 45 7
A getRequireStrongPassword() 0 3 1
A getMaxLength() 0 3 1
A isSaveable() 0 4 4
A setMinLength() 0 4 1
A performDisabledTransformation() 0 3 1
A getMinLength() 0 3 1
C setValue() 0 40 13
F validate() 0 139 22
A setMaxLength() 0 4 1
A getPasswordField() 0 3 1
A setRequireExistingPassword() 0 19 3
A getConfirmPasswordField() 0 3 1
A setRequireStrongPassword() 0 4 1
A performReadonlyTransformation() 0 8 2
A saveInto() 0 8 4
A getRequireExistingPassword() 0 3 1
A setName() 0 10 2

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\ORM\DataObject;
6
use SilverStripe\ORM\DataObjectInterface;
7
use SilverStripe\Security\Authenticator;
8
use SilverStripe\Security\Security;
9
use SilverStripe\View\HTML;
10
11
/**
12
 * Two masked input fields, checks for matching passwords.
13
 *
14
 * Optionally hides the fields by default and shows a link to toggle their
15
 * visibility.
16
 *
17
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
18
 * since the required frontend dependencies are included through CMS bundling.
19
 */
20
class ConfirmedPasswordField extends FormField
21
{
22
23
    /**
24
     * Minimum character length of the password.
25
     *
26
     * @var int
27
     */
28
    public $minLength = null;
29
30
    /**
31
     * Maximum character length of the password.
32
     *
33
     * @var int
34
     */
35
    public $maxLength = null;
36
37
    /**
38
     * Enforces at least one digit and one alphanumeric
39
     * character (in addition to {$minLength} and {$maxLength}
40
     *
41
     * @var boolean
42
     */
43
    public $requireStrongPassword = false;
44
45
    /**
46
     * Allow empty fields in serverside validation
47
     *
48
     * @var boolean
49
     */
50
    public $canBeEmpty = false;
51
52
    /**
53
     * If set to TRUE, the "password" and "confirm password" form fields will
54
     * be hidden via CSS and JavaScript by default, and triggered by a link.
55
     *
56
     * An additional hidden field determines if showing the fields has been
57
     * triggered and just validates/saves the input in this case.
58
     *
59
     * This behaviour works unobtrusively, without JavaScript enabled
60
     * the fields show, validate and save by default.
61
     *
62
     * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
63
     * since the required frontend dependencies are included through CMS bundling.
64
     *
65
     * @param boolean $showOnClick
66
     */
67
    protected $showOnClick = false;
68
69
    /**
70
     * Check if the existing password should be entered first
71
     *
72
     * @var bool
73
     */
74
    protected $requireExistingPassword = false;
75
76
77
    /**
78
     * A place to temporarily store the confirm password value
79
     *
80
     * @var string
81
     */
82
    protected $confirmValue;
83
84
    /**
85
     * Store value of "Current Password" field
86
     *
87
     * @var string
88
     */
89
    protected $currentPasswordValue;
90
91
    /**
92
     * Title for the link that triggers the visibility of password fields.
93
     *
94
     * @var string
95
     */
96
    public $showOnClickTitle;
97
98
    /**
99
     * Child fields (_Password, _ConfirmPassword)
100
     *
101
     * @var FieldList
102
     */
103
    public $children;
104
105
    protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
106
107
    /**
108
     * @var PasswordField
109
     */
110
    protected $passwordField = null;
111
112
    /**
113
     * @var PasswordField
114
     */
115
    protected $confirmPasswordfield = null;
116
117
    /**
118
     * @var HiddenField
119
     */
120
    protected $hiddenField = null;
121
122
    /**
123
     * @param string $name
124
     * @param string $title
125
     * @param mixed $value
126
     * @param Form $form Ignored for ConfirmedPasswordField.
127
     * @param boolean $showOnClick
128
     * @param string $titleConfirmField Alternate title (not localizeable)
129
     */
130
    public function __construct(
131
        $name,
132
        $title = null,
133
        $value = "",
134
        $form = null,
135
        $showOnClick = false,
136
        $titleConfirmField = null
137
    ) {
138
139
        // Set field title
140
        $title = isset($title) ? $title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password');
141
142
        // naming with underscores to prevent values from actually being saved somewhere
143
        $this->children = FieldList::create(
144
            $this->passwordField = PasswordField::create(
145
                "{$name}[_Password]",
146
                $title
147
            ),
148
            $this->confirmPasswordfield = PasswordField::create(
149
                "{$name}[_ConfirmPassword]",
150
                (isset($titleConfirmField))
151
                    ? $titleConfirmField
152
                    : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password')
153
            )
154
        );
155
156
        // has to be called in constructor because Field() isn't triggered upon saving the instance
157
        if ($showOnClick) {
158
            $this->getChildren()->push($this->hiddenField = HiddenField::create("{$name}[_PasswordFieldVisible]"));
159
        }
160
161
        // disable auto complete
162
        foreach ($this->getChildren() as $child) {
163
            /** @var FormField $child */
164
            $child->setAttribute('autocomplete', 'off');
165
        }
166
167
        $this->showOnClick = $showOnClick;
168
169
        parent::__construct($name, $title);
170
        $this->setValue($value);
171
    }
172
173
    public function Title()
174
    {
175
        // Title is displayed on nested field, not on the top level field
176
        return null;
177
    }
178
179
    public function setTitle($title)
180
    {
181
        $this->getPasswordField()->setTitle($title);
182
        return parent::setTitle($title);
183
    }
184
185
    /**
186
     * @param array $properties
187
     *
188
     * @return string
189
     */
190
    public function Field($properties = array())
191
    {
192
        // Build inner content
193
        $fieldContent = '';
194
        foreach ($this->getChildren() as $field) {
195
            /** @var FormField $field */
196
            $field->setDisabled($this->isDisabled());
197
            $field->setReadonly($this->isReadonly());
198
199
            if (count($this->attributes)) {
200
                foreach ($this->attributes as $name => $value) {
201
                    $field->setAttribute($name, $value);
202
                }
203
            }
204
205
            $fieldContent .= $field->FieldHolder();
206
        }
207
208
        if (!$this->showOnClick) {
209
            return $fieldContent;
210
        }
211
212
        if ($this->getShowOnClickTitle()) {
213
            $title = $this->getShowOnClickTitle();
214
        } else {
215
            $title = _t(
216
                __CLASS__ . '.SHOWONCLICKTITLE',
217
                'Change Password',
218
                'Label of the link which triggers display of the "change password" formfields'
219
            );
220
        }
221
222
        // Check if the field should be visible up front
223
        $visible = $this->hiddenField->Value();
224
        $classes = $visible
225
            ? 'showOnClickContainer'
226
            : 'showOnClickContainer d-none';
227
228
        // Build display holder
229
        $container = HTML::createTag('div', ['class' => $classes], $fieldContent);
230
        $actionLink = HTML::createTag('a', ['href' => '#'], $title);
231
        return HTML::createTag(
232
            'div',
233
            ['class' => 'showOnClick'],
234
            $actionLink . "\n" . $container
235
        );
236
    }
237
238
    /**
239
     * Returns the children of this field for use in templating.
240
     * @return FieldList
241
     */
242
    public function getChildren()
243
    {
244
        return $this->children;
245
    }
246
247
    /**
248
     * Can be empty is a flag that turns on / off empty field checking.
249
     *
250
     * For example, set this to false (the default) when creating a user account,
251
     * and true when displaying on an edit form.
252
     *
253
     * @param boolean $value
254
     *
255
     * @return ConfirmedPasswordField
256
     */
257
    public function setCanBeEmpty($value)
258
    {
259
        $this->canBeEmpty = (bool)$value;
260
261
        return $this;
262
    }
263
264
    /**
265
     * The title on the link which triggers display of the "password" and
266
     * "confirm password" formfields. Only used if {@link setShowOnClick()}
267
     * is set to TRUE.
268
     *
269
     * @param string $title
270
     *
271
     * @return ConfirmedPasswordField
272
     */
273
    public function setShowOnClickTitle($title)
274
    {
275
        $this->showOnClickTitle = $title;
276
277
        return $this;
278
    }
279
280
    /**
281
     * @return string $title
282
     */
283
    public function getShowOnClickTitle()
284
    {
285
        return $this->showOnClickTitle;
286
    }
287
288
    /**
289
     * @param string $title
290
     *
291
     * @return $this
292
     */
293
    public function setRightTitle($title)
294
    {
295
        foreach ($this->getChildren() as $field) {
296
            /** @var FormField $field */
297
            $field->setRightTitle($title);
298
        }
299
300
        return $this;
301
    }
302
303
    /**
304
     * Set child field titles. Titles in order should be:
305
     *  - "Current Password" (if getRequireExistingPassword() is set)
306
     *  - "Password"
307
     *  - "Confirm Password"
308
     *
309
     * @param array $titles List of child titles
310
     * @return $this
311
     */
312
    public function setChildrenTitles($titles)
313
    {
314
        $expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
315
        if (is_array($titles) && count($titles) === $expectedChildren) {
316
            foreach ($this->getChildren() as $field) {
317
                if (isset($titles[0])) {
318
                    /** @var FormField $field */
319
                    $field->setTitle($titles[0]);
320
321
                    array_shift($titles);
322
                }
323
            }
324
        }
325
326
        return $this;
327
    }
328
329
    /**
330
     * Value is sometimes an array, and sometimes a single value, so we need
331
     * to handle both cases.
332
     *
333
     * @param mixed $value
334
     * @param mixed $data
335
     * @return $this
336
     */
337
    public function setValue($value, $data = null)
338
    {
339
        // If $data is a DataObject, don't use the value, since it's a hashed value
340
        if ($data && $data instanceof DataObject) {
341
            $value = '';
342
        }
343
344
        //store this for later
345
        $oldValue = $this->value;
346
        $oldConfirmValue = $this->confirmValue;
347
348
        if (is_array($value)) {
349
            $this->value = $value['_Password'];
350
            $this->confirmValue = $value['_ConfirmPassword'];
351
            $this->currentPasswordValue = ($this->getRequireExistingPassword() && isset($value['_CurrentPassword']))
352
                ? $value['_CurrentPassword']
353
                : null;
354
355
            if ($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
356
                $this->getChildren()->fieldByName($this->getName() . '[_PasswordFieldVisible]')
357
                    ->setValue($value['_PasswordFieldVisible']);
358
            }
359
        } else {
360
            if ($value || (!$value && $this->canBeEmpty)) {
361
                $this->value = $value;
362
                $this->confirmValue = $value;
363
            }
364
        }
365
366
        //looking up field by name is expensive, so lets check it needs to change
367
        if ($oldValue != $this->value) {
368
            $this->getChildren()->fieldByName($this->getName() . '[_Password]')
369
                ->setValue($this->value);
370
        }
371
        if ($oldConfirmValue != $this->confirmValue) {
372
            $this->getChildren()->fieldByName($this->getName() . '[_ConfirmPassword]')
373
                ->setValue($this->confirmValue);
374
        }
375
376
        return $this;
377
    }
378
379
    /**
380
     * Update the names of the child fields when updating name of field.
381
     *
382
     * @param string $name new name to give to the field.
383
     * @return $this
384
     */
385
    public function setName($name)
386
    {
387
        $this->getPasswordField()->setName($name . '[_Password]');
388
        $this->getConfirmPasswordField()->setName($name . '[_ConfirmPassword]');
389
        if ($this->hiddenField) {
390
            $this->hiddenField->setName($name . '[_PasswordFieldVisible]');
391
        }
392
393
        parent::setName($name);
394
        return $this;
395
    }
396
397
    /**
398
     * Determines if the field was actually shown on the client side - if not,
399
     * we don't validate or save it.
400
     *
401
     * @return boolean
402
     */
403
    public function isSaveable()
404
    {
405
        return !$this->showOnClick
406
            || ($this->showOnClick && $this->hiddenField && $this->hiddenField->Value());
407
    }
408
409
    /**
410
     * Validate this field
411
     *
412
     * @param Validator $validator
413
     * @return bool
414
     */
415
    public function validate($validator)
416
    {
417
        $name = $this->name;
418
419
        // if field isn't visible, don't validate
420
        if (!$this->isSaveable()) {
421
            return true;
422
        }
423
424
        $this->getPasswordField()->setValue($this->value);
425
        $this->getConfirmPasswordField()->setValue($this->confirmValue);
426
        $value = $this->getPasswordField()->Value();
427
428
        // both password-fields should be the same
429
        if ($value != $this->getConfirmPasswordField()->Value()) {
430
            $validator->validationError(
431
                $name,
432
                _t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSDONTMATCH', "Passwords don't match"),
433
                "validation"
434
            );
435
436
            return false;
437
        }
438
439
        if (!$this->canBeEmpty) {
440
            // both password-fields shouldn't be empty
441
            if (!$value || !$this->getConfirmPasswordField()->Value()) {
442
                $validator->validationError(
443
                    $name,
444
                    _t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
445
                    "validation"
446
                );
447
448
                return false;
449
            }
450
        }
451
452
        // lengths
453
        $minLength = $this->getMinLength();
454
        $maxLength = $this->getMaxLength();
455
        if ($minLength || $maxLength) {
456
            $errorMsg = null;
457
            $limit = null;
458
            if ($minLength && $maxLength) {
459
                $limit = "{{$minLength},{$maxLength}}";
460
                $errorMsg = _t(
461
                    __CLASS__ . '.BETWEEN',
462
                    'Passwords must be {min} to {max} characters long.',
463
                    ['min' => $minLength, 'max' => $maxLength]
464
                );
465
            } elseif ($minLength) {
466
                $limit = "{{$minLength}}.*";
467
                $errorMsg = _t(
468
                    __CLASS__ . '.ATLEAST',
469
                    'Passwords must be at least {min} characters long.',
470
                    ['min' => $minLength]
471
                );
472
            } elseif ($maxLength) {
473
                $limit = "{0,{$maxLength}}";
474
                $errorMsg = _t(
475
                    __CLASS__ . '.MAXIMUM',
476
                    'Passwords must be at most {max} characters long.',
477
                    ['max' => $maxLength]
478
                );
479
            }
480
            $limitRegex = '/^.' . $limit . '$/';
481
            if (!empty($value) && !preg_match($limitRegex, $value)) {
482
                $validator->validationError(
483
                    $name,
484
                    $errorMsg,
485
                    "validation"
486
                );
487
488
                return false;
489
            }
490
        }
491
492
        if ($this->getRequireStrongPassword()) {
493
            if (!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/', $value)) {
494
                $validator->validationError(
495
                    $name,
496
                    _t(
497
                        'SilverStripe\\Forms\\Form.VALIDATIONSTRONGPASSWORD',
498
                        'Passwords must have at least one digit and one alphanumeric character'
499
                    ),
500
                    "validation"
501
                );
502
503
                return false;
504
            }
505
        }
506
507
        // Check if current password is valid
508
        if (!empty($value) && $this->getRequireExistingPassword()) {
509
            if (!$this->currentPasswordValue) {
510
                $validator->validationError(
511
                    $name,
512
                    _t(
513
                        __CLASS__ . '.CURRENT_PASSWORD_MISSING',
514
                        'You must enter your current password.'
515
                    ),
516
                    "validation"
517
                );
518
                return false;
519
            }
520
521
            // Check this password is valid for the current user
522
            $member = Security::getCurrentUser();
523
            if (!$member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
524
                $validator->validationError(
525
                    $name,
526
                    _t(
527
                        __CLASS__ . '.LOGGED_IN_ERROR',
528
                        "You must be logged in to change your password."
529
                    ),
530
                    "validation"
531
                );
532
                return false;
533
            }
534
535
            // With a valid user and password, check the password is correct
536
            $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
537
            foreach ($authenticators as $authenticator) {
538
                $checkResult = $authenticator->checkPassword($member, $this->currentPasswordValue);
539
                if (!$checkResult->isValid()) {
540
                    $validator->validationError(
541
                        $name,
542
                        _t(
543
                            __CLASS__ . '.CURRENT_PASSWORD_ERROR',
544
                            "The current password you have entered is not correct."
545
                        ),
546
                        "validation"
547
                    );
548
                    return false;
549
                }
550
            }
551
        }
552
553
        return true;
554
    }
555
556
    /**
557
     * Only save if field was shown on the client, and is not empty.
558
     *
559
     * @param DataObjectInterface $record
560
     */
561
    public function saveInto(DataObjectInterface $record)
562
    {
563
        if (!$this->isSaveable()) {
564
            return;
565
        }
566
567
        if (!($this->canBeEmpty && !$this->value)) {
568
            parent::saveInto($record);
569
        }
570
    }
571
572
    /**
573
     * Makes a read only field with some stars in it to replace the password
574
     *
575
     * @return ReadonlyField
576
     */
577
    public function performReadonlyTransformation()
578
    {
579
        /** @var ReadonlyField $field */
580
        $field = $this->castedCopy(ReadonlyField::class)
581
            ->setTitle($this->title ? $this->title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password'))
582
            ->setValue('*****');
583
584
        return $field;
585
    }
586
587
    public function performDisabledTransformation()
588
    {
589
        return $this->performReadonlyTransformation();
590
    }
591
592
    /**
593
     * Check if existing password is required
594
     *
595
     * @return bool
596
     */
597
    public function getRequireExistingPassword()
598
    {
599
        return $this->requireExistingPassword;
600
    }
601
602
    /**
603
     * Set if the existing password should be required
604
     *
605
     * @param bool $show Flag to show or hide this field
606
     * @return $this
607
     */
608
    public function setRequireExistingPassword($show)
609
    {
610
        // Don't modify if already added / removed
611
        if ((bool)$show === $this->requireExistingPassword) {
612
            return $this;
613
        }
614
        $this->requireExistingPassword = $show;
615
        $name = $this->getName();
616
        $currentName = "{$name}[_CurrentPassword]";
617
        if ($show) {
618
            $confirmField = PasswordField::create(
619
                $currentName,
620
                _t('SilverStripe\\Security\\Member.CURRENT_PASSWORD', 'Current Password')
621
            );
622
            $this->getChildren()->unshift($confirmField);
623
        } else {
624
            $this->getChildren()->removeByName($currentName, true);
625
        }
626
        return $this;
627
    }
628
629
    /**
630
     * @return PasswordField
631
     */
632
    public function getPasswordField()
633
    {
634
        return $this->passwordField;
635
    }
636
637
    /**
638
     * @return PasswordField
639
     */
640
    public function getConfirmPasswordField()
641
    {
642
        return $this->confirmPasswordfield;
643
    }
644
645
    /**
646
     * Set the minimum length required for passwords
647
     *
648
     * @param int $minLength
649
     * @return $this
650
     */
651
    public function setMinLength($minLength)
652
    {
653
        $this->minLength = (int) $minLength;
654
        return $this;
655
    }
656
657
    /**
658
     * @return int
659
     */
660
    public function getMinLength()
661
    {
662
        return $this->minLength;
663
    }
664
665
    /**
666
     * Set the maximum length required for passwords
667
     *
668
     * @param int $maxLength
669
     * @return $this
670
     */
671
    public function setMaxLength($maxLength)
672
    {
673
        $this->maxLength = (int) $maxLength;
674
        return $this;
675
    }
676
677
    /**
678
     * @return int
679
     */
680
    public function getMaxLength()
681
    {
682
        return $this->maxLength;
683
    }
684
685
    /**
686
     * @param bool $requireStrongPassword
687
     * @return $this
688
     */
689
    public function setRequireStrongPassword($requireStrongPassword)
690
    {
691
        $this->requireStrongPassword = (bool) $requireStrongPassword;
692
        return $this;
693
    }
694
695
    /**
696
     * @return bool
697
     */
698
    public function getRequireStrongPassword()
699
    {
700
        return $this->requireStrongPassword;
701
    }
702
}
703