Completed
Push — master ( c130e5...a809e8 )
by Sam
10:09
created

getRequireExistingPassword()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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;
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
		if($value != $this->confirmPasswordfield->Value()) {
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
			if(!$value || !$this->confirmPasswordfield->Value()) {
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)
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)
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)
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