Files
tubestation/browser/extensions/formautofill/content/editDialog.js
Ian Moody a54a2601c7 Bug 1432070 - Block context menu in Autofill preference dialogs, except on text fields. r=MattN,scottwu
The context menu shown is the standard web page one, and so inappropriate for
inside preferences.  Keep it on text fields so text editing commands are still
available, though unfortunately so are "This Frame", "Inspect Element", and
"Add a Keyword for this Search".

MozReview-Commit-ID: 7L012CZ4BhR
2018-01-21 22:50:07 +00:00

361 lines
9.9 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* exported EditAddress, EditCreditCard */
"use strict";
const AUTOFILL_BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties";
const REGIONS_BUNDLE_URI = "chrome://global/locale/regionNames.properties";
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
ChromeUtils.defineModuleGetter(this, "profileStorage",
"resource://formautofill/FormAutofillStorage.jsm");
ChromeUtils.defineModuleGetter(this, "MasterPassword",
"resource://formautofill/MasterPassword.jsm");
class EditDialog {
constructor(subStorageName, elements, record) {
this._storageInitPromise = profileStorage.initialize();
this._subStorageName = subStorageName;
this._elements = elements;
this._record = record;
this.localizeDocument();
window.addEventListener("DOMContentLoaded", this, {once: true});
}
async init() {
if (this._record) {
await this.loadInitialValues(this._record);
}
this.attachEventListeners();
// For testing only: loadInitialValues for credit card is an async method, and tests
// need to wait until the values have been filled before editing the fields.
window.dispatchEvent(new CustomEvent("FormReady"));
}
uninit() {
this.detachEventListeners();
this._elements = null;
}
/**
* Fill the form with a record object.
* @param {object} record
*/
loadInitialValues(record) {
for (let field in record) {
let input = document.getElementById(field);
if (input) {
input.value = record[field];
}
}
}
/**
* Get inputs from the form.
* @returns {object}
*/
buildFormObject() {
return Array.from(document.forms[0].elements).reduce((obj, input) => {
if (input.value) {
obj[input.id] = input.value;
}
return obj;
}, {});
}
/**
* Get storage and ensure it has been initialized.
* @returns {object}
*/
async getStorage() {
await this._storageInitPromise;
return profileStorage[this._subStorageName];
}
/**
* Asks FormAutofillParent to save or update an record.
* @param {object} record
* @param {string} guid [optional]
*/
async saveRecord(record, guid) {
let storage = await this.getStorage();
if (guid) {
storage.update(guid, record);
} else {
storage.add(record);
}
}
/**
* Handle events
*
* @param {DOMEvent} event
*/
handleEvent(event) {
switch (event.type) {
case "DOMContentLoaded": {
this.init();
break;
}
case "unload": {
this.uninit();
break;
}
case "click": {
this.handleClick(event);
break;
}
case "input": {
this.handleInput(event);
break;
}
case "keypress": {
this.handleKeyPress(event);
break;
}
case "change": {
this.handleChange(event);
break;
}
case "contextmenu": {
if (!(event.target instanceof HTMLInputElement) &&
!(event.target instanceof HTMLTextAreaElement)) {
event.preventDefault();
}
break;
}
}
}
/**
* Handle click events
*
* @param {DOMEvent} event
*/
handleClick(event) {
if (event.target == this._elements.cancel) {
window.close();
}
if (event.target == this._elements.save) {
this.handleSubmit();
}
}
/**
* Handle input events
*
* @param {DOMEvent} event
*/
handleInput(event) {
// Toggle disabled attribute on the save button based on
// whether the form is filled or empty.
if (Object.keys(this.buildFormObject()).length == 0) {
this._elements.save.setAttribute("disabled", true);
} else {
this._elements.save.removeAttribute("disabled");
}
}
/**
* Handle key press events
*
* @param {DOMEvent} event
*/
handleKeyPress(event) {
if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
window.close();
}
}
/**
* Attach event listener
*/
attachEventListeners() {
window.addEventListener("keypress", this);
window.addEventListener("contextmenu", this);
this._elements.controlsContainer.addEventListener("click", this);
document.addEventListener("input", this);
}
/**
* Remove event listener
*/
detachEventListeners() {
window.removeEventListener("keypress", this);
window.removeEventListener("contextmenu", this);
this._elements.controlsContainer.removeEventListener("click", this);
document.removeEventListener("input", this);
}
// An interface to be inherited.
localizeDocument() {}
// An interface to be inherited.
handleSubmit(event) {}
// An interface to be inherited.
handleChange(event) {}
}
class EditAddress extends EditDialog {
constructor(elements, record) {
let country = record ? record.country :
FormAutofillUtils.supportedCountries.find(supported => supported == FormAutofillUtils.DEFAULT_REGION);
super("addresses", elements, record || {country});
this.formatForm(country);
}
/**
* Format the form based on country. The address-level1 and postal-code labels
* should be specific to the given country.
* @param {string} country
*/
formatForm(country) {
const {addressLevel1Label, postalCodeLabel, fieldsOrder} = FormAutofillUtils.getFormFormat(country);
this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
this.arrangeFields(fieldsOrder);
}
arrangeFields(fieldsOrder) {
let fields = [
"name",
"organization",
"street-address",
"address-level2",
"address-level1",
"postal-code",
];
let inputs = [];
for (let i = 0; i < fieldsOrder.length; i++) {
let {fieldId, newLine} = fieldsOrder[i];
let container = document.getElementById(`${fieldId}-container`);
inputs.push(...container.querySelectorAll("input, textarea, select"));
container.style.display = "flex";
container.style.order = i;
container.style.pageBreakAfter = newLine ? "always" : "auto";
// Remove the field from the list of fields
fields.splice(fields.indexOf(fieldId), 1);
}
for (let i = 0; i < inputs.length; i++) {
// Assign tabIndex starting from 1
inputs[i].tabIndex = i + 1;
}
// Hide the remaining fields
for (let field of fields) {
let container = document.getElementById(`${field}-container`);
container.style.display = "none";
}
}
localizeDocument() {
if (this._record) {
this._elements.title.dataset.localization = "editAddressTitle";
}
let fragment = document.createDocumentFragment();
for (let country of FormAutofillUtils.supportedCountries) {
let option = new Option();
option.value = country;
option.dataset.localization = country.toLowerCase();
fragment.appendChild(option);
}
this._elements.country.appendChild(fragment);
FormAutofillUtils.localizeMarkup(REGIONS_BUNDLE_URI, this._elements.country);
}
async handleSubmit() {
await this.saveRecord(this.buildFormObject(), this._record ? this._record.guid : null);
window.close();
}
handleChange(event) {
this.formatForm(event.target.value);
}
attachEventListeners() {
this._elements.country.addEventListener("change", this);
super.attachEventListeners();
}
detachEventListeners() {
this._elements.country.removeEventListener("change", this);
super.detachEventListeners();
}
}
class EditCreditCard extends EditDialog {
constructor(elements, record) {
super("creditCards", elements, record);
this.generateYears();
}
generateYears() {
const count = 11;
const currentYear = new Date().getFullYear();
const ccExpYear = this._record && this._record["cc-exp-year"];
if (ccExpYear && ccExpYear < currentYear) {
this._elements.year.appendChild(new Option(ccExpYear));
}
for (let i = 0; i < count; i++) {
let year = currentYear + i;
let option = new Option(year);
this._elements.year.appendChild(option);
}
if (ccExpYear && ccExpYear > currentYear + count) {
this._elements.year.appendChild(new Option(ccExpYear));
}
}
localizeDocument() {
if (this._record) {
this._elements.title.dataset.localization = "editCreditCardTitle";
}
FormAutofillUtils.localizeMarkup(AUTOFILL_BUNDLE_URI, document);
}
/**
* Decrypt cc-number first and fill the form.
* @param {object} creditCard
*/
async loadInitialValues(creditCard) {
let decryptedCC = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
super.loadInitialValues(Object.assign({}, creditCard, {"cc-number": decryptedCC}));
}
async handleSubmit() {
let creditCard = this.buildFormObject();
// Show error on the cc-number field if it's empty or invalid
if (!FormAutofillUtils.isCCNumber(creditCard["cc-number"])) {
this._elements.ccNumber.setCustomValidity(true);
return;
}
// TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
// APIs are refactored to be async functions (bug 1399367).
if (await MasterPassword.ensureLoggedIn()) {
await this.saveRecord(creditCard, this._record ? this._record.guid : null);
}
window.close();
}
handleInput(event) {
// Clear the error message if cc-number is valid
if (event.target == this._elements.ccNumber &&
FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)) {
this._elements.ccNumber.setCustomValidity("");
}
super.handleInput(event);
}
}