Version: current

      1 <?php
      2 /**
      3  * Shows two password-fields, and checks for matching passwords.
      4  * Optionally hides the fields by default and shows
      5  * a link to toggle their visibility.
      6  *
      7  * @package forms
      8  * @subpackage fields-formattedinput
      9  */
     10 class ConfirmedPasswordField extends FormField {
     11
     12     /**
     13      * Minimum character length of the password.
     14      *
     15      * @var int
     16      */
     17     public $minLength = null;
     18
     19     /**
     20      * Maximum character length of the password.
     21      *
     22      * @var int
     23      */
     24     public $maxLength = null;
     25
     26     /**
     27      * Enforces at least one digit and one alphanumeric
     28      * character (in addition to {$minLength} and {$maxLength}
     29      *
     30      * @var boolean
     31      */
     32     public $requireStrongPassword = false;
     33
     34     /**
     35      * Allow empty fields in serverside validation
     36      *
     37      * @var boolean
     38      */
     39     public $canBeEmpty = false;
     40
     41     /**
     42      * If set to TRUE, the "password" and "confirm password"
     43      * formfields will be hidden via CSS and JavaScript by default,
     44      * and triggered by a link. An additional hidden field
     45      * determines if showing the fields has been triggered,
     46      * and just validates/saves the input in this case.
     47      * This behaviour works unobtrusively, without JavaScript enabled
     48      * the fields show, validate and save by default.
     49      *
     50      * @param boolean $showOnClick
     51      */
     52     protected $showOnClick = false;
     53
     54     /**
     55      * Title for the link that triggers
     56      * the visibility of password fields.
     57      *
     58      * @var string
     59      */
     60     public $showOnClickTitle;
     61
     62     /**
     63      * @param string $name
     64      * @param string $title
     65      * @param mixed $value
     66      * @param Form $form
     67      * @param boolean $showOnClick
     68      * @param string $titleConfirmField Alternate title (not localizeable)
     69      */
     70     function __construct($name, $title = null, $value = "", $form = null, $showOnClick = false, $titleConfirmField = null) {
     71         // naming with underscores to prevent values from actually being saved somewhere
     72         $this->children = new FieldSet(
     73             $pf = new PasswordField(
     74                 "{$name}[_Password]",
     75                 (isset($title)) ? $title : _t('Member.PASSWORD')),
     76             $cpf = new PasswordField(
     77                 "{$name}[_ConfirmPassword]",
     78                 (isset($titleConfirmField)) ? $titleConfirmField : _t('Member.CONFIRMPASSWORD', 'Confirm Password')
     79             )
     80         );
     81         /*$pf->isConfirmedPasswordField = true;*///~VA - валидация только на второе поле
     82         $cpf->isConfirmedPasswordField = true;
     83
     84         // has to be called in constructor because Field() isn't triggered upon saving the instance
     85         if($showOnClick) {
     86             $this->children->push(new HiddenField("{$name}[_PasswordFieldVisible]"));
     87         }
     88         $this->showOnClick = $showOnClick;
     89
     90         // we have labels for the subfields
     91         $title = false;
     92
     93         parent::__construct($name, $title, null, $form);
     94         $this->setValue($value);
     95     }
     96
     97     function Field() {
     98         return $this->renderWith($this->fieldTemplates());
     99     }
    100
    101     /**
    102      * Can be empty is a flag that turns on/off empty field checking.
    103      * For example, set this to false (the default) when creating a user account,
    104      * and true
    105      */
    106     function setCanBeEmpty($value) {
    107         $this->canBeEmpty = (bool)$value;
    108     }
    109
    110     /**
    111      * The title on the link which triggers display of the
    112      * "password" and "confirm password" formfields.
    113      * Only used if {@link setShowOnClick()} is set to TRUE.
    114      *
    115      * @param $title
    116      */
    117     public function setShowOnClickTitle($title) {
    118         $this->showOnClickTitle = $title;
    119     }
    120
    121     /**
    122      * @return string
    123      */
    124     public function getShowOnClickTitle() {
    125         return $this->showOnClickTitle;
    126     }
    127
    128     function setRightTitle($title) {
    129         foreach($this->children as $field) {
    130             $field->setRightTitle($title);
    131         }
    132     }
    133
    134     /**
    135      * @param array: 2 entrie array with the customised title for each of the 2 children.
    136      */
    137     function setChildrenTitles($titles) {
    138         if(is_array($titles)&&count($titles)==2){
    139             foreach($this->children as $field) {
    140                 if(isset($titles[0])){
    141                     $field->setTitle($titles[0]);
    142                     array_shift($titles);
    143                 }
    144             }
    145         }
    146     }
    147
    148     /**
    149      * Value is sometimes an array, and sometimes a single value, so we need to handle both cases
    150      */
    151     function setValue($value) {
    152         if(is_array($value)) {
    153             if($value['_Password'] || (!$value['_Password'] && !$this->canBeEmpty)) {
    154                 $this->value = $value['_Password'];
    155             }
    156             if(isset($value['_PasswordFieldVisible'])){
    157                 $this->children->fieldByName($this->Name() . '[_PasswordFieldVisible]')->setValue($value['_PasswordFieldVisible']);
    158             }
    159         } else {
    160             if($value || (!$value && !$this->canBeEmpty)) {
    161                 $this->value = $value;
    162             }
    163         }
    164         $this->children->fieldByName($this->Name() . '[_Password]')->setValue($this->value);
    165         $this->children->fieldByName($this->Name() . '[_ConfirmPassword]')->setValue($this->value);
    166     }
    167
    168     function jsValidation() {
    169         $formID = $this->form->FormName();
    170         $jsTests = '';
    171
    172         $jsTests .= "
    173             // if fields are hidden, reset values and don't validate
    174             var containers = $$('.showOnClickContainer', $('#'+fieldName));
    175             if(containers.length && !Element.visible(containers[0])) {
    176                 passEl.value = null;
    177                 confEl.value = null;
    178                 return true;
    179             }
    180         ";
    181
    182         $error1 = _t('ConfirmedPasswordField.HAVETOMATCH', 'Passwords have to match.');
    183         $jsTests .= "
    184             if(passEl.value != confEl.value) {
    185                 validationError(confEl, \"$error1\", \"error\");
    186                 return false;
    187             }
    188         ";
    189
    190         $error2 = _t('ConfirmedPasswordField.NOEMPTY', 'Passwords can\'t be empty.');
    191         if(!$this->canBeEmpty) {
    192             $jsTests .= "
    193                 if(!passEl.value || !confEl.value) {
    194                     validationError(confEl, \"$error2\", \"error\");
    195                     return false;
    196                 }
    197             ";
    198         }
    199
    200         if(($this->minLength || $this->maxLength)) {
    201             if($this->minLength && $this->maxLength) {
    202                 $limit = "{{$this->minLength},{$this->maxLength}}";
    203                 $errorMsg = sprintf(_t('ConfirmedPasswordField.BETWEEN', 'Passwords must be %s to %s characters long.'), $this->minLength, $this->maxLength);
    204             } elseif($this->minLength) {
    205                 $limit = "{{$this->minLength}}.*";
    206                 $errorMsg = sprintf(_t('ConfirmedPasswordField.ATLEAST', 'Passwords must be at least %s characters long.'), $this->minLength);
    207             } elseif($this->maxLength) {
    208                 $limit = "{0,{$this->maxLength}}";
    209                 $errorMsg = sprintf(_t('ConfirmedPasswordField.MAXIMUM', 'Passwords must be at most %s characters long.'), $this->maxLength);
    210             }
    211             $limitRegex = '/^.' . $limit . '$/';
    212             $jsTests .= "
    213             if(passEl.value && !passEl.value.match({$limitRegex})) {
    214                 validationError(confEl, \"{$errorMsg}\", \"error\");
    215                 return false;
    216             }
    217             ";
    218         }
    219
    220         $error3 = _t('ConfirmedPasswordField.LEASTONE', 'Passwords must have at least one digit and one alphanumeric character.');
    221         if($this->requireStrongPassword) {
    222             $jsTests .= "
    223                 if(!passEl.value.match(/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/)) {
    224                     validationError(
    225                         confEl,
    226                         \"$error3\",
    227                         \"error\"
    228                     );
    229                     return false;
    230                 }
    231             ";
    232         }
    233
    234         $jsFunc =<<<JS
    235 Behaviour.register({
    236     "#$formID": {
    237         validateConfirmedPassword: function(fieldName) {
    238             var passEl = _CURRENT_FORM.elements['Password[_Password]'];
    239             var confEl = _CURRENT_FORM.elements['Password[_ConfirmPassword]'];
    240             $jsTests
    241             return true;
    242         }
    243     }
    244 });
    245 JS;
    246         Requirements :: customScript($jsFunc, 'func_validateConfirmedPassword_' . $formID);
    247
    248         //return "\$('$formID').validateConfirmedPassword('$this->name');";
    249         return <<<JS
    250 if(typeof fromAnOnBlur != 'undefined'){
    251     if(fromAnOnBlur.name == '$this->name')
    252         $('$formID').validateConfirmedPassword('$this->name');
    253 }else{
    254     $('$formID').validateConfirmedPassword('$this->name');
    255 }
    256 JS;
    257     }
    258
    259     /**
    260      * Determines if the field was actually
    261      * shown on the clientside - if not,
    262      * we don't validate or save it.
    263      *
    264      * @return bool
    265      */
    266     function isSaveable() {
    267         $isVisible = $this->children->fieldByName($this->Name() . '[_PasswordFieldVisible]');
    268         return (!$this->showOnClick || ($this->showOnClick && $isVisible && $isVisible->Value()));
    269     }
    270
    271     function validate() {
    272         $validator = $this->form->getValidator();
    273         $name = $this->name;
    274
    275         // if field isn't visible, don't validate
    276         if(!$this->isSaveable()) return true;
    277
    278         $passwordField = $this->children->fieldByName($name.'[_Password]');
    279         $passwordConfirmField = $this->children->fieldByName($name.'[_ConfirmPassword]');
    280         $passwordField->setValue($_POST[$name]['_Password']);
    281         $passwordConfirmField->setValue($_POST[$name]['_ConfirmPassword']);
    282
    283         $value = $passwordField->Value();
    284
    285         // both password-fields should be the same
    286         if($value != $passwordConfirmField->Value()) {
    287             $validator->validationError($name, _t('Form.VALIDATIONPASSWORDSDONTMATCH',"Passwords don't match"), "validation", false);
    288             return false;
    289         }
    290
    291         if(!$this->canBeEmpty) {
    292             // both password-fields shouldn't be empty
    293             if(!$value || !$passwordConfirmField->Value()) {
    294                 $validator->validationError($name, _t('Form.VALIDATIONPASSWORDSNOTEMPTY', "Passwords can't be empty"), "validation", false);
    295                 return false;
    296             }
    297         }
    298
    299         // lengths
    300         if(($this->minLength || $this->maxLength)) {
    301             if($this->minLength && $this->maxLength) {
    302                 $limit = "{{$this->minLength},{$this->maxLength}}";
    303                 $errorMsg = sprintf(_t('ConfirmedPasswordField.BETWEEN', 'Passwords must be %s to %s characters long.'), $this->minLength, $this->maxLength);
    304             } elseif($this->minLength) {
    305                 $limit = "{{$this->minLength}}.*";
    306                 $errorMsg = sprintf(_t('ConfirmedPasswordField.ATLEAST', 'Passwords must be at least %s characters long.'), $this->minLength);
    307             } elseif($this->maxLength) {
    308                 $limit = "{0,{$this->maxLength}}";
    309                 $errorMsg = sprintf(_t('ConfirmedPasswordField.MAXIMUM', 'Passwords must be at most %s characters long.'), $this->maxLength);
    310             }
    311             $limitRegex = '/^.' . $limit . '$/';
    312             if(!empty($value) && !preg_match($limitRegex,$value)) {
    313                 $validator->validationError('Password', $errorMsg,
    314                     "validation",
    315                     false
    316                 );
    317             }
    318         }
    319
    320         if($this->requireStrongPassword) {
    321             if(!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/',$value)) {
    322                 $validator->validationError(
    323                     'Password',
    324                     _t('Form.VALIDATIONSTRONGPASSWORD', "Passwords must have at least one digit and one alphanumeric character."),
    325                     "validation",
    326                     false
    327                 );
    328                 return false;
    329             }
    330         }
    331         return true;
    332     }
    333
    334     /**
    335      * Only save if field was shown on the client,
    336      * and is not empty.
    337      *
    338      * @param DataObject $record
    339      * @return bool
    340      */
    341     function saveInto(DataObject $record) {
    342         if(!$this->isSaveable()) return false;
    343
    344         if(!($this->canBeEmpty && !$this->value)) {
    345             parent::saveInto($record);
    346         }
    347     }
    348
    349     /**
    350      * Makes a pretty readonly field with some stars in it
    351      */
    352     function performReadonlyTransformation() {
    353         $stars = '*****';
    354
    355         $field = new ReadonlyField($this->name, $this->title ? $this->title : _t('Member.PASSWORD'), $stars);
    356         $field->setForm($this->form);
    357         return $field;
    358     }
    359 }
    360

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -