__('Password', __FILE__), // Module Title 'summary' => __("Password input with confirmation field that doesn't ever echo the input back.", __FILE__), // Module Summary 'version' => 102, 'permanent' => true, ); } /** * Requirements: letter required * */ const requireLetter = 'letter'; /** * Requirements: lowercase letter required * */ const requireLowerLetter = 'lower'; /** * Requirements: uppercase letter required * */ const requireUpperLetter = 'upper'; /** * Requirements: digit required * */ const requireDigit = 'digit'; /** * Requirements: other character (symbol) required * */ const requireOther = 'other'; /** * Requirements: disable all above * */ const requireNone = 'none'; /** * Require old password before changes? Auto * */ const requireOldAuto = 0; /** * Require old password before changes? Yes * */ const requireOldYes = 1; /** * Require old password before changes? No * */ const requireOldNo = -1; /** * Page being edited, when applicable * * @var User|Page|null * */ protected $_page = null; /** * Construct and establish default settings * */ public function __construct() { parent::__construct(); $this->attr('type', 'password'); $this->attr('size', 30); $this->attr('maxlength', 256); $this->attr('minlength', 6); $this->set('requireOld', false); $this->set('requirements', array(self::requireLetter, self::requireDigit)); $this->set('complexifyFactor', 0.7); $this->set('complexifyBanMode', 'loose'); $this->set('requirementsLabels', array( self::requireLetter => $this->_('letter'), self::requireLowerLetter => $this->_('lowercase letter'), self::requireUpperLetter => $this->_('uppercase letter'), self::requireDigit => $this->_('digit'), self::requireOther => $this->_('symbol/punctuation'), self::requireNone => $this->_('none (disable all above)'), )); $this->set('showPass', false); // allow password to be rendered in renderValue and/or re-populated in form? $this->set('unmask', false); $this->set('oldPassLabel', $this->_('Current password')); $this->set('newPassLabel', $this->_('New password')); $this->set('confirmLabel', $this->_('Confirm')); } /** * Init Inputfield, establishing the label if none has been set * */ public function init() { parent::init(); $this->set('defaultLabel', $this->_('Set Password')); $this->label = $this->defaultLabel; } /** * Sets the page being edited, not always applicable * * @param Page $page * */ public function setPage(Page $page) { $this->_page = $page; if($page->hasStatus(Page::statusUnpublished)) $this->required = true; } /** * Called before render * * @param Inputfield $parent * @param bool $renderValueMode * @return bool * @throws WireException * */ public function renderReady(Inputfield $parent = null, $renderValueMode = false) { if($this->label == 'Set Password') $this->label = $this->defaultLabel; $config = $this->wire('config'); $url = $config->urls->InputfieldPassword . 'complexify/'; $config->scripts->add($url . 'jquery.complexify.min.js'); $config->scripts->add($url . 'jquery.complexify.banlist.js'); $this->wire('modules')->get('JqueryCore')->use('xregexp'); $page = $this->wire('page'); if(($page && $page->template == 'admin') || $this->wire('user')->isLoggedin()) { $this->attr('autocomplete', 'new-password'); // ProcessProfile and ProcessUser } return parent::renderReady($parent, $renderValueMode); } /** * Render Password input(s) * * @return string * */ public function ___render() { /** @var Sanitizer $sanitizer */ $sanitizer = $this->wire('sanitizer'); $minlength = (int) $this->attr('minlength'); $requirements = array(); if($minlength) { $requirements['minlength'] = "[span.pass-require.pass-require-minlength]" . sprintf($this->_('at least %d characters long'), $minlength) . "[/span]"; } $labels = $this->getSetting('requirementsLabels'); if(!in_array(self::requireNone, $this->getSetting('requirements'))) { foreach($this->getSetting('requirements') as $name) { $requirements[$name] = "[span.pass-require.pass-require-$name]" . $labels[$name] . "[/span]"; } } if(isset($requirements['upper']) || isset($requirements['lower'])) { unset($requirements['letter']); } if(count($requirements)) { $description = implode(", ", $requirements); $description = sprintf($this->_('Minimum requirements: %s.'), $description); $description = trim("$description " . $this->getSetting('description')); $this->set('description', $description); } $value = $this->attr('value'); $confirmValue = ''; $trackChanges = $this->trackChanges(); if($trackChanges) $this->setTrackChanges(false); if(!$this->getSetting('showPass')) { $this->attr('value', ''); } else { $confirmValue = $this->wire('sanitizer')->entities($value); } $this->attr('data-banMode', $this->complexifyBanMode ? $this->complexifyBanMode : 'loose'); $this->attr('data-factor', (float) $this->complexifyFactor >= 0 ? $this->complexifyFactor : 0); $inputClass = $this->wire('sanitizer')->entities($this->attr('class')); $this->addClass('InputfieldPasswordComplexify'); $failIcon = ""; $okIcon = ""; $goodIcon = ""; $oldPassLabel = $sanitizer->entities1($this->oldPassLabel); $newPassLabel = $sanitizer->entities1($this->newPassLabel); $confirmLabel = $sanitizer->entities1($this->confirmLabel); $name = $this->attr('name'); $id = $this->attr('id'); $size = $this->attr('size'); $out = ''; if((int) $this->requireOld > 0 && $this->wire('user')->isLoggedin()) { $out .= "
" . "" . " " . "
"; } $out .= "" . "" . "getAttributesString() . " /> " . "" . //"$angleIcon$newPassLabel" . "$failIcon" . $this->_('Not yet valid') . "" . "$failIcon" . $this->_('Invalid') . "" . "$failIcon" . $this->_('Too short') . "" . "$failIcon" . $this->_('Too common') . "" . "$failIcon" . $this->_('Same as old') . "" . "$okIcon" . $this->_('Weak') . "" . "$okIcon" . $this->_('Ok') . "" . "$goodIcon" . $this->_('Good') . "" . "$goodIcon" . $this->_('Excellent') . "" . "" . "
" . "" . "" . " " . "" . //"$angleIcon$newPassLabel ($confirmLabel)" . "$goodIcon" . $this->_('Matches') . "" . "$failIcon" . $this->_('Does not match') . "" . "$okIcon" . "" . "
"; if($this->unmask) { $out .= "" . "" . $this->_('Show Password') . "" . "" . $this->_('Hide Password') . "" . "
"; } $this->attr('value', $value); if($trackChanges) $this->setTrackChanges(true); return $out; } /** * Set Inputfield setting * * @param string $key * @param mixed $value * @return Inputfield|InputfieldPassword * */ public function set($key, $value) { if($key == 'collapsed' && $this->_page && $this->_page->hasStatus(Page::statusUnpublished)) { // prevent collapse of field when pass is for unpublished user $value = Inputfield::collapsedNo; } return parent::set($key, $value); } /** * Render non-editable Inputfield * * @return string * */ public function ___renderValue() { if(!$this->getSetting('showPass')) { $value = strlen($this->attr('value')) ? '******' : ''; } else { $value = $this->wire('sanitizer')->entities($this->attr('value')); } $value = strlen($value) ? "$value
" : ""; return $value; } /** * Process input * * @param WireInputData $input * @return $this * */ public function ___processInput(WireInputData $input) { parent::___processInput($input); /** @var User $user */ $user = $this->wire('user'); $key = $this->attr('name'); $value = $this->attr('value'); if($value) {} $confirmKey = "_" . $key; if(!isset($input->$key)) return $this; // form was submitted $pass = $input->$key; $confirmPass = $input->$confirmKey; if(strlen($pass) && strlen($confirmPass)) { // password was submitted (with confirmation) $allowInput = true; if($this->requireOld > 0 && $user->isLoggedin()) { // old password is required to change $oldKey = "_old_" . $key; $oldPass = $input->$oldKey; if(!strlen($oldPass)) { $this->error($this->_('Old password is required in order to enter a new one.')); $allowInput = false; } else if(!$this->wire('user')->pass->matches($oldPass)) { $this->error($this->_('The old password you entered is not correct.')); $allowInput = false; } } if($allowInput) { if($confirmPass !== $pass) { $this->error($this->_("Passwords do not match")); } $this->isValidPassword($pass); } } else if($this->required) { $this->error($this->_("Required password was not specified")); } if(count($this->getErrors())) { $this->attr('value', ''); $this->resetTrackChanges(); // don't record a change } return $this; } /** * Return whether or not the given password is valid according to configured requirements * * Exact error messages can be retrieved with $this->getErrors(). * * @param string $value Password to validate * @return bool * */ public function isValidPassword($value) { $numErrors = 0; $requirements = $this->getSetting('requirements'); if(preg_match('/[\t\r\n]/', $value)) { $this->error($this->_("Password contained invalid whitespace")); $numErrors++; } if(strlen($value) < $this->attr('minlength')) { $this->error($this->_("Password is less than required number of characters")); $numErrors++; } if(in_array(self::requireNone, $requirements)) { // early exit if all following requirements are disabled return $numErrors === 0; } if(in_array(self::requireLetter, $requirements)) { // if(!preg_match('/[a-zA-Z]/', $value)) { if(!preg_match('/\p{L}/', $value)) { $this->error($this->_("Password does not contain at least one letter (a-z A-Z)")); $numErrors++; } } if(in_array(self::requireLowerLetter, $requirements)) { if(!preg_match('/\p{Ll}/', $value)) { $this->error($this->_("Password must have at least one lowercase letter (a-z)")); $numErrors++; } } if(in_array(self::requireUpperLetter, $requirements)) { if(!preg_match('/\p{Lu}/', $value)) { $this->error($this->_("Password must have at least one uppercase letter (A-Z)")); $numErrors++; } } if(in_array(self::requireDigit, $requirements)) { if(!preg_match('/\p{N}/', $value)) { $this->error($this->_("Password does not contain at least one digit (0-9)")); $numErrors++; } } if(in_array(self::requireOther, $requirements)) { if(!preg_match('/\p{P}/', $value) && !preg_match('/\p{S}/', $value)) { $this->error($this->_("Password must have at least one non-letter, non-digit character (like punctuation)")); $numErrors++; } } return $numErrors === 0; } /** * Return the fields required to configure an instance of InputfieldPassword * * @return InputfieldWrapper * */ public function ___getConfigInputfields() { $inputfields = parent::___getConfigInputfields(); $skips = array( 'collapsed', 'showIf', 'placeholder', 'stripTags', 'pattern', 'visibility', 'minlength', 'maxlength', 'showCount', 'size', ); foreach($skips as $name) { $f = $inputfields->get($name); if($f) $inputfields->remove($f); } /** @var InputfieldCheckboxes $f */ $f = $this->wire('modules')->get('InputfieldCheckboxes'); $f->attr('name', 'requirements'); $f->label = $this->_('Password requirements'); foreach($this->getSetting('requirementsLabels') as $value => $label) { $f->addOption($value, $label); } $value = $this->getSetting('requirements'); if(in_array(self::requireNone, $value)) $value = array('none'); $f->attr('value', $value); $f->columnWidth = 50; $inputfields->add($f); /** @var InputfieldRadios $f */ $f = $this->wire('modules')->get('InputfieldRadios'); $f->attr('name', 'complexifyBanMode'); $f->label = $this->_('Word ban mode'); $f->description = $this->_('If you choose the strict mode, many passwords containing words will not be accepted.'); $f->addOption('loose', $this->_('Ban just common passwords (recommended)')); $f->addOption('strict', $this->_('Ban all passwords containing any common words (strict)')); $f->attr('value', $this->complexifyBanMode); $f->columnWidth = 50; $inputfields->add($f); /** @var InputfieldFloat $f */ $f = $this->wire('modules')->get('InputfieldFloat'); $f->attr('name', 'complexifyFactor'); $f->label = $this->_('Complexify factor'); $f->description = $this->_('Lower numbers allow weaker passwords, higher numbers require stronger passwords.'); $f->description .= ' ' . $this->_('Specify -1 to disable this feature.'); $f->notes = $this->_('We recommend something between 0.5 and 1.0'); $f->attr('value', $this->complexifyFactor); $f->columnWidth = 50; $inputfields->add($f); /** @var InputfieldInteger $f */ $f = $this->wire('modules')->get('InputfieldInteger'); $f->attr('name', 'minlength'); $f->label = $this->_('Minimum password length'); $f->attr('value', $this->attr('minlength')); $f->attr('min', 3); $f->columnWidth = 50; $inputfields->add($f); if(!$this->getSetting('hasFieldtype')) { /** @var InputfieldCheckbox $f */ $f = $this->wire('modules')->get('InputfieldCheckbox'); $f->attr('name', 'showPass'); $f->label = $this->_('Allow existing passwords to be shown and/or rendered in form?'); if($this->getSetting("showPass")) $f->attr('checked', 'checked'); $inputfields->add($f); } /** @var InputfieldRadios $f */ $f = $this->wire('modules')->get('InputfieldRadios'); $f->attr('name', 'requireOld'); $f->label = $this->_('Require old password before allowing changes?'); $f->description = $this->_('Applies to usages where this field appears to already logged-in users only.'); $f->addOption(self::requireOldAuto, $this->_('Auto')); $f->addOption(self::requireOldYes, $this->_('Yes')); $f->addOption(self::requireOldNo, $this->_('No')); $f->optionColumns = 1; $f->attr('value', (int) $this->requireOld); $inputfields->add($f); $f = $this->wire()->modules->get('InputfieldRadios'); $f->attr('name', 'unmask'); $f->label = $this->_('Allow user to show/unmask password during changes?'); $f->description = $this->_('Provides a show/hide password control so users can see what they type when in an appropriate environment.'); $f->addOption(1, $this->_('Yes')); $f->addOption(0, $this->_('No')); $f->optionColumns = 1; $f->attr('value', (int) $this->unmask); $inputfields->add($f); return $inputfields; } }