Completed
Push — namespace-template ( 7967f2...367a36 )
by Sam
10:48
created

ConfirmedPasswordField::setValue()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 22
nc 40
nop 2
dl 0
loc 36
rs 5.1612
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Two masked input fields, checks for matching passwords.
5
 *
6
 * Optionally hides the fields by default and shows a link to toggle their
7
 * visibility.
8
 *
9
 * @package forms
10
 * @subpackage fields-formattedinput
11
 */
12
class ConfirmedPasswordField extends FormField {
13
14
	/**
15
	 * Minimum character length of the password.
16
	 *
17
	 * @var int
18
	 */
19
	public $minLength = null;
20
21
	/**
22
	 * Maximum character length of the password.
23
	 *
24
	 * @var int
25
	 */
26
	public $maxLength = null;
27
28
	/**
29
	 * Enforces at least one digit and one alphanumeric
30
	 * character (in addition to {$minLength} and {$maxLength}
31
	 *
32
	 * @var boolean
33
	 */
34
	public $requireStrongPassword = false;
35
36
	/**
37
	 * Allow empty fields in serverside validation
38
	 *
39
	 * @var boolean
40
	 */
41
	public $canBeEmpty = false;
42
43
	/**
44
	 * If set to TRUE, the "password" and "confirm password" form fields will
45
	 * be hidden via CSS and JavaScript by default, and triggered by a link.
46
	 *
47
	 * An additional hidden field determines if showing the fields has been
48
	 * triggered and just validates/saves the input in this case.
49
	 *
50
	 * This behaviour works unobtrusively, without JavaScript enabled
51
	 * the fields show, validate and save by default.
52
	 *
53
	 * @param boolean $showOnClick
54
	 */
55
	protected $showOnClick = false;
56
57
	/**
58
	 * Check if the existing password should be entered first
59
	 *
60
	 * @var bool
61
	 */
62
	protected $requireExistingPassword = false;
63
64
65
	/**
66
	 * A place to temporarily store the confirm password value
67
	 *
68
	 * @var string
69
	 */
70
	protected $confirmValue;
71
72
	/**
73
	 * Store value of "Current Password" field
74
	 *
75
	 * @var string
76
	 */
77
	protected $currentPasswordValue;
78
79
	/**
80
	 * Title for the link that triggers the visibility of password fields.
81
	 *
82
	 * @var string
83
	 */
84
	public $showOnClickTitle;
85
86
	/**
87
	 * Child fields (_Password, _ConfirmPassword)
88
	 *
89
	 * @var FieldList
90
	 */
91
	public $children;
92
93
	protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
94
95
	/**
96
	 * @param string $name
97
	 * @param string $title
98
	 * @param mixed $value
99
	 * @param Form $form
100
	 * @param boolean $showOnClick
101
	 * @param string $titleConfirmField Alternate title (not localizeable)
102
	 */
103
	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...
104
			$titleConfirmField = null) {
105
106
		// naming with underscores to prevent values from actually being saved somewhere
107
		$this->children = new FieldList(
108
			new PasswordField(
109
				"{$name}[_Password]",
110
				(isset($title)) ? $title : _t('Member.PASSWORD', 'Password')
111
			),
112
			new PasswordField(
113
				"{$name}[_ConfirmPassword]",
114
				(isset($titleConfirmField)) ? $titleConfirmField : _t('Member.CONFIRMPASSWORD', 'Confirm Password')
115
			)
116
		);
117
118
		// has to be called in constructor because Field() isn't triggered upon saving the instance
119
		if($showOnClick) {
120
			$this->children->push(new HiddenField("{$name}[_PasswordFieldVisible]"));
121
		}
122
123
		// disable auto complete
124
		foreach($this->children as $child) {
125
			/** @var FormField $child */
126
			$child->setAttribute('autocomplete', 'off');
127
		}
128
129
		$this->showOnClick = $showOnClick;
130
131
		// we have labels for the subfields
132
		$title = false;
133
134
		parent::__construct($name, $title);
0 ignored issues
show
Documentation introduced by
$title is of type boolean, but the function expects a null|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...
135
		$this->setValue($value);
136
	}
137
138
	/**
139
	 * @param array $properties
140
	 *
141
	 * @return HTMLText
142
	 */
143
	public function Field($properties = array()) {
144
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
145
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/ConfirmedPasswordField.js');
146
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/ConfirmedPasswordField.css');
147
148
		$content = '';
149
150
		if($this->showOnClick) {
151
			if($this->showOnClickTitle) {
152
				$title = $this->showOnClickTitle;
153
			} else {
154
				$title = _t(
155
					'ConfirmedPasswordField.SHOWONCLICKTITLE',
156
					'Change Password',
157
158
					'Label of the link which triggers display of the "change password" formfields'
159
				);
160
			}
161
162
			$content .= "<div class=\"showOnClick\">\n";
163
			$content .= "<a href=\"#\">{$title}</a>\n";
164
			$content .= "<div class=\"showOnClickContainer\">";
165
		}
166
167
		foreach($this->children as $field) {
168
			/** @var FormField $field */
169
			$field->setDisabled($this->isDisabled());
170
			$field->setReadonly($this->isReadonly());
171
172
			if(count($this->attributes)) {
173
				foreach($this->attributes as $name => $value) {
174
					$field->setAttribute($name, $value);
175
				}
176
			}
177
178
			$content .= $field->FieldHolder();
179
		}
180
181
		if($this->showOnClick) {
182
			$content .= "</div>\n";
183
			$content .= "</div>\n";
184
		}
185
186
		return $content;
187
	}
188
189
	/**
190
	 * Returns the children of this field for use in templating.
191
	 * @return FieldList
192
	 */
193
	public function getChildren() {
194
		return $this->children;
195
	}
196
197
	/**
198
	 * Can be empty is a flag that turns on / off empty field checking.
199
	 *
200
	 * For example, set this to false (the default) when creating a user account,
201
	 * and true when displaying on an edit form.
202
	 *
203
	 * @param boolean $value
204
	 *
205
	 * @return ConfirmedPasswordField
206
	 */
207
	public function setCanBeEmpty($value) {
208
		$this->canBeEmpty = (bool)$value;
209
210
		return $this;
211
	}
212
213
	/**
214
	 * The title on the link which triggers display of the "password" and
215
	 * "confirm password" formfields. Only used if {@link setShowOnClick()}
216
	 * is set to TRUE.
217
	 *
218
	 * @param string $title
219
	 *
220
	 * @return ConfirmedPasswordField
221
	 */
222
	public function setShowOnClickTitle($title) {
223
		$this->showOnClickTitle = $title;
224
225
		return $this;
226
	}
227
228
	/**
229
	 * @return string $title
230
	 */
231
	public function getShowOnClickTitle() {
232
		return $this->showOnClickTitle;
233
	}
234
235
	/**
236
	 * @param string $title
237
	 *
238
	 * @return ConfirmedPasswordField
239
	 */
240
	public function setRightTitle($title) {
241
		foreach($this->children as $field) {
242
			/** @var FormField $field */
243
			$field->setRightTitle($title);
244
		}
245
246
		return $this;
247
	}
248
249
	/**
250
	 * Set child field titles. Titles in order should be:
251
	 *  - "Current Password" (if getRequireExistingPassword() is set)
252
	 *  - "Password"
253
	 *  - "Confirm Password"
254
	 *
255
	 * @param array $titles List of child titles
256
	 * @return $this
257
	 */
258
	public function setChildrenTitles($titles) {
259
		$expectedChildren = $this->getRequireExistingPassword() ? 3 : 2;
260
		if(is_array($titles) && count($titles) == $expectedChildren) {
261
			foreach($this->children as $field) {
262
				if(isset($titles[0])) {
263
					/** @var FormField $field */
264
					$field->setTitle($titles[0]);
265
266
					array_shift($titles);
267
				}
268
			}
269
		}
270
271
		return $this;
272
	}
273
274
	/**
275
	 * Value is sometimes an array, and sometimes a single value, so we need
276
	 * to handle both cases.
277
	 *
278
	 * @param mixed $value
279
	 * @param mixed $data
280
	 * @return $this
281
	 */
282
	public function setValue($value, $data = null) {
283
		// If $data is a DataObject, don't use the value, since it's a hashed value
284
		if ($data && $data instanceof DataObject) $value = '';
285
286
		//store this for later
287
		$oldValue = $this->value;
288
289
		if(is_array($value)) {
290
			$this->value = $value['_Password'];
291
			$this->confirmValue = $value['_ConfirmPassword'];
292
			$this->currentPasswordValue = ($this->getRequireExistingPassword() && isset($value['_CurrentPassword']))
293
				? $value['_CurrentPassword']
294
				: null;
295
296
			if($this->showOnClick && isset($value['_PasswordFieldVisible'])) {
297
				$this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]')
298
					->setValue($value['_PasswordFieldVisible']);
299
			}
300
		} else {
301
			if($value || (!$value && $this->canBeEmpty)) {
302
				$this->value = $value;
303
				$this->confirmValue = $value;
304
			}
305
		}
306
307
		//looking up field by name is expensive, so lets check it needs to change
308
		if ($oldValue != $this->value) {
309
			$this->children->fieldByName($this->getName() . '[_Password]')
310
				->setValue($this->value);
311
312
			$this->children->fieldByName($this->getName() . '[_ConfirmPassword]')
313
				->setValue($this->confirmValue);
314
		}
315
316
		return $this;
317
	}
318
319
	/**
320
	 * Update the names of the child fields when updating name of field.
321
	 *
322
	 * @param string $name new name to give to the field.
323
	 * @return $this
324
	 */
325
	public function setName($name) {
326
		$this->children->fieldByName($this->getName() . '[_Password]')
327
				->setName($name . '[_Password]');
328
		$this->children->fieldByName($this->getName() . '[_ConfirmPassword]')
329
				->setName($name . '[_ConfirmPassword]');
330
331
		return parent::setName($name);
332
	}
333
334
	/**
335
	 * Determines if the field was actually shown on the client side - if not,
336
	 * we don't validate or save it.
337
	 *
338
	 * @return boolean
339
	 */
340
	public function isSaveable() {
341
		$isVisible = $this->children->fieldByName($this->getName() . '[_PasswordFieldVisible]');
342
343
		return (!$this->showOnClick || ($this->showOnClick && $isVisible && $isVisible->Value()));
344
	}
345
346
	/**
347
	 * Validate this field
348
	 *
349
	 * @param Validator $validator
350
	 * @return bool
351
	 */
352
	public function validate($validator) {
353
		$name = $this->name;
354
355
		// if field isn't visible, don't validate
356
		if(!$this->isSaveable()) {
357
			return true;
358
		}
359
360
		$passwordField = $this->children->fieldByName($name.'[_Password]');
361
		$passwordConfirmField = $this->children->fieldByName($name.'[_ConfirmPassword]');
362
		$passwordField->setValue($this->value);
363
		$passwordConfirmField->setValue($this->confirmValue);
364
365
		$value = $passwordField->Value();
366
367
		// both password-fields should be the same
368 View Code Duplication
		if($value != $passwordConfirmField->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...
369
			$validator->validationError(
370
				$name,
371
				_t('Form.VALIDATIONPASSWORDSDONTMATCH',"Passwords don't match"),
372
				"validation"
373
			);
374
375
			return false;
376
		}
377
378
		if(!$this->canBeEmpty) {
379
			// both password-fields shouldn't be empty
380 View Code Duplication
			if(!$value || !$passwordConfirmField->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...
381
				$validator->validationError(
382
					$name,
383
					_t('Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"),
384
					"validation"
385
				);
386
387
				return false;
388
			}
389
		}
390
391
		// lengths
392
		if(($this->minLength || $this->maxLength)) {
393
			$errorMsg = null;
394
			$limit = null;
395
			if($this->minLength && $this->maxLength) {
396
				$limit = "{{$this->minLength},{$this->maxLength}}";
397
				$errorMsg = _t(
398
					'ConfirmedPasswordField.BETWEEN',
399
					'Passwords must be {min} to {max} characters long.',
400
					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...
401
				);
402
			} elseif($this->minLength) {
403
				$limit = "{{$this->minLength}}.*";
404
				$errorMsg = _t(
405
					'ConfirmedPasswordField.ATLEAST',
406
					'Passwords must be at least {min} characters long.',
407
					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...
408
				);
409
			} elseif($this->maxLength) {
410
				$limit = "{0,{$this->maxLength}}";
411
				$errorMsg = _t(
412
					'ConfirmedPasswordField.MAXIMUM',
413
					'Passwords must be at most {max} characters long.',
414
					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...
415
				);
416
			}
417
			$limitRegex = '/^.' . $limit . '$/';
418
			if(!empty($value) && !preg_match($limitRegex,$value)) {
419
				$validator->validationError(
420
					$name,
421
					$errorMsg,
422
					"validation"
423
				);
424
			}
425
		}
426
427
		if($this->requireStrongPassword) {
428
			if(!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/',$value)) {
429
				$validator->validationError(
430
					$name,
431
					_t('Form.VALIDATIONSTRONGPASSWORD',
432
						"Passwords must have at least one digit and one alphanumeric character"),
433
					"validation"
434
				);
435
436
				return false;
437
			}
438
		}
439
440
		// Check if current password is valid
441
		if(!empty($value) && $this->getRequireExistingPassword()) {
442
			if(!$this->currentPasswordValue) {
443
				$validator->validationError(
444
					$name,
445
					_t(
446
						'ConfirmedPasswordField.CURRENT_PASSWORD_MISSING',
447
						"You must enter your current password."
448
					),
449
					"validation"
450
				);
451
				return false;
452
			}
453
454
			// Check this password is valid for the current user
455
			$member = Member::currentUser();
456
			if(!$member) {
457
				$validator->validationError(
458
					$name,
459
					_t(
460
						'ConfirmedPasswordField.LOGGED_IN_ERROR',
461
						"You must be logged in to change your password."
462
					),
463
					"validation"
464
				);
465
				return false;
466
			}
467
468
			// With a valid user and password, check the password is correct
469
			$checkResult = $member->checkPassword($this->currentPasswordValue);
470
			if(!$checkResult->valid()) {
471
				$validator->validationError(
472
					$name,
473
					_t(
474
						'ConfirmedPasswordField.CURRENT_PASSWORD_ERROR',
475
						"The current password you have entered is not correct."
476
					),
477
					"validation"
478
				);
479
				return false;
480
			}
481
		}
482
483
		return true;
484
	}
485
486
	/**
487
	 * Only save if field was shown on the client, and is not empty.
488
	 *
489
	 * @param DataObjectInterface $record
490
	 *
491
	 * @return boolean
492
	 */
493
	public function saveInto(DataObjectInterface $record) {
494
		if(!$this->isSaveable()) {
495
			return false;
496
		}
497
498
		if(!($this->canBeEmpty && !$this->value)) {
499
			parent::saveInto($record);
500
		}
501
	}
502
503
	/**
504
	 * Makes a read only field with some stars in it to replace the password
505
	 *
506
	 * @return ReadonlyField
507
	 */
508
	public function performReadonlyTransformation() {
509
		$field = $this->castedCopy('ReadonlyField')
510
			->setTitle($this->title ? $this->title : _t('Member.PASSWORD'))
511
			->setValue('*****');
512
513
		return $field;
514
	}
515
516
	/**
517
	 * Check if existing password is required
518
	 *
519
	 * @return bool
520
	 */
521
	public function getRequireExistingPassword() {
522
		return $this->requireExistingPassword;
523
	}
524
525
	/**
526
	 * Set if the existing password should be required
527
	 *
528
	 * @param bool $show Flag to show or hide this field
529
	 * @return $this
530
	 */
531
	public function setRequireExistingPassword($show) {
532
		// Don't modify if already added / removed
533
		if((bool)$show === $this->requireExistingPassword) {
534
			return $this;
535
		}
536
		$this->requireExistingPassword = $show;
537
		$name = $this->getName();
538
		$currentName = "{$name}[_CurrentPassword]";
539
		if ($show) {
540
			$confirmField = PasswordField::create($currentName, _t('Member.CURRENT_PASSWORD', 'Current Password'));
541
			$this->children->unshift($confirmField);
542
		} else {
543
			$this->children->removeByName($currentName, true);
544
		}
545
		return $this;
546
	}
547
}
548