Completed
Push — correct-classname-values ( f9b487 )
by Sam
08:29
created

ConfirmedPasswordField::setTitle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 9.4285
1
<?php
2
3
use SilverStripe\ORM\DataObject;
4
use SilverStripe\ORM\DataObjectInterface;
5
use SilverStripe\Security\Member;
6
7
8
/**
9
 * Two masked input fields, checks for matching passwords.
10
 *
11
 * Optionally hides the fields by default and shows a link to toggle their
12
 * visibility.
13
 *
14
 * @package forms
15
 * @subpackage fields-formattedinput
16
 */
17
class ConfirmedPasswordField extends FormField {
18
19
	/**
20
	 * Minimum character length of the password.
21
	 *
22
	 * @var int
23
	 */
24
	public $minLength = null;
25
26
	/**
27
	 * Maximum character length of the password.
28
	 *
29
	 * @var int
30
	 */
31
	public $maxLength = null;
32
33
	/**
34
	 * Enforces at least one digit and one alphanumeric
35
	 * character (in addition to {$minLength} and {$maxLength}
36
	 *
37
	 * @var boolean
38
	 */
39
	public $requireStrongPassword = false;
40
41
	/**
42
	 * Allow empty fields in serverside validation
43
	 *
44
	 * @var boolean
45
	 */
46
	public $canBeEmpty = false;
47
48
	/**
49
	 * If set to TRUE, the "password" and "confirm password" form fields will
50
	 * be hidden via CSS and JavaScript by default, and triggered by a link.
51
	 *
52
	 * An additional hidden field determines if showing the fields has been
53
	 * triggered and just validates/saves the input in this case.
54
	 *
55
	 * This behaviour works unobtrusively, without JavaScript enabled
56
	 * the fields show, validate and save by default.
57
	 *
58
	 * @param boolean $showOnClick
59
	 */
60
	protected $showOnClick = false;
61
62
	/**
63
	 * Check if the existing password should be entered first
64
	 *
65
	 * @var bool
66
	 */
67
	protected $requireExistingPassword = false;
68
69
70
	/**
71
	 * A place to temporarily store the confirm password value
72
	 *
73
	 * @var string
74
	 */
75
	protected $confirmValue;
76
77
	/**
78
	 * Store value of "Current Password" field
79
	 *
80
	 * @var string
81
	 */
82
	protected $currentPasswordValue;
83
84
	/**
85
	 * Title for the link that triggers the visibility of password fields.
86
	 *
87
	 * @var string
88
	 */
89
	public $showOnClickTitle;
90
91
	/**
92
	 * Child fields (_Password, _ConfirmPassword)
93
	 *
94
	 * @var FieldList
95
	 */
96
	public $children;
97
98
	protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
99
100
	/**
101
	 * @var PasswordField
102
	 */
103
	protected $passwordField = null;
104
105
	/**
106
	 * @var PasswordField
107
	 */
108
	protected $confirmPasswordfield = null;
109
110
	/**
111
	 * @var HiddenField
112
	 */
113
	protected $hiddenField = null;
114
115
	/**
116
	 * @param string $name
117
	 * @param string $title
118
	 * @param mixed $value
119
	 * @param Form $form
120
	 * @param boolean $showOnClick
121
	 * @param string $titleConfirmField Alternate title (not localizeable)
122
	 */
123
	public function __construct($name, $title = null, $value = "", $form = null, $showOnClick = false,
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
124
			$titleConfirmField = null) {
125
126
		// Set field title
127
		$title = isset($title) ? $title : _t('Member.PASSWORD', 'Password');
128
129
		// naming with underscores to prevent values from actually being saved somewhere
130
		$this->children = new FieldList(
131
			$this->passwordField = new PasswordField(
132
				"{$name}[_Password]",
133
				$title
134
			),
135
			$this->confirmPasswordfield = new PasswordField(
136
				"{$name}[_ConfirmPassword]",
137
				(isset($titleConfirmField)) ? $titleConfirmField : _t('Member.CONFIRMPASSWORD', 'Confirm Password')
138
			)
139
		);
140
141
		// has to be called in constructor because Field() isn't triggered upon saving the instance
142
		if($showOnClick) {
143
			$this->children->push($this->hiddenField = new HiddenField("{$name}[_PasswordFieldVisible]"));
144
		}
145
146
		// disable auto complete
147
		foreach($this->children as $child) {
148
			/** @var FormField $child */
149
			$child->setAttribute('autocomplete', 'off');
150
		}
151
152
		$this->showOnClick = $showOnClick;
153
154
		parent::__construct($name, $title);
155
		$this->setValue($value);
156
	}
157
158
	public function Title()
159
	{
160
		// Title is displayed on nested field, not on the top level field
161
		return null;
162
	}
163
164
	public function setTitle($title)
165
	{
166
		parent::setTitle($title);
167
		$this->passwordField->setTitle($title);
168
	}
169
170
	/**
171
	 * @param array $properties
172
	 *
173
	 * @return string
174
	 */
175
	public function Field($properties = array()) {
176
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
177
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/ConfirmedPasswordField.js');
178
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/ConfirmedPasswordField.css');
179
180
		$content = '';
181
182
		if($this->showOnClick) {
183
			if($this->showOnClickTitle) {
184
				$title = $this->showOnClickTitle;
185
			} else {
186
				$title = _t(
187
					'ConfirmedPasswordField.SHOWONCLICKTITLE',
188
					'Change Password',
189
190
					'Label of the link which triggers display of the "change password" formfields'
191
				);
192
			}
193
194
			$content .= "<div class=\"showOnClick\">\n";
195
			$content .= "<a href=\"#\">{$title}</a>\n";
196
			$content .= "<div class=\"showOnClickContainer\">";
197
		}
198
199
		foreach($this->children as $field) {
200
			/** @var FormField $field */
201
			$field->setDisabled($this->isDisabled());
202
			$field->setReadonly($this->isReadonly());
203
204
			if(count($this->attributes)) {
205
				foreach($this->attributes as $name => $value) {
206
					$field->setAttribute($name, $value);
207
				}
208
			}
209
210
			$content .= $field->FieldHolder();
211
		}
212
213
		if($this->showOnClick) {
214
			$content .= "</div>\n";
215
			$content .= "</div>\n";
216
		}
217
218
		return $content;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $content; (string) is incompatible with the return type of the parent method FormField::Field of type SilverStripe\ORM\FieldType\DBHTMLText.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
219
	}
220
221
	/**
222
	 * Returns the children of this field for use in templating.
223
	 * @return FieldList
224
	 */
225
	public function getChildren() {
226
		return $this->children;
227
	}
228
229
	/**
230
	 * Can be empty is a flag that turns on / off empty field checking.
231
	 *
232
	 * For example, set this to false (the default) when creating a user account,
233
	 * and true when displaying on an edit form.
234
	 *
235
	 * @param boolean $value
236
	 *
237
	 * @return ConfirmedPasswordField
238
	 */
239
	public function setCanBeEmpty($value) {
240
		$this->canBeEmpty = (bool)$value;
241
242
		return $this;
243
	}
244
245
	/**
246
	 * The title on the link which triggers display of the "password" and
247
	 * "confirm password" formfields. Only used if {@link setShowOnClick()}
248
	 * is set to TRUE.
249
	 *
250
	 * @param string $title
251
	 *
252
	 * @return ConfirmedPasswordField
253
	 */
254
	public function setShowOnClickTitle($title) {
255
		$this->showOnClickTitle = $title;
256
257
		return $this;
258
	}
259
260
	/**
261
	 * @return string $title
262
	 */
263
	public function getShowOnClickTitle() {
264
		return $this->showOnClickTitle;
265
	}
266
267
	/**
268
	 * @param string $title
269
	 *
270
	 * @return ConfirmedPasswordField
271
	 */
272
	public function setRightTitle($title) {
273
		foreach($this->children as $field) {
274
			/** @var FormField $field */
275
			$field->setRightTitle($title);
276
		}
277
278
		return $this;
279
	}
280
281
	/**
282
	 * Set child field titles. Titles in order should be:
283
	 *  - "Current Password" (if getRequireExistingPassword() is set)
284
	 *  - "Password"
285
	 *  - "Confirm Password"
286
	 *
287
	 * @param array $titles List of child titles
288
	 * @return $this
289
	 */
290
	public function setChildrenTitles($titles) {
291
		$expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
292
		if(is_array($titles) && count($titles) == $expectedChildren) {
293
			foreach($this->children as $field) {
294
				if(isset($titles[0])) {
295
					/** @var FormField $field */
296
					$field->setTitle($titles[0]);
297
298
					array_shift($titles);
299
				}
300
			}
301
		}
302
303
		return $this;
304
	}
305
306
	/**
307
	 * Value is sometimes an array, and sometimes a single value, so we need
308
	 * to handle both cases.
309
	 *
310
	 * @param mixed $value
311
	 * @param mixed $data
312
	 * @return $this
313
	 */
314
	public function setValue($value, $data = null) {
315
		// If $data is a DataObject, don't use the value, since it's a hashed value
316
		if ($data && $data instanceof DataObject) $value = '';
317
318
		//store this for later
319
		$oldValue = $this->value;
320
321
		if(is_array($value)) {
322
			$this->value = $value['_Password'];
323
			$this->confirmValue = $value['_ConfirmPassword'];
324
			$this->currentPasswordValue = ($this->getRequireExistingPassword() && isset($value['_CurrentPassword']))
325
				? $value['_CurrentPassword']
326
				: null;
327
328
			if($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
329
				$this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]')
330
					->setValue($value['_PasswordFieldVisible']);
331
			}
332
		} else {
333
			if($value || (!$value && $this->canBeEmpty)) {
334
				$this->value = $value;
335
				$this->confirmValue = $value;
336
			}
337
		}
338
339
		//looking up field by name is expensive, so lets check it needs to change
340
		if ($oldValue != $this->value) {
341
			$this->children->fieldByName($this->getName() . '[_Password]')
342
				->setValue($this->value);
343
344
			$this->children->fieldByName($this->getName() . '[_ConfirmPassword]')
345
				->setValue($this->confirmValue);
346
		}
347
348
		return $this;
349
	}
350
351
	/**
352
	 * Update the names of the child fields when updating name of field.
353
	 *
354
	 * @param string $name new name to give to the field.
355
	 * @return $this
356
	 */
357
	public function setName($name) {
358
		$this->passwordField->setName($name . '[_Password]');
359
		$this->confirmPasswordfield->setName($name . '[_ConfirmPassword]');
360
		if($this->hiddenField) {
361
			$this->hiddenField->setName($name . '[_PasswordFieldVisible]');
362
		}
363
364
		return parent::setName($name);
365
	}
366
367
	/**
368
	 * Determines if the field was actually shown on the client side - if not,
369
	 * we don't validate or save it.
370
	 *
371
	 * @return boolean
372
	 */
373
	public function isSaveable() {
374
		return !$this->showOnClick
375
			|| ($this->showOnClick && $this->hiddenField && $this->hiddenField->Value());
376
	}
377
378
	/**
379
	 * Validate this field
380
	 *
381
	 * @param Validator $validator
382
	 * @return bool
383
	 */
384
	public function validate($validator) {
385
		$name = $this->name;
386
387
		// if field isn't visible, don't validate
388
		if(!$this->isSaveable()) {
389
			return true;
390
		}
391
392
		$this->passwordField->setValue($this->value);
393
		$this->confirmPasswordfield->setValue($this->confirmValue);
394
		$value = $this->passwordField->Value();
395
396
		// both password-fields should be the same
397 View Code Duplication
		if($value != $this->confirmPasswordfield->Value()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
398
			$validator->validationError(
399
				$name,
400
				_t('Form.VALIDATIONPASSWORDSDONTMATCH',"Passwords don't match"),
401
				"validation"
402
			);
403
404
			return false;
405
		}
406
407
		if(!$this->canBeEmpty) {
408
			// both password-fields shouldn't be empty
409 View Code Duplication
			if(!$value || !$this->confirmPasswordfield->Value()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
410
				$validator->validationError(
411
					$name,
412
					_t('Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
413
					"validation"
414
				);
415
416
				return false;
417
			}
418
		}
419
420
		// lengths
421
		if(($this->minLength || $this->maxLength)) {
422
			$errorMsg = null;
423
			$limit = null;
424
			if($this->minLength && $this->maxLength) {
425
				$limit = "{{$this->minLength},{$this->maxLength}}";
426
				$errorMsg = _t(
427
					'ConfirmedPasswordField.BETWEEN',
428
					'Passwords must be {min} to {max} characters long.',
429
					array('min' => $this->minLength, 'max' => $this->maxLength)
0 ignored issues
show
Documentation introduced by
array('min' => $this->mi...x' => $this->maxLength) is of type array<string,integer,{"m...eger","max":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
430
				);
431
			} elseif($this->minLength) {
432
				$limit = "{{$this->minLength}}.*";
433
				$errorMsg = _t(
434
					'ConfirmedPasswordField.ATLEAST',
435
					'Passwords must be at least {min} characters long.',
436
					array('min' => $this->minLength)
0 ignored issues
show
Documentation introduced by
array('min' => $this->minLength) is of type array<string,integer,{"min":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
437
				);
438
			} elseif($this->maxLength) {
439
				$limit = "{0,{$this->maxLength}}";
440
				$errorMsg = _t(
441
					'ConfirmedPasswordField.MAXIMUM',
442
					'Passwords must be at most {max} characters long.',
443
					array('max' => $this->maxLength)
0 ignored issues
show
Documentation introduced by
array('max' => $this->maxLength) is of type array<string,integer,{"max":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
444
				);
445
			}
446
			$limitRegex = '/^.' . $limit . '$/';
447
			if(!empty($value) && !preg_match($limitRegex,$value)) {
448
				$validator->validationError(
449
					$name,
450
					$errorMsg,
451
					"validation"
452
				);
453
			}
454
		}
455
456
		if($this->requireStrongPassword) {
457
			if(!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/',$value)) {
458
				$validator->validationError(
459
					$name,
460
					_t('Form.VALIDATIONSTRONGPASSWORD',
461
						"Passwords must have at least one digit and one alphanumeric character"),
462
					"validation"
463
				);
464
465
				return false;
466
			}
467
		}
468
469
		// Check if current password is valid
470
		if(!empty($value) && $this->getRequireExistingPassword()) {
471
			if(!$this->currentPasswordValue) {
472
				$validator->validationError(
473
					$name,
474
					_t(
475
						'ConfirmedPasswordField.CURRENT_PASSWORD_MISSING',
476
						"You must enter your current password."
477
					),
478
					"validation"
479
				);
480
				return false;
481
			}
482
483
			// Check this password is valid for the current user
484
			$member = Member::currentUser();
485
			if(!$member) {
486
				$validator->validationError(
487
					$name,
488
					_t(
489
						'ConfirmedPasswordField.LOGGED_IN_ERROR',
490
						"You must be logged in to change your password."
491
					),
492
					"validation"
493
				);
494
				return false;
495
			}
496
497
			// With a valid user and password, check the password is correct
498
			$checkResult = $member->checkPassword($this->currentPasswordValue);
499
			if(!$checkResult->valid()) {
500
				$validator->validationError(
501
					$name,
502
					_t(
503
						'ConfirmedPasswordField.CURRENT_PASSWORD_ERROR',
504
						"The current password you have entered is not correct."
505
					),
506
					"validation"
507
				);
508
				return false;
509
			}
510
		}
511
512
		return true;
513
	}
514
515
	/**
516
	 * Only save if field was shown on the client, and is not empty.
517
	 *
518
	 * @param DataObjectInterface $record
519
	 *
520
	 * @return boolean
521
	 */
522
	public function saveInto(DataObjectInterface $record) {
523
		if(!$this->isSaveable()) {
524
			return false;
525
		}
526
527
		if(!($this->canBeEmpty && !$this->value)) {
528
			parent::saveInto($record);
529
		}
530
	}
531
532
	/**
533
	 * Makes a read only field with some stars in it to replace the password
534
	 *
535
	 * @return ReadonlyField
536
	 */
537
	public function performReadonlyTransformation() {
538
		$field = $this->castedCopy('ReadonlyField')
539
			->setTitle($this->title ? $this->title : _t('Member.PASSWORD'))
540
			->setValue('*****');
541
542
		return $field;
543
	}
544
545
	public function performDisabledTransformation()
546
	{
547
		return $this->performReadonlyTransformation();
548
	}
549
550
	/**
551
	 * Check if existing password is required
552
	 *
553
	 * @return bool
554
	 */
555
	public function getRequireExistingPassword() {
556
		return $this->requireExistingPassword;
557
	}
558
559
	/**
560
	 * Set if the existing password should be required
561
	 *
562
	 * @param bool $show Flag to show or hide this field
563
	 * @return $this
564
	 */
565
	public function setRequireExistingPassword($show) {
566
		// Don't modify if already added / removed
567
		if((bool)$show === $this->requireExistingPassword) {
568
			return $this;
569
		}
570
		$this->requireExistingPassword = $show;
571
		$name = $this->getName();
572
		$currentName = "{$name}[_CurrentPassword]";
573
		if ($show) {
574
			$confirmField = PasswordField::create($currentName, _t('Member.CURRENT_PASSWORD', 'Current Password'));
575
			$this->children->unshift($confirmField);
576
		} else {
577
			$this->children->removeByName($currentName, true);
578
		}
579
		return $this;
580
	}
581
}
582