openui5-password
Version:
An OpenUI5 Control which checks password strength and validates it against predefined rules
373 lines (347 loc) • 11.3 kB
JavaScript
'use strict';
/*
* ${copyright}
*/
sap.ui.define([
'sap/m/InputBase',
'sap/m/List',
'sap/m/ResponsivePopover',
'sap/m/StandardListItem',
'sap/ui/core/ValueState',
'openui5/password/PasswordRenderer',
'openui5/password/thirdparty/zxcvbn'
],
/**
* Module Dependencies
* @param {sap.m.InputBase} InputBase InputBase control
* @param {sap.m.List} List List control
* @param {sap.m.ResponsivePopover} ResponsivePopover ResponsivePopover control
* @param {sap.m.StandardListItem} StandardListItem StandardListItem control
* @param {sap.ui.core.ValueState} ValueState ValueState control
* @param {object} PasswordRenderer PasswordRenderer render control
* @returns {object} Password control, an extended UI5 InputBase control
*/
function(InputBase, List, ResponsivePopover, StandardListItem, ValueState, PasswordRenderer) {
/**
* OpenUI5 Password.
* @author Mauricio Lauffer
* @version ${version}
* @class
* @namespace
* @name openui5.password
* @public
* @alias openui5.password.Password
*/
const Password = InputBase.extend('Password', {
metadata: {
library: 'openui5.password',
properties: {
/**
* Indicates that input must contain numbers
*/
requireNumbers: {type: 'boolean', group: 'Behavior', defaultValue: true},
/**
* Indicates that input must contain letters
*/
requireLetters: {type: 'boolean', group: 'Behavior', defaultValue: true},
/**
* Indicates that input must contain symbols
*/
requireSymbols: {type: 'boolean', group: 'Behavior', defaultValue: true},
/**
* Indicates that input must contain lowercase letters
*/
requireLowercase: {type: 'boolean', group: 'Behavior', defaultValue: true},
/**
* Indicates that input must contain uppercase letters
*/
requireUppercase: {type: 'boolean', group: 'Behavior', defaultValue: true},
/**
* Maximum number of characters. Value '0' means the feature is switched off.
*/
maxLength: {type: 'int', group: 'Behavior', defaultValue: 0},
/**
* Minimum number of characters. Value '0' means the feature is switched off.
*/
minLength: {type: 'int', group: 'Behavior', defaultValue: 0},
/**
* The score is a number which indicates the password strength.
*/
score: {type: 'int', group: 'Behavior', defaultValue: 0}
},
aggregations: {
/**
* ErrorItems are the items which will be shown in the error popup. Changing this aggregation (by calling addSuggestionItem, insertSuggestionItem, removeSuggestionItem, removeAllSuggestionItems, destroySuggestionItems) after input is rendered will open/close the suggestion popup.
*/
errorItems: {type: 'sap.m.StandardListItem', multiple: true, singularName: 'errorItem'}
}
},
/**
* Renderer for a new Password.
* @augments sap.m.InputBase
* @param {sap.ui.core.RenderManager} oRm the RenderManager that can be used for writing to the render output buffer
* @param {sap.ui.core.Control} oControl an object representation of the control that should be rendered
* @public
*/
renderer: function(oRm, oControl) {
PasswordRenderer.render(oRm, oControl);
}
});
/**
* Initializes the control
* @private
*/
Password.prototype.init = function() {
InputBase.prototype.init.call(this);
this._resourceBundle = sap.ui.getCore().getLibraryResourceBundle('openui5.password');
};
/**
* Destroys the control
* @private
*/
Password.prototype.exit = function() {
InputBase.prototype.exit.call(this);
if (this._popover) {
this._popover.destroy();
this._popover = null;
}
};
/**
* Handle onInput event
* @param {sap.ui.base.Event} oEvent Event handler
* @public
*/
Password.prototype.oninput = function(oEvent) {
InputBase.prototype.oninput.call(this, oEvent);
if (oEvent.isMarked('invalid')) {
return;
}
const score = this._calculateScore(this._$input.val());
this._setStatus(score);
this.setScore(score);
};
Password.prototype.onfocusin = function() {
InputBase.prototype.onfocusin.apply(this, arguments);
this.$().addClass('sapMInputFocused');
if (this._popover) {
this._popover.close();
}
};
Password.prototype.onfocusout = function() {
InputBase.prototype.onfocusout.apply(this, arguments);
this.$().removeClass('sapMInputFocused');
this.closeValueStateMessage(this);
};
Password.prototype.onsapfocusleave = function() {
this._showPasswordErrors();
InputBase.prototype.onsapfocusleave.apply(this, arguments);
};
/**
* Defines the width of the input. Default value is 100%
* @public
* @param {string} sWidth - CSS rule for width
* @returns {object} sap.m.InputBase
*/
Password.prototype.setWidth = function(sWidth) {
return InputBase.prototype.setWidth.call(this, sWidth || '100%');
};
/**
* Returns the width of the input.
* @public
* @returns {string} The current width or 100% as default
*/
Password.prototype.getWidth = function() {
return this.getProperty('width') || '100%';
};
/**
* Display password errors in a popup.
* @private
*/
Password.prototype._showPasswordErrors = function() {
const errors = this._getPasswordErrors(this._$input.val());
if (errors.length > 0) {
this._getPopover(errors).openBy(this);
this.setValueState(ValueState.Error);
} else {
this.setValueState(ValueState.Success);
}
};
/**
* Returns a Popover object which list errors.
* @private
* @param {Array} errors - Array with all errors to be displayed
* @returns {sap.m.ResponsivePopover} Popover with all errors
*/
Password.prototype._getPopover = function(errors) {
if (!this._popover) {
this._createPopover();
}
this._addPasswordErrorsToPopover(errors || []);
return this._popover;
};
/**
* Creates a new instance of a Popover object.
* @private
*/
Password.prototype._createPopover = function() {
this._popover = new ResponsivePopover(this.getId() + '-popover', {
title: this._resourceBundle.getText('PASSWORD_POPUP_TITLE'),
placement: 'Vertical',
icon: 'sap-icon://alert'
});
this._popover.addContent(new List({}));
this.addDependent(this._popover);
};
/**
* Add messages to the Popover object.
* @private
* @param {Array} errors - Array with all errors to be displayed
*/
Password.prototype._addPasswordErrorsToPopover = function(errors) {
const list = this._popover.getContent()[0];
list.removeAllItems();
list.destroyItems();
errors.forEach(function(item) {
list.addItem(item);
});
};
/**
* Check password rules and returns errors.
* @private
* @param {string} value - The password value to be checked against the rules
* @returns {Array} A list with all errors
*/
Password.prototype._getPasswordErrors = function(value) {
this.destroyAggregation('errorItems');
const that = this;
const errors = [];
let regxp = /\d/;
if (this.getRequireNumbers() && !regxp.test(value)) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_NUMBER'),
info: '[0-9]',
infoState: ValueState.Error
}));
}
regxp = /[a-zA-Z]/;
if (this.getRequireLetters() && !regxp.test(value)) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_LETTER'),
info: '[a-z , A-Z]',
infoState: ValueState.Error
}));
}
regxp = /[a-z]/;
if (this.getRequireLowercase() && !regxp.test(value)) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_LOWERCASE_LETTER'),
info: '[a-z]',
infoState: ValueState.Error
}));
}
regxp = /[A-Z]/;
if (this.getRequireUppercase() && !regxp.test(value)) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_UPPERCASE_LETTER'),
info: '[A-Z]',
infoState: ValueState.Error
}));
}
regxp = /\W/;
if (this.getRequireSymbols() && !regxp.test(value)) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_SYMBOL'),
info: '[!, @, #, $, %, &...]',
infoState: ValueState.Error
}));
}
if (this.getMinLength() > 0 && value.length < this.getMinLength()) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_NOT_LESS'),
info: this._resourceBundle.getText('PASSWORD_LIMIT_CHARACTERS', this.getMinLength()),
infoState: ValueState.Error
}));
}
if (this.getMaxLength() > 0 && value.length > this.getMaxLength()) {
errors.push(new StandardListItem({
title: this._resourceBundle.getText('PASSWORD_MUST_HAVE_NOT_MORE'),
info: this._resourceBundle.getText('PASSWORD_LIMIT_CHARACTERS', this.getMaxLength()),
infoState: ValueState.Error
}));
}
errors.forEach(function(errorItem) {
that.addAggregation('errorItems', errorItem, true);
});
return errors;
};
/**
* Returns the calculated score of the password.
* @private
* @param {string} value - The password value
* @returns {number} The current score or 0 as default
*/
Password.prototype._calculateScore = function(value) {
if (!value || value.length < 1) {
return 0;
}
return zxcvbn(value).score || 0;
};
/**
* Defines status according to a given score.
* @private
* @param {number} score - The score value
*/
Password.prototype._setStatus = function(score) {
const status = this._getStatus(score);
this.setValueState(status.state);
this.setValueStateText(status.text);
};
/**
* Returns the current status.
* @private
* @param {number} score - The score value
* @returns {object} The status object
*/
Password.prototype._getStatus = function(score) {
let status = {
state: ValueState.None,
text: ''
};
switch (score) {
case 0:
status = {
state: ValueState.Error,
text: this._resourceBundle.getText('PASSWORD_IS_VERY_WEAK')
};
break;
case 1:
status = {
state: ValueState.Error,
text: this._resourceBundle.getText('PASSWORD_IS_WEAK')
};
break;
case 2:
status = {
state: ValueState.Warning,
text: this._resourceBundle.getText('PASSWORD_IS_NOT_STRONG_ENOUGH')
};
break;
case 3:
status = {
state: ValueState.Success,
text: this._resourceBundle.getText('PASSWORD_IS_STRONG')
};
break;
case 4:
status = {
state: ValueState.Success,
text: this._resourceBundle.getText('PASSWORD_IS_VERY_STRONG')
};
break;
default:
break;
}
return status;
};
return Password;
});