/** * EGroupware eTemplate2 - JS Textbox object * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @link https://www.egroupware.org * @author Andreas Stöckel */ /*egw:uses /vendor/bower-asset/jquery/dist/jquery.js; et2_core_inputWidget; et2_core_valueWidget; */ import './et2_core_common'; import {ClassWithAttributes} from "./et2_core_inheritance"; import {et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; import {et2_inputWidget} from './et2_core_inputWidget' import {et2_button} from './et2_widget_button' import {et2_textbox, et2_textbox_ro} from "./et2_widget_textbox"; import {egw} from "../jsapi/egw_global"; import {Et2Dialog} from "./Et2Dialog/Et2Dialog"; /** * Class which implements the "textbox" XET-Tag * * @augments et2_inputWidget */ export class et2_password extends et2_textbox { static readonly _attributes : any = { "autocomplete": { "name": "Autocomplete", "type": "string", "default": "off", "description": "Whether or not browser should autocomplete that field: 'on', 'off', 'default' (use attribute from form). Default value is set to off." }, "viewable": { "name": "Viewable", "type": "boolean", "default": false, "description": "Allow password to be shown" }, "plaintext": { name: "Plaintext", type: "boolean", default: true, description: "Password is plaintext" }, "suggest": { name: "Suggest password", type: "integer", default: 0, description: "Suggest password length (0 for off)" } }; public static readonly DEFAULT_LENGTH = 16; wrapper : JQuery; private suggest_button: et2_button; private show_button: et2_button; // The password is stored encrypted server side, and passed encrypted. // This flag is for if we've decrypted the password to show it already private encrypted : boolean = true; /** * Constructor */ constructor(_parent, _attrs? : WidgetConfig, _child? : object) { // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_password._attributes, _child || {})); if(this.options.plaintext) { this.encrypted = false; } } createInputWidget() { this.wrapper = jQuery(document.createElement("div")) .addClass("et2_password"); this.input = jQuery(document.createElement("input")) this.input.attr("type", "password"); // Make autocomplete default value off for password field // seems browsers not respecting 'off' anymore and started to // implement a new key called "new-password" considered as switching // autocomplete off. // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion if (this.options.autocomplete === "" || this.options.autocomplete == "off") this.options.autocomplete = "new-password"; if(this.options.size) { this.set_size(this.options.size); } if(this.options.blur) { this.set_blur(this.options.blur); } if(this.options.readonly) { this.set_readonly(true); } this.input.addClass("et2_textbox") .appendTo(this.wrapper); this.setDOMNode(this.wrapper[0]); if(this.options.value) { this.set_value(this.options.value); } if (this.options.onkeypress && typeof this.options.onkeypress == 'function') { var self = this; this.input.on('keypress', function(_ev) { return self.options.onkeypress.call(this, _ev, self); }); } this.input.on('change', function() { this.encrypted = false; }.bind(this)); // Show button is needed from start as you can't turn viewable on via JS let attrs = { class: "show_hide", image: "visibility", onclick: this.toggle_visibility.bind(this), statustext: this.egw().lang("Show password") }; if(this.options.viewable) { this.show_button = <et2_button>et2_createWidget("button", attrs, this); } } getInputNode() { return this.input[0]; } /** * Override the parent set_id method to manuipulate the input DOM node * * @param {type} _value * @returns {undefined} */ set_id(_value) { super.set_id(_value); // Remove the name attribute inorder to affect autocomplete="off" // for no password save. ATM seems all browsers ignore autocomplete for // input field inside the form if (this.options.autocomplete === "off") this.input.removeAttr('name'); } /** * Set whether or not the password is allowed to be shown in clear text. * * @param viewable */ set_viewable(viewable: boolean) { this.options.viewable = viewable; if(viewable) { jQuery('.show_hide', this.wrapper).show(); } else { jQuery('.show_hide', this.wrapper).hide(); } } /** * Turn on or off the suggest password button. * * When clicked, a password of the set length will be generated. * * @param length Length of password to generate. 0 to disable. */ set_suggest(length: number) { if(typeof length !== "number") { length = typeof length === "string" ? parseInt(length) : (length ? et2_password.DEFAULT_LENGTH : 0); } this.options.suggest = length; if(length && !this.suggest_button) { let attrs = { class: "generate_password", image: "generate_password", onclick: this.suggest_password.bind(this), statustext: this.egw().lang("Suggest password") }; this.suggest_button = <et2_button> et2_createWidget("button", attrs, this); if(this.parentNode) { // Turned on after initial load, need to run loadingFinished() this.suggest_button.loadingFinished(); } } if(length) { jQuery('.generate_password', this.wrapper).show(); } else { jQuery('.generate_password', this.wrapper).hide(); } } /** * If the password is viewable, toggle the visibility. * If the password is still encrypted, we'll ask for the user's password then have the server decrypt it. * * @param on */ toggle_visibility(on : boolean | undefined) { if(typeof on !== "boolean") { on = this.input.attr("type") == "password"; } if(!this.options.viewable) { this.input.attr("type", "password"); return; } if(this.show_button) { this.show_button.set_image(this.egw().image(on ? 'visibility_off' : 'visibility')); } // If we are not encrypted or not showing it, we're done if(!this.encrypted || !on) { this.input.attr("type",on ? "textbox" : "password"); return; } // Need username & password to decrypt let callback = function(button, user_password) { if(button == Et2Dialog.CANCEL_BUTTON) { return this.toggle_visibility(false); } let request = egw.json( "EGroupware\\Api\\Etemplate\\Widget\\Password::ajax_decrypt", [user_password, this.options.value], function(decrypted) { if(decrypted) { this.encrypted = false; this.input.val(decrypted); this.input.attr("type", "textbox"); } else { this.set_validation_error(this.egw().lang("invalid password")); window.setTimeout(function() { this.set_validation_error(false); }.bind(this), 2000); } }, this, true, this ).sendRequest(); }.bind(this); let prompt = Et2Dialog.show_prompt( callback, this.egw().lang("Enter your password"), this.egw().lang("Authenticate") ); // Make the password prompt a password field prompt.div.on("load", function() { jQuery(prompt.template.widgetContainer.getWidgetById('value').getInputNode()) .attr("type","password"); }); } /** * Ask the server for a password suggestion */ suggest_password() { // They need to see the suggestion this.encrypted = false; this.options.viewable = true; this.toggle_visibility(true); let suggestion = "Suggestion"; let request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Password::ajax_suggest", [this.options.suggest], function(suggestion) { this.encrypted = false; this.input.val(suggestion); this.input.trigger('change'); // Check for second password, update it too let two = this.getParent().getWidgetById(this.id+'_2'); if(two && two.getType() == this.getType()) { two.options.viewable = true; two.toggle_visibility(true); two.set_value(suggestion); } }, this,true,this ).sendRequest(); } destroy() { super.destroy(); } getValue() { return this.input.val(); } } et2_register_widget(et2_password, [ "passwd"]); export class et2_password_ro extends et2_textbox_ro { set_value(value) { this.value_span.text(value ? "********" : ""); } } et2_register_widget(et2_password_ro, [ "passwd_ro"]);