diff --git a/CHANGELOG.md b/CHANGELOG.md index bb351c5..a66252f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,9 @@ ## 1.1 (19th Dec 2013) - - Bug fixes for LDAP plugin \ No newline at end of file + - Bug fixes for LDAP plugin + +## 1.2 (20th May 2015) + + - Bug fixes for LDAP plugin + - Improved UI \ No newline at end of file diff --git a/builds/passwd-1.2.zip b/builds/passwd-1.2.zip new file mode 100644 index 0000000..1706ae5 Binary files /dev/null and b/builds/passwd-1.2.zip differ diff --git a/js/external/PasswordMeter.js b/js/external/PasswordMeter.js new file mode 100644 index 0000000..b8ce416 --- /dev/null +++ b/js/external/PasswordMeter.js @@ -0,0 +1,191 @@ +Ext.namespace('Ext.ux.form.field'); + +/** + * @class Ext.ux.form.field.PasswordMeter + * @extends Ext.form.TextField + * @xtype ux.passwordmeterfield + * + * @author Christoph Haas + * @version 0.1 + * @license MIT License: http://www.opensource.org/licenses/mit-license.php + * + * This implementation of the password fields shows a nice graph of how + * secure the password is. + * Original implementation for ExtJS 1.1 from http://testcases.pagebakers.com/PasswordMeter/. + */ +Ext.ux.form.field.PasswordMeter = Ext.extend(Ext.form.TextField, { + + /** + * @constructor + * @param {Object} config configuration object + */ + constructor : function(config) + { + config = config || {}; + + Ext.applyIf(config, { + xtype : 'ux.passwordmeterfield', + inputType: 'password', + enableKeyEvents: true + }); + + Ext.ux.form.field.PasswordMeter.superclass.constructor.call(this, config); + }, + + // private + initComponent:function() + { + Ext.ux.form.field.PasswordMeter.superclass.initComponent.apply(this, arguments); + }, + + // private + reset: function() + { + Ext.ux.form.field.PasswordMeter.superclass.reset.call(this); + this.updateMeter(); + }, + + // private + onKeyUp : function(event) + { + Ext.ux.form.field.PasswordMeter.superclass.onKeyUp.call(this); + + this.updateMeter(this.getValue()); + + this.fireEvent('keyup', this, event); + }, + + // private + afterRender: function() + { + Ext.ux.form.field.PasswordMeter.superclass.afterRender.call(this); + + + var width = this.getEl().getWidth(); + this.strengthMeterID = newID = Ext.id(); + this.scoreBarID = Ext.id(); + var objMeter = Ext.DomHelper.insertAfter(this.getEl(), { + tag: "div", + 'class': "x-form-strengthmeter", + 'id': this.strengthMeterID, + 'style' : { + width: width + 'px' + } + }); + Ext.DomHelper.append(objMeter, { + tag: "div", + 'class': "x-form-strengthmeter-scorebar", + 'id': this.scoreBarID + }); + + this.fireEvent('afterrender', this); + }, + + /** + * Return the score of the entered password. + * It is a number between 0 and 100 where 100 is a very safe password. + * + * @returns {Number} + */ + getScore : function() + { + return this.calcStrength(this.getValue()); + }, + + /** + * Sets the width of the meter, based on the score + * + * @param {String} val The current password + */ + updateMeter : function(val) + { + var maxWidth, score, scoreWidth, objMeter, scoreBar; + + objMeter = Ext.get(this.strengthMeterID); + scoreBar = Ext.get(this.scoreBarID); + + maxWidth = objMeter.getWidth(); + if (val){ + score = this.calcStrength(val); + scoreWidth = maxWidth - (maxWidth / 100) * score; + scoreBar.applyStyles({margin: "0 0 0 " + (maxWidth - scoreWidth) + "px"}); // move the overlay to the right + scoreBar.setWidth(scoreWidth, false); // downsize the overlay + } else { + scoreBar.applyStyles({margin: "0"}); + scoreBar.setWidth(maxWidth, false); + } + }, + + /** + * Calculates the strength of a password + * + * @param {Object} p The password that needs to be calculated + * @return {int} intScore The strength score of the password + */ + calcStrength: function(p) + { + // PASSWORD LENGTH + var len = p.length, score = len; + + if (len > 0 && len <= 4) { // length 4 or + // less + score += len + } else if (len >= 5 && len <= 7) { + // length between 5 and 7 + score += 6; + } else if (len >= 8 && len <= 15) { + // length between 8 and 15 + score += 12; + } else if (len >= 16) { // length 16 or more + score += 18; + } + + // LETTERS (Not exactly implemented as dictacted above + // because of my limited understanding of Regex) + if (p.match(/[a-z]/)) { + // [verified] at least one lower case letter + score += 1; + } + if (p.match(/[A-Z]/)) { // [verified] at least one upper + // case letter + score += 5; + } + // NUMBERS + if (p.match(/\d/)) { // [verified] at least one + // number + score += 5; + } + if (p.match(/(?:.*?\d){3}/)) { + // [verified] at least three numbers + score += 5; + } + + // SPECIAL CHAR + if (p.match(/[\!,@,#,$,%,\^,&,\*,\?,_,~]/)) { + // [verified] at least one special character + score += 5; + } + // [verified] at least two special characters + if (p.match(/(?:.*?[\!,@,#,$,%,\^,&,\*,\?,_,~]){2}/)) { + score += 5; + } + + // COMBOS + if (p.match(/(?=.*[a-z])(?=.*[A-Z])/)) { + // [verified] both upper and lower case + score += 2; + } + if (p.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)) { + // [verified] both letters and numbers + score += 2; + } + // [verified] letters, numbers, and special characters + if (p.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\!,@,#,$,%,\^,&,\*,\?,_,~])/)) { + score += 2; + } + + return Math.min(Math.round(score * 2), 100); + } +}); + +Ext.reg('ux.passwordmeterfield', Ext.ux.form.field.PasswordMeter); \ No newline at end of file diff --git a/js/settings/PasswdPanel.js b/js/settings/PasswdPanel.js index 6083906..730d695 100644 --- a/js/settings/PasswdPanel.js +++ b/js/settings/PasswdPanel.js @@ -30,26 +30,30 @@ Zarafa.plugins.passwd.settings.PasswdPanel = Ext.extend(Ext.form.FormPanel, { }, { xtype : 'textfield', name : 'current_password', + ref : 'current_password', fieldLabel : dgettext("plugin_passwd", 'Current password'), inputType : 'password', + allowBlank : false, listeners : { change : this.onFieldChange, scope : this } }, { - xtype : 'textfield', + xtype : 'ux.passwordmeterfield', name : 'new_password', + ref : 'new_password', + allowBlank : false, fieldLabel : dgettext("plugin_passwd", 'New password'), - inputType : 'password', listeners : { change : this.onFieldChange, scope : this } }, { - xtype : 'textfield', + xtype : 'ux.passwordmeterfield', name : 'new_password_repeat', + allowBlank : false, + ref : 'new_password_repeat', fieldLabel : dgettext("plugin_passwd", 'Retype new password'), - inputType : 'password', listeners : { change : this.onFieldChange, scope : this diff --git a/js/settings/SettingsPasswdCategory.js b/js/settings/SettingsPasswdCategory.js index 6cd21d0..fd73c68 100644 --- a/js/settings/SettingsPasswdCategory.js +++ b/js/settings/SettingsPasswdCategory.js @@ -19,11 +19,14 @@ Zarafa.plugins.passwd.settings.SettingsPasswdCategory = Ext.extend(Zarafa.settin Ext.applyIf(config, { title : dgettext("plugin_passwd", 'Change Password'), categoryIndex : 9997, + iconCls : 'zarafa-settings-category-passwd', xtype : 'zarafa.settingspasswdcategory', items : [{ xtype : 'zarafa.settingspasswdwidget', settingsContext : config.settingsContext - }] + }, + container.populateInsertionPoint('context.settings.category.passwd', this) + ] }); Zarafa.plugins.passwd.settings.SettingsPasswdCategory.superclass.constructor.call(this, config); diff --git a/js/settings/SettingsPasswdWidget.js b/js/settings/SettingsPasswdWidget.js index c8b473b..59a93b6 100644 --- a/js/settings/SettingsPasswdWidget.js +++ b/js/settings/SettingsPasswdWidget.js @@ -19,14 +19,9 @@ Zarafa.plugins.passwd.settings.SettingsPasswdWidget = Ext.extend(Zarafa.settings config = config || {}; Ext.applyIf(config, { - height : 175, - width : 400, title : dgettext("plugin_passwd", 'Change Password'), xtype : 'zarafa.settingspasswdwidget', - layout : { - // override from SettingsWidget - type : 'fit' - }, + layout: 'form', items : [{ xtype : 'zarafa.passwdpanel', ref : 'passwdPanel', @@ -51,10 +46,42 @@ Zarafa.plugins.passwd.settings.SettingsPasswdWidget = Ext.extend(Zarafa.settings // listen to savesettings and discardsettings to save/discard delegation data var contextModel = this.settingsContext.getModel(); + this.mon(contextModel, 'beforesavesettings', this.onBeforeSaveSettings, this); this.mon(contextModel, 'savesettings', this.onSaveSettings, this); this.mon(contextModel, 'discardsettings', this.onDiscardSettings, this); }, + /** + * Event handler will be called when {@link Zarafa.settings.SettingsContextModel#beforesavesettings} event is fired. + * This function will validate the formdata. + * + * @private + */ + onBeforeSaveSettings : function() + { + // do some quick checks before submitting + if(this.passwdPanel.new_password.getValue() != this.passwdPanel.new_password_repeat.getValue()) { + Ext.MessageBox.alert(dgettext("plugin_passwd", 'Error'), dgettext("plugin_passwd", 'New passwords do not match.')); + return false; + } else if(Ext.isEmpty(this.passwdPanel.current_password.getValue())) { + Ext.MessageBox.alert(dgettext("plugin_passwd", 'Error'), dgettext("plugin_passwd", 'Current password is empty.')); + return false; + } else if(Ext.isEmpty(this.passwdPanel.new_password.getValue()) || Ext.isEmpty(this.passwdPanel.new_password_repeat.getValue())) { + Ext.MessageBox.alert(dgettext("plugin_passwd", 'Error'), dgettext("plugin_passwd", 'New password is empty.')); + return false; + } else if(!this.passwdPanel.getForm().isValid()) { + Ext.MessageBox.alert(dgettext("plugin_passwd", 'Error'), dgettext("plugin_passwd", 'One or more fields does contain errors.')); + return false; + } else { + // do a quick score check: + if(this.passwdPanel.new_password.getScore() < 70) { + Ext.MessageBox.alert(dgettext("plugin_passwd", 'Error'), dgettext("plugin_passwd", 'Password is weak. Password should contain capital, non-capital letters and numbers. Password should have 8 to 20 characters.')); + return false; + } + return true; + } + }, + /** * Event handler will be called when {@link Zarafa.settings.SettingsContextModel#savesettings} event is fired. * This will relay this event to {@link Zarafa.plugins.passwd.settings.PasswdPanel PasswdPanel} so it can @@ -71,8 +98,11 @@ Zarafa.plugins.passwd.settings.SettingsPasswdWidget = Ext.extend(Zarafa.settings // send request container.getRequest().singleRequest('passwdmodule', 'save', data, new Zarafa.plugins.passwd.data.PasswdResponseHandler({ - callbackFn : function(success, response) { + callbackFn: function (success, response) { this.ownerCt.hideSavingMask(success); + if(success) { + this.passwdPanel.getForm().reset(); + } }, scope : this })); diff --git a/lib/PasswordMeter.js b/lib/PasswordMeter.js deleted file mode 100644 index 7406beb..0000000 --- a/lib/PasswordMeter.js +++ /dev/null @@ -1,158 +0,0 @@ -// Create user extensions namespace (Ext.ux) -Ext.namespace('Ext.ux'); - -/** - * Ext.ux.PasswordMeter Extension Class - * - * @author Eelco Wiersma - * @version 0.2 - * - * Algorithm based on code of Tane - * http://digitalspaghetti.me.uk/index.php?q=jquery-pstrength - * and Steve Moitozo - * http://www.geekwisdom.com/dyn/passwdmeter - * - * @license MIT License: http://www.opensource.org/licenses/mit-license.php - * - * @class Ext.ux.PasswordMeter - * @extends Ext.form.TextField - * @constructor - * Creates new Ext.ux.PasswordMeter - */ -Ext.ux.PasswordMeter = function(config) { - - // call parent constructor - Ext.ux.PasswordMeter.superclass.constructor.call(this, config); - -}; - -Ext.extend(Ext.ux.PasswordMeter, Ext.form.TextField, { - /** - * @cfg {String} inputType The type attribute for input fields -- e.g. text, password (defaults to "password"). - */ - inputType: 'text', - // private - onRender: function(ct, position) { - Ext.ux.PasswordMeter.superclass.onRender.call(this, ct, position); - - var elp = this.el.findParent('.x-form-element', 5, true); - this.objMeter = ct.createChild({tag: "div", 'class': "strengthMeter"}); - - this.objMeter.setWidth(elp.getWidth(true)-17); - this.scoreBar = this.objMeter.createChild({tag: "div", 'class': "scoreBar"}); - - if(Ext.isIE && !Ext.isIE7) { // Fix style for IE6 - this.objMeter.setStyle('margin-left', '3px'); - } - }, - // private - initEvents: function() { - Ext.ux.PasswordMeter.superclass.initEvents.call(this); - - this.el.on('keyup', this.updateMeter, this); - }, - /** - * Sets the width of the meter, based on the score - * @param {Object} e - * Private function - */ - updateMeter: function(e) { - var score = 0 - var p = e.target.value; - - var maxWidth = this.objMeter.getWidth() - 2; - - var nScore = this.calcStrength(p); - - // Set new width - var nRound = Math.round(nScore * 2); - - if (nRound > 100) { - nRound = 100; - } - - var scoreWidth = (maxWidth / 100) * nRound; - this.scoreBar.setWidth(scoreWidth, true); - }, - /** - * Calculates the strength of a password - * @param {Object} p The password that needs to be calculated - * @return {int} intScore The strength score of the password - */ - calcStrength: function(p) { - var intScore = 0; - - // PASSWORD LENGTH - intScore += p.length; - - if(p.length > 0 && p.length <= 4) { // length 4 or less - intScore += p.length; - } - else if (p.length >= 5 && p.length <= 7) { // length between 5 and 7 - intScore += 6; - } - else if (p.length >= 8 && p.length <= 15) { // length between 8 and 15 - intScore += 12; - //alert(intScore); - } - else if (p.length >= 16) { // length 16 or more - intScore += 18; - //alert(intScore); - } - - // LETTERS (Not exactly implemented as dictacted above because of my limited understanding of Regex) - if (p.match(/[a-z]/)) { // [verified] at least one lower case letter - intScore += 1; - } - if (p.match(/[A-Z]/)) { // [verified] at least one upper case letter - intScore += 5; - } - // NUMBERS - if (p.match(/\d/)) { // [verified] at least one number - intScore += 5; - } - if (p.match(/.*\d.*\d.*\d/)) { // [verified] at least three numbers - intScore += 5; - } - - // SPECIAL CHAR - if (p.match(/[!,@,#,$,%,^,&,*,?,_,~]/)) { // [verified] at least one special character - intScore += 5; - } - // [verified] at least two special characters - if (p.match(/.*[!,@,#,$,%,^,&,*,?,_,~].*[!,@,#,$,%,^,&,*,?,_,~]/)) { - intScore += 5; - } - - // COMBOS - if (p.match(/(?=.*[a-z])(?=.*[A-Z])/)) { // [verified] both upper and lower case - intScore += 2; - } - if (p.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)) { // [verified] both letters and numbers - intScore += 2; - } - // [verified] letters, numbers, and special characters - if (p.match(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!,@,#,$,%,^,&,*,?,_,~])/)) { - intScore += 2; - } - - return intScore; - - }, - // private - onFocus: function() { - Ext.ux.PasswordMeter.superclass.onFocus.call(this); - - if(!Ext.isOpera) { // don't touch in Opera - this.objMeter.addClass('strengthMeter-focus'); - } - }, - // private - onBlur: function() { - Ext.ux.PasswordMeter.superclass.onBlur.call(this); - - if(!Ext.isOpera) { // don't touch in Opera - this.objMeter.removeClass('strengthMeter-focus'); - } - } -}); \ No newline at end of file diff --git a/manifest.xml b/manifest.xml index 56d418c..c9e6440 100644 --- a/manifest.xml +++ b/manifest.xml @@ -2,7 +2,7 @@ - 1.0 + 1.2 Passwd Password Change Plugin Saket Patel @@ -18,19 +18,6 @@ - @@ -40,13 +27,20 @@ js/passwd.js js/passwd-debug.js + js/PasswdPlugin.js js/settings/SettingsPasswdCategory.js js/settings/SettingsPasswdWidget.js js/settings/PasswdPanel.js js/data/PasswdResponseHandler.js js/ABOUT.js + js/external/PasswordMeter.js + + resources/css/passwd.css + resources/css/passwd.css + resources/css/passwd-main.css + diff --git a/resources/css/passwd-main.css b/resources/css/passwd-main.css new file mode 100644 index 0000000..7091cd7 --- /dev/null +++ b/resources/css/passwd-main.css @@ -0,0 +1,27 @@ +.zarafa-settings-category-passwd { + background-image: url(../images/icon_lock.png) !important; +} + +/* STYLES FOR THE PASSWORD METER */ +.x-form-strengthmeter { + width : 100%; + height: 5px; + /*float: right;*/ + background: #ff4c00; /* Old browsers */ + /* IE9 SVG, needs conditional override of 'filter' to 'none' */ + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmNGMwMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjUwJSIgc3RvcC1jb2xvcj0iI2VkZDcxMiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMxYWZmMDAiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); + background: -moz-linear-gradient(left, #ff4c00 0%, #edd712 50%, #1aff00 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, right top, color-stop(0%,#ff4c00), color-stop(50%,#edd712), color-stop(100%,#1aff00)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(left, #ff4c00 0%,#edd712 50%,#1aff00 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(left, #ff4c00 0%,#edd712 50%,#1aff00 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(left, #ff4c00 0%,#edd712 50%,#1aff00 100%); /* IE10+ */ + background: linear-gradient(left, #ff4c00 0%,#edd712 50%,#1aff00 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff4c00', endColorstr='#1aff00',GradientType=1 ); /* IE6-8 */ +} + +.x-form-strengthmeter-scorebar { + background-color: white; + opacity: 0.7; + height: 5px; + width: 100%; +} \ No newline at end of file diff --git a/resources/images/icon_lock.png b/resources/images/icon_lock.png new file mode 100644 index 0000000..be4689e Binary files /dev/null and b/resources/images/icon_lock.png differ diff --git a/resources/images/icon_lock.xcf b/resources/images/icon_lock.xcf new file mode 100644 index 0000000..38f1853 Binary files /dev/null and b/resources/images/icon_lock.xcf differ