Merge inbound to mozilla-central. a=merge
Merge inbound to mozilla-central. a=merge
--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -16,18 +16,18 @@ const paymentUISrv = Cc["@mozilla.org/do
.getService(Ci.nsIPaymentUIService);
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
"resource:///modules/BrowserWindowTracker.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
- "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+ "resource://formautofill/OSKeyStore.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => {
let storage;
try {
storage = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {})
.formAutofillStorage;
@@ -150,30 +150,30 @@ var paymentDialogWrapper = {
});
return address;
},
/**
* @param {string} guid The GUID of the basic card record from storage.
* @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
- * @throws if the user cancels entering their master password or an error decrypting
+ * @throws If there is an error decrypting
* @returns {nsIBasicCardResponseData?} returns response data or null (if the
* master password dialog was cancelled);
*/
async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
let cardData = this.temporaryStore.creditCards.get(guid) ||
await formAutofillStorage.creditCards.get(guid);
if (!cardData) {
throw new Error(`Basic card not found in storage: ${guid}`);
}
let cardNumber;
try {
- cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
+ cardNumber = await OSKeyStore.decrypt(cardData["cc-number-encrypted"], true);
} catch (ex) {
if (ex.result != Cr.NS_ERROR_ABORT) {
throw ex;
}
// User canceled master password entry
return null;
}
@@ -502,18 +502,26 @@ var paymentDialogWrapper = {
},
async onPay({
selectedPayerAddressGUID: payerGUID,
selectedPaymentCardGUID: paymentCardGUID,
selectedPaymentCardSecurityCode: cardSecurityCode,
selectedShippingAddressGUID: shippingGUID,
}) {
- let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
- cardSecurityCode);
+ let methodData;
+ try {
+ methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
+ cardSecurityCode);
+ } catch (ex) {
+ // TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user
+ // to re-enter credit card # from management UI.
+ Cu.reportError(ex);
+ return;
+ }
if (!methodData) {
// TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
// Master Password dialog.
Cu.reportError("Bug 1429265/Bug 1429205: User canceled master password entry");
return;
}
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -322,17 +322,17 @@ let DUPED_ADDRESSES = {
};
let BASIC_CARDS_1 = {
"53f9d009aed2": {
billingAddressGUID: "68gjdh354j",
methodName: "basic-card",
"cc-number": "************5461",
"guid": "53f9d009aed2",
- "version": 1,
+ "version": 2,
"timeCreated": 1505240896213,
"timeLastModified": 1515609524588,
"timeLastUsed": 10000,
"timesUsed": 0,
"cc-name": "John Smith",
"cc-exp-month": 6,
"cc-exp-year": 2024,
"cc-type": "visa",
@@ -340,17 +340,17 @@ let BASIC_CARDS_1 = {
"cc-additional-name": "",
"cc-family-name": "Smith",
"cc-exp": "2024-06",
},
"9h5d4h6f4d1s": {
methodName: "basic-card",
"cc-number": "************0954",
"guid": "9h5d4h6f4d1s",
- "version": 1,
+ "version": 2,
"timeCreated": 1517890536491,
"timeLastModified": 1517890564518,
"timeLastUsed": 50000,
"timesUsed": 0,
"cc-name": "Jane Doe",
"cc-exp-month": 5,
"cc-exp-year": 2023,
"cc-type": "mastercard",
@@ -358,17 +358,17 @@ let BASIC_CARDS_1 = {
"cc-additional-name": "",
"cc-family-name": "Doe",
"cc-exp": "2023-05",
},
"123456789abc": {
methodName: "basic-card",
"cc-number": "************1234",
"guid": "123456789abc",
- "version": 1,
+ "version": 2,
"timeCreated": 1517890536491,
"timeLastModified": 1517890564518,
"timeLastUsed": 90000,
"timesUsed": 0,
"cc-name": "Jane Fields",
"cc-given-name": "Jane",
"cc-additional-name": "",
"cc-family-name": "Fields",
@@ -392,17 +392,17 @@ let BASIC_CARDS_1 = {
"cc-exp-month": 6,
"cc-exp-year": 2023,
"cc-exp": "2023-06",
},
"missing-cc-name": {
methodName: "basic-card",
"cc-number": "************8563",
"guid": "missing-cc-name",
- "version": 1,
+ "version": 2,
"timeCreated": 1517890536491,
"timeLastModified": 1517890564518,
"timeLastUsed": 30000,
"timesUsed": 0,
"cc-exp-month": 8,
"cc-exp-year": 2024,
"cc-exp": "2024-08",
},
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -1,13 +1,14 @@
[DEFAULT]
head = head.js
prefs =
browser.pagethumbnails.capturing_disabled=true
dom.payments.request.enabled=true
+ extensions.formautofill.creditCards.available=true
skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
support-files =
blank_page.html
[browser_address_edit.js]
skip-if = verify && debug && os == 'mac'
[browser_card_edit.js]
skip-if = (verify && debug && os == 'mac') || (os == 'linux' && debug) # bug 1465673
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -16,16 +16,18 @@ const SAVE_ADDRESS_DEFAULT_PREF = "dom.p
const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"]
.getService(Ci.nsIPaymentRequestService);
const paymentUISrv = Cc["@mozilla.org/dom/payments/payment-ui-service;1"]
.getService(Ci.nsIPaymentUIService).wrappedJSObject;
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
const {formAutofillStorage} = ChromeUtils.import(
"resource://formautofill/FormAutofillStorage.jsm", {});
+const {OSKeyStoreTestUtils} = ChromeUtils.import(
+ "resource://testing-common/OSKeyStoreTestUtils.jsm", {});
const {PaymentTestUtils: PTU} = ChromeUtils.import(
"resource://testing-common/PaymentTestUtils.jsm", {});
ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
function getPaymentRequests() {
return Array.from(paymentSrv.enumerate());
}
@@ -357,20 +359,22 @@ add_task(async function setup_head() {
return;
}
if (msg.errorMessage == "AbortError: The operation was aborted. " &&
msg.sourceName == "" && msg.lineNumber == 0) {
return;
}
ok(false, msg.message || msg.errorMessage);
});
+ OSKeyStoreTestUtils.setup();
await setupFormAutofillStorage();
- registerCleanupFunction(function cleanup() {
+ registerCleanupFunction(async function cleanup() {
paymentSrv.cleanup();
cleanupFormAutofillStorage();
+ await OSKeyStoreTestUtils.cleanup();
Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
Services.prefs.clearUserPref(SAVE_ADDRESS_DEFAULT_PREF);
SpecialPowers.postConsoleSentinel();
// CreditCard.jsm is imported into the global scope. It needs to be deleted
// else it outlives the test and is reported as a leak.
delete window.CreditCard;
});
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -37,17 +37,17 @@ ChromeUtils.import("resource://gre/modul
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
CreditCard: "resource://gre/modules/CreditCard.jsm",
FormAutofillPreferences: "resource://formautofill/FormAutofillPreferences.jsm",
FormAutofillDoorhanger: "resource://formautofill/FormAutofillDoorhanger.jsm",
FormAutofillUtils: "resource://formautofill/FormAutofillUtils.jsm",
- MasterPassword: "resource://formautofill/MasterPassword.jsm",
+ OSKeyStore: "resource://formautofill/OSKeyStore.jsm",
});
this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
const {
ENABLED_AUTOFILL_ADDRESSES_PREF,
ENABLED_AUTOFILL_CREDITCARDS_PREF,
@@ -220,18 +220,18 @@ FormAutofillParent.prototype = {
if (data.guid) {
await this.formAutofillStorage.addresses.update(data.guid, data.address);
} else {
await this.formAutofillStorage.addresses.add(data.address);
}
break;
}
case "FormAutofill:SaveCreditCard": {
- if (!await MasterPassword.ensureLoggedIn()) {
- log.warn("User canceled master password entry");
+ if (!await OSKeyStore.ensureLoggedIn()) {
+ log.warn("User canceled encryption login");
return;
}
await this.formAutofillStorage.creditCards.add(data.creditcard);
break;
}
case "FormAutofill:RemoveAddresses": {
data.guids.forEach(guid => this.formAutofillStorage.addresses.remove(guid));
break;
@@ -248,22 +248,22 @@ FormAutofillParent.prototype = {
const win = BrowserWindowTracker.getTopWindow();
win.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
break;
}
case "FormAutofill:GetDecryptedString": {
let {cipherText, reauth} = data;
let string;
try {
- string = await MasterPassword.decrypt(cipherText, reauth);
+ string = await OSKeyStore.decrypt(cipherText, reauth);
} catch (e) {
if (e.result != Cr.NS_ERROR_ABORT) {
throw e;
}
- log.warn("User canceled master password entry");
+ log.warn("User canceled encryption login");
}
target.sendAsyncMessage("FormAutofill:DecryptedString", string);
break;
}
}
},
/**
@@ -287,17 +287,17 @@ FormAutofillParent.prototype = {
Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this);
Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
}
},
/**
* Get the records from profile store and return results back to content
* process. It will decrypt the credit card number and append
- * "cc-number-decrypted" to each record if MasterPassword isn't set.
+ * "cc-number-decrypted" to each record if OSKeyStore isn't set.
*
* @private
* @param {string} data.collectionName
* The name used to specify which collection to retrieve records.
* @param {string} data.searchString
* The typed string for filtering out the matched records.
* @param {string} data.info
* The input autocomplete property's information.
@@ -312,44 +312,33 @@ FormAutofillParent.prototype = {
}
let recordsInCollection = await collection.getAll();
if (!info || !info.fieldName || !recordsInCollection.length) {
target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
return;
}
- let isCCAndMPEnabled = collectionName == CREDITCARDS_COLLECTION_NAME && MasterPassword.isEnabled;
- // We don't filter "cc-number" when MasterPassword is set.
- if (isCCAndMPEnabled && info.fieldName == "cc-number") {
+ let isCC = collectionName == CREDITCARDS_COLLECTION_NAME;
+ // We don't filter "cc-number"
+ if (isCC && info.fieldName == "cc-number") {
recordsInCollection = recordsInCollection.filter(record => !!record["cc-number"]);
target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
return;
}
let records = [];
let lcSearchString = searchString.toLowerCase();
for (let record of recordsInCollection) {
let fieldValue = record[info.fieldName];
if (!fieldValue) {
continue;
}
- // Cache the decrypted "cc-number" in each record for content to preview
- // when MasterPassword isn't set.
- if (!isCCAndMPEnabled && record["cc-number-encrypted"]) {
- record["cc-number-decrypted"] = await MasterPassword.decrypt(record["cc-number-encrypted"]);
- }
-
- // Filter "cc-number" based on the decrypted one.
- if (info.fieldName == "cc-number") {
- fieldValue = record["cc-number-decrypted"];
- }
-
if (collectionName == ADDRESSES_COLLECTION_NAME && record.country
&& !FormAutofill.supportedCountries.includes(record.country)) {
// Address autofill isn't supported for the record's country so we don't
// want to attempt to potentially incorrectly fill the address fields.
continue;
}
if (lcSearchString && !String(fieldValue).toLowerCase().startsWith(lcSearchString)) {
@@ -533,18 +522,18 @@ FormAutofillParent.prototype = {
return;
}
if (state == "disable") {
Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false);
return;
}
- if (!await MasterPassword.ensureLoggedIn()) {
- log.warn("User canceled master password entry");
+ if (!await OSKeyStore.ensureLoggedIn()) {
+ log.warn("User canceled encryption login");
return;
}
let changedGUIDs = [];
if (creditCard.guid) {
if (state == "update") {
await this.formAutofillStorage.creditCards.update(creditCard.guid, creditCard.record, true);
changedGUIDs.push(creditCard.guid);
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -137,33 +137,36 @@ ChromeUtils.import("resource://formautof
ChromeUtils.defineModuleGetter(this, "CreditCard",
"resource://gre/modules/CreditCard.jsm");
ChromeUtils.defineModuleGetter(this, "JSONFile",
"resource://gre/modules/JSONFile.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillNameUtils",
"resource://formautofill/FormAutofillNameUtils.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
"resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
- "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+ "resource://formautofill/OSKeyStore.jsm");
ChromeUtils.defineModuleGetter(this, "PhoneNumber",
"resource://formautofill/phonenumberutils/PhoneNumber.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
+ "@mozilla.org/login-manager/crypto/SDR;1",
+ Ci.nsILoginManagerCrypto);
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
"nsICryptoHash", "initWithString");
const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
const STORAGE_SCHEMA_VERSION = 1;
const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
const VALID_ADDRESS_FIELDS = [
"given-name",
"additional-name",
"family-name",
"organization",
"street-address",
"address-level3",
@@ -259,23 +262,24 @@ class AutofillRecords {
this.VALID_FIELDS = validFields;
this.VALID_COMPUTED_FIELDS = validComputedFields;
this._store = store;
this._collectionName = collectionName;
this._schemaVersion = schemaVersion;
- Promise.all(this._data.map(record => this._migrateRecord(record)))
- .then(hasChangesArr => {
- let dataHasChanges = hasChangesArr.find(hasChanges => hasChanges);
- if (dataHasChanges) {
- this._store.saveSoon();
- }
- });
+ this._initializePromise =
+ Promise.all(this._data.map(async (record, index) => this._migrateRecord(record, index)))
+ .then(hasChangesArr => {
+ let dataHasChanges = hasChangesArr.includes(true);
+ if (dataHasChanges) {
+ this._store.saveSoon();
+ }
+ });
}
/**
* Gets the schema version number.
*
* @returns {number}
* The current schema version number.
*/
@@ -299,16 +303,24 @@ class AutofillRecords {
_ensureMatchingVersion(record) {
if (record.version != this.version) {
throw new Error(`Got unknown record version ${
record.version}; want ${this.version}`);
}
}
/**
+ * Initialize the records in the collection, resolves when the migration completes.
+ * @returns {Promise}
+ */
+ initialize() {
+ return this._initializePromise;
+ }
+
+ /**
* Adds a new record.
*
* @param {Object} record
* The new record for saving.
* @param {boolean} [options.sourceSync = false]
* Did sync generate this addition?
* @returns {Promise<string>}
* The GUID of the newly added item..
@@ -1171,36 +1183,47 @@ class AutofillRecords {
}
_findIndexByGUID(guid, {includeDeleted = false} = {}) {
return this._data.findIndex(record => {
return record.guid == guid && (!record.deleted || includeDeleted);
});
}
- async _migrateRecord(record) {
+ async _migrateRecord(record, index) {
let hasChanges = false;
if (record.deleted) {
return hasChanges;
}
if (!record.version || isNaN(record.version) || record.version < 1) {
this.log.warn("Invalid record version:", record.version);
// Force to run the migration.
record.version = 0;
}
if (record.version < this.version) {
hasChanges = true;
- record.version = this.version;
+
+ record = await this._computeMigratedRecord(record);
- // Force to recompute fields if we upgrade the schema.
- await this._stripComputedFields(record);
+ if (record.deleted) {
+ // record is deleted by _computeMigratedRecord(),
+ // go ahead and put it in the store.
+ this._data[index] = record;
+ return hasChanges;
+ }
+
+ // Compute the computed fields before putting it to store.
+ await this.computeFields(record);
+ this._data[index] = record;
+
+ return hasChanges;
}
hasChanges |= await this.computeFields(record);
return hasChanges;
}
_normalizeRecord(record, preserveEmptyFields = false) {
this._normalizeFields(record);
@@ -1251,16 +1274,34 @@ class AutofillRecords {
this._store.data[this._collectionName] = [];
this._store.saveSoon();
Services.obs.notifyObservers({wrappedJSObject: {
sourceSync,
collectionName: this._collectionName,
}}, "formautofill-storage-changed", "removeAll");
}
+ /**
+ * Strip the computed fields based on the record version.
+ * @param {Object} record The record to migrate
+ * @returns {Object} Migrated record.
+ * Record is always cloned, with version updated,
+ * with computed fields stripped.
+ * Could be a tombstone record, if the record
+ * should be discorded.
+ */
+ async _computeMigratedRecord(record) {
+ if (!record.deleted) {
+ record = this._clone(record);
+ await this._stripComputedFields(record);
+ record.version = this.version;
+ }
+ return record;
+ }
+
async _stripComputedFields(record) {
this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
}
// An interface to be inherited.
_recordReadProcessor(record) {}
// An interface to be inherited.
@@ -1599,28 +1640,82 @@ class CreditCards extends AutofillRecord
hasNewComputedFields = true;
}
// Encrypt credit card number
if (!("cc-number-encrypted" in creditCard)) {
if ("cc-number" in creditCard) {
let ccNumber = creditCard["cc-number"];
creditCard["cc-number"] = CreditCard.getLongMaskedNumber(ccNumber);
- creditCard["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+ creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
} else {
creditCard["cc-number-encrypted"] = "";
}
}
return hasNewComputedFields;
}
+ async _computeMigratedRecord(creditCard) {
+ if (creditCard["cc-number-encrypted"]) {
+ switch (creditCard.version) {
+ case 1: {
+ if (!cryptoSDR.isLoggedIn) {
+ // We cannot decrypt the data, so silently remove the record for
+ // the user.
+ if (creditCard.deleted) {
+ break;
+ }
+
+ this.log.warn("Removing version 1 credit card record to migrate to new encryption:", creditCard.guid);
+
+ // Replace the record with a tombstone record here,
+ // regardless of existence of sync metadata.
+ let existingSync = this._getSyncMetaData(creditCard);
+ creditCard = {
+ guid: creditCard.guid,
+ timeLastModified: Date.now(),
+ deleted: true,
+ };
+
+ if (existingSync) {
+ creditCard._sync = existingSync;
+ existingSync.changeCounter++;
+ }
+ break;
+ }
+
+ creditCard = this._clone(creditCard);
+
+ // Decrypt the cc-number using version 1 encryption.
+ let ccNumber = cryptoSDR.decrypt(creditCard["cc-number-encrypted"]);
+ // Re-encrypt the cc-number with version 2 encryption.
+ creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
+ break;
+ }
+
+ default:
+ throw new Error("Unknown credit card version to migrate: " + creditCard.version);
+ }
+ }
+ return super._computeMigratedRecord(creditCard);
+ }
+
async _stripComputedFields(creditCard) {
if (creditCard["cc-number-encrypted"]) {
- creditCard["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+ try {
+ creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ABORT) {
+ throw ex;
+ }
+ // Quietly recover from encryption error,
+ // so existing credit card entry with undecryptable number
+ // can be updated.
+ }
}
await super._stripComputedFields(creditCard);
}
_normalizeFields(creditCard) {
this._normalizeCCName(creditCard);
this._normalizeCCNumber(creditCard);
this._normalizeCCExpirationDate(creditCard);
@@ -1671,38 +1766,62 @@ class CreditCards extends AutofillRecord
}
_validateFields(creditCard) {
if (!creditCard["cc-number"]) {
throw new Error("Missing/invalid cc-number");
}
}
+ _ensureMatchingVersion(record) {
+ if (!record.version || isNaN(record.version) || record.version < 1) {
+ throw new Error(`Got invalid record version ${
+ record.version}; want ${this.version}`);
+ }
+
+ if (record.version < this.version) {
+ switch (record.version) {
+ case 1:
+ // The difference between version 1 and 2 is only about the encryption
+ // method used for the cc-number-encrypted field.
+ // As long as the record is already decrypted, it is safe to bump the
+ // version directly.
+ if (!record["cc-number-encrypted"]) {
+ record.version = this.version;
+ } else {
+ throw new Error("Unexpected record migration path.");
+ }
+ break;
+ default:
+ throw new Error("Unknown credit card version to match: " + record.version);
+ }
+ }
+
+ return super._ensureMatchingVersion(record);
+ }
+
/**
* Normalize the given record and return the first matched guid if storage has the same record.
* @param {Object} targetCreditCard
* The credit card for duplication checking.
* @returns {Promise<string|null>}
* Return the first guid if storage has the same credit card and null otherwise.
*/
async getDuplicateGuid(targetCreditCard) {
let clonedTargetCreditCard = this._clone(targetCreditCard);
this._normalizeRecord(clonedTargetCreditCard);
for (let creditCard of this._data) {
let isDuplicate = await Promise.all(this.VALID_FIELDS.map(async field => {
if (!clonedTargetCreditCard[field]) {
return !creditCard[field];
}
if (field == "cc-number" && creditCard[field]) {
- if (MasterPassword.isEnabled) {
- // Compare the masked numbers instead when the master password is
- // enabled because we don't want to leak the credit card number.
- return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
- }
- return (clonedTargetCreditCard[field] == await MasterPassword.decrypt(creditCard["cc-number-encrypted"]));
+ // Compare the masked numbers instead when decryption requires a password
+ // because we don't want to leak the credit card number.
+ return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
}
return clonedTargetCreditCard[field] == creditCard[field];
})).then(fieldResults => fieldResults.every(result => result));
if (isDuplicate) {
return creditCard.guid;
}
}
return null;
@@ -1800,17 +1919,33 @@ FormAutofillStorage.prototype = {
* @rejects JavaScript exception.
*/
initialize() {
if (!this._initializePromise) {
this._store = new JSONFile({
path: this._path,
dataPostProcessor: this._dataPostProcessor.bind(this),
});
- this._initializePromise = this._store.load();
+ this._initializePromise = this._store.load()
+ .then(() => {
+ let initializeAutofillRecords = [this.addresses.initialize()];
+ if (FormAutofill.isAutofillCreditCardsEnabled) {
+ initializeAutofillRecords.push(this.creditCards.initialize());
+ } else {
+ // Make creditCards records unavailable to other modules
+ // because we never initialize it.
+ Object.defineProperty(this, "creditCards", {
+ get() {
+ throw new Error("CreditCards is not initialized. " +
+ "Please restart if you flip the pref manually.");
+ },
+ });
+ }
+ return Promise.all(initializeAutofillRecords);
+ });
}
return this._initializePromise;
},
_dataPostProcessor(data) {
data.version = this.version;
if (!data.addresses) {
data.addresses = [];
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -12,17 +12,17 @@ const ADDRESS_REFERENCES_EXT = "addressR
const ADDRESSES_COLLECTION_NAME = "addresses";
const CREDITCARDS_COLLECTION_NAME = "creditCards";
const MANAGE_ADDRESSES_KEYWORDS = ["manageAddressesTitle", "addNewAddressTitle"];
const EDIT_ADDRESS_KEYWORDS = [
"givenName", "additionalName", "familyName", "organization2", "streetAddress",
"state", "province", "city", "country", "zip", "postalCode", "email", "tel",
];
-const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle", "showCreditCardsBtnLabel"];
+const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle"];
const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear", "cardNetwork"];
const FIELD_STATES = {
NORMAL: "NORMAL",
AUTO_FILLED: "AUTO_FILLED",
PREVIEW: "PREVIEW",
};
const SECTION_TYPES = {
ADDRESS: "address",
rename from browser/extensions/formautofill/MasterPassword.jsm
rename to browser/extensions/formautofill/OSKeyStore.jsm
--- a/browser/extensions/formautofill/MasterPassword.jsm
+++ b/browser/extensions/formautofill/OSKeyStore.jsm
@@ -1,184 +1,251 @@
/* 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/. */
/**
- * Helpers for the Master Password Dialog.
- * In the future the Master Password implementation may move here.
+ * Helpers for using OS Key Store.
*/
"use strict";
var EXPORTED_SYMBOLS = [
- "MasterPassword",
+ "OSKeyStore",
];
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
- "@mozilla.org/login-manager/crypto/SDR;1",
- Ci.nsILoginManagerCrypto);
+ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
+ "@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
+
+// Skip reauth during tests, only works in non-official builds.
+const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
+
+var OSKeyStore = {
+ /**
+ * On macOS this becomes part of the name label visible on Keychain Acesss as
+ * "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
+ */
+ STORE_LABEL: AppConstants.MOZ_APP_NAME,
-var MasterPassword = {
- get _token() {
- let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
- return tokendb.getInternalKeyToken();
+ /**
+ * Consider the module is initialized as locked. OS might unlock without a
+ * prompt.
+ * @type {Boolean}
+ */
+ _isLocked: true,
+
+ _pendingUnlockPromise: null,
+
+ /**
+ * @returns {boolean} True if logged in (i.e. decrypt(reauth = false) will
+ * not retrigger a dialog) and false if not.
+ * User might log out elsewhere in the OS, so even if this
+ * is true a prompt might still pop up.
+ */
+ get isLoggedIn() {
+ return !this._isLocked;
},
/**
- * @returns {boolean} True if a master password is set and false otherwise.
+ * @returns {boolean} True if there is another login dialog existing and false
+ * otherwise.
*/
- get isEnabled() {
- return this._token.hasPassword;
+ get isUIBusy() {
+ return !!this._pendingUnlockPromise;
},
/**
- * @returns {boolean} True if master password is logged in and false if not.
+ * If the test pref exist and applicable,
+ * this method will dispatch a observer message and return
+ * to simulate successful reauth, or throw to simulate
+ * failed reauth.
+ *
+ * @returns {boolean} True when reauth should NOT be skipped,
+ * false when reauth has been skipped.
+ * @throws If it needs to simulate reauth login failure.
*/
- get isLoggedIn() {
- return Services.logins.isLoggedIn;
+ _maybeSkipReauthForTest() {
+ // Don't take test reauth pref in the following configurations.
+ if (nativeOSKeyStore.isNSSKeyStore ||
+ AppConstants.MOZILLA_OFFICIAL ||
+ !this._testReauth) {
+ return true;
+ }
+
+ // Skip this reauth because there is no way to mock the
+ // native dialog in the testing environment, for now.
+ log.debug("_ensureReauth: _testReauth: ", this._testReauth);
+ switch (this._testReauth) {
+ case "pass":
+ Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
+ return false;
+ case "cancel":
+ Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
+ throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
+ default:
+ throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
+ }
},
/**
- * @returns {boolean} True if there is another master password login dialog
- * existing and false otherwise.
- */
- get isUIBusy() {
- return Services.logins.uiBusy;
- },
-
- /**
- * Ensure the master password is logged in. It will display the master password
- * login prompt or do nothing if it's logged in already. If an existing MP
+ * Ensure the store in use is logged in. It will display the OS login
+ * login prompt or do nothing if it's logged in already. If an existing login
* prompt is already prompted, the result from it will be used instead.
*
+ * Note: This method must set _pendingUnlockPromise before returning the
+ * promise (i.e. the first |await|), otherwise we'll risk re-entry.
+ * This is why there aren't an |await| in the method. The method is marked as
+ * |async| to communicate that it's async.
+ *
* @param {boolean} reauth Prompt the login dialog no matter it's logged in
* or not if it's set to true.
* @returns {Promise<boolean>} True if it's logged in or no password is set
* and false if it's still not logged in (prompt
* canceled or other error).
*/
async ensureLoggedIn(reauth = false) {
- if (!this.isEnabled) {
- return true;
+ if (this._pendingUnlockPromise) {
+ log.debug("ensureLoggedIn: Has a pending unlock operation");
+ return this._pendingUnlockPromise;
}
+ log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);
+
+ // TODO: Implementing re-auth by passing this value to the native implementation
+ // in some way. Set this to false for now to ignore the reauth request (bug 1429265).
+ reauth = false;
+
+ let unlockPromise = Promise.resolve().then(async () => {
+ if (reauth) {
+ reauth = this._maybeSkipReauthForTest();
+ }
- if (this.isLoggedIn && !reauth) {
- return true;
- }
+ if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
+ log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
+ let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
+ // TODO We should somehow have a dialog to ask the user to write this down,
+ // and another dialog somewhere for the user to restore the secret with it.
+ // (Intentionally not printing it out in the console)
+ log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
+ }
+ });
- // If a prompt is already showing then wait for and focus it.
- if (this.isUIBusy) {
- return this.waitForExistingDialog();
+ if (nativeOSKeyStore.isNSSKeyStore) {
+ // Workaround bug 1492305: NSS-implemented methods don't reject when user cancels.
+ unlockPromise = unlockPromise.then(() => {
+ log.debug("ensureLoggedIn: isNSSKeyStore: ", reauth, Services.logins.isLoggedIn);
+ // User has hit the cancel button on the master password prompt.
+ // We must reject the promise chain here.
+ if (!Services.logins.isLoggedIn) {
+ throw Components.Exception("User canceled OS unlock entry (Workaround)", Cr.NS_ERROR_FAILURE);
+ }
+ });
}
- let token = this._token;
- try {
- // 'true' means always prompt for token password. User will be prompted until
- // clicking 'Cancel' or entering the correct password.
- token.login(true);
- } catch (e) {
- // An exception will be thrown if the user cancels the login prompt dialog.
- // User is also logged out.
- }
+ unlockPromise = unlockPromise.then(() => {
+ log.debug("ensureLoggedIn: Logged in");
+ this._pendingUnlockPromise = null;
+ this._isLocked = false;
- // If we triggered a master password prompt, notify observers.
- if (token.isLoggedIn()) {
- Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
- } else {
- Services.obs.notifyObservers(null, "passwordmgr-crypto-loginCanceled");
- }
+ return true;
+ }, (err) => {
+ log.debug("ensureLoggedIn: Not logged in", err);
+ this._pendingUnlockPromise = null;
+ this._isLocked = true;
- return token.isLoggedIn();
+ return false;
+ });
+
+ this._pendingUnlockPromise = unlockPromise;
+
+ return this._pendingUnlockPromise;
},
/**
* Decrypts cipherText.
*
+ * Note: In the event of an rejection, check the result property of the Exception
+ * object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
+ * don't show that dialog), apart from other errors (e.g., gracefully
+ * recover from that and still shows the dialog.)
+ *
* @param {string} cipherText Encrypted string including the algorithm details.
* @param {boolean} reauth True if we want to force the prompt to show up
* even if the user is already logged in.
* @returns {Promise<string>} resolves to the decrypted string, or rejects otherwise.
*/
async decrypt(cipherText, reauth = false) {
if (!await this.ensureLoggedIn(reauth)) {
- throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
}
- return cryptoSDR.decrypt(cipherText);
+ let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
+ return String.fromCharCode.apply(String, bytes);
},
/**
* Encrypts a string and returns cipher text containing algorithm information used for decryption.
*
* @param {string} plainText Original string without encryption.
* @returns {Promise<string>} resolves to the encrypted string (with algorithm), otherwise rejects.
*/
async encrypt(plainText) {
if (!await this.ensureLoggedIn()) {
- throw Components.Exception("User canceled master password entry", Cr.NS_ERROR_ABORT);
+ throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
}
- return cryptoSDR.encrypt(plainText);
+ // Convert plain text into a UTF-8 binary string
+ plainText = unescape(encodeURIComponent(plainText));
+
+ // Convert it to an array
+ let textArr = [];
+ for (let char of plainText) {
+ textArr.push(char.charCodeAt(0));
+ }
+
+ let rawEncryptedText = await nativeOSKeyStore.asyncEncryptBytes(this.STORE_LABEL, textArr.length, textArr);
+
+ // Mark the output with a version number.
+ return rawEncryptedText;
},
/**
- * Resolve when master password dialogs are closed, immediately if none are open.
+ * Resolve when the login dialogs are closed, immediately if none are open.
*
* An existing MP dialog will be focused and will request attention.
*
* @returns {Promise<boolean>}
* Resolves with whether the user is logged in to MP.
*/
async waitForExistingDialog() {
- if (!this.isUIBusy) {
- log.debug("waitForExistingDialog: Dialog isn't showing. isLoggedIn:", this.isLoggedIn);
- return this.isLoggedIn;
+ if (this.isUIBusy) {
+ return this._pendingUnlockPromise;
}
-
- return new Promise((resolve) => {
- log.debug("waitForExistingDialog: Observing the open dialog");
- let observer = {
- QueryInterface: ChromeUtils.generateQI([
- Ci.nsIObserver,
- Ci.nsISupportsWeakReference,
- ]),
+ return this.isLoggedIn;
+ },
- observe(subject, topic, data) {
- log.debug("waitForExistingDialog: Got notification:", topic);
- // Only run observer once.
- Services.obs.removeObserver(this, "passwordmgr-crypto-login");
- Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
- if (topic == "passwordmgr-crypto-loginCanceled") {
- resolve(false);
- return;
- }
-
- resolve(true);
- },
- };
+ /**
+ * Remove the store. For tests.
+ */
+ async cleanup() {
+ return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
+ },
- // Possible leak: it's possible that neither of these notifications
- // will fire, and if that happens, we'll leak the observer (and
- // never return). We should guarantee that at least one of these
- // will fire.
- // See bug XXX.
- Services.obs.addObserver(observer, "passwordmgr-crypto-login");
- Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled");
-
- // Focus and draw attention to the existing master password dialog for the
- // occassions where it's not attached to the current window.
- let promptWin = Services.wm.getMostRecentWindow("prompt:promptPassword");
- promptWin.focus();
- promptWin.getAttention();
- });
+ /**
+ * Check if the implementation is using the NSS key store.
+ * If so, tests will be able to handle the reauth dialog.
+ */
+ get isNSSKeyStore() {
+ return nativeOSKeyStore.isNSSKeyStore;
},
};
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
return new ConsoleAPI({
- maxLogLevelPref: "masterPassword.loglevel",
- prefix: "Master Password",
+ maxLogLevelPref: "extensions.formautofill.loglevel",
+ prefix: "OSKeyStore",
});
});
+
+XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_testReauth", TEST_ONLY_REAUTH, "");
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -15,29 +15,27 @@
</head>
<body dir="&locale.dir;">
<fieldset>
<legend data-localization="creditCardsListHeader"/>
<select id="credit-cards" size="9" multiple="multiple"/>
</fieldset>
<div id="controls-container">
<button id="remove" disabled="disabled" data-localization="removeBtnLabel"/>
- <button id="show-hide-credit-cards" data-localization="showCreditCardsBtnLabel"/>
<!-- Wrapper is used to properly compute the search tooltip position -->
<div>
<button id="add" data-localization="addBtnLabel"/>
</div>
<button id="edit" disabled="disabled" data-localization="editBtnLabel"/>
</div>
<script type="application/javascript">
"use strict";
/* global ManageCreditCards */
new ManageCreditCards({
records: document.getElementById("credit-cards"),
controlsContainer: document.getElementById("controls-container"),
remove: document.getElementById("remove"),
- showHideCreditCards: document.getElementById("show-hide-credit-cards"),
add: document.getElementById("add"),
edit: document.getElementById("edit"),
});
</script>
</body>
</html>
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -14,18 +14,18 @@ ChromeUtils.import("resource://formautof
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "CreditCard",
"resource://gre/modules/CreditCard.jsm");
ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
"resource://formautofill/FormAutofillStorage.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
"resource://formautofill/FormAutofillUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
- "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+ "resource://formautofill/OSKeyStore.jsm");
this.log = null;
FormAutofill.defineLazyLogGetter(this, "manageAddresses");
class ManageRecords {
constructor(subStorageName, elements) {
this._storageInitPromise = formAutofillStorage.initialize();
this._subStorageName = subStorageName;
@@ -312,35 +312,43 @@ class ManageAddresses extends ManageReco
}
class ManageCreditCards extends ManageRecords {
constructor(elements) {
super("creditCards", elements);
elements.add.setAttribute("searchkeywords", FormAutofillUtils.EDIT_CREDITCARD_KEYWORDS
.map(key => FormAutofillUtils.stringBundle.GetStringFromName(key))
.join("\n"));
- this._hasMasterPassword = MasterPassword.isEnabled;
this._isDecrypted = false;
- if (this._hasMasterPassword) {
- elements.showHideCreditCards.setAttribute("hidden", true);
- }
}
/**
* Open the edit address dialog to create/edit a credit card.
*
* @param {object} creditCard [optional]
*/
async openEditDialog(creditCard) {
- // If master password is set, ask for password if user is trying to edit an
- // existing credit card.
- if (!creditCard || !this._hasMasterPassword || await MasterPassword.ensureLoggedIn(true)) {
+ // Ask for reauth if user is trying to edit an existing credit card.
+ if (!creditCard || await OSKeyStore.ensureLoggedIn(true)) {
let decryptedCCNumObj = {};
if (creditCard) {
- decryptedCCNumObj["cc-number"] = await MasterPassword.decrypt(creditCard["cc-number-encrypted"]);
+ try {
+ decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ABORT) {
+ // User shouldn't be ask to reauth here, but it could happen.
+ // Return here and skip opening the dialog.
+ return;
+ }
+ // We've got ourselves a real error.
+ // Recover from encryption error so the user gets a chance to re-enter
+ // unencrypted credit card number.
+ decryptedCCNumObj["cc-number"] = "";
+ Cu.reportError(ex);
+ }
}
let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, "resizable=no", {
record: decryptedCreditCard,
});
}
}
@@ -358,22 +366,16 @@ class ManageCreditCards extends ManageRe
encryptedNumber: creditCard["cc-number-encrypted"],
number: creditCard["cc-number"],
name: creditCard["cc-name"],
network: creditCard["cc-type"],
});
return cardObj.getLabel({showNumbers: showCreditCards});
}
- async toggleShowHideCards(options) {
- this._isDecrypted = !this._isDecrypted;
- this.updateShowHideButtonState();
- await this.updateLabels(options, this._isDecrypted);
- }
-
async updateLabels(options, isDecrypted) {
for (let option of options) {
option.text = await this.getLabel(option.record, isDecrypted);
}
// For testing only: Notify when credit cards labels have been updated
this._elements.records.dispatchEvent(new CustomEvent("LabelsUpdated"));
}
@@ -391,30 +393,15 @@ class ManageCreditCards extends ManageRe
option.setAttribute("cc-type", record["cc-type"]);
} else {
option.removeAttribute("cc-type");
}
}
}
updateButtonsStates(selectedCount) {
- this.updateShowHideButtonState();
super.updateButtonsStates(selectedCount);
}
- updateShowHideButtonState() {
- if (this._elements.records.length) {
- this._elements.showHideCreditCards.removeAttribute("disabled");
- } else {
- this._elements.showHideCreditCards.setAttribute("disabled", true);
- }
- this._elements.showHideCreditCards.textContent =
- this._isDecrypted ? FormAutofillUtils.stringBundle.GetStringFromName("hideCreditCardsBtnLabel") :
- FormAutofillUtils.stringBundle.GetStringFromName("showCreditCardsBtnLabel");
- }
-
handleClick(event) {
- if (event.target == this._elements.showHideCreditCards) {
- this.toggleShowHideCards(this._elements.records.options);
- }
super.handleClick(event);
}
}
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -99,18 +99,16 @@ savedCreditCardsBtnLabel = Saved Credit Cards…
# LOCALIZATION NOTE (manageAddressesTitle, manageCreditCardsTitle): The dialog title for the list of addresses or
# credit cards in browser preferences.
manageAddressesTitle = Saved Addresses
manageCreditCardsTitle = Saved Credit Cards
# LOCALIZATION NOTE (addressesListHeader, creditCardsListHeader): The header for the list of addresses or credit cards
# in browser preferences.
addressesListHeader = Addresses
creditCardsListHeader = Credit Cards
-showCreditCardsBtnLabel = Show Credit Cards
-hideCreditCardsBtnLabel = Hide Credit Cards
removeBtnLabel = Remove
addBtnLabel = Add…
editBtnLabel = Edit…
# LOCALIZATION NOTE (manageDialogsWidth): This strings sets the default width for windows used to manage addresses and
# credit cards.
manageDialogsWidth = 560px
# LOCALIZATION NOTE (addNewAddressTitle, editAddressTitle): The dialog title for creating or editing addresses
--- a/browser/extensions/formautofill/moz.build
+++ b/browser/extensions/formautofill/moz.build
@@ -27,16 +27,18 @@ elif CONFIG['OS_ARCH'] == 'Darwin':
'skin/osx/editDialog.css',
]
elif CONFIG['OS_ARCH'] == 'WINNT':
FINAL_TARGET_FILES.features['formautofill@mozilla.org'].chrome.res += [
'skin/windows/autocomplete-item.css',
'skin/windows/editDialog.css',
]
+TESTING_JS_MODULES += ['test/fixtures/OSKeyStoreTestUtils.jsm']
+
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
JAR_MANIFESTS += ['jar.mn']
--- a/browser/extensions/formautofill/test/browser/browser.ini
+++ b/browser/extensions/formautofill/test/browser/browser.ini
@@ -10,17 +10,18 @@ support-files =
[browser_autocomplete_footer.js]
skip-if = verify
[browser_autocomplete_marked_back_forward.js]
[browser_autocomplete_marked_detached_tab.js]
skip-if = (verify && (os == 'win' || os == 'mac'))
[browser_check_installed.js]
[browser_creditCard_doorhanger.js]
skip-if = (os == "linux") || (os == "mac" && debug) || (os == "win") # bug 1425884
-[browser_creditCard_fill_master_password.js]
+[browser_creditCard_fill_cancel_login.js]
+skip-if = true # Re-auth is not implemented, cannot cancel OS key store login (bug 1429265)
[browser_dropdown_layout.js]
[browser_editAddressDialog.js]
[browser_editCreditCardDialog.js]
skip-if = (verify && (os == 'linux'))
[browser_first_time_use_doorhanger.js]
skip-if = verify
[browser_insecure_form.js]
skip-if = (os == 'linux' && !debug) || (os == 'win') # bug 1456284
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_doorhanger.js
@@ -46,16 +46,17 @@ add_task(async function test_submit_cred
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
+ let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.focus();
name.setUserInput("User 1");
form.querySelector("#cc-number").setUserInput("5038146897157463");
form.querySelector("#cc-exp-month").setUserInput("12");
@@ -64,54 +65,66 @@ add_task(async function test_submit_cred
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
+ await onChanged;
}
);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
add_task(async function test_submit_untouched_creditCard_form() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "notifyUsed");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
}
);
+ await onUsed;
creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card");
is(creditCards[0].timesUsed, 1, "timesUsed field set to 1");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
@@ -120,16 +133,19 @@ add_task(async function test_submit_chan
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+
+ let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "notifyUsed");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
@@ -144,16 +160,17 @@ add_task(async function test_submit_chan
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
}
);
+ await onUsed;
creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card in storage");
is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"], "name field still exists");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
@@ -269,88 +286,16 @@ add_task(async function test_submit_cred
let creditCardPref = SpecialPowers.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
is(creditCards.length, 0, "No credit card in storage");
is(creditCardPref, false, "Credit card is disabled");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF);
});
-add_task(async function test_submit_creditCard_saved_with_mp_enabled() {
- LoginTestUtils.masterPassword.enable();
- // Login with the masterPassword in LoginTestUtils.
- let masterPasswordDialogShown = waitForMasterPasswordDialog(true);
- await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
- async function(browser) {
- let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
- "popupshown");
- await ContentTask.spawn(browser, null, async function() {
- let form = content.document.getElementById("form");
- let name = form.querySelector("#cc-name");
- name.focus();
- await new Promise(resolve => setTimeout(resolve, 1000));
- name.setUserInput("User 0");
-
- let number = form.querySelector("#cc-number");
- number.setUserInput("6387060366272981");
-
- // Wait 1000ms before submission to make sure the input value applied
- await new Promise(resolve => setTimeout(resolve, 1000));
- form.querySelector("input[type=submit]").click();
- });
-
- await promiseShown;
- await clickDoorhangerButton(MAIN_BUTTON);
- await masterPasswordDialogShown;
- await TestUtils.topicObserved("formautofill-storage-changed");
- }
- );
-
- let creditCards = await getCreditCards();
- is(creditCards.length, 1, "1 credit card in storage");
- is(creditCards[0]["cc-name"], "User 0", "Verify the name field");
- is(creditCards[0]["cc-number"], "************2981", "Verify the card number field");
- LoginTestUtils.masterPassword.disable();
- await removeAllRecords();
-});
-
-add_task(async function test_submit_creditCard_saved_with_mp_enabled_but_canceled() {
- LoginTestUtils.masterPassword.enable();
- let masterPasswordDialogShown = waitForMasterPasswordDialog();
- await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
- async function(browser) {
- let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
- "popupshown");
- await ContentTask.spawn(browser, null, async function() {
- let form = content.document.getElementById("form");
- let name = form.querySelector("#cc-name");
- name.focus();
- await new Promise(resolve => setTimeout(resolve, 1000));
- name.setUserInput("User 2");
-
- let number = form.querySelector("#cc-number");
- number.setUserInput("5471839082338112");
-
- // Wait 1000ms before submission to make sure the input value applied
- await new Promise(resolve => setTimeout(resolve, 1000));
- form.querySelector("input[type=submit]").click();
- });
-
- await promiseShown;
- await clickDoorhangerButton(MAIN_BUTTON);
- await masterPasswordDialogShown;
- }
- );
-
- await sleep(1000);
- let creditCards = await getCreditCards();
- is(creditCards.length, 0, "No credit cards in storage");
- LoginTestUtils.masterPassword.disable();
-});
-
add_task(async function test_submit_creditCard_with_sync_account() {
await SpecialPowers.pushPrefEnv({
"set": [
[SYNC_USERNAME_PREF, "foo@bar.com"],
[SYNC_CREDITCARDS_AVAILABLE_PREF, true],
],
});
@@ -440,16 +385,18 @@ add_task(async function test_submit_manu
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard(TEST_CREDIT_CARD_3);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "notifyUsed");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.focus();
@@ -462,195 +409,255 @@ add_task(async function test_submit_manu
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
}
);
+ await onUsed;
creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card in storage");
is(creditCards[0]["cc-name"], "User 3", "Verify the name field");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 2, "User has seen the doorhanger");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
add_task(async function test_update_autofill_form_name() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "notifyUsed");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
+ await osKeyStoreLoginShown;
await ContentTask.spawn(browser, null, async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let form = content.document.getElementById("form");
+ let name = form.querySelector("#cc-name");
+ return name.value == "John Doe";
+ }, "Credit card detail never fills");
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.setUserInput("User 1");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
-
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
}
);
+ await onUsed;
creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card");
is(creditCards[0]["cc-name"], "User 1", "cc-name field is updated");
is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
add_task(async function test_update_autofill_form_exp_date() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "notifyUsed");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
await ContentTask.spawn(browser, null, async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let form = content.document.getElementById("form");
+ let name = form.querySelector("#cc-name");
+ return name.value == "John Doe";
+ }, "Credit card detail never fills");
let form = content.document.getElementById("form");
let year = form.querySelector("#cc-exp-year");
year.setUserInput("2020");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
+ await osKeyStoreLoginShown;
}
);
+ await onUsed;
creditCards = await getCreditCards();
is(creditCards.length, 1, "Still 1 credit card");
is(creditCards[0]["cc-exp-year"], "2020", "cc-exp-year field is updated");
is(creditCards[0]["cc-number"], "************1111", "Verify the card number field");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
add_task(async function test_create_new_autofill_form() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
+ let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
await ContentTask.spawn(browser, null, async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let form = content.document.getElementById("form");
+ let name = form.querySelector("#cc-name");
+ return name.value == "John Doe";
+ }, "Credit card detail never fills");
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.setUserInput("User 1");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
await clickDoorhangerButton(SECONDARY_BUTTON);
+ await osKeyStoreLoginShown;
+ await onChanged;
}
);
creditCards = await getCreditCards();
is(creditCards.length, 2, "2 credit cards in storage");
is(creditCards[0]["cc-name"], TEST_CREDIT_CARD_1["cc-name"],
"Original record's cc-name field is unchanged");
is(creditCards[1]["cc-name"], "User 1", "cc-name field in the new record");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 3, "User has used autofill");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
add_task(async function test_update_duplicate_autofill_form() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await SpecialPowers.pushPrefEnv({
"set": [
[CREDITCARDS_USED_STATUS_PREF, 0],
],
});
await saveCreditCard({
"cc-number": "6387060366272981",
});
await saveCreditCard({
"cc-number": "5038146897157463",
});
let creditCards = await getCreditCards();
is(creditCards.length, 2, "2 credit card in storage");
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let onUsed = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "notifyUsed");
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
await openPopupOn(browser, "form #cc-number");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
await ContentTask.spawn(browser, null, async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let form = content.document.getElementById("form");
+ let number = form.querySelector("#cc-number");
+ return number.value == "6387060366272981";
+ }, "Should be the first credit card number");
+
+ // Change number to the second credit card number
let form = content.document.getElementById("form");
let number = form.querySelector("#cc-number");
- is(number.value, "6387060366272981", "Should be the first credit card number");
- // Change number to the second credit card number
number.setUserInput("5038146897157463");
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
+ await osKeyStoreLoginShown;
}
);
+ await onUsed;
creditCards = await getCreditCards();
is(creditCards.length, 2, "Still 2 credit card");
is(SpecialPowers.getIntPref(CREDITCARDS_USED_STATUS_PREF), 1,
"User neither sees the doorhanger nor uses autofill but somehow has a record in the storage");
SpecialPowers.clearUserPref(CREDITCARDS_USED_STATUS_PREF);
await removeAllRecords();
});
add_task(async function test_submit_creditCard_with_invalid_network() {
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
"popupshown");
+ let onChanged = TestUtils.topicObserved("formautofill-storage-changed");
await ContentTask.spawn(browser, null, async function() {
let form = content.document.getElementById("form");
let name = form.querySelector("#cc-name");
name.focus();
name.setUserInput("User 1");
form.querySelector("#cc-number").setUserInput("5038146897157463");
form.querySelector("#cc-exp-month").setUserInput("12");
@@ -659,16 +666,17 @@ add_task(async function test_submit_cred
// Wait 1000ms before submission to make sure the input value applied
await new Promise(resolve => setTimeout(resolve, 1000));
form.querySelector("input[type=submit]").click();
});
await promiseShown;
await clickDoorhangerButton(MAIN_BUTTON);
+ await onChanged;
}
);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
is(creditCards[0]["cc-name"], "User 1", "Verify the name field");
is(creditCards[0]["cc-type"], undefined, "Invalid network/cc-type was not saved");
rename from browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
rename to browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
--- a/browser/extensions/formautofill/test/browser/browser_creditCard_fill_master_password.js
+++ b/browser/extensions/formautofill/test/browser/browser_creditCard_fill_cancel_login.js
@@ -1,25 +1,25 @@
"use strict";
-add_task(async function test_fill_creditCard_with_mp_enabled_but_canceled() {
+add_task(async function test_fill_creditCard_but_cancel_login() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await saveCreditCard(TEST_CREDIT_CARD_2);
- LoginTestUtils.masterPassword.enable();
- registerCleanupFunction(() => {
- LoginTestUtils.masterPassword.disable();
- });
-
- let masterPasswordDialogShown = waitForMasterPasswordDialog(false); // cancel
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); // cancel
await BrowserTestUtils.withNewTab({gBrowser, url: CREDITCARD_FORM_URL},
async function(browser) {
await openPopupOn(browser, "#cc-name");
const ccItem = getDisplayedPopupItems(browser)[0];
await EventUtils.synthesizeMouseAtCenter(ccItem, {});
- await Promise.all([masterPasswordDialogShown, expectPopupClose(browser)]);
+ await Promise.all([osKeyStoreLoginShown, expectPopupClose(browser)]);
await ContentTask.spawn(browser, {}, async function() {
is(content.document.querySelector("#cc-name").value, "", "Check name");
is(content.document.querySelector("#cc-number").value, "", "Check number");
});
}
);
});
--- a/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_manageCreditCardsDialog.js
@@ -1,32 +1,29 @@
"use strict";
const TEST_SELECTORS = {
selRecords: "#credit-cards",
btnRemove: "#remove",
- btnShowHideCreditCards: "#show-hide-credit-cards",
btnAdd: "#add",
btnEdit: "#edit",
};
const DIALOG_SIZE = "width=600,height=400";
add_task(async function test_manageCreditCardsInitialState() {
await BrowserTestUtils.withNewTab({gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL}, async function(browser) {
await ContentTask.spawn(browser, TEST_SELECTORS, (args) => {
let selRecords = content.document.querySelector(args.selRecords);
let btnRemove = content.document.querySelector(args.btnRemove);
- let btnShowHideCreditCards = content.document.querySelector(args.btnShowHideCreditCards);
let btnAdd = content.document.querySelector(args.btnAdd);
let btnEdit = content.document.querySelector(args.btnEdit);
is(selRecords.length, 0, "No credit card");
is(btnRemove.disabled, true, "Remove button disabled");
- is(btnShowHideCreditCards.disabled, true, "Show Credit Cards button disabled");
is(btnAdd.disabled, false, "Add button enabled");
is(btnEdit.disabled, true, "Edit button disabled");
});
});
});
add_task(async function test_cancelManageCreditCardsDialogWithESC() {
let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL);
@@ -102,67 +99,16 @@ add_task(async function test_creditCards
is(selRecords.length, 1, "One credit card is shown");
await removeCreditCards([selRecords.options[0].value]);
await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
is(selRecords.length, 0, "Credit card is removed");
win.close();
});
-add_task(async function test_showCreditCards() {
- await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
- await saveCreditCard(TEST_CREDIT_CARD_1);
- await saveCreditCard(TEST_CREDIT_CARD_2);
- await saveCreditCard(TEST_CREDIT_CARD_3);
-
- let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
- await waitForFocusAndFormReady(win);
-
- let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
- let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
-
- is(btnShowHideCreditCards.disabled, false, "Show credit cards button enabled");
- is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
-
- // Show credit card numbers
- EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
- await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
- is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
- is(selRecords[1].text, "4929001587121045, Timothy Berners-Lee", "Decrypted credit card 2");
- is(selRecords[2].text, "4111111111111111, John Doe", "Decrypted credit card 1");
- is(btnShowHideCreditCards.textContent, "Hide Credit Cards", "Label should be 'Hide Credit Cards'");
-
- // Hide credit card numbers
- EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
- await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
- is(selRecords[0].text, "**** 7870", "Masked credit card 3");
- is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
- is(selRecords[2].text, "**** 1111, John Doe", "Masked credit card 1");
- is(btnShowHideCreditCards.textContent, "Show Credit Cards", "Label should be 'Show Credit Cards'");
-
- // Show credit card numbers again to test if they revert back to masked form when reloaded
- EventUtils.synthesizeMouseAtCenter(btnShowHideCreditCards, {}, win);
- await BrowserTestUtils.waitForEvent(selRecords, "LabelsUpdated");
- // Ensure credit card numbers are shown again
- is(selRecords[0].text, "5103059495477870", "Decrypted credit card 3");
- // Remove a card to trigger reloading
- await removeCreditCards([selRecords.options[2].value]);
- await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
- is(selRecords[0].text, "**** 7870", "Masked credit card 3");
- is(selRecords[1].text, "**** 1045, Timothy Berners-Lee", "Masked credit card 2");
-
- // Remove the rest of the cards
- await removeCreditCards([selRecords.options[1].value]);
- await removeCreditCards([selRecords.options[0].value]);
- await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
- is(btnShowHideCreditCards.disabled, true, "Show credit cards button is disabled when there is no card");
-
- win.close();
-});
-
add_task(async function test_showCreditCardIcons() {
await SpecialPowers.pushPrefEnv({"set": [["privacy.reduceTimerPrecision", false]]});
await saveCreditCard(TEST_CREDIT_CARD_1);
let unknownCard = Object.assign({}, TEST_CREDIT_CARD_3, {"cc-type": "gringotts"});
await saveCreditCard(unknownCard);
let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
await waitForFocusAndFormReady(win);
@@ -188,45 +134,51 @@ add_task(async function test_showCreditC
}
await removeCreditCards([option0.value, option1.value]);
await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded");
is(selRecords.length, 0, "Credit card is removed");
win.close();
});
+add_task(async function test_hasEditLoginPrompt() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ todo(OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), "Cannot test OS key store login on official builds.");
+ return;
+ }
-add_task(async function test_hasMasterPassword() {
await saveCreditCard(TEST_CREDIT_CARD_1);
- LoginTestUtils.masterPassword.enable();
let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL, null, DIALOG_SIZE);
await waitForFocusAndFormReady(win);
let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords);
let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove);
- let btnShowHideCreditCards = win.document.querySelector(TEST_SELECTORS.btnShowHideCreditCards);
let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd);
- let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
- let masterPasswordDialogShown = waitForMasterPasswordDialog();
+ // let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit);
- is(btnShowHideCreditCards.hidden, true, "Show credit cards button is hidden");
+ EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
- // Master password dialog should show when trying to edit a credit card record.
- EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win);
+ // Login dialog should show when trying to edit a credit card record.
+ // TODO: test disabled because re-auth is not implemented yet (bug 1429265).
+ /*
+ let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel
EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win);
- await masterPasswordDialogShown;
+ await osKeyStoreLoginShown;
+ await new Promise(resolve => waitForFocus(resolve, win));
+ await new Promise(resolve => executeSoon(resolve));
+ */
- // Master password is not required for removing credit cards.
+ // Login is not required for removing credit cards.
EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win);
await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved");
is(selRecords.length, 0, "Credit card is removed");
// gSubDialog.open should be called when trying to add a credit card,
- // no master password is required.
+ // no OS login dialog is required.
window.gSubDialog = {
open: url => is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"),
};
EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win);
delete window.gSubDialog;
win.close();
});
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -3,32 +3,31 @@
TEST_ADDRESS_IE_1,
TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3, FORM_URL, CREDITCARD_FORM_URL,
FTU_PREF, ENABLED_AUTOFILL_ADDRESSES_PREF, AUTOFILL_CREDITCARDS_AVAILABLE_PREF, ENABLED_AUTOFILL_CREDITCARDS_PREF,
SUPPORTED_COUNTRIES_PREF,
SYNC_USERNAME_PREF, SYNC_ADDRESSES_PREF, SYNC_CREDITCARDS_PREF, SYNC_CREDITCARDS_AVAILABLE_PREF, CREDITCARDS_USED_STATUS_PREF,
DEFAULT_REGION_PREF,
sleep, expectPopupOpen, openPopupOn, expectPopupClose, closePopup, clickDoorhangerButton,
getAddresses, saveAddress, removeAddresses, saveCreditCard,
- getDisplayedPopupItems, getDoorhangerCheckbox, waitForMasterPasswordDialog,
+ getDisplayedPopupItems, getDoorhangerCheckbox,
getNotification, getDoorhangerButton, removeAllRecords, testDialog */
"use strict";
-ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
-ChromeUtils.import("resource://formautofill/MasterPassword.jsm", this);
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", this);
const MANAGE_ADDRESSES_DIALOG_URL = "chrome://formautofill/content/manageAddresses.xhtml";
const MANAGE_CREDIT_CARDS_DIALOG_URL = "chrome://formautofill/content/manageCreditCards.xhtml";
const EDIT_ADDRESS_DIALOG_URL = "chrome://formautofill/content/editAddress.xhtml";
const EDIT_CREDIT_CARD_DIALOG_URL = "chrome://formautofill/content/editCreditCard.xhtml";
const BASE_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/";
const FORM_URL = "http://mochi.test:8888/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html";
-const CREDITCARD_FORM_URL =
- "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
+const CREDITCARD_FORM_URL = "https://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_creditcard_basic.html";
const FTU_PREF = "extensions.formautofill.firstTimeUse";
const CREDITCARDS_USED_STATUS_PREF = "extensions.formautofill.creditCards.used";
const ENABLED_AUTOFILL_ADDRESSES_PREF = "extensions.formautofill.addresses.enabled";
const AUTOFILL_CREDITCARDS_AVAILABLE_PREF = "extensions.formautofill.creditCards.available";
const ENABLED_AUTOFILL_CREDITCARDS_PREF = "extensions.formautofill.creditCards.enabled";
const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";
const SYNC_USERNAME_PREF = "services.sync.username";
const SYNC_ADDRESSES_PREF = "services.sync.engine.addresses";
@@ -321,34 +320,16 @@ async function clickDoorhangerButton(but
function getDoorhangerCheckbox() {
return getNotification().checkbox;
}
function getDoorhangerButton(button) {
return getNotification()[button];
}
-
-// Wait for the master password dialog to popup and enter the password to log in
-// if "login" is "true" or dismiss it directly if otherwise.
-function waitForMasterPasswordDialog(login = false) {
- info("expecting master password dialog loaded");
- let dialogShown = TestUtils.topicObserved("common-dialog-loaded");
- return dialogShown.then(([subject]) => {
- let dialog = subject.Dialog;
- is(dialog.args.title, "Password Required", "Master password dialog shown");
- if (login) {
- dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
- dialog.ui.button0.click();
- } else {
- dialog.ui.button1.click();
- }
- });
-}
-
async function removeAllRecords() {
let addresses = await getAddresses();
if (addresses.length) {
await removeAddresses(addresses.map(address => address.guid));
}
let creditCards = await getCreditCards();
if (creditCards.length) {
@@ -361,19 +342,26 @@ async function waitForFocusAndFormReady(
new Promise(resolve => waitForFocus(resolve, win)),
BrowserTestUtils.waitForEvent(win, "FormReady"),
]);
}
async function testDialog(url, testFn, arg = undefined) {
if (url == EDIT_CREDIT_CARD_DIALOG_URL && arg && arg.record) {
arg.record = Object.assign({}, arg.record, {
- "cc-number": await MasterPassword.decrypt(arg.record["cc-number-encrypted"]),
+ "cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
});
}
let win = window.openDialog(url, null, "width=600,height=600", arg);
await waitForFocusAndFormReady(win);
let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
await testFn(win);
return unloadPromise;
}
+add_task(function setup() {
+ OSKeyStoreTestUtils.setup();
+});
+
registerCleanupFunction(removeAllRecords);
+registerCleanupFunction(async () => {
+ await OSKeyStoreTestUtils.cleanup();
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/fixtures/OSKeyStoreTestUtils.jsm
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "OSKeyStoreTestUtils",
+];
+
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", this);
+// TODO: Consider AppConstants.MOZILLA_OFFICIAL to decide if we could test re-auth (bug 1429265).
+/*
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+*/
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+ChromeUtils.import("resource://testing-common/TestUtils.jsm");
+
+var OSKeyStoreTestUtils = {
+ /*
+ TEST_ONLY_REAUTH: "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin",
+ */
+
+ setup() {
+ // TODO: run tests with master password enabled to ensure NSS-implemented
+ // key store prompts on re-auth (bug 1429265)
+ /*
+ LoginTestUtils.masterPassword.enable();
+ */
+
+ this.ORIGINAL_STORE_LABEL = OSKeyStore.STORE_LABEL;
+ OSKeyStore.STORE_LABEL = "test-" + Math.random().toString(36).substr(2);
+ },
+
+ async cleanup() {
+ // TODO: run tests with master password enabled to ensure NSS-implemented
+ // key store prompts on re-auth (bug 1429265)
+ /*
+ LoginTestUtils.masterPassword.disable();
+ */
+
+ await OSKeyStore.cleanup();
+ OSKeyStore.STORE_LABEL = this.ORIGINAL_STORE_LABEL;
+ },
+
+ canTestOSKeyStoreLogin() {
+ // TODO: return true based on whether or not we could test the prompt on
+ // the platform (bug 1429265).
+ /*
+ return OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+ */
+ return true;
+ },
+
+ // Wait for the master password dialog to popup and enter the password to log in
+ // if "login" is "true" or dismiss it directly if otherwise.
+ async waitForOSKeyStoreLogin(login = false) {
+ // TODO: Always resolves for now, because we are skipping re-auth on all
+ // platforms (bug 1429265).
+ /*
+ if (OSKeyStore.isNSSKeyStore) {
+ await this.waitForMasterPasswordDialog(login);
+ return;
+ }
+
+ const str = login ? "pass" : "cancel";
+
+ Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, str);
+
+ await TestUtils.topicObserved("oskeystore-testonly-reauth",
+ (subject, data) => data == str);
+
+ Services.prefs.setStringPref(this.TEST_ONLY_REAUTH, "");
+ */
+ },
+
+ async waitForMasterPasswordDialog(login = false) {
+ let [subject] = await TestUtils.topicObserved("common-dialog-loaded");
+
+ let dialog = subject.Dialog;
+ if (dialog.args.title !== "Password Required") {
+ throw new Error("Incorrect master password dialog title");
+ }
+
+ if (login) {
+ dialog.ui.password1Textbox.value = LoginTestUtils.masterPassword.masterPassword;
+ dialog.ui.button0.click();
+ } else {
+ dialog.ui.button1.click();
+ }
+ await TestUtils.waitForTick();
+ },
+};
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -1,10 +1,11 @@
/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/SimpleTest.js */
/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
+/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/AddTask.js */
/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
/* eslint-disable no-unused-vars */
"use strict";
let formFillChromeScript;
let defaultTextColor;
let expectingPopup = null;
@@ -196,16 +197,25 @@ async function cleanUpCreditCards() {
return invokeAsyncChromeTask("FormAutofillTest:CleanUpCreditCards", "FormAutofillTest:CreditCardsCleanedUp");
}
async function cleanUpStorage() {
await cleanUpAddresses();
await cleanUpCreditCards();
}
+async function canTestOSKeyStoreLogin() {
+ let {canTest} = await invokeAsyncChromeTask("FormAutofillTest:CanTestOSKeyStoreLogin", "FormAutofillTest:CanTestOSKeyStoreLoginResult");
+ return canTest;
+}
+
+async function waitForOSKeyStoreLogin(login = false) {
+ await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", "FormAutofillTest:OSKeyStoreLoggedIn", {login});
+}
+
function patchRecordCCNumber(record) {
const number = record["cc-number"];
const ccNumberFmt = {
affix: "****",
label: number.substr(-4),
};
return Object.assign({}, record, {ccNumberFmt});
@@ -258,16 +268,22 @@ function formAutoFillCommonSetup() {
formFillChromeScript = SpecialPowers.loadChromeScript(chromeURL);
formFillChromeScript.addMessageListener("onpopupshown", ({results}) => {
gLastAutoCompleteResults = results;
if (gPopupShownListener) {
gPopupShownListener({results});
}
});
+ add_task(async function setup() {
+ formFillChromeScript.sendAsyncMessage("setup");
+ info(`expecting the storage setup`);
+ await formFillChromeScript.promiseOneMessage("setup-finished");
+ });
+
SimpleTest.registerCleanupFunction(async () => {
formFillChromeScript.sendAsyncMessage("cleanup");
info(`expecting the storage cleanup`);
await formFillChromeScript.promiseOneMessage("cleanup-finished");
formFillChromeScript.destroy();
expectingPopup = null;
});
--- a/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_parent_utils.js
@@ -1,16 +1,19 @@
// assert is available to chrome scripts loaded via SpecialPowers.loadChromeScript.
/* global assert */
/* eslint-env mozilla/frame-script */
"use strict";
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
const {ADDRESSES_COLLECTION_NAME, CREDITCARDS_COLLECTION_NAME} = FormAutofillUtils;
var ParentUtils = {
async _getRecords(collectionName) {
return new Promise(resolve => {
@@ -109,19 +112,25 @@ var ParentUtils = {
if (guids.length == 0) {
sendAsyncMessage("FormAutofillTest:CreditCardsCleanedUp");
return;
}
await this.operateCreditCard("remove", {guids}, "FormAutofillTest:CreditCardsCleanedUp");
},
+ setup() {
+ OSKeyStoreTestUtils.setup();
+ },
+
async cleanup() {
await this.cleanUpAddresses();
await this.cleanUpCreditCards();
+ await OSKeyStoreTestUtils.cleanup();
+
Services.obs.removeObserver(this, "formautofill-storage-changed");
},
_areRecordsMatching(recordA, recordB, collectionName) {
for (let field of formAutofillStorage[collectionName].VALID_FIELDS) {
if (recordA[field] !== recordB[field]) {
return false;
}
@@ -210,13 +219,28 @@ addMessageListener("FormAutofillTest:Rem
addMessageListener("FormAutofillTest:CheckCreditCards", (msg) => {
ParentUtils.checkCreditCards(msg);
});
addMessageListener("FormAutofillTest:CleanUpCreditCards", (msg) => {
ParentUtils.cleanUpCreditCards();
});
-addMessageListener("cleanup", () => {
- ParentUtils.cleanup().then(() => {
- sendAsyncMessage("cleanup-finished", {});
- });
+addMessageListener("FormAutofillTest:CanTestOSKeyStoreLogin", (msg) => {
+ sendAsyncMessage("FormAutofillTest:CanTestOSKeyStoreLoginResult",
+ {canTest: OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL});
+});
+
+addMessageListener("FormAutofillTest:OSKeyStoreLogin", async (msg) => {
+ await OSKeyStoreTestUtils.waitForOSKeyStoreLogin(msg.login);
+ sendAsyncMessage("FormAutofillTest:OSKeyStoreLoggedIn");
});
+
+addMessageListener("setup", async () => {
+ ParentUtils.setup();
+ sendAsyncMessage("setup-finished", {});
+});
+
+addMessageListener("cleanup", async () => {
+ await ParentUtils.cleanup();
+
+ sendAsyncMessage("cleanup-finished", {});
+});
--- a/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_creditcard_autocomplete_form.html
@@ -145,41 +145,60 @@ add_task(async function check_search_res
await setInput("#cc-name", "");
synthesizeKey("KEY_ArrowDown");
await expectPopup();
checkMenuEntries(["John Smith"], false);
await SpecialPowers.popPrefEnv();
});
+let canTest;
+
// Autofill the credit card from dropdown menu.
add_task(async function check_fields_after_form_autofill() {
+ canTest = await canTestOSKeyStoreLogin();
+ if (!canTest) {
+ todo(canTest, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
await setInput("#cc-exp-year", 202);
synthesizeKey("KEY_ArrowDown");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(cc => JSON.stringify({
primary: cc["cc-exp-year"],
secondary: cc.ccNumberFmt.affix + cc.ccNumberFmt.label,
})));
synthesizeKey("KEY_ArrowDown");
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ await new Promise(resolve => SimpleTest.executeSoon(resolve));
await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]);
+ await osKeyStoreLoginShown;
});
// Fallback to history search after autofill address.
add_task(async function check_fallback_after_form_autofill() {
+ if (!canTest) {
+ return;
+ }
+
await setInput("#cc-name", "", true);
synthesizeKey("KEY_ArrowDown");
await expectPopup();
checkMenuEntries(["John Smith"], false);
});
// Resume form autofill once all the autofilled fileds are changed.
add_task(async function check_form_autofill_resume() {
+ if (!canTest) {
+ return;
+ }
+
document.querySelector("#cc-name").blur();
document.querySelector("#form1").reset();
await setInput("#cc-name", "");
synthesizeKey("KEY_ArrowDown");
await expectPopup();
checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(cc => JSON.stringify({
primary: cc["cc-name"],
--- a/browser/extensions/formautofill/test/mochitest/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_clear_form.html
@@ -98,19 +98,26 @@ add_task(async function clear_modified_f
await setInput("#tel", "+1111111111", true);
await triggerPopupAndHoverItem("#street-address", 0);
await confirmClear("#street-address");
checkIsFormCleared({tel: "+1111111111"});
});
add_task(async function clear_distinct_section() {
+ if (!(await canTestOSKeyStoreLogin())) {
+ todo(false, "Cannot test OS key store login on official builds.");
+ return;
+ }
+
document.getElementById("form1").reset();
await triggerPopupAndHoverItem("#cc-name", 0);
+ let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
+ await osKeyStoreLoginShown;
await triggerPopupAndHoverItem("#organization", 0);
await triggerAutofillAndCheckProfile(MOCK_ADDR_STORAGE[0]);
await triggerPopupAndHoverItem("#street-address", 0);
await confirmClear("#street-address");
for (const [id, val] of Object.entries(MOCK_CC_STORAGE[0])) {
const element = document.getElementById(id);
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -94,17 +94,17 @@ async function initProfileStorage(fileNa
let onChanged = TestUtils.topicObserved(
"formautofill-storage-changed",
(subject, data) =>
data == "add" &&
subject.wrappedJSObject.collectionName == collectionName
);
for (let record of records) {
- Assert.ok(profileStorage[collectionName].add(record));
+ Assert.ok(await profileStorage[collectionName].add(record));
await onChanged;
}
await profileStorage._saveImmediately();
return profileStorage;
}
function verifySectionFieldDetails(sections, expectedResults) {
Assert.equal(sections.length, expectedResults.length, "Expected section count.");
@@ -218,8 +218,18 @@ add_task(async function head_initialize(
Services.prefs.clearUserPref("extensions.formautofill.creditCards.available");
Services.prefs.clearUserPref("extensions.formautofill.heuristics.enabled");
Services.prefs.clearUserPref("extensions.formautofill.section.enabled");
Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
});
await loadExtension();
});
+
+let OSKeyStoreTestUtils;
+add_task(async function os_key_store_setup() {
+ ({OSKeyStoreTestUtils} =
+ ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm", {}));
+ OSKeyStoreTestUtils.setup();
+ registerCleanupFunction(async function cleanup() {
+ await OSKeyStoreTestUtils.cleanup();
+ });
+});
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -1,19 +1,18 @@
/*
* Test for form auto fill content helper fill all inputs function.
*/
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
-let MasterPassword;
add_task(async function setup() {
ChromeUtils.import("resource://formautofill/FormAutofillHandler.jsm");
- ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
+ ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
});
const TESTCASES = [
{
description: "Form without autocomplete property",
document: `<form><input id="given-name"><input id="family-name">
<input id="street-addr"><input id="city"><select id="country"></select>
<input id='email'><input id="tel"></form>`,
@@ -472,38 +471,31 @@ const TESTCASES_FILL_SELECT = [
function do_test(testcases, testFn) {
for (let tc of testcases) {
(function() {
let testcase = tc;
add_task(async function() {
info("Starting testcase: " + testcase.description);
let ccNumber = testcase.profileData["cc-number"];
if (ccNumber) {
- testcase.profileData["cc-number-encrypted"] = await MasterPassword.encrypt(ccNumber);
+ testcase.profileData["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
delete testcase.profileData["cc-number"];
}
let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
testcase.document);
let form = doc.querySelector("form");
let formLike = FormLikeFactory.createFromForm(form);
let handler = new FormAutofillHandler(formLike);
let promises = [];
- // Replace the interal decrypt method with MasterPassword API
+ // Replace the internal decrypt method with OSKeyStore API,
+ // but don't pass the reauth parameter to avoid triggering
+ // reauth login dialog in these tests.
let decryptHelper = async (cipherText, reauth) => {
- let string;
- try {
- string = await MasterPassword.decrypt(cipherText, reauth);
- } catch (e) {
- if (e.result != Cr.NS_ERROR_ABORT) {
- throw e;
- }
- info("User canceled master password entry");
- }
- return string;
+ return OSKeyStore.decrypt(cipherText, false);
};
handler.collectFormFields();
let focusedInput = doc.getElementById(testcase.focusedInputId);
handler.focusedInput = focusedInput;
for (let section of handler.sections) {
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -311,17 +311,17 @@ add_task(async function test_add() {
let creditCards = await profileStorage.creditCards.getAll();
Assert.equal(creditCards.length, 2);
do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
Assert.notEqual(creditCards[0].guid, undefined);
- Assert.equal(creditCards[0].version, 1);
+ Assert.equal(creditCards[0].version, 2);
Assert.notEqual(creditCards[0].timeCreated, undefined);
Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated);
Assert.equal(creditCards[0].timeLastUsed, 0);
Assert.equal(creditCards[0].timesUsed, 0);
// Empty string should be deleted before saving.
await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
let creditCard = profileStorage.creditCards._data[2];
@@ -417,16 +417,24 @@ add_task(async function test_update() {
creditCard = profileStorage.creditCards._data[0];
Assert.equal(creditCard["cc-number"],
CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
await profileStorage.creditCards.update(profileStorage.creditCards._data[1].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, true);
creditCard = profileStorage.creditCards._data[1];
Assert.equal(creditCard["cc-number"],
CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
+ // Decryption failure of existing record should not prevent it from being updated.
+ creditCard = profileStorage.creditCards._data[0];
+ creditCard["cc-number-encrypted"] = "INVALID";
+ await profileStorage.creditCards.update(profileStorage.creditCards._data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD, false);
+ creditCard = profileStorage.creditCards._data[0];
+ Assert.equal(creditCard["cc-number"],
+ CreditCard.getLongMaskedNumber(TEST_CREDIT_CARD_WITH_EMPTY_COMPUTED_FIELD["cc-number"]));
+
await Assert.rejects(profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
/No matching record\./
);
await Assert.rejects(profileStorage.creditCards.update(guid, TEST_CREDIT_CARD_WITH_INVALID_FIELD),
/"invalidField" is not a valid field\./
);
@@ -650,23 +658,17 @@ add_task(async function test_getDuplicat
Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
// Numbers with the same last 4 digits shouldn't be treated as a duplicate.
record = Object.assign({}, TEST_CREDIT_CARD_3);
let last4Digits = record["cc-number"].substr(-4);
// This number differs from TEST_CREDIT_CARD_3 by swapping the order of the
// 09 and 90 adjacent digits, which is still a valid credit card number.
record["cc-number"] = "358999378390" + last4Digits;
- Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
- // ... However, we treat numbers with the same last 4 digits as a duplicate if
- // the master password is enabled.
- let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(Ci.nsIPK11TokenDB);
- let token = tokendb.getInternalKeyToken();
- token.reset();
- token.initPassword("password");
+ // We treat numbers with the same last 4 digits as a duplicate.
Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), guid);
- // ... Even though the master password is enabled and the last 4 digits are the
- // same, an invalid credit card number should never be treated as a duplicate.
+ // Even though the last 4 digits are the same, an invalid credit card number
+ // should never be treated as a duplicate.
record["cc-number"] = "************" + last4Digits;
Assert.equal(await profileStorage.creditCards.getDuplicateGuid(record), null);
});
--- a/browser/extensions/formautofill/test/unit/test_getRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_getRecords.js
@@ -2,19 +2,20 @@
* Test for make sure getRecords can retrieve right collection from storage.
*/
"use strict";
ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
let FormAutofillParent;
+let OSKeyStore;
add_task(async function setup() {
({FormAutofillParent} = ChromeUtils.import("resource://formautofill/FormAutofillParent.jsm", {}));
- ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
+ ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
});
const TEST_ADDRESS_1 = {
"given-name": "Timothy",
"additional-name": "John",
"family-name": "Berners-Lee",
organization: "World Wide Web Consortium",
"street-address": "32 Vassar Street\nMIT Room 32-G524",
@@ -171,84 +172,63 @@ add_task(async function test_getRecords_
let formAutofillParent = new FormAutofillParent();
await formAutofillParent.init();
await formAutofillParent.formAutofillStorage.initialize();
let collection = formAutofillParent.formAutofillStorage.creditCards;
let encryptedCCRecords = await Promise.all([TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2].map(async record => {
let clonedRecord = Object.assign({}, record);
clonedRecord["cc-number"] = CreditCard.getLongMaskedNumber(record["cc-number"]);
- clonedRecord["cc-number-encrypted"] = await MasterPassword.encrypt(record["cc-number"]);
+ clonedRecord["cc-number-encrypted"] = await OSKeyStore.encrypt(record["cc-number"]);
return clonedRecord;
}));
sinon.stub(collection, "getAll", () =>
Promise.resolve([Object.assign({}, encryptedCCRecords[0]), Object.assign({}, encryptedCCRecords[1])]));
- let CreditCardsWithDecryptedNumber = [
- Object.assign({}, encryptedCCRecords[0], {"cc-number-decrypted": TEST_CREDIT_CARD_1["cc-number"]}),
- Object.assign({}, encryptedCCRecords[1], {"cc-number-decrypted": TEST_CREDIT_CARD_2["cc-number"]}),
- ];
let testCases = [
{
- description: "If the search string could match 1 creditCard (without masterpassword)",
- filter: {
- collectionName: "creditCards",
- info: {fieldName: "cc-name"},
- searchString: "John Doe",
- },
- expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
- },
- {
- description: "If the search string could match multiple creditCards (without masterpassword)",
+ description: "If the search string could match multiple creditCards",
filter: {
collectionName: "creditCards",
info: {fieldName: "cc-name"},
searchString: "John",
},
- expectedResult: CreditCardsWithDecryptedNumber,
+ expectedResult: encryptedCCRecords,
},
{
- description: "If the search string could not match any creditCard (without masterpassword)",
+ description: "If the search string could not match any creditCard",
filter: {
collectionName: "creditCards",
info: {fieldName: "cc-name"},
searchString: "T",
},
expectedResult: [],
},
{
- description: "If the search number string could match 1 creditCard (without masterpassword)",
- filter: {
- collectionName: "creditCards",
- info: {fieldName: "cc-number"},
- searchString: "411",
- },
- expectedResult: CreditCardsWithDecryptedNumber.slice(0, 1),
- },
- {
- description: "If the search string could match multiple creditCards (without masterpassword)",
+ description: "Return all creditCards if focused field is cc number; " +
+ "if the search string could match multiple creditCards",
filter: {
collectionName: "creditCards",
info: {fieldName: "cc-number"},
searchString: "4",
},
- expectedResult: CreditCardsWithDecryptedNumber,
+ expectedResult: encryptedCCRecords,
},
{
- description: "If the search string could match 1 creditCard (with masterpassword)",
+ description: "If the search string could match 1 creditCard",
filter: {
collectionName: "creditCards",
info: {fieldName: "cc-name"},
searchString: "John Doe",
},
mpEnabled: true,
expectedResult: encryptedCCRecords.slice(0, 1),
},
{
- description: "Return all creditCards if focused field is cc number (with masterpassword)",
+ description: "Return all creditCards if focused field is cc number",
filter: {
collectionName: "creditCards",
info: {fieldName: "cc-number"},
searchString: "411",
},
mpEnabled: true,
expectedResult: encryptedCCRecords,
},
deleted file mode 100644
--- a/browser/extensions/formautofill/test/unit/test_masterPassword.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Tests of MasterPassword.jsm
- */
-
-"use strict";
-const {MockRegistrar} =
- ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", {});
-
-let MasterPassword;
-add_task(async function setup() {
- ({MasterPassword} = ChromeUtils.import("resource://formautofill/MasterPassword.jsm", {}));
-});
-
-const TESTCASES = [{
- description: "With master password set",
- masterPassword: "fakemp",
- mpEnabled: true,
-},
-{
- description: "Without master password set",
- masterPassword: "", // "" means no master password
- mpEnabled: false,
-}];
-
-
-// Tests that PSM can successfully ask for a password from the user and relay it
-// back to NSS. Does so by mocking out the actual dialog and "filling in" the
-// password. Also tests that providing an incorrect password will fail (well,
-// technically the user will just get prompted again, but if they then cancel
-// the dialog the overall operation will fail).
-
-let gMockPrompter = {
- passwordToTry: null,
- numPrompts: 0,
-
- // This intentionally does not use arrow function syntax to avoid an issue
- // where in the context of the arrow function, |this != gMockPrompter| due to
- // how objects get wrapped when going across xpcom boundaries.
- promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
- this.numPrompts++;
- if (this.numPrompts > 1) { // don't keep retrying a bad password
- return false;
- }
- equal(text,
- "Please enter your master password.",
- "password prompt text should be as expected");
- equal(checkMsg, null, "checkMsg should be null");
- ok(this.passwordToTry, "passwordToTry should be non-null");
- password.value = this.passwordToTry;
- return true;
- },
-
- QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
-};
-
-// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
-// to call promptPassword. We return the mock one, above.
-let gWindowWatcher = {
- getNewPrompter: () => gMockPrompter,
- QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
-};
-
-// Ensure that the appropriate initialization has happened.
-do_get_profile();
-
-let windowWatcherCID =
- MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
- gWindowWatcher);
-registerCleanupFunction(() => {
- MockRegistrar.unregister(windowWatcherCID);
-});
-
-TESTCASES.forEach(testcase => {
- add_task(async function test_encrypt_decrypt() {
- info("Starting testcase: " + testcase.description);
-
- let token = MasterPassword._token;
- token.initPassword(testcase.masterPassword);
-
- // Test only: Force the token login without asking for master password
- token.login(/* force */ false);
- Assert.equal(testcase.mpEnabled, token.isLoggedIn(), "Token should now be logged into");
- Assert.equal(MasterPassword.isEnabled, testcase.mpEnabled);
-
- let testText = "test string";
- let cipherText = await MasterPassword.encrypt(testText);
- Assert.notEqual(testText, cipherText);
- let plainText = await MasterPassword.decrypt(cipherText);
- Assert.equal(testText, plainText);
- if (token.isLoggedIn()) {
- // Reset state.
- gMockPrompter.numPrompts = 0;
- token.logoutSimple();
-
- ok(!token.isLoggedIn(),
- "Token should be logged out after calling logoutSimple()");
-
- // Try with the correct password.
- gMockPrompter.passwordToTry = testcase.masterPassword;
- await MasterPassword.encrypt(testText);
- Assert.equal(gMockPrompter.numPrompts, 1, "should have prompted for encryption");
-
- // Reset state.
- gMockPrompter.numPrompts = 0;
- token.logoutSimple();
-
- try {
- // Try with the incorrect password.
- gMockPrompter.passwordToTry = "XXX";
- await MasterPassword.decrypt(cipherText);
- throw new Error("Not receiving canceled master password error");
- } catch (e) {
- Assert.equal(e.message, "User canceled master password entry");
- }
- }
-
- token.reset();
- });
-});
--- a/browser/extensions/formautofill/test/unit/test_migrateRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_migrateRecords.js
@@ -1,24 +1,27 @@
/**
* Tests the migration algorithm in profileStorage.
*/
"use strict";
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+
let FormAutofillStorage;
-
+let OSKeyStore;
add_task(async function setup() {
({FormAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {}));
+ ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
});
const TEST_STORE_FILE_NAME = "test-profile.json";
const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
const ADDRESS_TESTCASES = [
{
description: "The record version is equal to the current version. The migration shouldn't be invoked.",
record: {
guid: "test-guid",
version: ADDRESS_SCHEMA_VERSION,
"given-name": "Timothy",
@@ -241,27 +244,98 @@ let do_check_record_matches = (expectedR
};
add_task(async function test_migrateAddressRecords() {
let path = getTempFile(TEST_STORE_FILE_NAME).path;
let profileStorage = new FormAutofillStorage(path);
await profileStorage.initialize();
- await Promise.all(ADDRESS_TESTCASES.map(async testcase => {
+ for (let testcase of ADDRESS_TESTCASES) {
info(testcase.description);
- await profileStorage.addresses._migrateRecord(testcase.record);
- do_check_record_matches(testcase.expectedResult, testcase.record);
- }));
+ profileStorage._store.data.addresses = [testcase.record];
+ await profileStorage.addresses._migrateRecord(testcase.record, 0);
+ do_check_record_matches(testcase.expectedResult, profileStorage.addresses._data[0]);
+ }
});
add_task(async function test_migrateCreditCardRecords() {
let path = getTempFile(TEST_STORE_FILE_NAME).path;
let profileStorage = new FormAutofillStorage(path);
await profileStorage.initialize();
- await Promise.all(CREDIT_CARD_TESTCASES.map(async testcase => {
+ for (let testcase of CREDIT_CARD_TESTCASES) {
info(testcase.description);
- await profileStorage.creditCards._migrateRecord(testcase.record);
- do_check_record_matches(testcase.expectedResult, testcase.record);
- }));
+ profileStorage._store.data.creditCards = [testcase.record];
+ await profileStorage.creditCards._migrateRecord(testcase.record, 0);
+ do_check_record_matches(testcase.expectedResult, profileStorage.creditCards._data[0]);
+ }
});
+
+add_task(async function test_migrateEncryptedCreditCardNumber() {
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+ let profileStorage = new FormAutofillStorage(path);
+ await profileStorage.initialize();
+
+ const ccNumber = "4111111111111111";
+ let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"]
+ .createInstance(Ci.nsILoginManagerCrypto);
+
+ info("Encrypted credit card should be migrated from v1 to v2");
+
+ let record = {
+ guid: "test-guid",
+ version: 1,
+ "cc-name": "Timothy",
+ "cc-number-encrypted": cryptoSDR.encrypt(ccNumber),
+ };
+
+ let expectedRecord = {
+ guid: "test-guid",
+ version: CREDIT_CARD_SCHEMA_VERSION,
+ "cc-name": "Timothy",
+ "cc-given-name": "Timothy",
+ };
+ profileStorage._store.data.creditCards = [record];
+ await profileStorage.creditCards._migrateRecord(record, 0);
+ record = profileStorage.creditCards._data[0];
+
+ Assert.equal(expectedRecord.guid, record.guid);
+ Assert.equal(expectedRecord.version, record.version);
+ Assert.equal(expectedRecord["cc-name"], record["cc-name"]);
+ Assert.equal(expectedRecord["cc-given-name"], record["cc-given-name"]);
+
+ // Ciphertext of OS Key Store is not stable, must compare decrypted text here.
+ Assert.equal(ccNumber, await OSKeyStore.decrypt(record["cc-number-encrypted"]));
+});
+
+add_task(async function test_migrateEncryptedCreditCardNumberWithMP() {
+ LoginTestUtils.masterPassword.enable();
+
+ let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+ let profileStorage = new FormAutofillStorage(path);
+ await profileStorage.initialize();
+
+ info("Encrypted credit card should be migrated a tombstone if MP is enabled");
+
+ let record = {
+ guid: "test-guid",
+ version: 1,
+ "cc-name": "Timothy",
+ "cc-number-encrypted": "(encrypted to be discarded)",
+ };
+
+ profileStorage._store.data.creditCards = [record];
+ await profileStorage.creditCards._migrateRecord(record, 0);
+ record = profileStorage.creditCards._data[0];
+
+ Assert.equal(record.guid, "test-guid");
+ Assert.equal(record.deleted, true);
+ Assert.equal(typeof record.version, "undefined");
+ Assert.equal(typeof record["cc-name"], "undefined");
+ Assert.equal(typeof record["cc-number-encrypted"], "undefined");
+
+ LoginTestUtils.masterPassword.disable();
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_osKeyStore.js
@@ -0,0 +1,144 @@
+/**
+ * Tests of OSKeyStore.jsm
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://testing-common/MockRegistrar.jsm");
+
+let OSKeyStore;
+add_task(async function setup() {
+ ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
+});
+
+// Ensure that the appropriate initialization has happened.
+do_get_profile();
+
+// For NSS key store, mocking out the dialog and control it from here.
+let gMockPrompter = {
+ passwordToTry: "hunter2",
+ resolve: null,
+ login: undefined,
+
+ // This intentionally does not use arrow function syntax to avoid an issue
+ // where in the context of the arrow function, |this != gMockPrompter| due to
+ // how objects get wrapped when going across xpcom boundaries.
+ promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
+ equal(text,
+ "Please enter your master password.",
+ "password prompt text should be as expected");
+ equal(checkMsg, null, "checkMsg should be null");
+ if (this.login) {
+ password.value = this.passwordToTry;
+ }
+ this.resolve();
+ this.resolve = null;
+
+ return this.login;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
+};
+
+// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
+// to call promptPassword. We return the mock one, above.
+let gWindowWatcher = {
+ getNewPrompter: () => gMockPrompter,
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIWindowWatcher]),
+};
+
+let nssToken;
+
+const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
+
+async function waitForReauth(login = false) {
+ if (OSKeyStore.isNSSKeyStore) {
+ gMockPrompter.login = login;
+ await new Promise(resolve => { gMockPrompter.resolve = resolve; });
+
+ return;
+ }
+
+ let value = login ? "pass" : "cancel";
+ Services.prefs.setStringPref(TEST_ONLY_REAUTH, value);
+ await TestUtils.topicObserved("oskeystore-testonly-reauth",
+ (subject, data) => data == value);
+}
+
+const testText = "test string";
+let cipherText;
+
+add_task(async function test_encrypt_decrypt() {
+ Assert.equal(await OSKeyStore.ensureLoggedIn(), true, "Started logged in.");
+
+ cipherText = await OSKeyStore.encrypt(testText);
+ Assert.notEqual(testText, cipherText);
+
+ let plainText = await OSKeyStore.decrypt(cipherText);
+ Assert.equal(testText, plainText);
+});
+
+// TODO: skipped because re-auth is not implemented (bug 1429265).
+add_task(async function test_reauth() {
+ let canTest = OSKeyStore.isNSSKeyStore || !AppConstants.MOZILLA_OFFICIAL;
+ if (!canTest) {
+ todo_check_false(canTest,
+ "test_reauth: Cannot test OS key store login on official builds.");
+ return;
+ }
+
+ if (OSKeyStore.isNSSKeyStore) {
+ let windowWatcherCID;
+ windowWatcherCID =
+ MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+ gWindowWatcher);
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(windowWatcherCID);
+ });
+
+ // If we use the NSS key store implementation test that everything works
+ // when a master password is set.
+ // Set an initial password.
+ let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Ci.nsIPK11TokenDB);
+ nssToken = tokenDB.getInternalKeyToken();
+ nssToken.initPassword("hunter2");
+ }
+
+ let reauthObserved = waitForReauth(false);
+ await new Promise(resolve => TestUtils.executeSoon(resolve));
+ try {
+ await OSKeyStore.decrypt(cipherText, true);
+ throw new Error("Not receiving canceled OS unlock error");
+ } catch (ex) {
+ Assert.equal(ex.message, "User canceled OS unlock entry");
+ Assert.equal(ex.result, Cr.NS_ERROR_ABORT);
+ }
+ await reauthObserved;
+
+ reauthObserved = waitForReauth(false);
+ await new Promise(resolve => TestUtils.executeSoon(resolve));
+ Assert.equal(await OSKeyStore.ensureLoggedIn(true), false, "Reauth cancelled.");
+ await reauthObserved;
+
+ reauthObserved = waitForReauth(true);
+ await new Promise(resolve => TestUtils.executeSoon(resolve));
+ let plainText2 = await OSKeyStore.decrypt(cipherText, true);
+ await reauthObserved;
+ Assert.equal(testText, plainText2);
+
+ reauthObserved = waitForReauth(true);
+ await new Promise(resolve => TestUtils.executeSoon(resolve));
+ Assert.equal(await OSKeyStore.ensureLoggedIn(true), true, "Reauth logged in.");
+ await reauthObserved;
+}).skip();
+
+add_task(async function test_decryption_failure() {
+ try {
+ await OSKeyStore.decrypt("Malformed cipher text");
+ throw new Error("Not receiving decryption error");
+ } catch (ex) {
+ Assert.notEqual(ex.result, Cr.NS_ERROR_ABORT);
+ }
+});
--- a/browser/extensions/formautofill/test/unit/test_reconcile.js
+++ b/browser/extensions/formautofill/test/unit/test_reconcile.js
@@ -465,220 +465,220 @@ const ADDRESS_RECONCILE_TESTCASES = [
];
const CREDIT_CARD_RECONCILE_TESTCASES = [
{
description: "Local change",
parent: {
// So when we last wrote the record to the server, it had these values.
"guid": "2bbd2d8fbc6b",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
// The current local record - by comparing against parent we can see that
// only the cc-number has changed locally.
"cc-name": "John Doe",
"cc-number": "4929001587121045",
}],
remote: {
// This is the incoming record. It has the same values as "parent", so
// we can deduce the record hasn't actually been changed remotely so we
// can safely ignore the incoming record and write our local changes.
"guid": "2bbd2d8fbc6b",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
reconciled: {
"guid": "2bbd2d8fbc6b",
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
},
{
description: "Remote change",
parent: {
"guid": "e3680e9f890d",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
}],
remote: {
"guid": "e3680e9f890d",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
reconciled: {
"guid": "e3680e9f890d",
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
},
{
description: "New local field",
parent: {
"guid": "0cba738b1be0",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
}],
remote: {
"guid": "0cba738b1be0",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
reconciled: {
"guid": "0cba738b1be0",
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
},
{
description: "New remote field",
parent: {
"guid": "be3ef97f8285",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
}],
remote: {
"guid": "be3ef97f8285",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
reconciled: {
"guid": "be3ef97f8285",
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
},
{
description: "Deleted field locally",
parent: {
"guid": "9627322248ec",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
}],
remote: {
"guid": "9627322248ec",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
reconciled: {
"guid": "9627322248ec",
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
},
{
description: "Deleted field remotely",
parent: {
"guid": "7d7509f3eeb2",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
}],
remote: {
"guid": "7d7509f3eeb2",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
reconciled: {
"guid": "7d7509f3eeb2",
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
},
{
description: "Local and remote changes to unrelated fields",
parent: {
// The last time we wrote this to the server, "cc-exp-month" was 12.
"guid": "e087a06dfc57",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
local: [{
// The current local record - so locally we've changed "cc-number".
"cc-name": "John Doe",
"cc-number": "4929001587121045",
"cc-exp-month": 12,
}],
remote: {
// Remotely, we've changed "cc-exp-month" to 1.
"guid": "e087a06dfc57",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 1,
},
reconciled: {
"guid": "e087a06dfc57",
"cc-name": "John Doe",
"cc-number": "4929001587121045",
"cc-exp-month": 1,
},
},
{
description: "Multiple local changes",
parent: {
"guid": "340a078c596f",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
"cc-name": "Skip",
"cc-number": "4111111111111111",
}, {
"cc-name": "Skip",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
}],
remote: {
"guid": "340a078c596f",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-year": 2000,
},
reconciled: {
"guid": "340a078c596f",
"cc-name": "Skip",
"cc-number": "4111111111111111",
@@ -687,54 +687,54 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
},
},
{
// Local and remote diverged from the shared parent, but the values are the
// same, so we shouldn't fork.
description: "Same change to local and remote",
parent: {
"guid": "0b3a72a1bea2",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
"cc-name": "John Doe",
"cc-number": "4929001587121045",
}],
remote: {
"guid": "0b3a72a1bea2",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
reconciled: {
"guid": "0b3a72a1bea2",
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
},
{
description: "Conflicting changes to single field",
parent: {
// This is what we last wrote to the sync server.
"guid": "62068784d089",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
local: [{
// The current version of the local record - the cc-number has changed locally.
"cc-name": "John Doe",
"cc-number": "5103059495477870",
}],
remote: {
// An incoming record has a different cc-number than any of the above!
"guid": "62068784d089",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
forked: {
// So we've forked the local record to a new GUID (and the next sync is
// going to write this as a new record)
"cc-name": "John Doe",
"cc-number": "5103059495477870",
@@ -745,29 +745,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
"cc-name": "John Doe",
"cc-number": "4929001587121045",
},
},
{
description: "Conflicting changes to multiple fields",
parent: {
"guid": "244dbb692e94",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
local: [{
"cc-name": "John Doe",
"cc-number": "5103059495477870",
"cc-exp-month": 1,
}],
remote: {
"guid": "244dbb692e94",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4929001587121045",
"cc-exp-month": 3,
},
forked: {
"cc-name": "John Doe",
"cc-number": "5103059495477870",
"cc-exp-month": 1,
@@ -778,28 +778,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
"cc-number": "4929001587121045",
"cc-exp-month": 3,
},
},
{
description: "Field deleted locally, changed remotely",
parent: {
"guid": "6fc45e03d19a",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
}],
remote: {
"guid": "6fc45e03d19a",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 3,
},
forked: {
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
@@ -809,29 +809,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
"cc-number": "4111111111111111",
"cc-exp-month": 3,
},
},
{
description: "Field changed locally, deleted remotely",
parent: {
"guid": "fff9fa27fa18",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 12,
},
local: [{
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 3,
}],
remote: {
"guid": "fff9fa27fa18",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
},
forked: {
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"cc-exp-month": 3,
},
@@ -843,28 +843,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
},
{
// Created, last modified should be synced; last used and times used should
// be local. Remote created time older than local, remote modified time
// newer than local.
description: "Created, last modified time reconciliation without local changes",
parent: {
"guid": "5113f329c42f",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"timeCreated": 1234,
"timeLastModified": 5678,
"timeLastUsed": 5678,
"timesUsed": 6,
},
local: [],
remote: {
"guid": "5113f329c42f",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"timeCreated": 1200,
"timeLastModified": 5700,
"timeLastUsed": 5700,
"timesUsed": 3,
},
reconciled: {
@@ -878,31 +878,31 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
},
},
{
// Local changes, remote created time newer than local, remote modified time
// older than local.
description: "Created, last modified time reconciliation with local changes",
parent: {
"guid": "791e5608b80a",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"timeCreated": 1234,
"timeLastModified": 5678,
"timeLastUsed": 5678,
"timesUsed": 6,
},
local: [{
"cc-name": "John Doe",
"cc-number": "4929001587121045",
}],
remote: {
"guid": "791e5608b80a",
- "version": 1,
+ "version": 2,
"cc-name": "John Doe",
"cc-number": "4111111111111111",
"timeCreated": 1300,
"timeLastModified": 5000,
"timeLastUsed": 5000,
"timesUsed": 3,
},
reconciled: {
@@ -917,17 +917,17 @@ const CREDIT_CARD_RECONCILE_TESTCASES =
];
add_task(async function test_reconcile_unknown_version() {
let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
// Cross-version reconciliation isn't supported yet. See bug 1377204.
await Assert.rejects(profileStorage.addresses.reconcile({
"guid": "31d83d2725ec",
- "version": 2,
+ "version": 3,
"given-name": "Mark",
"family-name": "Hammond",
}), /Got unknown record version/);
});
add_task(async function test_reconcile_idempotent() {
let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -33,20 +33,20 @@ support-files =
[test_getCategoriesFromFieldNames.js]
[test_getFormInputDetails.js]
[test_getInfo.js]
[test_getRecords.js]
[test_isAvailable.js]
[test_isCJKName.js]
[test_isFieldEligibleForAutofill.js]
[test_markAsAutofillField.js]
-[test_masterPassword.js]
[test_migrateRecords.js]
[test_nameUtils.js]
[test_onFormSubmitted.js]
+[test_osKeyStore.js]
[test_parseAddressFormat.js]
[test_profileAutocompleteResult.js]
[test_phoneNumber.js]
[test_reconcile.js]
[test_savedFieldNames.js]
[test_toOneLineAddress.js]
[test_storage_tombstones.js]
[test_storage_remove.js]
--- a/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -60,16 +60,18 @@
#include "nsIAsyncVerifyRedirectCallback.h"
#include "mozilla/Preferences.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/NullPrincipal.h"
#include <stdint.h>
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
#include "nsContentUtils.h"
#include "nsJSUtils.h"
#include "nsILoadInfo.h"
#include "nsIDOMXULCommandDispatcher.h"
#include "nsITreeSelection.h"
// This should be probably defined on some other place... but I couldn't find it
#define WEBAPPS_PERM_NAME "webapps-manage"
@@ -487,16 +489,25 @@ nsScriptSecurityManager::ContentSecurity
nsCOMPtr<nsIContentSecurityPolicy> csp;
nsresult rv = subjectPrincipal->GetCsp(getter_AddRefs(csp));
NS_ASSERTION(NS_SUCCEEDED(rv), "CSP: Failed to get CSP from principal.");
// don't do anything unless there's a CSP
if (!csp)
return true;
+ nsCOMPtr<nsICSPEventListener> cspEventListener;
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate =
+ mozilla::dom::GetWorkerPrivateFromContext(cx);
+ if (workerPrivate) {
+ cspEventListener = workerPrivate->CSPEventListener();
+ }
+ }
+
bool evalOK = true;
bool reportViolation = false;
rv = csp->GetAllowsEval(&reportViolation, &evalOK);
if (NS_FAILED(rv))
{
NS_WARNING("CSP: failed to get allowsEval");
return true; // fail open to not break sites.
@@ -524,16 +535,17 @@ nsScriptSecurityManager::ContentSecurity
if (const char *file = scriptFilename.get()) {
CopyUTF8toUTF16(nsDependentCString(file), fileName);
}
} else {
MOZ_ASSERT(!JS_IsExceptionPending(cx));
}
csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
nullptr, // triggering element
+ cspEventListener,
fileName,
scriptSample,
lineNum,
columnNum,
EmptyString(),
EmptyString());
}
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -12373,16 +12373,17 @@ nsIDocument::InlineScriptAllowedByCSP()
nsresult rv = NodePrincipal()->GetCsp(getter_AddRefs(csp));
NS_ENSURE_SUCCESS(rv, true);
bool allowsInlineScript = true;
if (csp) {
nsresult rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
EmptyString(), // aNonce
true, // aParserCreated
nullptr, // aTriggeringElement
+ nullptr, // aCSPEventListener
EmptyString(), // FIXME get script sample (bug 1314567)
0, // aLineNumber
0, // aColumnNumber
&allowsInlineScript);
NS_ENSURE_SUCCESS(rv, true);
}
return allowsInlineScript;
}
--- a/dom/canvas/ImageBitmap.cpp
+++ b/dom/canvas/ImageBitmap.cpp
@@ -1392,17 +1392,17 @@ ImageBitmap::Create(nsIGlobalObject* aGl
RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
if (aCropRect.isSome() && (aCropRect->Width() == 0 || aCropRect->Height() == 0)) {
- aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ aRv.Throw(NS_ERROR_RANGE_ERR);
return promise.forget();
}
RefPtr<ImageBitmap> imageBitmap;
if (aSrc.IsHTMLImageElement()) {
MOZ_ASSERT(NS_IsMainThread(),
"Creating ImageBitmap from HTMLImageElement off the main thread.");
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -850,16 +850,17 @@ EventListenerManager::SetEventHandler(ns
}
if (csp) {
bool allowsInlineScript = true;
rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
EmptyString(), // aNonce
true, // aParserCreated (true because attribute event handler)
aElement,
+ nullptr, // nsICSPEventListener
aBody,
lineNum,
columnNum,
&allowsInlineScript);
NS_ENSURE_SUCCESS(rv, rv);
// return early if CSP wants us to block inline scripts
if (!allowsInlineScript) {
--- a/dom/fetch/Fetch.cpp
+++ b/dom/fetch/Fetch.cpp
@@ -398,27 +398,30 @@ private:
}
};
class MainThreadFetchRunnable : public Runnable
{
RefPtr<WorkerFetchResolver> mResolver;
const ClientInfo mClientInfo;
const Maybe<ServiceWorkerDescriptor> mController;
+ nsCOMPtr<nsICSPEventListener> mCSPEventListener;
RefPtr<InternalRequest> mRequest;
public:
MainThreadFetchRunnable(WorkerFetchResolver* aResolver,
const ClientInfo& aClientInfo,
const Maybe<ServiceWorkerDescriptor>& aController,
+ nsICSPEventListener* aCSPEventListener,
InternalRequest* aRequest)
: Runnable("dom::MainThreadFetchRunnable")
, mResolver(aResolver)
, mClientInfo(aClientInfo)
, mController(aController)
+ , mCSPEventListener(aCSPEventListener)
, mRequest(aRequest)
{
MOZ_ASSERT(mResolver);
}
NS_IMETHOD
Run() override
{
@@ -449,16 +452,17 @@ public:
nsAutoCString spec;
if (proxy->GetWorkerPrivate()->GetBaseURI()) {
proxy->GetWorkerPrivate()->GetBaseURI()->GetAsciiSpec(spec);
}
fetch->SetWorkerScript(spec);
fetch->SetClientInfo(mClientInfo);
fetch->SetController(mController);
+ fetch->SetCSPEventListener(mCSPEventListener);
}
RefPtr<AbortSignalImpl> signalImpl =
mResolver->GetAbortSignalForMainThread();
// ...but release it before calling Fetch, because mResolver's callback can
// be called synchronously and they want the mutex, too.
return fetch->Fetch(signalImpl, mResolver);
@@ -581,17 +585,18 @@ FetchRequest(nsIGlobalObject* aGlobal, c
Maybe<ClientInfo> clientInfo(worker->GetClientInfo());
if (clientInfo.isNothing()) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return nullptr;
}
RefPtr<MainThreadFetchRunnable> run =
new MainThreadFetchRunnable(resolver, clientInfo.ref(),
- worker->GetController(), r);
+ worker->GetController(),
+ worker->CSPEventListener(), r);
worker->DispatchToMainThread(run.forget());
}
return p.forget();
}
void
MainThreadFetchResolver::OnResponseAvailableInternal(InternalResponse* aResponse)
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -575,16 +575,26 @@ FetchDriver::HttpFetch(const nsACString&
mPerformanceStorage,
mLoadGroup,
nullptr, /* aCallbacks */
loadFlags,
ios);
}
NS_ENSURE_SUCCESS(rv, rv);
+ if (mCSPEventListener) {
+ nsCOMPtr<nsILoadInfo> loadInfo = chan->GetLoadInfo();
+ if (NS_WARN_IF(!loadInfo)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = loadInfo->SetCspEventListener(mCSPEventListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
// Insert ourselves into the notification callbacks chain so we can set
// headers on redirects.
#ifdef DEBUG
{
nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks;
chan->GetNotificationCallbacks(getter_AddRefs(notificationCallbacks));
MOZ_ASSERT(!notificationCallbacks);
}
@@ -1411,16 +1421,23 @@ void
FetchDriver::SetDocument(nsIDocument* aDocument)
{
// Cannot set document after Fetch() has been called.
MOZ_ASSERT(!mFetchCalled);
mDocument = aDocument;
}
void
+FetchDriver::SetCSPEventListener(nsICSPEventListener* aCSPEventListener)
+{
+ MOZ_ASSERT(!mFetchCalled);
+ mCSPEventListener = aCSPEventListener;
+}
+
+void
FetchDriver::SetClientInfo(const ClientInfo& aClientInfo)
{
MOZ_ASSERT(!mFetchCalled);
mClientInfo.emplace(aClientInfo);
}
void
FetchDriver::SetController(const Maybe<ServiceWorkerDescriptor>& aController)
--- a/dom/fetch/FetchDriver.h
+++ b/dom/fetch/FetchDriver.h
@@ -16,16 +16,17 @@
#include "mozilla/dom/AbortSignal.h"
#include "mozilla/dom/SRIMetadata.h"
#include "mozilla/RefPtr.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/net/ReferrerPolicy.h"
class nsIConsoleReportCollector;
+class nsICSPEventListener;
class nsIDocument;
class nsIEventTarget;
class nsIOutputStream;
class nsILoadGroup;
class nsIPrincipal;
namespace mozilla {
namespace dom {
@@ -115,16 +116,19 @@ public:
nsresult Fetch(AbortSignalImpl* aSignalImpl,
FetchDriverObserver* aObserver);
void
SetDocument(nsIDocument* aDocument);
void
+ SetCSPEventListener(nsICSPEventListener* aCSPEventListener);
+
+ void
SetClientInfo(const ClientInfo& aClientInfo);
void
SetController(const Maybe<ServiceWorkerDescriptor>& aController);
void
SetWorkerScript(const nsACString& aWorkerScirpt)
{
@@ -139,16 +143,17 @@ public:
private:
nsCOMPtr<nsIPrincipal> mPrincipal;
nsCOMPtr<nsILoadGroup> mLoadGroup;
RefPtr<InternalRequest> mRequest;
RefPtr<InternalResponse> mResponse;
nsCOMPtr<nsIOutputStream> mPipeOutputStream;
RefPtr<FetchDriverObserver> mObserver;
nsCOMPtr<nsIDocument> mDocument;
+ nsCOMPtr<nsICSPEventListener> mCSPEventListener;
Maybe<ClientInfo> mClientInfo;
Maybe<ServiceWorkerDescriptor> mController;
nsCOMPtr<nsIChannel> mChannel;
nsAutoPtr<SRICheckDataVerifier> mSRIDataVerifier;
nsCOMPtr<nsIEventTarget> mMainThreadEventTarget;
// This is set only when Fetch is used in workers.
RefPtr<PerformanceStorage> mPerformanceStorage;
--- a/dom/html/HTMLFormElement.cpp
+++ b/dom/html/HTMLFormElement.cpp
@@ -1674,17 +1674,17 @@ HTMLFormElement::GetActionURL(nsIURI** a
rv = NodePrincipal()->GetCsp(getter_AddRefs(csp));
NS_ENSURE_SUCCESS(rv, rv);
if (csp) {
bool permitsFormAction = true;
// form-action is only enforced if explicitly defined in the
// policy - do *not* consult default-src, see:
// http://www.w3.org/TR/CSP2/#directive-default-src
- rv = csp->Permits(this, actionURL,
+ rv = csp->Permits(this, nullptr /* nsICSPEventListener */, actionURL,
nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE,
true, &permitsFormAction);
NS_ENSURE_SUCCESS(rv, rv);
if (!permitsFormAction) {
return NS_ERROR_CSP_FORM_ACTION_VIOLATION;
}
}
--- a/dom/html/HTMLSharedElement.cpp
+++ b/dom/html/HTMLSharedElement.cpp
@@ -170,17 +170,18 @@ SetBaseURIUsingFirstBaseWithHref(nsIDocu
if (NS_FAILED(rv)) {
newBaseURI = nullptr;
}
if (csp && newBaseURI) {
// base-uri is only enforced if explicitly defined in the
// policy - do *not* consult default-src, see:
// http://www.w3.org/TR/CSP2/#directive-default-src
bool cspPermitsBaseURI = true;
- rv = csp->Permits(child->AsElement(), newBaseURI,
+ rv = csp->Permits(child->AsElement(), nullptr /* nsICSPEventListener */,
+ newBaseURI,
nsIContentSecurityPolicy::BASE_URI_DIRECTIVE,
true, &cspPermitsBaseURI);
if (NS_FAILED(rv) || !cspPermitsBaseURI) {
newBaseURI = nullptr;
}
}
aDocument->SetBaseURI(newBaseURI);
aDocument->SetChromeXHRDocBaseURI(nullptr);
--- a/dom/interfaces/security/nsIContentSecurityPolicy.idl
+++ b/dom/interfaces/security/nsIContentSecurityPolicy.idl
@@ -136,16 +136,17 @@ interface nsIContentSecurityPolicy : nsI
* @return
* Whether or not the effects of the inline style should be allowed
* (block the rules if false).
*/
boolean getAllowsInline(in nsContentPolicyType aContentPolicyType,
in AString aNonce,
in boolean aParserCreated,
in Element aTriggeringElement,
+ in nsICSPEventListener aCSPEventListener,
in AString aContentOfPseudoScript,
in unsigned long aLineNumber,
in unsigned long aColumnNumber);
/**
* whether this policy allows eval and eval-like functions
* such as setTimeout("code string", time).
* @param shouldReportViolations
@@ -194,16 +195,17 @@ interface nsIContentSecurityPolicy : nsI
* @param aContent
* (optional) If this is a hash violation, include contents of the inline
* resource in the question so we can recheck the hash in order to
* determine which policies were violated and send the appropriate
* reports.
*/
void logViolationDetails(in unsigned short violationType,
in Element triggeringElement,
+ in nsICSPEventListener aCSPEventListener,
in AString sourceFile,
in AString scriptSample,
in int32_t lineNum,
in int32_t columnNum,
[optional] in AString nonce,
[optional] in AString content);
const unsigned short VIOLATION_TYPE_INLINE_SCRIPT = 1;
@@ -221,23 +223,16 @@ interface nsIContentSecurityPolicy : nsI
* context. Either use
* * aDocument (preferred), or if no document is available, then provide
* * aPrincipal
*/
void setRequestContext(in Document aDocument,
in nsIPrincipal aPrincipal);
/**
- * Called after the CSP object is created to fill in event listener.
- * @param aListener this listener will be notified when a CSP violation event
- * must be dispatched.
- */
- void setEventListener(in nsICSPEventListener aListener);
-
- /**
* Ensure we have a nsIEventTarget to use to label CSPReportSenderRunnable
*/
[noscript] void ensureEventTarget(in nsIEventTarget aEventTarget);
/*
* Checks if a CSP requires Subresource Integrity (SRI)
* for a given nsContentPolicyType.
*/
@@ -278,16 +273,17 @@ interface nsIContentSecurityPolicy : nsI
* "false" allows CSP to fall back to default-src. This function
* behaves the same for both values of canUseDefault when querying
* directives that don't fall-back.
* @return
* Whether or not the provided URI is allowed by CSP under the given
* directive. (block the pending operation if false).
*/
boolean permits(in Element aTriggeringElement,
+ in nsICSPEventListener aCSPEventListener,
in nsIURI aURI,
in CSPDirective aDir,
in boolean aSpecific);
/**
* Delegate method called by the service when sub-elements of the protected
* document are being loaded. Given a bit of information about the request,
* decides whether or not the policy is satisfied.
@@ -295,16 +291,17 @@ interface nsIContentSecurityPolicy : nsI
* Calls to this may trigger violation reports when queried, so
* this value should not be cached.
*
* aOriginalURIIfRedirect must be passed only if this loading is the result
* of a redirect. In this case, aOriginalURIIfRedirect must be the original
* URL.
*/
short shouldLoad(in nsContentPolicyType aContentType,
+ in nsICSPEventListener aCSPEventListener,
in nsIURI aContentLocation,
in nsIURI aRequestOrigin,
in nsISupports aContext,
in ACString aMimeTypeGuess,
in nsIURI aOriginalURIIfRedirect,
in bool aSendViolationReports);
%{ C++
--- a/dom/jsurl/nsJSProtocolHandler.cpp
+++ b/dom/jsurl/nsJSProtocolHandler.cpp
@@ -176,16 +176,17 @@ nsresult nsJSThunk::EvaluateScript(nsICh
rv = principal->GetCsp(getter_AddRefs(csp));
NS_ENSURE_SUCCESS(rv, rv);
if (csp) {
bool allowsInlineScript = true;
rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
EmptyString(), // aNonce
true, // aParserCreated
nullptr, // aElement,
+ nullptr, // nsICSPEventListener
EmptyString(), // aContent
0, // aLineNumber
0, // aColumnNumber
&allowsInlineScript);
//return early if inline scripts are not allowed
if (!allowsInlineScript) {
return NS_ERROR_DOM_RETVAL_UNDEFINED;
--- a/dom/media/CubebUtils.cpp
+++ b/dom/media/CubebUtils.cpp
@@ -53,18 +53,20 @@
#define PREF_AUDIOIPC_STACK_SIZE "media.audioipc.stack_size"
#if (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)) || defined(XP_MACOSX)
#define MOZ_CUBEB_REMOTING
#endif
extern "C" {
+// This must match AudioIpcInitParams in media/audioipc/client/src/lib.rs.
+// TODO: Generate this from the Rust definition rather than duplicating it.
struct AudioIpcInitParams {
- int mServerConnection;
+ mozilla::ipc::FileDescriptor::PlatformHandleType mServerConnection;
size_t mPoolSize;
size_t mStackSize;
void (*mThreadCreateCallback)(const char*);
};
// These functions are provided by audioipc-server crate
extern void* audioipc_server_start();
extern mozilla::ipc::FileDescriptor::PlatformHandleType audioipc_server_new_client(void*);
@@ -406,19 +408,29 @@ void InitAudioIPCConnection()
});
}
#endif
ipc::FileDescriptor CreateAudioIPCConnection()
{
#ifdef MOZ_CUBEB_REMOTING
MOZ_ASSERT(sServerHandle);
- int rawFD = audioipc_server_new_client(sServerHandle);
+ ipc::FileDescriptor::PlatformHandleType rawFD = audioipc_server_new_client(sServerHandle);
ipc::FileDescriptor fd(rawFD);
+ if (!fd.IsValid()) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("audioipc_server_new_client failed"));
+ return ipc::FileDescriptor();
+ }
+ // Close rawFD since FileDescriptor's ctor cloned it.
+ // TODO: Find cleaner cross-platform way to close rawFD.
+#ifdef XP_WIN
+ CloseHandle(rawFD);
+#else
close(rawFD);
+#endif
return fd;
#else
return ipc::FileDescriptor();
#endif
}
cubeb* GetCubebContextUnlocked()
{
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -1773,41 +1773,44 @@ PeerConnectionWrapper.prototype = {
/**
* Checks that we are getting the media streams we expect.
*
* @param {object} stats
* The stats to check from this PeerConnectionWrapper
*/
checkStats : function(stats, twoMachines) {
+ // Allow for clock drift observed on Windows 7. (Bug 979649)
+ const isWin7 = navigator.userAgent.includes("Windows NT 6.1");
+ const clockDriftAllowance = isWin7 ? 1000 : 0;
+
// Use spec way of enumerating stats
var counters = {};
for (let [key, res] of stats) {
info("Checking stats for " + key + " : " + res);
// validate stats
ok(res.id == key, "Coherent stats id");
- var nowish = Date.now();
+ var nowish = Date.now() + clockDriftAllowance;
var minimum = this.whenCreated;
- if (false) { // Bug 1325430 - timestamps aren't working properly in update 49
- // if (!twoMachines) {
+ if (!twoMachines) {
// Bug 1225729: On android, sometimes the first RTCP of the first
// test run gets this value, likely because no RTP has been sent yet.
- if (res.timestamp != 2085978496000) {
+ if (false) {
ok(res.timestamp >= minimum,
"Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
res.timestamp + " >= " + minimum + " (" +
(res.timestamp - minimum) + " ms)");
- ok(res.timestamp <= nowish,
- "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
- res.timestamp + " <= " + nowish + " (" +
- (res.timestamp - nowish) + " ms)");
} else {
- info("Bug 1225729: Uninitialized timestamp (" + res.timestamp +
- "), should be >=" + minimum + " and <= " + nowish);
+ info("FIXME bug 1495446: uncomment the timestamp test case " +
+ "above after RTCP epoch bug 1495446 is fixed.");
}
+ ok(res.timestamp <= nowish,
+ "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
+ res.timestamp + " <= " + nowish + " (" +
+ (res.timestamp - nowish) + " ms)");
}
if (res.isRemote) {
continue;
}
counters[res.type] = (counters[res.type] || 0) + 1;
switch (res.type) {
case "inbound-rtp":
@@ -1834,27 +1837,27 @@ PeerConnectionWrapper.prototype = {
var rem = stats.get(res.remoteId);
ok(rem.isRemote, "Remote is rtcp");
ok(rem.remoteId == res.id, "Remote backlink match");
if(res.type == "outbound-rtp") {
ok(rem.type == "inbound-rtp", "Rtcp is inbound");
ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived");
ok(rem.packetsLost !== undefined, "Rtcp packetsLost");
ok(rem.bytesReceived >= rem.packetsReceived, "Rtcp bytesReceived");
- if (false) { // Bug 1325430 if (!this.disableRtpCountChecking) {
- // no guarantee which one is newer!
- // Note: this must change when we add a timestamp field to remote RTCP reports
- // and make rem.timestamp be the reception time
- if (res.timestamp >= rem.timestamp) {
- ok(rem.packetsReceived <= res.packetsSent, "No more than sent packets");
- } else {
+ if (!this.disableRtpCountChecking) {
+ // no guarantee which one is newer!
+ // Note: this must change when we add a timestamp field to remote RTCP reports
+ // and make rem.timestamp be the reception time
+ if (res.timestamp >= rem.timestamp) {
+ ok(rem.packetsReceived <= res.packetsSent, "No more than sent packets");
+ } else {
info("REVERSED timestamps: rec:" +
- rem.packetsReceived + " time:" + rem.timestamp + " sent:" + res.packetsSent + " time:" + res.timestamp);
- }
- // Else we may have received more than outdated Rtcp packetsSent
+ rem.packetsReceived + " time:" + rem.timestamp + " sent:" + res.packetsSent + " time:" + res.timestamp);
+ }
+ // Else we may have received more than outdated Rtcp packetsSent
ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
}
ok(rem.jitter !== undefined, "Rtcp jitter");
if (rem.roundTripTime) {
ok(rem.roundTripTime > 0,
"Rtcp rtt " + rem.roundTripTime + " >= 0");
ok(rem.roundTripTime < 60000,
"Rtcp rtt " + rem.roundTripTime + " < 1 min");
--- a/dom/script/ScriptLoader.cpp
+++ b/dom/script/ScriptLoader.cpp
@@ -1245,17 +1245,19 @@ CSPAllowsInlineScript(nsIScriptElement*
// query the nonce
nsCOMPtr<Element> scriptContent = do_QueryInterface(aElement);
nsAutoString nonce;
scriptContent->GetAttr(kNameSpaceID_None, nsGkAtoms::nonce, nonce);
bool parserCreated = aElement->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER;
bool allowInlineScript = false;
rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_SCRIPT,
- nonce, parserCreated, scriptContent, EmptyString(),
+ nonce, parserCreated, scriptContent,
+ nullptr /* nsICSPEventListener */,
+ EmptyString(),
aElement->GetScriptLineNumber(),
aElement->GetScriptColumnNumber(),
&allowInlineScript);
return allowInlineScript;
}
ScriptLoadRequest*
ScriptLoader::CreateLoadRequest(ScriptKind aKind,
@@ -2937,16 +2939,17 @@ ScriptLoader::VerifySRI(ScriptLoadReques
loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
nsAutoCString violationURISpec;
mDocument->GetDocumentURI()->GetAsciiSpec(violationURISpec);
uint32_t lineNo = aRequest->Element() ? aRequest->Element()->GetScriptLineNumber() : 0;
uint32_t columnNo = aRequest->Element() ? aRequest->Element()->GetScriptColumnNumber() : 0;
csp->LogViolationDetails(
nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT,
nullptr, // triggering element
+ nullptr, // nsICSPEventListener
NS_ConvertUTF8toUTF16(violationURISpec),
EmptyString(), lineNo, columnNo, EmptyString(), EmptyString());
rv = NS_ERROR_SRI_CORRUPT;
}
}
return rv;
}
--- a/dom/security/CSPEvalChecker.cpp
+++ b/dom/security/CSPEvalChecker.cpp
@@ -15,16 +15,17 @@
using namespace mozilla;
using namespace mozilla::dom;
namespace {
nsresult
CheckInternal(nsIContentSecurityPolicy* aCSP,
+ nsICSPEventListener* aCSPEventListener,
const nsAString& aExpression,
const nsAString& aFileNameString,
uint32_t aLineNum,
uint32_t aColumnNum,
bool* aAllowed)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aAllowed);
@@ -42,16 +43,17 @@ CheckInternal(nsIContentSecurityPolicy*
if (NS_WARN_IF(NS_FAILED(rv))) {
*aAllowed = false;
return rv;
}
if (reportViolation) {
aCSP->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
nullptr, // triggering element
+ aCSPEventListener,
aFileNameString, aExpression, aLineNum,
aColumnNum, EmptyString(), EmptyString());
}
return NS_OK;
}
class WorkerCSPCheckRunnable final : public WorkerMainThreadRunnable
@@ -69,18 +71,19 @@ public:
, mLineNum(aLineNum)
, mColumnNum(aColumnNum)
, mEvalAllowed(false)
{}
bool
MainThreadRun() override
{
- mResult = CheckInternal(mWorkerPrivate->GetCSP(), mExpression,
- mFileNameString, mLineNum, mColumnNum,
+ mResult = CheckInternal(mWorkerPrivate->GetCSP(),
+ mWorkerPrivate->CSPEventListener(),
+ mExpression, mFileNameString, mLineNum, mColumnNum,
&mEvalAllowed);
return true;
}
nsresult
GetResult(bool* aAllowed)
{
MOZ_ASSERT(aAllowed);
@@ -130,17 +133,18 @@ CSPEvalChecker::CheckForWindow(JSContext
uint32_t lineNum = 0;
uint32_t columnNum = 0;
nsAutoString fileNameString;
if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum,
&columnNum)) {
fileNameString.AssignLiteral("unknown");
}
- rv = CheckInternal(csp, aExpression, fileNameString, lineNum, columnNum,
+ rv = CheckInternal(csp, nullptr /* no CSPEventListener for window */,
+ aExpression, fileNameString, lineNum, columnNum,
aAllowEval);
if (NS_WARN_IF(NS_FAILED(rv))) {
*aAllowEval = false;
return rv;
}
return NS_OK;
}
--- a/dom/security/nsCSPContext.cpp
+++ b/dom/security/nsCSPContext.cpp
@@ -113,16 +113,17 @@ BlockedContentSourceToString(nsCSPContex
break;
}
}
/* ===== nsIContentSecurityPolicy impl ====== */
NS_IMETHODIMP
nsCSPContext::ShouldLoad(nsContentPolicyType aContentType,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aContentLocation,
nsIURI* aRequestOrigin,
nsISupports* aRequestContext,
const nsACString& aMimeTypeGuess,
nsIURI* aOriginalURIIfRedirect,
bool aSendViolationReports,
int16_t* outDecision)
{
@@ -173,16 +174,17 @@ nsCSPContext::ShouldLoad(nsContentPolicy
nsCOMPtr<nsIScriptElement> script = do_QueryInterface(aRequestContext);
if (script && script->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER) {
parserCreated = true;
}
}
bool permitted = permitsInternal(dir,
nullptr, // aTriggeringElement
+ aCSPEventListener,
aContentLocation,
aOriginalURIIfRedirect,
nonce,
isPreload,
false, // allow fallback to default-src
aSendViolationReports,
true, // send blocked URI in violation reports
parserCreated);
@@ -197,16 +199,17 @@ nsCSPContext::ShouldLoad(nsContentPolicy
aContentLocation->GetSpecOrDefault().get()));
}
return NS_OK;
}
bool
nsCSPContext::permitsInternal(CSPDirective aDir,
Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aContentLocation,
nsIURI* aOriginalURIIfRedirect,
const nsAString& aNonce,
bool aIsPreload,
bool aSpecific,
bool aSendViolationReports,
bool aSendContentLocationInViolationReports,
bool aParserCreated)
@@ -229,16 +232,17 @@ nsCSPContext::permitsInternal(CSPDirecti
permits = false;
}
// Do not send a report or notify observers if this is a preload - the
// decision may be wrong due to the inability to get the nonce, and will
// incorrectly fail the unit tests.
if (!aIsPreload && aSendViolationReports) {
AsyncReportViolation(aTriggeringElement,
+ aCSPEventListener,
(aSendContentLocationInViolationReports ?
aContentLocation : nullptr),
BlockedContentSource::eUnknown, /* a BlockedContentSource */
aOriginalURIIfRedirect, /* in case of redirect originalURI is not null */
violatedDirective,
p, /* policy index */
EmptyString(), /* no observer subject */
EmptyString(), /* no source file */
@@ -400,16 +404,17 @@ nsCSPContext::GetAllowsEval(bool* outSho
}
return NS_OK;
}
// Helper function to report inline violations
void
nsCSPContext::reportInlineViolation(nsContentPolicyType aContentType,
Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
const nsAString& aNonce,
const nsAString& aContent,
const nsAString& aViolatedDirective,
uint32_t aViolatedPolicyIndex, // TODO, use report only flag for that
uint32_t aLineNumber,
uint32_t aColumnNumber)
{
nsString observerSubject;
@@ -429,33 +434,35 @@ nsCSPContext::reportInlineViolation(nsCo
// use selfURI as the sourceFile
nsAutoCString sourceFile;
if (mSelfURI) {
mSelfURI->GetSpec(sourceFile);
}
AsyncReportViolation(aTriggeringElement,
+ aCSPEventListener,
nullptr, // aBlockedURI
- BlockedContentSource::eInline, // aBloeckedSource
+ BlockedContentSource::eInline, // aBlockedSource
mSelfURI, // aOriginalURI
aViolatedDirective, // aViolatedDirective
aViolatedPolicyIndex, // aViolatedPolicyIndex
observerSubject, // aObserverSubject
NS_ConvertUTF8toUTF16(sourceFile), // aSourceFile
aContent, // aScriptSample
aLineNumber, // aLineNum
aColumnNumber); // aColumnNum
}
NS_IMETHODIMP
nsCSPContext::GetAllowsInline(nsContentPolicyType aContentType,
const nsAString& aNonce,
bool aParserCreated,
Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
const nsAString& aContentOfPseudoScript,
uint32_t aLineNumber,
uint32_t aColumnNumber,
bool* outAllowsInline)
{
*outAllowsInline = true;
MOZ_ASSERT(aContentType == nsContentUtils::InternalContentPolicyTypeToExternal(aContentType),
@@ -506,16 +513,17 @@ nsCSPContext::GetAllowsInline(nsContentP
}
nsAutoString violatedDirective;
bool reportSample = false;
mPolicies[i]->getDirectiveStringAndReportSampleForContentType(aContentType,
violatedDirective,
&reportSample);
reportInlineViolation(aContentType,
aTriggeringElement,
+ aCSPEventListener,
aNonce,
reportSample ? content : EmptyString(),
violatedDirective,
i,
aLineNumber,
aColumnNumber);
}
}
@@ -558,17 +566,18 @@ nsCSPContext::GetAllowsInline(nsContentP
if (!mPolicies[p]->allows(nsIContentPolicy::TYPE_ ## contentPolicyType, \
keyword, nonceOrHash, false)) \
{ \
nsAutoString violatedDirective; \
bool reportSample = false; \
mPolicies[p]->getDirectiveStringAndReportSampleForContentType( \
nsIContentPolicy::TYPE_ ## contentPolicyType, \
violatedDirective, &reportSample); \
- AsyncReportViolation(aTriggeringElement, nullptr, blockedContentSource, \
+ AsyncReportViolation(aTriggeringElement, aCSPEventListener, \
+ nullptr, blockedContentSource, \
nullptr, violatedDirective, p, \
NS_LITERAL_STRING(observerTopic), \
aSourceFile, \
reportSample \
? aScriptSample : EmptyString(), \
aLineNum, aColumnNum); \
} \
PR_END_MACRO; \
@@ -596,16 +605,17 @@ nsCSPContext::GetAllowsInline(nsContentP
* (optional) If this is a hash violation, include contents of the inline
* resource in the question so we can recheck the hash in order to
* determine which policies were violated and send the appropriate
* reports.
*/
NS_IMETHODIMP
nsCSPContext::LogViolationDetails(uint16_t aViolationType,
Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
const nsAString& aSourceFile,
const nsAString& aScriptSample,
int32_t aLineNum,
int32_t aColumnNum,
const nsAString& aNonce,
const nsAString& aContent)
{
for (uint32_t p = 0; p < mPolicies.Length(); p++) {
@@ -687,23 +697,16 @@ nsCSPContext::SetRequestContext(nsIDocum
mQueueUpMessages = false;
}
NS_ASSERTION(mSelfURI, "mSelfURI not available, can not translate 'self' into actual URI");
return NS_OK;
}
NS_IMETHODIMP
-nsCSPContext::SetEventListener(nsICSPEventListener* aEventListener)
-{
- mEventListener = aEventListener;
- return NS_OK;
-}
-
-NS_IMETHODIMP
nsCSPContext::EnsureEventTarget(nsIEventTarget* aEventTarget)
{
NS_ENSURE_ARG(aEventTarget);
// Don't bother if we did have a valid event target (if the csp object is
// tied to a document in SetRequestContext)
if (mEventTarget) {
return NS_OK;
}
@@ -1130,22 +1133,23 @@ nsCSPContext::SendReports(
}
}
return NS_OK;
}
nsresult
nsCSPContext::FireViolationEvent(
Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit)
{
- if (mEventListener) {
+ if (aCSPEventListener) {
nsAutoString json;
if (aViolationEventInit.ToJSON(json)) {
- mEventListener->OnCSPViolationEvent(json);
+ aCSPEventListener->OnCSPViolationEvent(json);
}
}
// 1. If target is not null, and global is a Window, and target’s
// shadow-including root is not global’s associated Document, set target to
// null.
RefPtr<EventTarget> eventTarget = aTriggeringElement;
@@ -1180,30 +1184,32 @@ nsCSPContext::FireViolationEvent(
/**
* Dispatched from the main thread to send reports for one CSP violation.
*/
class CSPReportSenderRunnable final : public Runnable
{
public:
CSPReportSenderRunnable(Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aBlockedURI,
nsCSPContext::BlockedContentSource aBlockedContentSource,
nsIURI* aOriginalURI,
uint32_t aViolatedPolicyIndex,
bool aReportOnlyFlag,
const nsAString& aViolatedDirective,
const nsAString& aObserverSubject,
const nsAString& aSourceFile,
const nsAString& aScriptSample,
uint32_t aLineNum,
uint32_t aColumnNum,
nsCSPContext* aCSPContext)
: mozilla::Runnable("CSPReportSenderRunnable")
, mTriggeringElement(aTriggeringElement)
+ , mCSPEventListener(aCSPEventListener)
, mBlockedURI(aBlockedURI)
, mBlockedContentSource(aBlockedContentSource)
, mOriginalURI(aOriginalURI)
, mViolatedPolicyIndex(aViolatedPolicyIndex)
, mReportOnlyFlag(aReportOnlyFlag)
, mViolatedDirective(aViolatedDirective)
, mSourceFile(aSourceFile)
, mScriptSample(aScriptSample)
@@ -1287,23 +1293,24 @@ class CSPReportSenderRunnable final : pu
blockedContentSource16.get() };
mCSPContext->logToConsole(mReportOnlyFlag ? "CSPROViolationWithURI" :
"CSPViolationWithURI",
params, ArrayLength(params), mSourceFile, mScriptSample,
mLineNum, mColumnNum, nsIScriptError::errorFlag);
}
// 4) fire violation event
- mCSPContext->FireViolationEvent(mTriggeringElement, init);
+ mCSPContext->FireViolationEvent(mTriggeringElement, mCSPEventListener, init);
return NS_OK;
}
private:
RefPtr<Element> mTriggeringElement;
+ nsCOMPtr<nsICSPEventListener> mCSPEventListener;
nsCOMPtr<nsIURI> mBlockedURI;
nsCSPContext::BlockedContentSource mBlockedContentSource;
nsCOMPtr<nsIURI> mOriginalURI;
uint32_t mViolatedPolicyIndex;
bool mReportOnlyFlag;
nsString mViolatedDirective;
nsCOMPtr<nsISupports> mObserverSubject;
nsString mSourceFile;
@@ -1339,31 +1346,33 @@ class CSPReportSenderRunnable final : pu
* a sample of the violating inline script
* @param aLineNum
* source line number of the violation (if available)
* @param aColumnNum
* source column number of the violation (if available)
*/
nsresult
nsCSPContext::AsyncReportViolation(Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aBlockedURI,
BlockedContentSource aBlockedContentSource,
nsIURI* aOriginalURI,
const nsAString& aViolatedDirective,
uint32_t aViolatedPolicyIndex,
const nsAString& aObserverSubject,
const nsAString& aSourceFile,
const nsAString& aScriptSample,
uint32_t aLineNum,
uint32_t aColumnNum)
{
NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
nsCOMPtr<nsIRunnable> task =
new CSPReportSenderRunnable(aTriggeringElement,
+ aCSPEventListener,
aBlockedURI,
aBlockedContentSource,
aOriginalURI,
aViolatedPolicyIndex,
mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(),
aViolatedDirective,
aObserverSubject,
aSourceFile,
@@ -1484,17 +1493,18 @@ nsCSPContext::PermitsAncestry(nsIDocShel
ancestorsArray[a]->GetSpecOrDefault().get()));
}
// omit the ancestor URI in violation reports if cross-origin as per spec
// (it is a violation of the same-origin policy).
bool okToSendAncestor = NS_SecurityCompareURIs(ancestorsArray[a], mSelfURI, true);
bool permits = permitsInternal(nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE,
- nullptr,
+ nullptr, // triggering element
+ nullptr, // nsICSPEventListener
ancestorsArray[a],
nullptr, // no redirect here.
EmptyString(), // no nonce
false, // not a preload.
true, // specific, do not use default-src
true, // send violation reports
okToSendAncestor,
false); // not parser created
@@ -1502,28 +1512,30 @@ nsCSPContext::PermitsAncestry(nsIDocShel
*outPermitsAncestry = false;
}
}
return NS_OK;
}
NS_IMETHODIMP
nsCSPContext::Permits(Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aURI,
CSPDirective aDir,
bool aSpecific,
bool* outPermits)
{
// Can't perform check without aURI
if (aURI == nullptr) {
return NS_ERROR_FAILURE;
}
*outPermits = permitsInternal(aDir,
aTriggeringElement,
+ aCSPEventListener,
aURI,
nullptr, // no original (pre-redirect) URI
EmptyString(), // no nonce
false, // not a preload.
aSpecific,
true, // send violation reports
true, // send blocked URI in violation reports
false); // not parser created
--- a/dom/security/nsCSPContext.h
+++ b/dom/security/nsCSPContext.h
@@ -100,27 +100,29 @@ class nsCSPContext : public nsIContentSe
mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit);
nsresult SendReports(
const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit,
uint32_t aViolatedPolicyIndex);
nsresult FireViolationEvent(
mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit);
enum BlockedContentSource
{
eUnknown,
eInline,
eEval,
eSelf,
};
nsresult AsyncReportViolation(mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aBlockedURI,
BlockedContentSource aBlockedContentSource,
nsIURI* aOriginalURI,
const nsAString& aViolatedDirective,
uint32_t aViolatedPolicyIndex,
const nsAString& aObserverSubject,
const nsAString& aSourceFile,
const nsAString& aScriptSample,
@@ -143,28 +145,30 @@ class nsCSPContext : public nsIContentSe
return std::max(
mozilla::StaticPrefs::security_csp_reporting_script_sample_max_length(),
0);
}
private:
bool permitsInternal(CSPDirective aDir,
mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
nsIURI* aContentLocation,
nsIURI* aOriginalURIIfRedirect,
const nsAString& aNonce,
bool aIsPreload,
bool aSpecific,
bool aSendViolationReports,
bool aSendContentLocationInViolationReports,
bool aParserCreated);
// helper to report inline script/style violations
void reportInlineViolation(nsContentPolicyType aContentType,
mozilla::dom::Element* aTriggeringElement,
+ nsICSPEventListener* aCSPEventListener,
const nsAString& aNonce,
const nsAString& aContent,
const nsAString& aViolatedDirective,
uint32_t aViolatedPolicyIndex,
uint32_t aLineNumber,
uint32_t aColumnNumber);
nsString mReferrer;
@@ -172,17 +176,16 @@ class nsCSPContext : public nsIContentSe
nsTArray<nsCSPPolicy*> mPolicies;
nsCOMPtr<nsIURI> mSelfURI;
nsCOMPtr<nsILoadGroup> mCallingChannelLoadGroup;
nsWeakPtr mLoadingContext;
// The CSP hangs off the principal, so let's store a raw pointer of the principal
// to avoid memory leaks. Within the destructor of the principal we explicitly
// set mLoadingPrincipal to null.
nsIPrincipal* mLoadingPrincipal;
- nsCOMPtr<nsICSPEventListener> mEventListener;
// helper members used to queue up web console messages till
// the windowID becomes available. see flushConsoleMessages()
nsTArray<ConsoleMsgQueueElem> mConsoleMsgQueue;
bool mQueueUpMessages;
nsCOMPtr<nsIEventTarget> mEventTarget;
};
--- a/dom/security/nsCSPService.cpp
+++ b/dom/security/nsCSPService.cpp
@@ -131,16 +131,20 @@ CSPService::ShouldLoad(nsIURI *aContentL
nsCOMPtr<nsISupports> requestContext = aLoadInfo->GetLoadingContext();
nsCOMPtr<nsIPrincipal> requestPrincipal = aLoadInfo->TriggeringPrincipal();
nsCOMPtr<nsIURI> requestOrigin;
nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->LoadingPrincipal();
if (loadingPrincipal) {
loadingPrincipal->GetURI(getter_AddRefs(requestOrigin));
}
+ nsCOMPtr<nsICSPEventListener> cspEventListener;
+ nsresult rv = aLoadInfo->GetCspEventListener(getter_AddRefs(cspEventListener));
+ NS_ENSURE_SUCCESS(rv, rv);
+
if (MOZ_LOG_TEST(gCspPRLog, LogLevel::Debug)) {
MOZ_LOG(gCspPRLog, LogLevel::Debug,
("CSPService::ShouldLoad called for %s",
aContentLocation->GetSpecOrDefault().get()));
}
// default decision, CSP can revise it if there's a policy to enforce
*aDecision = nsIContentPolicy::ACCEPT;
@@ -166,29 +170,29 @@ CSPService::ShouldLoad(nsIURI *aContentL
principal = requestPrincipal;
} else {
principal = node->NodePrincipal();
}
if (!principal) {
// if we can't query a principal, then there is nothing to do.
return NS_OK;
}
- nsresult rv = NS_OK;
// 1) Apply speculate CSP for preloads
bool isPreload = nsContentUtils::IsPreloadType(contentType);
if (isPreload) {
nsCOMPtr<nsIContentSecurityPolicy> preloadCsp;
rv = principal->GetPreloadCsp(getter_AddRefs(preloadCsp));
NS_ENSURE_SUCCESS(rv, rv);
if (preloadCsp) {
// obtain the enforcement decision
rv = preloadCsp->ShouldLoad(contentType,
+ cspEventListener,
aContentLocation,
requestOrigin,
requestContext,
aMimeTypeGuess,
nullptr, // no redirect, aOriginal URL is null.
aLoadInfo->GetSendCSPViolationEvents(),
aDecision);
NS_ENSURE_SUCCESS(rv, rv);
@@ -204,16 +208,17 @@ CSPService::ShouldLoad(nsIURI *aContentL
// 2) Apply actual CSP to all loads
nsCOMPtr<nsIContentSecurityPolicy> csp;
rv = principal->GetCsp(getter_AddRefs(csp));
NS_ENSURE_SUCCESS(rv, rv);
if (csp) {
// obtain the enforcement decision
rv = csp->ShouldLoad(contentType,
+ cspEventListener,
aContentLocation,
requestOrigin,
requestContext,
aMimeTypeGuess,
nullptr, // no redirect, aOriginal URL is null.
aLoadInfo->GetSendCSPViolationEvents(),
aDecision);
NS_ENSURE_SUCCESS(rv, rv);
@@ -267,16 +272,20 @@ CSPService::AsyncOnChannelRedirect(nsICh
net::nsAsyncRedirectAutoCallback autoCallback(callback);
nsCOMPtr<nsIURI> newUri;
nsresult rv = newChannel->GetURI(getter_AddRefs(newUri));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsILoadInfo> loadInfo = oldChannel->GetLoadInfo();
+ nsCOMPtr<nsICSPEventListener> cspEventListener;
+ rv = loadInfo->GetCspEventListener(getter_AddRefs(cspEventListener));
+ NS_ENSURE_SUCCESS(rv, rv);
+
// if no loadInfo on the channel, nothing for us to do
if (!loadInfo) {
return NS_OK;
}
// No need to continue processing if CSP is disabled or if the protocol
// is *not* subject to CSP.
// Please note, the correct way to opt-out of CSP using a custom
@@ -317,16 +326,17 @@ CSPService::AsyncOnChannelRedirect(nsICh
// 1) Apply speculative CSP for preloads
if (isPreload) {
nsCOMPtr<nsIContentSecurityPolicy> preloadCsp;
loadInfo->LoadingPrincipal()->GetPreloadCsp(getter_AddRefs(preloadCsp));
if (preloadCsp) {
// Pass originalURI to indicate the redirect
preloadCsp->ShouldLoad(policyType, // load type per nsIContentPolicy (uint32_t)
+ cspEventListener,
newUri, // nsIURI
nullptr, // nsIURI
requestContext, // nsISupports
EmptyCString(), // ACString - MIME guess
originalUri, // Original nsIURI
true, // aSendViolationReports
&aDecision);
@@ -342,16 +352,17 @@ CSPService::AsyncOnChannelRedirect(nsICh
// 2) Apply actual CSP to all loads
nsCOMPtr<nsIContentSecurityPolicy> csp;
loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
if (csp) {
// Pass originalURI to indicate the redirect
csp->ShouldLoad(policyType, // load type per nsIContentPolicy (uint32_t)
+ cspEventListener,
newUri, // nsIURI
nullptr, // nsIURI
requestContext, // nsISupports
EmptyCString(), // ACString - MIME guess
originalUri, // Original nsIURI
true, // aSendViolationReports
&aDecision);
}
--- a/dom/security/test/unit/test_csp_reports.js
+++ b/dom/security/test/unit/test_csp_reports.js
@@ -105,16 +105,17 @@ function run_test() {
// test that inline script violations cause a report.
makeTest(0, {"blocked-uri": "inline"}, false,
function(csp) {
let inlineOK = true;
inlineOK = csp.getAllowsInline(Ci.nsIContentPolicy.TYPE_SCRIPT,
"", // aNonce
false, // aParserCreated
null, // aTriggeringElement
+ null, // nsICSPEventListener
"", // aContentOfPseudoScript
0, // aLineNumber
0); // aColumnNumber
// this is not a report only policy, so it better block inline scripts
Assert.ok(!inlineOK);
});
@@ -132,42 +133,45 @@ function run_test() {
Assert.ok(!evalOK);
// ... and cause reports to go out
Assert.ok(oReportViolation.value);
if (oReportViolation.value) {
// force the logging, since the getter doesn't.
csp.logViolationDetails(Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL,
null, // aTriggeringElement
+ null, // nsICSPEventListener
selfuri.asciiSpec,
// sending UTF-16 script sample to make sure
// csp report in JSON is not cut-off, please
// note that JSON is UTF8 encoded.
"\u00a3\u00a5\u00b5\u5317\ud841\udf79",
1, // line number
2); // column number
}
});
makeTest(2, {"blocked-uri": "http://blocked.test/foo.js"}, false,
function(csp) {
// shouldLoad creates and sends out the report here.
csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT,
+ null, // nsICSPEventListener
NetUtil.newURI("http://blocked.test/foo.js"),
null, null, null, null, true);
});
// test that inline script violations cause a report in report-only policy
makeTest(3, {"blocked-uri": "inline"}, true,
function(csp) {
let inlineOK = true;
inlineOK = csp.getAllowsInline(Ci.nsIContentPolicy.TYPE_SCRIPT,
"", // aNonce
false, // aParserCreated
null, // aTriggeringElement
+ null, // nsICSPEventListener
"", // aContentOfPseudoScript
0, // aLineNumber
0); // aColumnNumber
// this is a report only policy, so it better allow inline scripts
Assert.ok(inlineOK);
});
@@ -181,56 +185,61 @@ function run_test() {
Assert.ok(evalOK);
// ... but still cause reports to go out
Assert.ok(oReportViolation.value);
if (oReportViolation.value) {
// force the logging, since the getter doesn't.
csp.logViolationDetails(Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_SCRIPT,
null, // aTriggeringElement
+ null, // nsICSPEventListener
selfuri.asciiSpec,
"script sample",
4, // line number
5); // column number
}
});
// test that only the uri's scheme is reported for globally unique identifiers
makeTest(5, {"blocked-uri": "data"}, false,
function(csp) {
var base64data =
"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
"P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
// shouldLoad creates and sends out the report here.
csp.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE,
+ null, // nsICSPEventListener
NetUtil.newURI("data:image/png;base64," + base64data),
null, null, null, null, true);
});
// test that only the uri's scheme is reported for globally unique identifiers
makeTest(6, {"blocked-uri": "intent"}, false,
function(csp) {
// shouldLoad creates and sends out the report here.
csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+ null, // nsICSPEventListener
NetUtil.newURI("intent://mymaps.com/maps?um=1&ie=UTF-8&fb=1&sll"),
null, null, null, null, true);
});
// test fragment removal
var selfSpec = REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self/foo.js";
makeTest(7, {"blocked-uri": selfSpec}, false,
function(csp) {
var uri = NetUtil
// shouldLoad creates and sends out the report here.
csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT,
+ null, // nsICSPEventListener
NetUtil.newURI(selfSpec + "#bar"),
null, null, null, null, true);
});
// test scheme of ftp:
makeTest(8, {"blocked-uri": "ftp://blocked.test/profile.png"}, false,
function(csp) {
// shouldLoad creates and sends out the report here.
csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT,
+ null, // nsICSPEventListener
NetUtil.newURI("ftp://blocked.test/profile.png"),
null, null, null, null, true);
});
}
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -2668,16 +2668,17 @@ LogViolationDetailsRunnable::MainThreadR
{
AssertIsOnMainThread();
nsIContentSecurityPolicy* csp = mWorkerPrivate->GetCSP();
if (csp) {
if (mWorkerPrivate->GetReportCSPViolations()) {
csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL,
nullptr, // triggering element
+ mWorkerPrivate->CSPEventListener(),
mFileName, mScriptSample, mLineNum, mColumnNum,
EmptyString(), EmptyString());
}
}
return true;
}
--- a/dom/workers/ScriptLoader.cpp
+++ b/dom/workers/ScriptLoader.cpp
@@ -216,25 +216,28 @@ ChannelFromScriptURL(nsIPrincipal* princ
parentDoc,
secFlags,
contentPolicyType,
nullptr, // aPerformanceStorage
loadGroup,
nullptr, // aCallbacks
aLoadFlags,
ios);
+ NS_ENSURE_SUCCESS(rv, rv);
} else {
// We must have a loadGroup with a load context for the principal to
// traverse the channel correctly.
MOZ_ASSERT(loadGroup);
MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal));
RefPtr<PerformanceStorage> performanceStorage;
+ nsCOMPtr<nsICSPEventListener> cspEventListener;
if (aWorkerPrivate && !aIsMainScript) {
performanceStorage = aWorkerPrivate->GetPerformanceStorage();
+ cspEventListener = aWorkerPrivate->CSPEventListener();
}
if (aClientInfo.isSome()) {
rv = NS_NewChannel(getter_AddRefs(channel),
uri,
principal,
aClientInfo.ref(),
aController,
@@ -252,20 +255,30 @@ ChannelFromScriptURL(nsIPrincipal* princ
secFlags,
contentPolicyType,
performanceStorage,
loadGroup,
nullptr, // aCallbacks
aLoadFlags,
ios);
}
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (cspEventListener) {
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo();
+ if (NS_WARN_IF(!loadInfo)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = loadInfo->SetCspEventListener(cspEventListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
}
- NS_ENSURE_SUCCESS(rv, rv);
-
if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) {
mozilla::net::ReferrerPolicy referrerPolicy = parentDoc ?
parentDoc->GetReferrerPolicy() : mozilla::net::RP_Unset;
rv = nsContentUtils::SetFetchReferrerURIWithPolicy(principal, parentDoc,
httpChannel, referrerPolicy);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
@@ -1224,16 +1237,17 @@ private:
("Scriptloader::Load, SRI required but not supported in workers"));
nsCOMPtr<nsIContentSecurityPolicy> wcsp;
chanLoadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(wcsp));
MOZ_ASSERT(wcsp, "We should have a CSP for the worker here");
if (wcsp) {
wcsp->LogViolationDetails(
nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT,
nullptr, // triggering element
+ mWorkerPrivate->CSPEventListener(),
aLoadInfo.mURL, EmptyString(), 0, 0, EmptyString(), EmptyString());
}
return NS_ERROR_SRI_CORRUPT;
}
// Update the principal of the worker and its base URI if we just loaded the
// worker's primary script.
if (IsMainWorkerScript()) {
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -1468,17 +1468,16 @@ WorkerPrivate::SetCSP(nsIContentSecurity
{
AssertIsOnMainThread();
if (!aCSP) {
return;
}
aCSP->EnsureEventTarget(mMainThreadEventTarget);
mLoadInfo.mCSP = aCSP;
- EnsureCSPEventListener();
}
nsresult
WorkerPrivate::SetCSPFromHeaderValues(const nsACString& aCSPHeaderValue,
const nsACString& aCSPReportOnlyHeaderValue)
{
AssertIsOnMainThread();
MOZ_DIAGNOSTIC_ASSERT(!mLoadInfo.mCSP);
@@ -1509,17 +1508,16 @@ WorkerPrivate::SetCSPFromHeaderValues(co
bool evalAllowed = false;
bool reportEvalViolations = false;
rv = csp->GetAllowsEval(&reportEvalViolations, &evalAllowed);
NS_ENSURE_SUCCESS(rv, rv);
mLoadInfo.mCSP = csp;
mLoadInfo.mEvalAllowed = evalAllowed;
mLoadInfo.mReportCSPViolations = reportEvalViolations;
- EnsureCSPEventListener();
return NS_OK;
}
void
WorkerPrivate::SetReferrerPolicyFromHeaderValue(const nsACString& aReferrerPolicyHeaderValue)
{
NS_ConvertUTF8toUTF16 headerValue(aReferrerPolicyHeaderValue);
@@ -3434,24 +3432,26 @@ bool
WorkerPrivate::EnsureCSPEventListener()
{
if (!mCSPEventListener) {
mCSPEventListener = WorkerCSPEventListener::Create(this);
if (NS_WARN_IF(!mCSPEventListener)) {
return false;
}
}
-
- if (mLoadInfo.mCSP) {
- mLoadInfo.mCSP->SetEventListener(mCSPEventListener);
- }
-
return true;
}
+nsICSPEventListener*
+WorkerPrivate::CSPEventListener() const
+{
+ MOZ_ASSERT(mCSPEventListener);
+ return mCSPEventListener;
+}
+
void
WorkerPrivate::EnsurePerformanceStorage()
{
AssertIsOnWorkerThread();
if (!mPerformanceStorage) {
mPerformanceStorage = PerformanceStorageWorker::Create(this);
}
--- a/dom/workers/WorkerPrivate.h
+++ b/dom/workers/WorkerPrivate.h
@@ -407,16 +407,19 @@ public:
WorkerDebuggerGlobalScope*
DebuggerGlobalScope() const
{
AssertIsOnWorkerThread();
return mDebuggerScope;
}
+ nsICSPEventListener*
+ CSPEventListener() const;
+
void
SetThread(WorkerThread* aThread);
bool
IsOnWorkerThread() const;
void
AssertIsOnWorkerThread() const
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -2472,16 +2472,26 @@ XMLHttpRequestMainThread::CreateChannel(
nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST,
mPerformanceStorage, // aPerformanceStorage
loadGroup,
nullptr, // aCallbacks
loadFlags);
}
NS_ENSURE_SUCCESS(rv, rv);
+ if (mCSPEventListener) {
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo();
+ if (NS_WARN_IF(!loadInfo)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ rv = loadInfo->SetCspEventListener(mCSPEventListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel));
if (httpChannel) {
rv = httpChannel->SetRequestMethod(mRequestMethod);
NS_ENSURE_SUCCESS(rv, rv);
// Set the initiator type
nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel));
if (timedChannel) {
--- a/dom/xhr/XMLHttpRequestMainThread.h
+++ b/dom/xhr/XMLHttpRequestMainThread.h
@@ -22,16 +22,17 @@
#include "nsJSUtils.h"
#include "nsTArray.h"
#include "nsITimer.h"
#include "nsIPrincipal.h"
#include "nsIScriptObjectPrincipal.h"
#include "nsISizeOfEventTarget.h"
#include "nsIXPConnect.h"
#include "nsIInputStream.h"
+#include "nsIContentSecurityPolicy.h"
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/Maybe.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/NotNull.h"
#include "mozilla/dom/MutableBlobStorage.h"
#include "mozilla/dom/BodyExtractor.h"
@@ -198,24 +199,26 @@ public:
};
XMLHttpRequestMainThread();
void Construct(nsIPrincipal* aPrincipal,
nsIGlobalObject* aGlobalObject,
nsIURI* aBaseURI = nullptr,
nsILoadGroup* aLoadGroup = nullptr,
- PerformanceStorage* aPerformanceStorage = nullptr)
+ PerformanceStorage* aPerformanceStorage = nullptr,
+ nsICSPEventListener* aCSPEventListener = nullptr)
{
MOZ_ASSERT(aPrincipal);
mPrincipal = aPrincipal;
BindToOwner(aGlobalObject);
mBaseURI = aBaseURI;
mLoadGroup = aLoadGroup;
mPerformanceStorage = aPerformanceStorage;
+ mCSPEventListener = aCSPEventListener;
}
void InitParameters(bool aAnon, bool aSystem);
void SetParameters(bool aAnon, bool aSystem)
{
mIsAnon = aAnon || aSystem;
mIsSystem = aSystem;
@@ -541,16 +544,17 @@ protected:
nsCOMPtr<nsIChannel> mChannel;
nsCString mRequestMethod;
nsCOMPtr<nsIURI> mRequestURL;
nsCOMPtr<nsIDocument> mResponseXML;
nsCOMPtr<nsIStreamListener> mXMLParserStreamListener;
RefPtr<PerformanceStorage> mPerformanceStorage;
+ nsCOMPtr<nsICSPEventListener> mCSPEventListener;
// used to implement getAllResponseHeaders()
class nsHeaderVisitor : public nsIHttpHeaderVisitor
{
struct HeaderEntry final
{
nsCString mName;
nsCString mValue;
--- a/dom/xhr/XMLHttpRequestWorker.cpp
+++ b/dom/xhr/XMLHttpRequestWorker.cpp
@@ -862,17 +862,18 @@ Proxy::Init()
return false;
}
mXHR = new XMLHttpRequestMainThread();
mXHR->Construct(mWorkerPrivate->GetPrincipal(),
ownerWindow ? ownerWindow->AsGlobal() : nullptr,
mWorkerPrivate->GetBaseURI(),
mWorkerPrivate->GetLoadGroup(),
- mWorkerPrivate->GetPerformanceStorage());
+ mWorkerPrivate->GetPerformanceStorage(),
+ mWorkerPrivate->CSPEventListener());
mXHR->SetParameters(mMozAnon, mMozSystem);
mXHR->SetClientInfoAndController(mClientInfo, mController);
ErrorResult rv;
mXHRUpload = mXHR->GetUpload(rv);
if (NS_WARN_IF(rv.Failed())) {
mXHR = nullptr;
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -330,19 +330,23 @@ RenderThread::RunEvent(wr::WindowId aWin
aEvent->Run(*this, aWindowId);
aEvent = nullptr;
}
static void
NotifyDidRender(layers::CompositorBridgeParent* aBridge,
wr::WrPipelineInfo aInfo,
TimeStamp aStart,
- TimeStamp aEnd)
+ TimeStamp aEnd,
+ bool aRender)
{
- if (aBridge->GetWrBridge()) {
+ if (aRender && aBridge->GetWrBridge()) {
+ // We call this here to mimic the behavior in LayerManagerComposite, as to
+ // not change what Talos measures. That is, we do not record an empty frame
+ // as a frame.
aBridge->GetWrBridge()->RecordFrame();
}
for (uintptr_t i = 0; i < aInfo.epochs.length; i++) {
aBridge->NotifyPipelineRendered(
aInfo.epochs.data[i].pipeline_id,
aInfo.epochs.data[i].epoch,
aStart,
@@ -371,16 +375,18 @@ RenderThread::UpdateAndRender(wr::Window
auto& renderer = it->second;
if (aRender) {
renderer->UpdateAndRender(aReadbackSize, aReadbackBuffer);
} else {
renderer->Update();
}
+ // Check graphics reset status even when rendering is skipped.
+ renderer->CheckGraphicsResetStatus();
TimeStamp end = TimeStamp::Now();
auto info = renderer->FlushPipelineInfo();
RefPtr<layers::AsyncImagePipelineManager> pipelineMgr =
renderer->GetCompositorBridge()->GetAsyncImagePipelineManager();
// pipelineMgr should always be non-null here because it is only nulled out
// after the WebRenderAPI instance for the CompositorBridgeParent is
@@ -390,17 +396,18 @@ RenderThread::UpdateAndRender(wr::Window
MOZ_ASSERT(pipelineMgr);
pipelineMgr->NotifyPipelinesUpdated(info);
layers::CompositorThreadHolder::Loop()->PostTask(NewRunnableFunction(
"NotifyDidRenderRunnable",
&NotifyDidRender,
renderer->GetCompositorBridge(),
info,
- aStartTime, end
+ aStartTime, end,
+ aRender
));
}
void
RenderThread::Pause(wr::WindowId aWindowId)
{
MOZ_ASSERT(IsInRenderThread());
--- a/gfx/webrender_bindings/RendererOGL.cpp
+++ b/gfx/webrender_bindings/RendererOGL.cpp
@@ -163,32 +163,40 @@ RendererOGL::UpdateAndRender(const Maybe
if (mFrameStartTime) {
uint32_t latencyMs = round((TimeStamp::Now() - mFrameStartTime).ToMilliseconds());
printf_stderr("generate frame latencyMs latencyMs %d\n", latencyMs);
}
// Clear frame start time
mFrameStartTime = TimeStamp();
#endif
+ // TODO: Flush pending actions such as texture deletions/unlocks and
+ // textureHosts recycling.
+
+ return true;
+}
+
+void
+RendererOGL::CheckGraphicsResetStatus()
+{
+ if (!mCompositor || !mCompositor->gl()) {
+ return;
+ }
+
gl::GLContext* gl = mCompositor->gl();
if (gl->IsSupported(gl::GLFeature::robustness)) {
GLenum resetStatus = gl->fGetGraphicsResetStatus();
if (resetStatus == LOCAL_GL_PURGED_CONTEXT_RESET_NV) {
layers::CompositorThreadHolder::Loop()->PostTask(NewRunnableFunction(
"DoNotifyWebRenderContextPurgeRunnable",
&DoNotifyWebRenderContextPurge,
mBridge
));
}
}
-
- // TODO: Flush pending actions such as texture deletions/unlocks and
- // textureHosts recycling.
-
- return true;
}
void
RendererOGL::Pause()
{
mCompositor->Pause();
}
--- a/gfx/webrender_bindings/RendererOGL.h
+++ b/gfx/webrender_bindings/RendererOGL.h
@@ -79,16 +79,19 @@ public:
layers::CompositorBridgeParent* aBridge);
/// This can be called on the render thread only.
void Pause();
/// This can be called on the render thread only.
bool Resume();
+ /// This can be called on the render thread only.
+ void CheckGraphicsResetStatus();
+
layers::SyncObjectHost* GetSyncObject() const;
layers::CompositorBridgeParent* GetCompositorBridge() { return mBridge; }
wr::WrPipelineInfo FlushPipelineInfo();
RenderTextureHost* GetRenderTexture(wr::WrExternalImageId aExternalImageId);
--- a/layout/style/Loader.cpp
+++ b/layout/style/Loader.cpp
@@ -812,16 +812,17 @@ SheetLoadData::VerifySheetReadyToParse(n
nsCOMPtr<nsIContentSecurityPolicy> csp;
loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp));
nsAutoCString spec;
mLoader->mDocument->GetDocumentURI()->GetAsciiSpec(spec);
// line number unknown. mRequestingNode doesn't bear this info.
csp->LogViolationDetails(
nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_STYLE,
nullptr, // triggering element
+ nullptr, // nsICSPEventListener
NS_ConvertUTF8toUTF16(spec), EmptyString(),
0, 0, EmptyString(), EmptyString());
return NS_OK;
}
} else {
nsAutoCString sourceUri;
if (mLoader->mDocument && mLoader->mDocument->GetDocumentURI()) {
mLoader->mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri);
--- a/layout/style/nsStyleUtil.cpp
+++ b/layout/style/nsStyleUtil.cpp
@@ -478,17 +478,18 @@ nsStyleUtil::CSPAllowsInlineStyle(Elemen
if (aElement && aElement->NodeInfo()->NameAtom() == nsGkAtoms::style) {
aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::nonce, nonce);
}
bool allowInlineStyle = true;
rv = csp->GetAllowsInline(nsIContentPolicy::TYPE_STYLESHEET,
nonce,
false, // aParserCreated only applies to scripts
- aElement, aStyleText, aLineNumber, aColumnNumber,
+ aElement, nullptr, // nsICSPEventListener
+ aStyleText, aLineNumber, aColumnNumber,
&allowInlineStyle);
NS_ENSURE_SUCCESS(rv, false);
return allowInlineStyle;
}
void
nsStyleUtil::AppendFontSlantStyle(const FontSlantStyle& aStyle, nsAString& aOut)
--- a/media/audioipc/README_MOZILLA
+++ b/media/audioipc/README_MOZILLA
@@ -1,8 +1,8 @@
The source from this directory was copied from the audioipc-2
git repository using the update.sh script. The only changes
made were those applied by update.sh and the addition of
Makefile.in build files for the Mozilla build system.
The audioipc-2 git repository is: https://github.com/djg/audioipc-2.git
-The git commit ID used was 3d716fe897ccb3ea43a2af0c794ea57c433400d7 (2018-07-30 08:51:04 +1200)
+The git commit ID used was 572d6a6a16501cde726dcc09604a0cbc895d93e3 (2018-10-23 16:43:12 +1300)
--- a/media/audioipc/audioipc/src/async.rs
+++ b/media/audioipc/audioipc/src/async.rs
@@ -1,19 +1,19 @@
// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license. See the
// accompanying file LICENSE for details
//! Various async helpers modelled after futures-rs and tokio-io.
-use {RecvMsg, SendMsg};
use bytes::{Buf, BufMut};
use futures::{Async, Poll};
use iovec::IoVec;
+use msg::{RecvMsg, SendMsg};
use std::io;
use tokio_io::{AsyncRead, AsyncWrite};
use tokio_uds::UnixStream;
pub trait AsyncRecvMsg: AsyncRead {
/// Pull some bytes from this source into the specified `Buf`, returning
/// how many bytes were read.
///
@@ -134,17 +134,17 @@ impl AsyncSendMsg for UnixStream {
}
let r = {
// The `IoVec` type can't have a zero-length size, so create a dummy
// version from a 1-length slice which we'll overwrite with the
// `bytes_vec` method.
static DUMMY: &[u8] = &[0];
let nom = <&IoVec>::from(DUMMY);
let mut bufs = [
- nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom
+ nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom, nom,
];
let n = buf.bytes_vec(&mut bufs);
self.send_msg(&bufs[..n], cmsg.bytes())
};
match r {
Ok(n) => {
buf.advance(n);
Ok(Async::Ready(n))
--- a/media/audioipc/audioipc/src/cmsg.rs
+++ b/media/audioipc/audioipc/src/cmsg.rs
@@ -1,17 +1,17 @@
// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license. See the
// accompanying file LICENSE for details
use bytes::{BufMut, Bytes, BytesMut};
use libc::{self, cmsghdr};
+use std::os::unix::io::RawFd;
use std::{convert, mem, ops, slice};
-use std::os::unix::io::RawFd;
#[derive(Clone, Debug)]
pub struct Fds {
fds: Bytes,
}
impl convert::AsRef<[RawFd]> for Fds {
fn as_ref(&self) -> &[RawFd] {
--- a/media/audioipc/audioipc/src/codec.rs
+++ b/media/audioipc/audioipc/src/codec.rs
@@ -159,17 +159,17 @@ where
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"encoded message too big",
));
}
buf.reserve((encoded_len + 2) as usize);
- buf.put_u16::<LittleEndian>(encoded_len as u16);
+ buf.put_u16_le(encoded_len as u16);
if let Err(e) = bincode::config()
.limit(encoded_len)
.serialize_into::<_, Self::In>(&mut buf.writer(), &item)
{
match *e {
bincode::ErrorKind::Io(e) => return Err(e),
_ => return Err(io::Error::new(io::ErrorKind::Other, *e)),
--- a/media/audioipc/audioipc/src/core.rs
+++ b/media/audioipc/audioipc/src/core.rs
@@ -1,14 +1,19 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
// Ease accessing reactor::Core handles.
+use futures::sync::oneshot;
use futures::{Future, IntoFuture};
-use futures::sync::oneshot;
+use std::sync::mpsc;
use std::{fmt, io, thread};
-use std::sync::mpsc;
use tokio_core::reactor::{Core, Handle, Remote};
scoped_thread_local! {
static HANDLE: Handle
}
pub fn handle() -> Handle {
HANDLE.with(|handle| handle.clone())
--- a/media/audioipc/audioipc/src/errors.rs
+++ b/media/audioipc/audioipc/src/errors.rs
@@ -1,8 +1,13 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
use bincode;
use cubeb;
use std;
error_chain! {
// Maybe replace with chain_err to improve the error info.
foreign_links {
Bincode(bincode::Error);
--- a/media/audioipc/audioipc/src/fd_passing.rs
+++ b/media/audioipc/audioipc/src/fd_passing.rs
@@ -5,19 +5,19 @@
use async::{AsyncRecvMsg, AsyncSendMsg};
use bytes::{Bytes, BytesMut, IntoBuf};
use cmsg;
use codec::Codec;
use futures::{AsyncSink, Poll, Sink, StartSend, Stream};
use libc;
use messages::AssocRawFd;
-use std::{fmt, io, mem};
use std::collections::VecDeque;
use std::os::unix::io::RawFd;
+use std::{fmt, io, mem};
const INITIAL_CAPACITY: usize = 1024;
const BACKPRESSURE_THRESHOLD: usize = 4 * INITIAL_CAPACITY;
const FDS_CAPACITY: usize = 16;
struct IncomingFds {
cmsg: BytesMut,
recv_fds: Option<cmsg::ControlMsgIter>,
@@ -29,17 +29,18 @@ impl IncomingFds {
IncomingFds {
cmsg: BytesMut::with_capacity(capacity),
recv_fds: None,
}
}
pub fn take_fds(&mut self) -> Option<[RawFd; 3]> {
loop {
- let fds = self.recv_fds
+ let fds = self
+ .recv_fds
.as_mut()
.and_then(|recv_fds| recv_fds.next())
.and_then(|fds| Some(clone_into_array(&fds)));
if fds.is_some() {
return fds;
}
@@ -288,20 +289,16 @@ pub fn framed_with_fds<A, C>(io: A, code
frames: VecDeque::new(),
write_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
outgoing_fds: BytesMut::with_capacity(
FDS_CAPACITY * cmsg::space(mem::size_of::<[RawFd; 3]>()),
),
}
}
-fn write_zero() -> io::Error {
- io::Error::new(io::ErrorKind::WriteZero, "failed to write frame to io")
-}
-
fn clone_into_array<A, T>(slice: &[T]) -> A
where
A: Sized + Default + AsMut<[T]>,
T: Clone,
{
let mut a = Default::default();
<A as AsMut<[T]>>::as_mut(&mut a).clone_from_slice(slice);
a
--- a/media/audioipc/audioipc/src/frame.rs
+++ b/media/audioipc/audioipc/src/frame.rs
@@ -142,20 +142,16 @@ where
}
fn close(&mut self) -> Poll<(), Self::SinkError> {
try_ready!(self.poll_complete());
self.io.shutdown()
}
}
-fn write_zero() -> io::Error {
- io::Error::new(io::ErrorKind::WriteZero, "failed to write frame to io")
-}
-
pub fn framed<A, C>(io: A, codec: C) -> Framed<A, C> {
Framed {
io: io,
codec: codec,
read_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
write_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
frame: None,
is_readable: false,
--- a/media/audioipc/audioipc/src/lib.rs
+++ b/media/audioipc/audioipc/src/lib.rs
@@ -1,13 +1,13 @@
// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license. See the
// accompanying file LICENSE for details
-#![allow(dead_code)] // TODO: Remove.
+
#![recursion_limit = "1024"]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
#[macro_use]
@@ -24,109 +24,37 @@ extern crate memmap;
#[macro_use]
extern crate scoped_tls;
extern crate serde;
extern crate tokio_core;
#[macro_use]
extern crate tokio_io;
extern crate tokio_uds;
-pub mod async;
-pub mod cmsg;
+mod async;
+mod cmsg;
pub mod codec;
+pub mod core;
pub mod errors;
pub mod fd_passing;
pub mod frame;
-pub mod rpc;
-pub mod core;
pub mod messages;
mod msg;
+pub mod rpc;
pub mod shm;
-use iovec::IoVec;
-#[cfg(target_os = "linux")]
-use libc::MSG_CMSG_CLOEXEC;
pub use messages::{ClientMessage, ServerMessage};
use std::env::temp_dir;
-use std::io;
-use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use std::path::PathBuf;
-#[cfg(not(target_os = "linux"))]
-const MSG_CMSG_CLOEXEC: libc::c_int = 0;
-
-// Extend sys::os::unix::net::UnixStream to support sending and receiving a single file desc.
-// We can extend UnixStream by using traits, eliminating the need to introduce a new wrapped
-// UnixStream type.
-pub trait RecvMsg {
- fn recv_msg(
- &mut self,
- iov: &mut [&mut IoVec],
- cmsg: &mut [u8],
- ) -> io::Result<(usize, usize, i32)>;
-}
-
-pub trait SendMsg {
- fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize>;
-}
-
-impl<T: AsRawFd> RecvMsg for T {
- fn recv_msg(
- &mut self,
- iov: &mut [&mut IoVec],
- cmsg: &mut [u8],
- ) -> io::Result<(usize, usize, i32)> {
- msg::recv_msg_with_flags(self.as_raw_fd(), iov, cmsg, MSG_CMSG_CLOEXEC)
- }
-}
-
-impl<T: AsRawFd> SendMsg for T {
- fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize> {
- msg::send_msg_with_flags(self.as_raw_fd(), iov, cmsg, 0)
- }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-#[derive(Debug)]
-pub struct AutoCloseFd(RawFd);
-
-impl Drop for AutoCloseFd {
- fn drop(&mut self) {
- unsafe {
- libc::close(self.0);
- }
- }
-}
-
-impl FromRawFd for AutoCloseFd {
- unsafe fn from_raw_fd(fd: RawFd) -> Self {
- AutoCloseFd(fd)
- }
-}
-
-impl IntoRawFd for AutoCloseFd {
- fn into_raw_fd(self) -> RawFd {
- let fd = self.0;
- ::std::mem::forget(self);
- fd
- }
-}
-
-impl AsRawFd for AutoCloseFd {
- fn as_raw_fd(&self) -> RawFd {
- self.0
- }
-}
-
-impl<'a> AsRawFd for &'a AutoCloseFd {
- fn as_raw_fd(&self) -> RawFd {
- self.0
- }
-}
-
-////////////////////////////////////////////////////////////////////////////////
+// This must match the definition of
+// ipc::FileDescriptor::PlatformHandleType in Gecko.
+#[cfg(target_os = "windows")]
+pub type PlatformHandleType = *mut std::os::raw::c_void;
+#[cfg(not(target_os = "windows"))]
+pub type PlatformHandleType = libc::c_int;
pub fn get_shm_path(dir: &str) -> PathBuf {
let pid = unsafe { libc::getpid() };
let mut temp = temp_dir();
temp.push(&format!("cubeb-shm-{}-{}", pid, dir));
temp
}
--- a/media/audioipc/audioipc/src/msg.rs
+++ b/media/audioipc/audioipc/src/msg.rs
@@ -1,13 +1,53 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
+use iovec::unix as iovec;
use iovec::IoVec;
-use iovec::unix as iovec;
use libc;
+use std::os::unix::io::{AsRawFd, RawFd};
use std::{cmp, io, mem, ptr};
-use std::os::unix::io::RawFd;
+
+// Extend sys::os::unix::net::UnixStream to support sending and receiving a single file desc.
+// We can extend UnixStream by using traits, eliminating the need to introduce a new wrapped
+// UnixStream type.
+pub trait RecvMsg {
+ fn recv_msg(
+ &mut self,
+ iov: &mut [&mut IoVec],
+ cmsg: &mut [u8],
+ ) -> io::Result<(usize, usize, i32)>;
+}
+
+pub trait SendMsg {
+ fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize>;
+}
+
+impl<T: AsRawFd> RecvMsg for T {
+ fn recv_msg(
+ &mut self,
+ iov: &mut [&mut IoVec],
+ cmsg: &mut [u8],
+ ) -> io::Result<(usize, usize, i32)> {
+ #[cfg(target_os = "linux")]
+ let flags = libc::MSG_CMSG_CLOEXEC;
+ #[cfg(not(target_os = "linux"))]
+ let flags = 0;
+ recv_msg_with_flags(self.as_raw_fd(), iov, cmsg, flags)
+ }
+}
+
+impl<T: AsRawFd> SendMsg for T {
+ fn send_msg(&mut self, iov: &[&IoVec], cmsg: &[u8]) -> io::Result<usize> {
+ send_msg_with_flags(self.as_raw_fd(), iov, cmsg, 0)
+ }
+}
fn cvt(r: libc::ssize_t) -> io::Result<usize> {
if r == -1 {
Err(io::Error::last_os_error())
} else {
Ok(r as usize)
}
}
--- a/media/audioipc/audioipc/src/rpc/client/mod.rs
+++ b/media/audioipc/audioipc/src/rpc/client/mod.rs
@@ -34,20 +34,20 @@
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
+use futures::sync::oneshot;
use futures::{Async, Future, Poll, Sink, Stream};
-use futures::sync::oneshot;
+use rpc::driver::Driver;
use rpc::Handler;
-use rpc::driver::Driver;
use std::collections::VecDeque;
use std::io;
use tokio_core::reactor::Handle;
mod proxy;
pub use self::proxy::{ClientProxy, Response};
--- a/media/audioipc/audioipc/src/rpc/client/proxy.rs
+++ b/media/audioipc/audioipc/src/rpc/client/proxy.rs
@@ -38,18 +38,18 @@
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
+use futures::sync::{mpsc, oneshot};
use futures::{Async, Future, Poll};
-use futures::sync::{mpsc, oneshot};
use std::fmt;
use std::io;
/// Message used to dispatch requests to the task managing the
/// client connection.
pub type Request<R, Q> = (R, oneshot::Sender<Q>);
/// Receive requests submitted to the client
--- a/media/audioipc/audioipc/src/rpc/mod.rs
+++ b/media/audioipc/audioipc/src/rpc/mod.rs
@@ -1,18 +1,18 @@
// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license. See the
// accompanying file LICENSE for details
use futures::{Poll, Sink, Stream};
use std::io;
+mod client;
mod driver;
-mod client;
mod server;
pub use self::client::{bind_client, Client, ClientProxy, Response};
pub use self::server::{bind_server, Server};
pub trait Handler {
/// Message type read from transport
type In;
--- a/media/audioipc/audioipc/src/rpc/server.rs
+++ b/media/audioipc/audioipc/src/rpc/server.rs
@@ -35,18 +35,18 @@
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
use futures::{Async, Future, Poll, Sink, Stream};
+use rpc::driver::Driver;
use rpc::Handler;
-use rpc::driver::Driver;
use std::collections::VecDeque;
use std::io;
use tokio_core::reactor::Handle;
/// Bind an async I/O object `io` to the `server`.
pub fn bind_server<S>(transport: S::Transport, server: S, handle: &Handle)
where
S: Server,
--- a/media/audioipc/audioipc/src/shm.rs
+++ b/media/audioipc/audioipc/src/shm.rs
@@ -1,8 +1,13 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
use errors::*;
use memmap::{Mmap, MmapViewSync, Protection};
use std::fs::{remove_file, File, OpenOptions};
use std::path::Path;
use std::sync::atomic;
pub struct SharedMemReader {
mmap: Mmap,
--- a/media/audioipc/client/src/context.rs
+++ b/media/audioipc/client/src/context.rs
@@ -1,34 +1,36 @@
// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license. See the
// accompanying file LICENSE for details
-use {ClientStream, CPUPOOL_INIT_PARAMS, G_SERVER_FD};
use assert_not_in_callback;
-use audioipc::{messages, ClientMessage, ServerMessage};
-use audioipc::{core, rpc};
use audioipc::codec::LengthDelimitedCodec;
use audioipc::fd_passing::{framed_with_fds, FramedWithFds};
-use cubeb_backend::{ffi, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceType, Error,
- Ops, Result, Stream, StreamParams, StreamParamsRef};
+use audioipc::{core, rpc};
+use audioipc::{messages, ClientMessage, ServerMessage};
+use cubeb_backend::{
+ ffi, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceType, Error, Ops, Result,
+ Stream, StreamParams, StreamParamsRef,
+};
use futures::Future;
use futures_cpupool::{self, CpuPool};
use libc;
-use std::{fmt, io, mem, ptr};
use std::ffi::{CStr, CString};
use std::os::raw::c_void;
use std::os::unix::io::FromRawFd;
use std::os::unix::net;
use std::sync::mpsc;
use std::thread;
+use std::{fmt, io, mem, ptr};
use stream;
use tokio_core::reactor::{Handle, Remote};
use tokio_uds::UnixStream;
+use {ClientStream, CPUPOOL_INIT_PARAMS, G_SERVER_FD};
struct CubebClient;
impl rpc::Client for CubebClient {
type Request = ServerMessage;
type Response = ClientMessage;
type Transport = FramedWithFds<UnixStream, LengthDelimitedCodec<Self::Request, Self::Response>>;
}
@@ -95,19 +97,17 @@ impl ContextOps for ClientContext {
let _ = tx_rpc.send(rpc);
Some(())
}
assert_not_in_callback();
let (tx_rpc, rx_rpc) = mpsc::channel();
- let params = CPUPOOL_INIT_PARAMS.with(|p| {
- p.replace(None).unwrap()
- });
+ let params = CPUPOOL_INIT_PARAMS.with(|p| p.replace(None).unwrap());
let thread_create_callback = params.thread_create_callback;
let register_thread = move || {
if let Some(func) = thread_create_callback {
let thr = thread::current();
let name = CString::new(thr.name().unwrap()).unwrap();
func(name.as_ptr());
@@ -129,21 +129,21 @@ impl ContextOps for ClientContext {
"Failed to open stream and create rpc.",
)
})
}));
let rpc = t!(rx_rpc.recv());
let cpupool = futures_cpupool::Builder::new()
- .name_prefix("AudioIPC")
- .after_start(register_thread)
- .pool_size(params.pool_size)
- .stack_size(params.stack_size)
- .create();
+ .name_prefix("AudioIPC")
+ .after_start(register_thread)
+ .pool_size(params.pool_size)
+ .stack_size(params.stack_size)
+ .create();
let ctx = Box::new(ClientContext {
_ops: &CLIENT_OPS as *const _,
rpc: rpc,
core: core,
cpu_pool: cpupool,
});
Ok(unsafe { Context::from_ptr(Box::into_raw(ctx) as *mut _) })
--- a/media/audioipc/client/src/lib.rs
+++ b/media/audioipc/client/src/lib.rs
@@ -15,45 +15,49 @@ extern crate log;
extern crate tokio_core;
extern crate tokio_uds;
#[macro_use]
mod send_recv;
mod context;
mod stream;
+use audioipc::PlatformHandleType;
use context::ClientContext;
use cubeb_backend::{capi, ffi};
use std::os::raw::{c_char, c_int};
use std::os::unix::io::RawFd;
use stream::ClientStream;
type InitParamsTls = std::cell::RefCell<Option<CpuPoolInitParams>>;
thread_local!(static IN_CALLBACK: std::cell::RefCell<bool> = std::cell::RefCell::new(false));
thread_local!(static CPUPOOL_INIT_PARAMS: InitParamsTls = std::cell::RefCell::new(None));
+// This must match the definition of AudioIpcInitParams in
+// dom/media/CubebUtils.cpp in Gecko.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct AudioIpcInitParams {
- pub server_connection: c_int,
+ // Fields only need to be public for ipctest.
+ pub server_connection: PlatformHandleType,
pub pool_size: usize,
pub stack_size: usize,
pub thread_create_callback: Option<extern "C" fn(*const ::std::os::raw::c_char)>,
}
#[derive(Clone, Copy, Debug)]
struct CpuPoolInitParams {
- pub pool_size: usize,
- pub stack_size: usize,
- pub thread_create_callback: Option<extern "C" fn(*const ::std::os::raw::c_char)>,
+ pool_size: usize,
+ stack_size: usize,
+ thread_create_callback: Option<extern "C" fn(*const ::std::os::raw::c_char)>,
}
impl CpuPoolInitParams {
- pub fn init_with(params: &AudioIpcInitParams) -> Self {
+ fn init_with(params: &AudioIpcInitParams) -> Self {
CpuPoolInitParams {
pool_size: params.pool_size,
stack_size: params.stack_size,
thread_create_callback: params.thread_create_callback,
}
}
}
--- a/media/audioipc/client/src/send_recv.rs
+++ b/media/audioipc/client/src/send_recv.rs
@@ -1,8 +1,13 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
use cubeb_backend::Error;
use std::os::raw::c_int;
#[doc(hidden)]
pub fn _err<E>(e: E) -> Error
where
E: Into<Option<c_int>>,
{
--- a/media/audioipc/client/src/stream.rs
+++ b/media/audioipc/client/src/stream.rs
@@ -1,31 +1,31 @@
// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license. See the
// accompanying file LICENSE for details
-use {assert_not_in_callback, set_in_callback};
-use ClientContext;
use audioipc::codec::LengthDelimitedCodec;
use audioipc::frame::{framed, Framed};
use audioipc::messages::{self, CallbackReq, CallbackResp, ClientMessage, ServerMessage};
use audioipc::rpc;
use audioipc::shm::{SharedMemMutSlice, SharedMemSlice};
use cubeb_backend::{ffi, DeviceRef, Error, Result, Stream, StreamOps};
use futures::Future;
use futures_cpupool::{CpuFuture, CpuPool};
use std::ffi::CString;
use std::fs::File;
use std::os::raw::c_void;
use std::os::unix::io::FromRawFd;
use std::os::unix::net;
use std::ptr;
use std::sync::mpsc;
use tokio_uds::UnixStream;
+use ClientContext;
+use {assert_not_in_callback, set_in_callback};
// TODO: Remove and let caller allocate based on cubeb backend requirements.
const SHM_AREA_SIZE: usize = 2 * 1024 * 1024;
pub struct Device(ffi::cubeb_device);
impl Drop for Device {
fn drop(&mut self) {
@@ -90,24 +90,24 @@ impl rpc::Server for CallbackServer {
self.cpu_pool.spawn_fn(move || {
// TODO: This is proof-of-concept. Make it better.
let input_ptr: *const u8 = match input_shm {
Some(shm) => shm
.get_slice(nframes as usize * frame_size)
.unwrap()
.as_ptr(),
- None => ptr::null()
+ None => ptr::null(),
};
let output_ptr: *mut u8 = match output_shm {
Some(ref mut shm) => shm
.get_mut_slice(nframes as usize * frame_size)
.unwrap()
.as_mut_ptr(),
- None => ptr::null_mut()
+ None => ptr::null_mut(),
};
set_in_callback(true);
let nframes = unsafe {
cb(
ptr::null_mut(),
user_ptr as *mut c_void,
input_ptr as *const _,
--- a/media/audioipc/server/src/lib.rs
+++ b/media/audioipc/server/src/lib.rs
@@ -1,451 +1,59 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details
+
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
extern crate audioipc;
extern crate bytes;
extern crate cubeb_core as cubeb;
extern crate futures;
extern crate lazycell;
extern crate libc;
extern crate slab;
extern crate tokio_core;
extern crate tokio_uds;
-use audioipc::codec::LengthDelimitedCodec;
use audioipc::core;
-use audioipc::fd_passing::{framed_with_fds, FramedWithFds};
-use audioipc::frame::{framed, Framed};
-use audioipc::messages::{CallbackReq, CallbackResp, ClientMessage, Device, DeviceInfo,
- ServerMessage, StreamCreate, StreamInitParams, StreamParams};
+use audioipc::fd_passing::framed_with_fds;
use audioipc::rpc;
-use audioipc::shm::{SharedMemReader, SharedMemWriter};
-use cubeb::ffi;
-use futures::future::{self, FutureResult};
+use audioipc::PlatformHandleType;
use futures::sync::oneshot;
use futures::Future;
-use std::cell::RefCell;
-use std::convert::From;
use std::error::Error;
-use std::ffi::{CStr, CString};
-use std::mem::{size_of, ManuallyDrop};
-use std::os::raw::{c_long, c_void};
+use std::os::raw::c_void;
+use std::os::unix::io::IntoRawFd;
use std::os::unix::net;
-use std::os::unix::prelude::*;
-use std::{panic, ptr, slice};
-use tokio_core::reactor::Remote;
+use std::ptr;
use tokio_uds::UnixStream;
+mod server;
+
pub mod errors {
error_chain! {
links {
AudioIPC(::audioipc::errors::Error, ::audioipc::errors::ErrorKind);
}
foreign_links {
Cubeb(::cubeb::Error);
Io(::std::io::Error);
Canceled(::futures::sync::oneshot::Canceled);
}
}
}
use errors::*;
-type ContextKey = RefCell<Option<cubeb::Result<cubeb::Context>>>;
-thread_local!(static CONTEXT_KEY:ContextKey = RefCell::new(None));
-
-fn with_local_context<T, F>(f: F) -> T
-where
- F: FnOnce(&cubeb::Result<cubeb::Context>) -> T,
-{
- CONTEXT_KEY.with(|k| {
- let mut context = k.borrow_mut();
- if context.is_none() {
- let name = CString::new("AudioIPC Server").unwrap();
- *context = Some(cubeb::Context::init(Some(name.as_c_str()), None));
- }
- f(context.as_ref().unwrap())
- })
-}
-
-// TODO: Remove and let caller allocate based on cubeb backend requirements.
-const SHM_AREA_SIZE: usize = 2 * 1024 * 1024;
-
-// The size in which the stream slab is grown.
-const STREAM_CONN_CHUNK_SIZE: usize = 64;
-
-struct CallbackClient;
-
-impl rpc::Client for CallbackClient {
- type Request = CallbackReq;
- type Response = CallbackResp;
- type Transport = Framed<UnixStream, LengthDelimitedCodec<Self::Request, Self::Response>>;
-}
-
-struct ServerStreamCallbacks {
- /// Size of input frame in bytes
- input_frame_size: u16,
- /// Size of output frame in bytes
- output_frame_size: u16,
- /// Shared memory buffer for sending input data to client
- input_shm: SharedMemWriter,
- /// Shared memory buffer for receiving output data from client
- output_shm: SharedMemReader,
- /// RPC interface to callback server running in client
- rpc: rpc::ClientProxy<CallbackReq, CallbackResp>,
-}
-
-impl ServerStreamCallbacks {
- fn data_callback(&mut self, input: &[u8], output: &mut [u8]) -> isize {
- trace!("Stream data callback: {} {}", input.len(), output.len());
-
- // len is of input and output is frame len. Turn these into the real lengths.
- let real_input = unsafe {
- let nbytes = input.len() * self.input_frame_size as usize;
- slice::from_raw_parts(input.as_ptr(), nbytes)
- };
-
- self.input_shm.write(real_input).unwrap();
-
- let r = self.rpc
- .call(CallbackReq::Data(
- output.len() as isize,
- self.output_frame_size as usize,
- ))
- .wait();
-
- match r {
- Ok(CallbackResp::Data(frames)) => {
- if frames >= 0 {
- let nbytes = frames as usize * self.output_frame_size as usize;
- let real_output = unsafe {
- trace!("Resize output to {}", nbytes);
- slice::from_raw_parts_mut(output.as_mut_ptr(), nbytes)
- };
- self.output_shm.read(&mut real_output[..nbytes]).unwrap();
- }
- frames
- }
- _ => {
- debug!("Unexpected message {:?} during data_callback", r);
- -1
- }
- }
- }
-
- fn state_callback(&mut self, state: cubeb::State) {
- trace!("Stream state callback: {:?}", state);
- let r = self.rpc.call(CallbackReq::State(state.into())).wait();
- match r {
- Ok(CallbackResp::State) => {}
- _ => {
- debug!("Unexpected message {:?} during callback", r);
- }
- }
- }
-}
-
-struct ServerStream {
- stream: ManuallyDrop<cubeb::Stream>,
- cbs: ManuallyDrop<Box<ServerStreamCallbacks>>,
-}
-
-impl Drop for ServerStream {
- fn drop(&mut self) {
- unsafe {
- ManuallyDrop::drop(&mut self.stream);
- ManuallyDrop::drop(&mut self.cbs);
- }
- }
-}
-
-type StreamSlab = slab::Slab<ServerStream, usize>;
-
-pub struct CubebServer {
- cb_remote: Remote,
- streams: StreamSlab,
-}
-
-impl rpc::Server for CubebServer {
- type Request = ServerMessage;
- type Response = ClientMessage;
- type Future = FutureResult<Self::Response, ()>;
- type Transport = FramedWithFds<UnixStream, LengthDelimitedCodec<Self::Response, Self::Request>>;
-
- fn process(&mut self, req: Self::Request) -> Self::Future {
- let resp = with_local_context(|context| match *context {
- Err(_) => error(cubeb::Error::error()),
- Ok(ref context) => self.process_msg(context, &req),
- });
- future::ok(resp)
- }
-}
-
-impl CubebServer {
- pub fn new(cb_remote: Remote) -> Self {
- CubebServer {
- cb_remote: cb_remote,
- streams: StreamSlab::with_capacity(STREAM_CONN_CHUNK_SIZE),
- }
- }
-
- // Process a request coming from the client.
- fn process_msg(&mut self, context: &cubeb::Context, msg: &ServerMessage) -> ClientMessage {
- let resp: ClientMessage = match *msg {
- ServerMessage::ClientConnect => panic!("already connected"),
-
- ServerMessage::ClientDisconnect => {
- // TODO:
- //self.connection.client_disconnect();
- ClientMessage::ClientDisconnected
- }
-
- ServerMessage::ContextGetBackendId => ClientMessage::ContextBackendId(),
-
- ServerMessage::ContextGetMaxChannelCount => context
- .max_channel_count()
- .map(ClientMessage::ContextMaxChannelCount)
- .unwrap_or_else(error),
-
- ServerMessage::ContextGetMinLatency(ref params) => {
- let format = cubeb::SampleFormat::from(params.format);
- let layout = cubeb::ChannelLayout::from(params.layout);
-
- let params = cubeb::StreamParamsBuilder::new()
- .format(format)
- .rate(u32::from(params.rate))
- .channels(u32::from(params.channels))
- .layout(layout)
- .take();
-
- context
- .min_latency(¶ms)
- .map(ClientMessage::ContextMinLatency)
- .unwrap_or_else(error)
- }
-
- ServerMessage::ContextGetPreferredSampleRate => context
- .preferred_sample_rate()
- .map(ClientMessage::ContextPreferredSampleRate)
- .unwrap_or_else(error),
-
- ServerMessage::ContextGetDeviceEnumeration(device_type) => context
- .enumerate_devices(cubeb::DeviceType::from_bits_truncate(device_type))
- .map(|devices| {
- let v: Vec<DeviceInfo> = devices.iter().map(|i| i.as_ref().into()).collect();
- ClientMessage::ContextEnumeratedDevices(v)
- })
- .unwrap_or_else(error),
-
- ServerMessage::StreamInit(ref params) => self.process_stream_init(context, params)
- .unwrap_or_else(|_| error(cubeb::Error::error())),
-
- ServerMessage::StreamDestroy(stm_tok) => {
- self.streams.remove(stm_tok);
- ClientMessage::StreamDestroyed
- }
-
- ServerMessage::StreamStart(stm_tok) => self.streams[stm_tok]
- .stream
- .start()
- .map(|_| ClientMessage::StreamStarted)
- .unwrap_or_else(error),
-
- ServerMessage::StreamStop(stm_tok) => self.streams[stm_tok]
- .stream
- .stop()
- .map(|_| ClientMessage::StreamStopped)
- .unwrap_or_else(error),
-
- ServerMessage::StreamResetDefaultDevice(stm_tok) => self.streams[stm_tok]
- .stream
- .reset_default_device()
- .map(|_| ClientMessage::StreamDefaultDeviceReset)
- .unwrap_or_else(error),
-
- ServerMessage::StreamGetPosition(stm_tok) => self.streams[stm_tok]
- .stream
- .position()
- .map(ClientMessage::StreamPosition)
- .unwrap_or_else(error),
-
- ServerMessage::StreamGetLatency(stm_tok) => self.streams[stm_tok]
- .stream
- .latency()
- .map(ClientMessage::StreamLatency)
- .unwrap_or_else(error),
-
- ServerMessage::StreamSetVolume(stm_tok, volume) => self.streams[stm_tok]
- .stream
- .set_volume(volume)
- .map(|_| ClientMessage::StreamVolumeSet)
- .unwrap_or_else(error),
-
- ServerMessage::StreamSetPanning(stm_tok, panning) => self.streams[stm_tok]
- .stream
- .set_panning(panning)
- .map(|_| ClientMessage::StreamPanningSet)
- .unwrap_or_else(error),
-
- ServerMessage::StreamGetCurrentDevice(stm_tok) => self.streams[stm_tok]
- .stream
- .current_device()
- .map(|device| ClientMessage::StreamCurrentDevice(Device::from(device)))
- .unwrap_or_else(error),
- };
-
- trace!("process_msg: req={:?}, resp={:?}", msg, resp);
-
- resp
- }
-
- // Stream init is special, so it's been separated from process_msg.
- fn process_stream_init(
- &mut self,
- context: &cubeb::Context,
- params: &StreamInitParams,
- ) -> Result<ClientMessage> {
- fn frame_size_in_bytes(params: Option<&StreamParams>) -> u16 {
- params
- .map(|p| {
- let format = p.format.into();
- let sample_size = match format {
- cubeb::SampleFormat::S16LE
- | cubeb::SampleFormat::S16BE
- | cubeb::SampleFormat::S16NE => 2,
- cubeb::SampleFormat::Float32LE
- | cubeb::SampleFormat::Float32BE
- | cubeb::SampleFormat::Float32NE => 4,
- };
- let channel_count = p.channels as u16;
- sample_size * channel_count
- })
- .unwrap_or(0u16)
- }
-
- // Create the callback handling struct which is attached the cubeb stream.
- let input_frame_size = frame_size_in_bytes(params.input_stream_params.as_ref());
- let output_frame_size = frame_size_in_bytes(params.output_stream_params.as_ref());
-
- let (stm1, stm2) = net::UnixStream::pair()?;
- debug!("Created callback pair: {:?}-{:?}", stm1, stm2);
- let (input_shm, input_file) =
- SharedMemWriter::new(&audioipc::get_shm_path("input"), SHM_AREA_SIZE)?;
- let (output_shm, output_file) =
- SharedMemReader::new(&audioipc::get_shm_path("output"), SHM_AREA_SIZE)?;
-
- // This code is currently running on the Client/Server RPC
- // handling thread. We need to move the registration of the
- // bind_client to the callback RPC handling thread. This is
- // done by spawning a future on cb_remote.
-
- let id = core::handle().id();
-
- let (tx, rx) = oneshot::channel();
- self.cb_remote.spawn(move |handle| {
- // Ensure we're running on a loop different to the one
- // invoking spawn_fn.
- assert_ne!(id, handle.id());
- let stream = UnixStream::from_stream(stm2, handle).unwrap();
- let transport = framed(stream, Default::default());
- let rpc = rpc::bind_client::<CallbackClient>(transport, handle);
- drop(tx.send(rpc));
- Ok(())
- });
-
- let rpc: rpc::ClientProxy<CallbackReq, CallbackResp> = match rx.wait() {
- Ok(rpc) => rpc,
- Err(_) => bail!("Failed to create callback rpc."),
- };
-
- let cbs = Box::new(ServerStreamCallbacks {
- input_frame_size,
- output_frame_size,
- input_shm,
- output_shm,
- rpc,
- });
-
- // Create cubeb stream from params
- let stream_name = params
- .stream_name
- .as_ref()
- .and_then(|name| CStr::from_bytes_with_nul(name).ok());
-
- let input_device = params.input_device as *const _;
- let input_stream_params = params.input_stream_params.as_ref().map(|isp| unsafe {
- cubeb::StreamParamsRef::from_ptr(isp as *const StreamParams as *mut _)
- });
-
- let output_device = params.output_device as *const _;
- let output_stream_params = params.output_stream_params.as_ref().map(|osp| unsafe {
- cubeb::StreamParamsRef::from_ptr(osp as *const StreamParams as *mut _)
- });
-
- let latency = params.latency_frames;
- assert!(size_of::<Box<ServerStreamCallbacks>>() == size_of::<usize>());
- let user_ptr = cbs.as_ref() as *const ServerStreamCallbacks as *mut c_void;
-
- unsafe {
- context
- .stream_init(
- stream_name,
- input_device,
- input_stream_params,
- output_device,
- output_stream_params,
- latency,
- Some(data_cb_c),
- Some(state_cb_c),
- user_ptr,
- )
- .and_then(|stream| {
- if !self.streams.has_available() {
- trace!(
- "server connection ran out of stream slots. reserving {} more.",
- STREAM_CONN_CHUNK_SIZE
- );
- self.streams.reserve_exact(STREAM_CONN_CHUNK_SIZE);
- }
-
- let stm_tok = match self.streams.vacant_entry() {
- Some(entry) => {
- debug!("Registering stream {:?}", entry.index(),);
-
- entry
- .insert(ServerStream {
- stream: ManuallyDrop::new(stream),
- cbs: ManuallyDrop::new(cbs),
- })
- .index()
- }
- None => {
- // TODO: Turn into error
- panic!("Failed to insert stream into slab. No entries")
- }
- };
-
- Ok(ClientMessage::StreamCreated(StreamCreate {
- token: stm_tok,
- fds: [
- stm1.into_raw_fd(),
- input_file.into_raw_fd(),
- output_file.into_raw_fd(),
- ],
- }))
- })
- .map_err(|e| e.into())
- }
- }
-}
-
struct ServerWrapper {
core_thread: core::CoreThread,
callback_thread: core::CoreThread,
}
fn run() -> Result<ServerWrapper> {
trace!("Starting up cubeb audio server event loop thread...");
@@ -482,17 +90,17 @@ fn run() -> Result<ServerWrapper> {
pub extern "C" fn audioipc_server_start() -> *mut c_void {
match run() {
Ok(server) => Box::into_raw(Box::new(server)) as *mut _,
Err(_) => ptr::null_mut() as *mut _,
}
}
#[no_mangle]
-pub extern "C" fn audioipc_server_new_client(p: *mut c_void) -> libc::c_int {
+pub extern "C" fn audioipc_server_new_client(p: *mut c_void) -> PlatformHandleType {
let (wait_tx, wait_rx) = oneshot::channel();
let wrapper: &ServerWrapper = unsafe { &*(p as *mut _) };
let cb_remote = wrapper.callback_thread.remote();
// We create a pair of connected unix domain sockets. One socket is
// registered with the reactor core, the other is returned to the
// caller.
@@ -500,70 +108,26 @@ pub extern "C" fn audioipc_server_new_cl
.and_then(|(sock1, sock2)| {
// Spawn closure to run on same thread as reactor::Core
// via remote handle.
wrapper.core_thread.remote().spawn(|handle| {
trace!("Incoming connection");
UnixStream::from_stream(sock2, handle)
.and_then(|sock| {
let transport = framed_with_fds(sock, Default::default());
- rpc::bind_server(transport, CubebServer::new(cb_remote), handle);
+ rpc::bind_server(transport, server::CubebServer::new(cb_remote), handle);
Ok(())
- })
- .map_err(|_| ())
+ }).map_err(|_| ())
// Notify waiting thread that sock2 has been registered.
.and_then(|_| wait_tx.send(()))
});
// Wait for notification that sock2 has been registered
// with reactor::Core.
let _ = wait_rx.wait();
Ok(sock1.into_raw_fd())
- })
- .unwrap_or(-1)
+ }).unwrap_or(-1)
}
#[no_mangle]
pub extern "C" fn audioipc_server_stop(p: *mut c_void) {
let wrapper = unsafe { Box::<ServerWrapper>::from_raw(p as *mut _) };
drop(wrapper);
}
-
-fn error(error: cubeb::Error) -> ClientMessage {
- ClientMessage::Error(error.raw_code())
-}
-
-// C callable callbacks
-unsafe extern "C" fn data_cb_c(
- _: *mut ffi::cubeb_stream,
- user_ptr: *mut c_void,
- input_buffer: *const c_void,
- output_buffer: *mut c_void,
- nframes: c_long,
-) -> c_long {
- let ok = panic::catch_unwind(|| {
- let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
- let input = if input_buffer.is_null() {
- &[]
- } else {
- slice::from_raw_parts(input_buffer as *const u8, nframes as usize)
- };
- let output: &mut [u8] = if output_buffer.is_null() {
- &mut []
- } else {
- slice::from_raw_parts_mut(output_buffer as *mut u8, nframes as usize)
- };
- cbs.data_callback(input, output) as c_long
- });
- ok.unwrap_or(0)
-}
-
-unsafe extern "C" fn state_cb_c(
- _: *mut ffi::cubeb_stream,
- user_ptr: *mut c_void,
- state: ffi::cubeb_state,
-) {
- let ok = panic::catch_unwind(|| {
- let state = cubeb::State::from(state);
- let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
- cbs.state_callback(state);
- });
- ok.expect("State callback panicked");
-}
new file mode 100644
--- /dev/null
+++ b/media/audioipc/server/src/server.rs
@@ -0,0 +1,460 @@
+// Copyright © 2017 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details
+
+use audioipc;
+use audioipc::codec::LengthDelimitedCodec;
+use audioipc::core;
+use audioipc::fd_passing::FramedWithFds;
+use audioipc::frame::{framed, Framed};
+use audioipc::messages::{
+ CallbackReq, CallbackResp, ClientMessage, Device, DeviceInfo, ServerMessage, StreamCreate,
+ StreamInitParams, StreamParams,
+};
+use audioipc::rpc;
+use audioipc::shm::{SharedMemReader, SharedMemWriter};
+use cubeb;
+use cubeb::ffi;
+use futures::future::{self, FutureResult};
+use futures::sync::oneshot;
+use futures::Future;
+use slab;
+use std::cell::RefCell;
+use std::convert::From;
+use std::ffi::{CStr, CString};
+use std::mem::{size_of, ManuallyDrop};
+use std::os::raw::{c_long, c_void};
+use std::os::unix::io::IntoRawFd;
+use std::os::unix::net;
+use std::{panic, slice};
+use tokio_core::reactor::Remote;
+use tokio_uds::UnixStream;
+
+use errors::*;
+
+fn error(error: cubeb::Error) -> ClientMessage {
+ ClientMessage::Error(error.raw_code())
+}
+
+type ContextKey = RefCell<Option<cubeb::Result<cubeb::Context>>>;
+thread_local!(static CONTEXT_KEY:ContextKey = RefCell::new(None));
+
+fn with_local_context<T, F>(f: F) -> T
+where
+ F: FnOnce(&cubeb::Result<cubeb::Context>) -> T,
+{
+ CONTEXT_KEY.with(|k| {
+ let mut context = k.borrow_mut();
+ if context.is_none() {
+ let name = CString::new("AudioIPC Server").unwrap();
+ *context = Some(cubeb::Context::init(Some(name.as_c_str()), None));
+ }
+ f(context.as_ref().unwrap())
+ })
+}
+
+// TODO: Remove and let caller allocate based on cubeb backend requirements.
+const SHM_AREA_SIZE: usize = 2 * 1024 * 1024;
+
+// The size in which the stream slab is grown.
+const STREAM_CONN_CHUNK_SIZE: usize = 64;
+
+struct CallbackClient;
+
+impl rpc::Client for CallbackClient {
+ type Request = CallbackReq;
+ type Response = CallbackResp;
+ type Transport = Framed<UnixStream, LengthDelimitedCodec<Self::Request, Self::Response>>;
+}
+
+struct ServerStreamCallbacks {
+ /// Size of input frame in bytes
+ input_frame_size: u16,
+ /// Size of output frame in bytes
+ output_frame_size: u16,
+ /// Shared memory buffer for sending input data to client
+ input_shm: SharedMemWriter,
+ /// Shared memory buffer for receiving output data from client
+ output_shm: SharedMemReader,
+ /// RPC interface to callback server running in client
+ rpc: rpc::ClientProxy<CallbackReq, CallbackResp>,
+}
+
+impl ServerStreamCallbacks {
+ fn data_callback(&mut self, input: &[u8], output: &mut [u8]) -> isize {
+ trace!("Stream data callback: {} {}", input.len(), output.len());
+
+ // len is of input and output is frame len. Turn these into the real lengths.
+ let real_input = unsafe {
+ let nbytes = input.len() * self.input_frame_size as usize;
+ slice::from_raw_parts(input.as_ptr(), nbytes)
+ };
+
+ self.input_shm.write(real_input).unwrap();
+
+ let r = self
+ .rpc
+ .call(CallbackReq::Data(
+ output.len() as isize,
+ self.output_frame_size as usize,
+ )).wait();
+
+ match r {
+ Ok(CallbackResp::Data(frames)) => {
+ if frames >= 0 {
+ let nbytes = frames as usize * self.output_frame_size as usize;
+ let real_output = unsafe {
+ trace!("Resize output to {}", nbytes);
+ slice::from_raw_parts_mut(output.as_mut_ptr(), nbytes)
+ };
+ self.output_shm.read(&mut real_output[..nbytes]).unwrap();
+ }
+ frames
+ }
+ _ => {
+ debug!("Unexpected message {:?} during data_callback", r);
+ -1
+ }
+ }
+ }
+
+ fn state_callback(&mut self, state: cubeb::State) {
+ trace!("Stream state callback: {:?}", state);
+ let r = self.rpc.call(CallbackReq::State(state.into())).wait();
+ match r {
+ Ok(CallbackResp::State) => {}
+ _ => {
+ debug!("Unexpected message {:?} during callback", r);
+ }
+ }
+ }
+}
+
+struct ServerStream {
+ stream: ManuallyDrop<cubeb::Stream>,
+ cbs: ManuallyDrop<Box<ServerStreamCallbacks>>,
+}
+
+impl Drop for ServerStream {
+ fn drop(&mut self) {
+ unsafe {
+ ManuallyDrop::drop(&mut self.stream);
+ ManuallyDrop::drop(&mut self.cbs);
+ }
+ }
+}
+
+type StreamSlab = slab::Slab<ServerStream, usize>;
+
+pub struct CubebServer {
+ cb_remote: Remote,
+ streams: StreamSlab,
+}
+
+impl rpc::Server for CubebServer {
+ type Request = ServerMessage;
+ type Response = ClientMessage;
+ type Future = FutureResult<Self::Response, ()>;
+ type Transport = FramedWithFds<UnixStream, LengthDelimitedCodec<Self::Response, Self::Request>>;
+
+ fn process(&mut self, req: Self::Request) -> Self::Future {
+ let resp = with_local_context(|context| match *context {
+ Err(_) => error(cubeb::Error::error()),
+ Ok(ref context) => self.process_msg(context, &req),
+ });
+ future::ok(resp)
+ }
+}
+
+impl CubebServer {
+ pub fn new(cb_remote: Remote) -> Self {
+ CubebServer {
+ cb_remote: cb_remote,
+ streams: StreamSlab::with_capacity(STREAM_CONN_CHUNK_SIZE),
+ }
+ }
+
+ // Process a request coming from the client.
+ fn process_msg(&mut self, context: &cubeb::Context, msg: &ServerMessage) -> ClientMessage {
+ let resp: ClientMessage = match *msg {
+ ServerMessage::ClientConnect => panic!("already connected"),
+
+ ServerMessage::ClientDisconnect => {
+ // TODO:
+ //self.connection.client_disconnect();
+ ClientMessage::ClientDisconnected
+ }
+
+ ServerMessage::ContextGetBackendId => ClientMessage::ContextBackendId(),
+
+ ServerMessage::ContextGetMaxChannelCount => context
+ .max_channel_count()
+ .map(ClientMessage::ContextMaxChannelCount)
+ .unwrap_or_else(error),
+
+ ServerMessage::ContextGetMinLatency(ref params) => {
+ let format = cubeb::SampleFormat::from(params.format);
+ let layout = cubeb::ChannelLayout::from(params.layout);
+
+ let params = cubeb::StreamParamsBuilder::new()
+ .format(format)
+ .rate(u32::from(params.rate))
+ .channels(u32::from(params.channels))
+ .layout(layout)
+ .take();
+
+ context
+ .min_latency(¶ms)
+ .map(ClientMessage::ContextMinLatency)
+ .unwrap_or_else(error)
+ }
+
+ ServerMessage::ContextGetPreferredSampleRate => context
+ .preferred_sample_rate()
+ .map(ClientMessage::ContextPreferredSampleRate)
+ .unwrap_or_else(error),
+
+ ServerMessage::ContextGetDeviceEnumeration(device_type) => context
+ .enumerate_devices(cubeb::DeviceType::from_bits_truncate(device_type))
+ .map(|devices| {
+ let v: Vec<DeviceInfo> = devices.iter().map(|i| i.as_ref().into()).collect();
+ ClientMessage::ContextEnumeratedDevices(v)
+ }).unwrap_or_else(error),
+
+ ServerMessage::StreamInit(ref params) => self
+ .process_stream_init(context, params)
+ .unwrap_or_else(|_| error(cubeb::Error::error())),
+
+ ServerMessage::StreamDestroy(stm_tok) => {
+ self.streams.remove(stm_tok);
+ ClientMessage::StreamDestroyed
+ }
+
+ ServerMessage::StreamStart(stm_tok) => self.streams[stm_tok]
+ .stream
+ .start()
+ .map(|_| ClientMessage::StreamStarted)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamStop(stm_tok) => self.streams[stm_tok]
+ .stream
+ .stop()
+ .map(|_| ClientMessage::StreamStopped)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamResetDefaultDevice(stm_tok) => self.streams[stm_tok]
+ .stream
+ .reset_default_device()
+ .map(|_| ClientMessage::StreamDefaultDeviceReset)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamGetPosition(stm_tok) => self.streams[stm_tok]
+ .stream
+ .position()
+ .map(ClientMessage::StreamPosition)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamGetLatency(stm_tok) => self.streams[stm_tok]
+ .stream
+ .latency()
+ .map(ClientMessage::StreamLatency)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamSetVolume(stm_tok, volume) => self.streams[stm_tok]
+ .stream
+ .set_volume(volume)
+ .map(|_| ClientMessage::StreamVolumeSet)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamSetPanning(stm_tok, panning) => self.streams[stm_tok]
+ .stream
+ .set_panning(panning)
+ .map(|_| ClientMessage::StreamPanningSet)
+ .unwrap_or_else(error),
+
+ ServerMessage::StreamGetCurrentDevice(stm_tok) => self.streams[stm_tok]
+ .stream
+ .current_device()
+ .map(|device| ClientMessage::StreamCurrentDevice(Device::from(device)))
+ .unwrap_or_else(error),
+ };
+
+ trace!("process_msg: req={:?}, resp={:?}", msg, resp);
+
+ resp
+ }
+
+ // Stream init is special, so it's been separated from process_msg.
+ fn process_stream_init(
+ &mut self,
+ context: &cubeb::Context,
+ params: &StreamInitParams,
+ ) -> Result<ClientMessage> {
+ fn frame_size_in_bytes(params: Option<&StreamParams>) -> u16 {
+ params
+ .map(|p| {
+ let format = p.format.into();
+ let sample_size = match format {
+ cubeb::SampleFormat::S16LE
+ | cubeb::SampleFormat::S16BE
+ | cubeb::SampleFormat::S16NE => 2,
+ cubeb::SampleFormat::Float32LE
+ | cubeb::SampleFormat::Float32BE
+ | cubeb::SampleFormat::Float32NE => 4,
+ };
+ let channel_count = p.channels as u16;
+ sample_size * channel_count
+ }).unwrap_or(0u16)
+ }
+
+ // Create the callback handling struct which is attached the cubeb stream.
+ let input_frame_size = frame_size_in_bytes(params.input_stream_params.as_ref());
+ let output_frame_size = frame_size_in_bytes(params.output_stream_params.as_ref());
+
+ let (stm1, stm2) = net::UnixStream::pair()?;
+ debug!("Created callback pair: {:?}-{:?}", stm1, stm2);
+ let (input_shm, input_file) =
+ SharedMemWriter::new(&audioipc::get_shm_path("input"), SHM_AREA_SIZE)?;
+ let (output_shm, output_file) =
+ SharedMemReader::new(&audioipc::get_shm_path("output"), SHM_AREA_SIZE)?;
+
+ // This code is currently running on the Client/Server RPC
+ // handling thread. We need to move the registration of the
+ // bind_client to the callback RPC handling thread. This is
+ // done by spawning a future on cb_remote.
+
+ let id = core::handle().id();
+
+ let (tx, rx) = oneshot::channel();
+ self.cb_remote.spawn(move |handle| {
+ // Ensure we're running on a loop different to the one
+ // invoking spawn_fn.
+ assert_ne!(id, handle.id());
+ let stream = UnixStream::from_stream(stm2, handle).unwrap();
+ let transport = framed(stream, Default::default());
+ let rpc = rpc::bind_client::<CallbackClient>(transport, handle);
+ drop(tx.send(rpc));
+ Ok(())
+ });
+
+ let rpc: rpc::ClientProxy<CallbackReq, CallbackResp> = match rx.wait() {
+ Ok(rpc) => rpc,
+ Err(_) => bail!("Failed to create callback rpc."),
+ };
+
+ let cbs = Box::new(ServerStreamCallbacks {
+ input_frame_size,
+ output_frame_size,
+ input_shm,
+ output_shm,
+ rpc,
+ });
+
+ // Create cubeb stream from params
+ let stream_name = params
+ .stream_name
+ .as_ref()
+ .and_then(|name| CStr::from_bytes_with_nul(name).ok());
+
+ let input_device = params.input_device as *const _;
+ let input_stream_params = params.input_stream_params.as_ref().map(|isp| unsafe {
+ cubeb::StreamParamsRef::from_ptr(isp as *const StreamParams as *mut _)
+ });
+
+ let output_device = params.output_device as *const _;
+ let output_stream_params = params.output_stream_params.as_ref().map(|osp| unsafe {
+ cubeb::StreamParamsRef::from_ptr(osp as *const StreamParams as *mut _)
+ });
+
+ let latency = params.latency_frames;
+ assert!(size_of::<Box<ServerStreamCallbacks>>() == size_of::<usize>());
+ let user_ptr = cbs.as_ref() as *const ServerStreamCallbacks as *mut c_void;
+
+ unsafe {
+ context
+ .stream_init(
+ stream_name,
+ input_device,
+ input_stream_params,
+ output_device,
+ output_stream_params,
+ latency,
+ Some(data_cb_c),
+ Some(state_cb_c),
+ user_ptr,
+ ).and_then(|stream| {
+ if !self.streams.has_available() {
+ trace!(
+ "server connection ran out of stream slots. reserving {} more.",
+ STREAM_CONN_CHUNK_SIZE
+ );
+ self.streams.reserve_exact(STREAM_CONN_CHUNK_SIZE);
+ }
+
+ let stm_tok = match self.streams.vacant_entry() {
+ Some(entry) => {
+ debug!("Registering stream {:?}", entry.index(),);
+
+ entry
+ .insert(ServerStream {
+ stream: ManuallyDrop::new(stream),
+ cbs: ManuallyDrop::new(cbs),
+ }).index()
+ }
+ None => {
+ // TODO: Turn into error
+ panic!("Failed to insert stream into slab. No entries")
+ }
+ };
+
+ Ok(ClientMessage::StreamCreated(StreamCreate {
+ token: stm_tok,
+ fds: [
+ stm1.into_raw_fd(),
+ input_file.into_raw_fd(),
+ output_file.into_raw_fd(),
+ ],
+ }))
+ }).map_err(|e| e.into())
+ }
+ }
+}
+
+// C callable callbacks
+unsafe extern "C" fn data_cb_c(
+ _: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: c_long,
+) -> c_long {
+ let ok = panic::catch_unwind(|| {
+ let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
+ let input = if input_buffer.is_null() {
+ &[]
+ } else {
+ slice::from_raw_parts(input_buffer as *const u8, nframes as usize)
+ };
+ let output: &mut [u8] = if output_buffer.is_null() {
+ &mut []
+ } else {
+ slice::from_raw_parts_mut(output_buffer as *mut u8, nframes as usize)
+ };
+ cbs.data_callback(input, output) as c_long
+ });
+ ok.unwrap_or(0)
+}
+
+unsafe extern "C" fn state_cb_c(
+ _: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+) {
+ let ok = panic::catch_unwind(|| {
+ let state = cubeb::State::from(state);
+ let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
+ cbs.state_callback(state);
+ });
+ ok.expect("State callback panicked");
+}
--- a/netwerk/base/LoadInfo.cpp
+++ b/netwerk/base/LoadInfo.cpp
@@ -1568,10 +1568,24 @@ LoadInfo::SetPerformanceStorage(Performa
}
PerformanceStorage*
LoadInfo::GetPerformanceStorage()
{
return mPerformanceStorage;
}
+NS_IMETHODIMP
+LoadInfo::GetCspEventListener(nsICSPEventListener** aCSPEventListener)
+{
+ NS_IF_ADDREF(*aCSPEventListener = mCSPEventListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LoadInfo::SetCspEventListener(nsICSPEventListener* aCSPEventListener)
+{
+ mCSPEventListener = aCSPEventListener;
+ return NS_OK;
+}
+
} // namespace net
} // namespace mozilla
--- a/netwerk/base/LoadInfo.h
+++ b/netwerk/base/LoadInfo.h
@@ -162,16 +162,17 @@ private:
// it should be merged from parent channel through ParentLoadInfoForwarderArgs.
nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
nsCOMPtr<nsIPrincipal> mTriggeringPrincipal;
nsCOMPtr<nsIPrincipal> mPrincipalToInherit;
nsCOMPtr<nsIPrincipal> mSandboxedLoadingPrincipal;
nsCOMPtr<nsIPrincipal> mTopLevelPrincipal;
nsCOMPtr<nsIPrincipal> mTopLevelStorageAreaPrincipal;
nsCOMPtr<nsIURI> mResultPrincipalURI;
+ nsCOMPtr<nsICSPEventListener> mCSPEventListener;
Maybe<mozilla::dom::ClientInfo> mClientInfo;
UniquePtr<mozilla::dom::ClientSource> mReservedClientSource;
Maybe<mozilla::dom::ClientInfo> mReservedClientInfo;
Maybe<mozilla::dom::ClientInfo> mInitialClientInfo;
Maybe<mozilla::dom::ServiceWorkerDescriptor> mController;
RefPtr<mozilla::dom::PerformanceStorage> mPerformanceStorage;
--- a/netwerk/base/nsILoadInfo.idl
+++ b/netwerk/base/nsILoadInfo.idl
@@ -3,16 +3,17 @@
* 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/. */
#include "nsISupports.idl"
#include "nsIContentPolicy.idl"
interface nsIChannel;
+interface nsICSPEventListener;
interface nsINode;
interface nsIPrincipal;
interface nsIRedirectHistoryEntry;
interface nsIURI;
webidl Document;
native LoadContextRef(already_AddRefed<nsISupports>);
%{C++
#include "nsTArray.h"
@@ -1067,9 +1068,16 @@ interface nsILoadInfo : nsISupports
*/
[infallible] attribute boolean documentHasUserInteracted;
/**
* This attribute represents whether the document to which this
* load belongs had finished loading when the load was initiated.
*/
[infallible] attribute boolean documentHasLoaded;
+
+ /**
+ * The object in charged to receive CSP violation events. It can be null.
+ * This attribute will be merged into the CSP object eventually.
+ * See bug 1500908.
+ */
+ attribute nsICSPEventListener cspEventListener;
};
--- a/security/manager/ssl/nsIOSKeyStore.idl
+++ b/security/manager/ssl/nsIOSKeyStore.idl
@@ -10,17 +10,17 @@ interface nsIOSKeyStore: nsISupports {
/**
* This interface provides encryption and decryption operations for data at
* rest. The key used to encrypt and decrypt the data is stored in the OS
* key store.
*
* Usage:
*
* // obtain the singleton OSKeyStore instance
- * const oskeystore = Cc["@mozilla.org/oskeystore;1"].getService(Ci.nsIOSKeyStore);
+ * const oskeystore = Cc["@mozilla.org/security/oskeystore;1"].getService(Ci.nsIOSKeyStore);
*
* const PASSWORD_LABEL = "mylabel1";
* const COOKIE_LABEL = "mylabel2";
*
* // Unlock the key store.
* // Note that this is not necesssary. The key store will be unlocked
* // automatically when an operation is performed on it.
* await oskeystore.asyncUnlock();
--- a/services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/formautofill.jsm
@@ -9,18 +9,18 @@
var EXPORTED_SYMBOLS = ["Address", "CreditCard", "DumpAddresses", "DumpCreditCards"];
ChromeUtils.import("resource://tps/logger.jsm");
ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
"resource://formautofill/FormAutofillStorage.jsm");
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
- "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+ "resource://formautofill/OSKeyStore.jsm");
class FormAutofillBase {
constructor(props, subStorageName, fields) {
this._subStorageName = subStorageName;
this._fields = fields;
this.props = {};
this.updateProps = null;
@@ -106,17 +106,17 @@ const CREDIT_CARD_FIELDS = [
class CreditCard extends FormAutofillBase {
constructor(props) {
super(props, "creditCards", CREDIT_CARD_FIELDS);
}
async Find() {
const storage = await this.getStorage();
await Promise.all(storage._data.map(
- async entry => entry["cc-number"] = await MasterPassword.decrypt(entry["cc-number-encrypted"])));
+ async entry => entry["cc-number"] = await OSKeyStore.decrypt(entry["cc-number-encrypted"])));
return storage._data.find(entry => {
return this._fields.every(field => entry[field] === this.props[field]);
});
}
}
async function DumpCreditCards() {
await DumpStorage("creditCards");
--- a/testing/web-platform/meta/2dcontext/imagebitmap/createImageBitmap-invalid-args.html.ini
+++ b/testing/web-platform/meta/2dcontext/imagebitmap/createImageBitmap-invalid-args.html.ini
@@ -1,139 +1,46 @@
[createImageBitmap-invalid-args.html]
- [createImageBitmap with a HTMLImageElement source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a HTMLImageElement source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a HTMLImageElement source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
- expected: FAIL
-
- [createImageBitmap with a HTMLVideoElement source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a HTMLVideoElement source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a HTMLVideoElement source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
- expected: FAIL
-
- [createImageBitmap with a HTMLCanvasElement source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a HTMLCanvasElement source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a HTMLCanvasElement source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
- expected: FAIL
-
- [createImageBitmap with a OffscreenCanvas source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a OffscreenCanvas source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a OffscreenCanvas source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
- expected: FAIL
-
- [createImageBitmap with a ImageData source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a ImageData source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a ImageData source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
- expected: FAIL
-
- [createImageBitmap with a ImageBitmap source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a ImageBitmap source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a ImageBitmap source and oversized (unallocatable) crop region rejects with an InvalidStateError DOMException.]
- expected: FAIL
-
- [createImageBitmap with a Blob source and sw set to 0 rejects with a RangeError.]
- expected: FAIL
-
- [createImageBitmap with a Blob source and sh set to 0 rejects with a RangeError.]
- expected: FAIL
-
[createImageBitmap with an oversized canvas source rejects with a RangeError.]
expected: FAIL
[createImageBitmap with an invalid OffscreenCanvas source rejects with a RangeError.]
expected: FAIL
[createImageBitmap with a broken image source rejects with an InvalidStateError.]
expected: FAIL
[createImageBitmap with an available but undecodable image source rejects with an InvalidStateError.]
expected: FAIL
- [createImageBitmap with a an HTMLCanvasElement source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a an HTMLCanvasElement source and sh set to 0]
- expected: FAIL
-
- [createImageBitmap with a an HTMLCanvasElement source and oversized (unallocatable) crop region]
- expected: FAIL
-
- [createImageBitmap with a an HTMLVideoElement source and sw set to 0]
+ [createImageBitmap with an HTMLCanvasElement source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a an HTMLVideoElement source and sh set to 0]
- expected: FAIL
-
- [createImageBitmap with a an HTMLVideoElement source and oversized (unallocatable) crop region]
+ [createImageBitmap with an HTMLVideoElement source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a an HTMLImageElement source and sw set to 0]
+ [createImageBitmap with an HTMLImageElement source and sw set to 0]
expected: FAIL
- [createImageBitmap with a an HTMLImageElement source and sh set to 0]
- expected: FAIL
-
- [createImageBitmap with a an HTMLImageElement source and oversized (unallocatable) crop region]
+ [createImageBitmap with an HTMLImageElement source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a an OffscreenCanvas source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a an OffscreenCanvas source and sh set to 0]
+ [createImageBitmap with an OffscreenCanvas source and sw set to 0]
expected: FAIL
- [createImageBitmap with a an OffscreenCanvas source and oversized (unallocatable) crop region]
- expected: FAIL
-
- [createImageBitmap with a an ImageData source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a an ImageData source and sh set to 0]
+ [createImageBitmap with an OffscreenCanvas source and sh set to 0]
expected: FAIL
- [createImageBitmap with a an ImageData source and oversized (unallocatable) crop region]
- expected: FAIL
-
- [createImageBitmap with a an ImageBitmap source and sw set to 0]
+ [createImageBitmap with an OffscreenCanvas source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a an ImageBitmap source and sh set to 0]
+ [createImageBitmap with an ImageData source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a an ImageBitmap source and oversized (unallocatable) crop region]
- expected: FAIL
-
- [createImageBitmap with a a Blob source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a a Blob source and sh set to 0]
+ [createImageBitmap with an ImageBitmap source and oversized (unallocatable) crop region]
expected: FAIL
[createImageBitmap with an oversized canvas source.]
expected: FAIL
[createImageBitmap with an invalid OffscreenCanvas source.]
expected: FAIL
@@ -141,59 +48,41 @@
expected: FAIL
[createImageBitmap with an available but undecodable image source.]
expected: FAIL
[createImageBitmap with a closed ImageBitmap.]
expected: FAIL
- [createImageBitmap with a a bitmap HTMLImageElement source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a a bitmap HTMLImageElement source and sh set to 0]
+ [createImageBitmap with a bitmap HTMLImageElement source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a a bitmap HTMLImageElement source and oversized (unallocatable) crop region]
+ [createImageBitmap with a bitmap SVGImageElement source and sw set to 0]
expected: FAIL
- [createImageBitmap with a a vector HTMLImageElement source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a a vector HTMLImageElement source and sh set to 0]
+ [createImageBitmap with a bitmap SVGImageElement source and sh set to 0]
expected: FAIL
- [createImageBitmap with a a bitmap SVGImageElement source and sw set to 0]
- expected: FAIL
-
- [createImageBitmap with a a bitmap SVGImageElement source and sh set to 0]
+ [createImageBitmap with a bitmap SVGImageElement source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a a bitmap SVGImageElement source and oversized (unallocatable) crop region]
+ [createImageBitmap with a vector SVGImageElement source and sw set to 0]
expected: FAIL
- [createImageBitmap with a a vector SVGImageElement source and sw set to 0]
+ [createImageBitmap with a vector SVGImageElement source and sh set to 0]
expected: FAIL
- [createImageBitmap with a a vector SVGImageElement source and sh set to 0]
- expected: FAIL
-
- [createImageBitmap with a a vector SVGImageElement source and oversized (unallocatable) crop region]
+ [createImageBitmap with a vector SVGImageElement source and oversized (unallocatable) crop region]
expected: FAIL
[createImageBitmap with CanvasRenderingContext2D image source.]
expected: FAIL
[createImageBitmap with Uint8Array image source.]
expected: FAIL
[createImageBitmap with ArrayBuffer image source.]
expected: FAIL
- [createImageBitmap with a an HTMLVideoElement from a data URL source and sw set to 0]
+ [createImageBitmap with an HTMLVideoElement from a data URL source and oversized (unallocatable) crop region]
expected: FAIL
- [createImageBitmap with a an HTMLVideoElement from a data URL source and sh set to 0]
- expected: FAIL
-
- [createImageBitmap with a an HTMLVideoElement from a data URL source and oversized (unallocatable) crop region]
- expected: FAIL
-
--- a/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-invalid-args.html
+++ b/testing/web-platform/tests/2dcontext/imagebitmap/createImageBitmap-invalid-args.html
@@ -44,38 +44,38 @@ function makeAvailableButBrokenImage(pat
image.src = path;
image.onload = () => resolve(image);
image.onerror = reject;
});
}
testCases = [
{
- description: 'createImageBitmap with a <sourceType> source and sw set to 0',
+ description: 'createImageBitmap with <sourceType> source and sw set to 0',
promiseTestFunction:
(source, t) => {
return promise_rejects(t, new RangeError(),
createImageBitmap(source, 0, 0, 0, 10));
}
},
{
- description: 'createImageBitmap with a <sourceType> source and sh set to 0',
+ description: 'createImageBitmap with <sourceType> source and sh set to 0',
promiseTestFunction:
(source, t) => {
return promise_rejects(t, new RangeError(),
createImageBitmap(source, 0, 0, 10, 0));
}
},
{
// This case is not explicitly documented in the specification for
// createImageBitmap, but it is expected that internal failures cause
// InvalidStateError.
//
// Note: https://bugs.chromium.org/p/chromium/issues/detail?id=799025
- description: 'createImageBitmap with a <sourceType> source and oversized ' +
+ description: 'createImageBitmap with <sourceType> source and oversized ' +
'(unallocatable) crop region',
promiseTestFunction:
(source, t) => {
return promise_rejects(t, new DOMException('', 'InvalidStateError'),
createImageBitmap(source, 0, 0, 100000000, 100000000));
}
},
];
--- a/toolkit/components/gfx/SanityTest.js
+++ b/toolkit/components/gfx/SanityTest.js
@@ -137,17 +137,21 @@ function testCompositor(test, win, ctx)
takeWindowSnapshot(win, ctx);
var testPassed = true;
if (!verifyLayersRendering(ctx)) {
// Try disabling advanced layers if it was enabled. Also trigger
// a device reset so the screen redraws.
if (Services.prefs.getBoolPref(AL_ENABLED_PREF, false)) {
Services.prefs.setBoolPref(AL_TEST_FAILED_PREF, true);
- test.utils.triggerDeviceReset();
+ // Do not need to reset device when WebRender is used.
+ // When WebRender is used, advanced layers are not used.
+ if (test.utils.layerManagerType != "WebRender") {
+ test.utils.triggerDeviceReset();
+ }
}
reportResult(TEST_FAILED_RENDER);
testPassed = false;
} else {
Services.prefs.setBoolPref(AL_TEST_FAILED_PREF, false);
if (!verifyVideoRendering(ctx)) {
reportResult(TEST_FAILED_VIDEO);
Services.prefs.setBoolPref(DISABLE_VIDEO_PREF, true);
--- a/toolkit/modules/CreditCard.jsm
+++ b/toolkit/modules/CreditCard.jsm
@@ -1,18 +1,18 @@
/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["CreditCard"];
-ChromeUtils.defineModuleGetter(this, "MasterPassword",
- "resource://formautofill/MasterPassword.jsm");
+ChromeUtils.defineModuleGetter(this, "OSKeyStore",
+ "resource://formautofill/OSKeyStore.jsm");
// The list of known and supported credit card network ids ("types")
// This list mirrors the networks from dom/payments/BasicCardPayment.cpp
// and is defined by https://www.w3.org/Payments/card-network-ids
const SUPPORTED_NETWORKS = Object.freeze([
"amex",
"cartebancaire",
"diners",
@@ -203,17 +203,23 @@ class CreditCard {
* true, decrypted credit card numbers are shown instead.
*/
async getLabel({showNumbers} = {}) {
let parts = [];
let label;
if (showNumbers) {
if (this._encryptedNumber) {
- label = await MasterPassword.decrypt(this._encryptedNumber);
+ try {
+ label = await OSKeyStore.decrypt(this._encryptedNumber);
+ } catch (ex) {
+ // Quietly recover from decryption error.
+ label = this._number;
+ Cu.reportError(ex);
+ }
} else {
label = this._number;
}
}
if (this._unmodifiedNumber && !label) {
if (this.isValidNumber()) {
label = this.maskedNumber;
} else {
--- a/toolkit/modules/tests/browser/browser_CreditCard.js
+++ b/toolkit/modules/tests/browser/browser_CreditCard.js
@@ -1,45 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
ChromeUtils.import("resource://gre/modules/CreditCard.jsm");
-ChromeUtils.import("resource://formautofill/MasterPassword.jsm");
+ChromeUtils.import("resource://formautofill/OSKeyStore.jsm");
+ChromeUtils.import("resource://testing-common/OSKeyStoreTestUtils.jsm");
let oldGetters = {};
let gFakeLoggedIn = true;
add_task(function setup() {
- oldGetters._token = Object.getOwnPropertyDescriptor(MasterPassword, "_token").get;
- oldGetters.isEnabled = Object.getOwnPropertyDescriptor(MasterPassword, "isEnabled").get;
- oldGetters.isLoggedIn = Object.getOwnPropertyDescriptor(MasterPassword, "isLoggedIn").get;
- MasterPassword.__defineGetter__("_token", () => { return {hasPassword: true}; });
- MasterPassword.__defineGetter__("isEnabled", () => true);
- MasterPassword.__defineGetter__("isLoggedIn", () => gFakeLoggedIn);
- registerCleanupFunction(() => {
- MasterPassword.__defineGetter__("_token", oldGetters._token);
- MasterPassword.__defineGetter__("isEnabled", oldGetters.isEnabled);
- MasterPassword.__defineGetter__("isLoggedIn", oldGetters.isLoggedIn);
+ OSKeyStoreTestUtils.setup();
+ oldGetters.isLoggedIn = Object.getOwnPropertyDescriptor(OSKeyStore, "isLoggedIn").get;
+ OSKeyStore.__defineGetter__("isLoggedIn", () => gFakeLoggedIn);
+ registerCleanupFunction(async () => {
+ OSKeyStore.__defineGetter__("isLoggedIn", oldGetters.isLoggedIn);
+ await OSKeyStoreTestUtils.cleanup();
- // CreditCard.jsm and MasterPassword.jsm are imported into the global scope
- // -- the window -- above. If they're not deleted, they outlive the test and
- // are reported as a leak.
- delete window.MasterPassword;
+ // CreditCard.jsm, OSKeyStore.jsm, and OSKeyStoreTestUtils.jsm are imported
+ // into the global scope -- the window -- above. If they're not deleted,
+ // they outlive the test and are reported as a leak.
+ delete window.OSKeyStore;
delete window.CreditCard;
+ delete window.OSKeyStoreTestUtils;
});
});
-add_task(async function test_getLabel_withMasterPassword() {
- ok(MasterPassword.isEnabled, "Confirm that MasterPassword is faked and thinks it is enabled");
- ok(MasterPassword.isLoggedIn, "Confirm that MasterPassword is faked and thinks it is logged in");
+add_task(async function test_getLabel_withOSKeyStore() {
+ ok(OSKeyStore.isLoggedIn, "Confirm that OSKeyStore is faked and thinks it is logged in");
const ccNumber = "4111111111111111";
- const encryptedNumber = await MasterPassword.encrypt(ccNumber);
- const decryptedNumber = await MasterPassword.decrypt(encryptedNumber);
+ const encryptedNumber = await OSKeyStore.encrypt(ccNumber);
+ const decryptedNumber = await OSKeyStore.decrypt(encryptedNumber);
is(decryptedNumber, ccNumber, "Decrypted CC number should match original");
const name = "Foxkeh";
const creditCard = new CreditCard({encryptedNumber, name: "Foxkeh"});
const label = await creditCard.getLabel({showNumbers: true});
is(label, `${ccNumber}, ${name}`);
});