Passed
Pull Request — 4 (#10239)
by James
09:02
created

ConfirmedPasswordField::setForm()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 10
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
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)) ? $titleConfirmField : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password')
151
            )
152
        );
153
154
        // has to be called in constructor because Field() isn't triggered upon saving the instance
155
        if ($showOnClick) {
156
            $this->getChildren()->push($this->hiddenField = HiddenField::create("{$name}[_PasswordFieldVisible]"));
157
        }
158
159
        // disable auto complete
160
        foreach ($this->getChildren() as $child) {
161
            /** @var FormField $child */
162
            $child->setAttribute('autocomplete', 'off');
163
        }
164
165
        $this->showOnClick = $showOnClick;
166
167
        parent::__construct($name, $title);
168
        $this->setValue($value);
169
    }
170
171
    public function Title()
172
    {
173
        // Title is displayed on nested field, not on the top level field
174
        return null;
175
    }
176
177
    public function setTitle($title)
178
    {
179
        $this->getPasswordField()->setTitle($title);
180
        return parent::setTitle($title);
181
    }
182
183
    /**
184
     * @param array $properties
185
     *
186
     * @return string
187
     */
188
    public function Field($properties = [])
189
    {
190
        // Build inner content
191
        $fieldContent = '';
192
        foreach ($this->getChildren() as $field) {
193
            /** @var FormField $field */
194
            $field->setDisabled($this->isDisabled());
195
            $field->setReadonly($this->isReadonly());
196
197
            if (count($this->attributes)) {
198
                foreach ($this->attributes as $name => $value) {
199
                    $field->setAttribute($name, $value);
200
                }
201
            }
202
203
            $fieldContent .= $field->FieldHolder();
204
        }
205
206
        if (!$this->showOnClick) {
207
            return $fieldContent;
208
        }
209
210
        if ($this->getShowOnClickTitle()) {
211
            $title = $this->getShowOnClickTitle();
212
        } else {
213
            $title = _t(
214
                __CLASS__ . '.SHOWONCLICKTITLE',
215
                'Change Password',
216
                'Label of the link which triggers display of the "change password" formfields'
217
            );
218
        }
219
220
        // Check if the field should be visible up front
221
        $visible = $this->hiddenField->Value();
222
        $classes = $visible
223
            ? 'showOnClickContainer'
224
            : 'showOnClickContainer d-none';
225
226
        // Build display holder
227
        $container = HTML::createTag('div', ['class' => $classes], $fieldContent);
228
        $actionLink = HTML::createTag('a', ['href' => '#'], $title);
229
        return HTML::createTag(
230
            'div',
231
            ['class' => 'showOnClick'],
232
            $actionLink . "\n" . $container
233
        );
234
    }
235
236
    /**
237
     * Returns the children of this field for use in templating.
238
     * @return FieldList
239
     */
240
    public function getChildren()
241
    {
242
        return $this->children;
243
    }
244
245
    /**
246
     * Can be empty is a flag that turns on / off empty field checking.
247
     *
248
     * For example, set this to false (the default) when creating a user account,
249
     * and true when displaying on an edit form.
250
     *
251
     * @param boolean $value
252
     *
253
     * @return ConfirmedPasswordField
254
     */
255
    public function setCanBeEmpty($value)
256
    {
257
        $this->canBeEmpty = (bool)$value;
258
259
        return $this;
260
    }
261
262
    /**
263
     * The title on the link which triggers display of the "password" and
264
     * "confirm password" formfields. Only used if {@link setShowOnClick()}
265
     * is set to TRUE.
266
     *
267
     * @param string $title
268
     *
269
     * @return ConfirmedPasswordField
270
     */
271
    public function setShowOnClickTitle($title)
272
    {
273
        $this->showOnClickTitle = $title;
274
275
        return $this;
276
    }
277
278
    /**
279
     * @return string $title
280
     */
281
    public function getShowOnClickTitle()
282
    {
283
        return $this->showOnClickTitle;
284
    }
285
286
    /**
287
     * @param string $title
288
     *
289
     * @return $this
290
     */
291
    public function setRightTitle($title)
292
    {
293
        foreach ($this->getChildren() as $field) {
294
            /** @var FormField $field */
295
            $field->setRightTitle($title);
296
        }
297
298
        return $this;
299
    }
300
301
    /**
302
     * Set child field titles. Titles in order should be:
303
     *  - "Current Password" (if getRequireExistingPassword() is set)
304
     *  - "Password"
305
     *  - "Confirm Password"
306
     *
307
     * @param array $titles List of child titles
308
     * @return $this
309
     */
310
    public function setChildrenTitles($titles)
311
    {
312
        $expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
313
        if (is_array($titles) && count($titles) === $expectedChildren) {
314
            foreach ($this->getChildren() as $field) {
315
                if (isset($titles[0])) {
316
                    /** @var FormField $field */
317
                    $field->setTitle($titles[0]);
318
319
                    array_shift($titles);
320
                }
321
            }
322
        }
323
324
        return $this;
325
    }
326
327
    /**
328
     * Value is sometimes an array, and sometimes a single value, so we need
329
     * to handle both cases.
330
     *
331
     * @param mixed $value
332
     * @param mixed $data
333
     * @return $this
334
     */
335
    public function setValue($value, $data = null)
336
    {
337
        // If $data is a DataObject, don't use the value, since it's a hashed value
338
        if ($data && $data instanceof DataObject) {
339
            $value = '';
340
        }
341
342
        //store this for later
343
        $oldValue = $this->value;
344
        $oldConfirmValue = $this->confirmValue;
345
346
        if (is_array($value)) {
347
            $this->value = $value['_Password'];
348
            $this->confirmValue = $value['_ConfirmPassword'];
349
            $this->currentPasswordValue = ($this->getRequireExistingPassword() && isset($value['_CurrentPassword']))
350
                ? $value['_CurrentPassword']
351
                : null;
352
353
            if ($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
354
                $this->getChildren()->fieldByName($this->getName() . '[_PasswordFieldVisible]')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getChildren()->fi...PasswordFieldVisible]') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

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

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

}

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

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

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

Loading history...
355
                    ->setValue($value['_PasswordFieldVisible']);
356
            }
357
        } else {
358
            if ($value || (!$value && $this->canBeEmpty)) {
359
                $this->value = $value;
360
                $this->confirmValue = $value;
361
            }
362
        }
363
364
        //looking up field by name is expensive, so lets check it needs to change
365
        if ($oldValue != $this->value) {
366
            $this->getChildren()->fieldByName($this->getName() . '[_Password]')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getChildren()->fi...Name() . '[_Password]') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

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

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

}

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

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

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

Loading history...
367
                ->setValue($this->value);
368
        }
369
        if ($oldConfirmValue != $this->confirmValue) {
370
            $this->getChildren()->fieldByName($this->getName() . '[_ConfirmPassword]')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getChildren()->fi.... '[_ConfirmPassword]') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

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

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

}

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

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

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

Loading history...
371
                ->setValue($this->confirmValue);
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378
     * Update the names of the child fields when updating name of field.
379
     *
380
     * @param string $name new name to give to the field.
381
     * @return $this
382
     */
383
    public function setName($name)
384
    {
385
        $this->getPasswordField()->setName($name . '[_Password]');
386
        $this->getConfirmPasswordField()->setName($name . '[_ConfirmPassword]');
387
        if ($this->hiddenField) {
388
            $this->hiddenField->setName($name . '[_PasswordFieldVisible]');
389
        }
390
391
        parent::setName($name);
392
        return $this;
393
    }
394
395
    /**
396
     * Determines if the field was actually shown on the client side - if not,
397
     * we don't validate or save it.
398
     *
399
     * @return boolean
400
     */
401
    public function isSaveable()
402
    {
403
        return !$this->showOnClick
404
            || ($this->showOnClick && $this->hiddenField && $this->hiddenField->Value());
405
    }
406
407
    /**
408
     * Validate this field
409
     *
410
     * @param Validator $validator
411
     * @return bool
412
     */
413
    public function validate($validator)
414
    {
415
        $name = $this->name;
416
417
        // if field isn't visible, don't validate
418
        if (!$this->isSaveable()) {
419
            return true;
420
        }
421
422
        $this->getPasswordField()->setValue($this->value);
423
        $this->getConfirmPasswordField()->setValue($this->confirmValue);
424
        $value = $this->getPasswordField()->Value();
425
426
        // both password-fields should be the same
427
        if ($value != $this->getConfirmPasswordField()->Value()) {
428
            $validator->validationError(
429
                $name,
430
                _t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSDONTMATCH', "Passwords don't match"),
431
                "validation"
432
            );
433
434
            return false;
435
        }
436
437
        if (!$this->canBeEmpty) {
438
            // both password-fields shouldn't be empty
439
            if (!$value || !$this->getConfirmPasswordField()->Value()) {
440
                $validator->validationError(
441
                    $name,
442
                    _t('SilverStripe\\Forms\\Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
443
                    "validation"
444
                );
445
446
                return false;
447
            }
448
        }
449
450
        // lengths
451
        $minLength = $this->getMinLength();
452
        $maxLength = $this->getMaxLength();
453
        if ($minLength || $maxLength) {
454
            $errorMsg = null;
455
            $limit = null;
456
            if ($minLength && $maxLength) {
457
                $limit = "{{$minLength},{$maxLength}}";
458
                $errorMsg = _t(
459
                    __CLASS__ . '.BETWEEN',
460
                    'Passwords must be {min} to {max} characters long.',
461
                    ['min' => $minLength, 'max' => $maxLength]
462
                );
463
            } elseif ($minLength) {
464
                $limit = "{{$minLength}}.*";
465
                $errorMsg = _t(
466
                    __CLASS__ . '.ATLEAST',
467
                    'Passwords must be at least {min} characters long.',
468
                    ['min' => $minLength]
469
                );
470
            } elseif ($maxLength) {
471
                $limit = "{0,{$maxLength}}";
472
                $errorMsg = _t(
473
                    __CLASS__ . '.MAXIMUM',
474
                    'Passwords must be at most {max} characters long.',
475
                    ['max' => $maxLength]
476
                );
477
            }
478
            $limitRegex = '/^.' . $limit . '$/';
479
            if (!empty($value) && !preg_match($limitRegex, $value)) {
480
                $validator->validationError(
481
                    $name,
482
                    $errorMsg,
483
                    "validation"
484
                );
485
486
                return false;
487
            }
488
        }
489
490
        if ($this->getRequireStrongPassword()) {
491
            if (!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/', $value)) {
492
                $validator->validationError(
493
                    $name,
494
                    _t(
495
                        'SilverStripe\\Forms\\Form.VALIDATIONSTRONGPASSWORD',
496
                        'Passwords must have at least one digit and one alphanumeric character'
497
                    ),
498
                    "validation"
499
                );
500
501
                return false;
502
            }
503
        }
504
505
        // Check if current password is valid
506
        if (!empty($value) && $this->getRequireExistingPassword()) {
507
            if (!$this->currentPasswordValue) {
508
                $validator->validationError(
509
                    $name,
510
                    _t(
511
                        __CLASS__ . '.CURRENT_PASSWORD_MISSING',
512
                        'You must enter your current password.'
513
                    ),
514
                    "validation"
515
                );
516
                return false;
517
            }
518
519
            // Check this password is valid for the current user
520
            $member = Security::getCurrentUser();
521
            if (!$member) {
0 ignored issues
show
introduced by
$member is of type SilverStripe\Security\Member, thus it always evaluated to true.
Loading history...
522
                $validator->validationError(
523
                    $name,
524
                    _t(
525
                        __CLASS__ . '.LOGGED_IN_ERROR',
526
                        "You must be logged in to change your password."
527
                    ),
528
                    "validation"
529
                );
530
                return false;
531
            }
532
533
            // With a valid user and password, check the password is correct
534
            $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
535
            foreach ($authenticators as $authenticator) {
536
                $checkResult = $authenticator->checkPassword($member, $this->currentPasswordValue);
537
                if (!$checkResult->isValid()) {
538
                    $validator->validationError(
539
                        $name,
540
                        _t(
541
                            __CLASS__ . '.CURRENT_PASSWORD_ERROR',
542
                            "The current password you have entered is not correct."
543
                        ),
544
                        "validation"
545
                    );
546
                    return false;
547
                }
548
            }
549
        }
550
551
        return true;
552
    }
553
554
    /**
555
     * Only save if field was shown on the client, and is not empty.
556
     *
557
     * @param DataObjectInterface $record
558
     */
559
    public function saveInto(DataObjectInterface $record)
560
    {
561
        if (!$this->isSaveable()) {
562
            return;
563
        }
564
565
        if (!($this->canBeEmpty && !$this->value)) {
566
            parent::saveInto($record);
567
        }
568
    }
569
570
    /**
571
     * Makes a read only field with some stars in it to replace the password
572
     *
573
     * @return ReadonlyField
574
     */
575
    public function performReadonlyTransformation()
576
    {
577
        /** @var ReadonlyField $field */
578
        $field = $this->castedCopy(ReadonlyField::class)
579
            ->setTitle($this->title ? $this->title : _t('SilverStripe\\Security\\Member.PASSWORD', 'Password'))
580
            ->setValue('*****');
581
582
        return $field;
583
    }
584
585
    public function performDisabledTransformation()
586
    {
587
        return $this->performReadonlyTransformation();
588
    }
589
590
    /**
591
     * Check if existing password is required
592
     *
593
     * @return bool
594
     */
595
    public function getRequireExistingPassword()
596
    {
597
        return $this->requireExistingPassword;
598
    }
599
600
    /**
601
     * Set if the existing password should be required
602
     *
603
     * @param bool $show Flag to show or hide this field
604
     * @return $this
605
     */
606
    public function setRequireExistingPassword($show)
607
    {
608
        // Don't modify if already added / removed
609
        if ((bool)$show === $this->requireExistingPassword) {
610
            return $this;
611
        }
612
        $this->requireExistingPassword = $show;
613
        $name = $this->getName();
614
        $currentName = "{$name}[_CurrentPassword]";
615
        if ($show) {
616
            $confirmField = PasswordField::create($currentName, _t('SilverStripe\\Security\\Member.CURRENT_PASSWORD', 'Current Password'));
617
            $this->getChildren()->unshift($confirmField);
618
        } else {
619
            $this->getChildren()->removeByName($currentName, true);
620
        }
621
        return $this;
622
    }
623
624
    /**
625
     * @return PasswordField
626
     */
627
    public function getPasswordField()
628
    {
629
        return $this->passwordField;
630
    }
631
632
    /**
633
     * @return PasswordField
634
     */
635
    public function getConfirmPasswordField()
636
    {
637
        return $this->confirmPasswordfield;
638
    }
639
640
    /**
641
     * Set the minimum length required for passwords
642
     *
643
     * @param int $minLength
644
     * @return $this
645
     */
646
    public function setMinLength($minLength)
647
    {
648
        $this->minLength = (int) $minLength;
649
        return $this;
650
    }
651
652
    /**
653
     * @return int
654
     */
655
    public function getMinLength()
656
    {
657
        return $this->minLength;
658
    }
659
660
    /**
661
     * Set the maximum length required for passwords
662
     *
663
     * @param int $maxLength
664
     * @return $this
665
     */
666
    public function setMaxLength($maxLength)
667
    {
668
        $this->maxLength = (int) $maxLength;
669
        return $this;
670
    }
671
672
    /**
673
     * @return int
674
     */
675
    public function getMaxLength()
676
    {
677
        return $this->maxLength;
678
    }
679
680
    /**
681
     * @param bool $requireStrongPassword
682
     * @return $this
683
     */
684
    public function setRequireStrongPassword($requireStrongPassword)
685
    {
686
        $this->requireStrongPassword = (bool) $requireStrongPassword;
687
        return $this;
688
    }
689
690
    /**
691
     * @return bool
692
     */
693
    public function getRequireStrongPassword()
694
    {
695
        return $this->requireStrongPassword;
696
    }
697
    
698
    /**
699
     * Set the container form.
700
     *
701
     * This is called automatically when fields are added to forms.
702
     *
703
     * @param Form $form
704
     *
705
     * @return $this
706
     */
707
    public function setForm($form)
708
    {
709
        $this->getPasswordField()->setForm($form);
710
        $this->getConfirmPasswordField()->setForm($form);
711
        
712
        parent::setForm($form);
713
        return $this;
714
    }
715
}
716