зеркало из https://github.com/mozilla/gecko-dev.git
Backed out changeset 10c9e57fb4f8 (bug 1641777) for perma failures on browser_openImportCSV.js.
This commit is contained in:
Родитель
8ab981b6ee
Коммит
1fa2c4a121
|
@ -178,6 +178,8 @@ var whitelist = [
|
|||
},
|
||||
// Bug 1356031 (only used by devtools)
|
||||
{ file: "chrome://global/skin/icons/error-16.png" },
|
||||
// Bug 1641777 (only used by devtools)
|
||||
{ file: "chrome://global/content/third_party/d3/d3.js" },
|
||||
// Bug 1344267
|
||||
{ file: "chrome://marionette/content/test.xhtml" },
|
||||
{ file: "chrome://marionette/content/test_dialog.properties" },
|
||||
|
|
|
@ -96,8 +96,7 @@ let JSWINDOWACTORS = {
|
|||
AboutLoginsDeleteLogin: { wantUntrusted: true },
|
||||
AboutLoginsDismissBreachAlert: { wantUntrusted: true },
|
||||
AboutLoginsHideFooter: { wantUntrusted: true },
|
||||
AboutLoginsImportFromBrowser: { wantUntrusted: true },
|
||||
AboutLoginsImportFromFile: { wantUntrusted: true },
|
||||
AboutLoginsImport: { wantUntrusted: true },
|
||||
AboutLoginsInit: { wantUntrusted: true },
|
||||
AboutLoginsGetHelp: { wantUntrusted: true },
|
||||
AboutLoginsOpenMobileAndroid: { wantUntrusted: true },
|
||||
|
|
|
@ -125,10 +125,6 @@ class AboutLoginsChild extends JSWindowActorChild {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "AboutLoginsExportPasswords": {
|
||||
this.sendAsyncMessage("AboutLogins:ExportPasswords");
|
||||
break;
|
||||
}
|
||||
case "AboutLoginsGetHelp": {
|
||||
this.sendAsyncMessage("AboutLogins:GetHelp");
|
||||
break;
|
||||
|
@ -137,7 +133,7 @@ class AboutLoginsChild extends JSWindowActorChild {
|
|||
this.sendAsyncMessage("AboutLogins:HideFooter");
|
||||
break;
|
||||
}
|
||||
case "AboutLoginsImportFromBrowser": {
|
||||
case "AboutLoginsImport": {
|
||||
this.sendAsyncMessage("AboutLogins:Import");
|
||||
recordTelemetryEvent({
|
||||
object: "import_from_browser",
|
||||
|
@ -145,14 +141,6 @@ class AboutLoginsChild extends JSWindowActorChild {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "AboutLoginsImportFromFile": {
|
||||
this.sendAsyncMessage("AboutLogins:ImportPasswords");
|
||||
recordTelemetryEvent({
|
||||
object: "import_from_csv",
|
||||
method: "mgmt_menu_item_used",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "AboutLoginsOpenMobileAndroid": {
|
||||
this.sendAsyncMessage("AboutLogins:OpenMobileAndroid", {
|
||||
source: event.detail,
|
||||
|
@ -215,6 +203,10 @@ class AboutLoginsChild extends JSWindowActorChild {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "AboutLoginsExportPasswords": {
|
||||
this.sendAsyncMessage("AboutLogins:ExportPasswords");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
LoginBreaches: "resource:///modules/LoginBreaches.jsm",
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
LoginExport: "resource://gre/modules/LoginExport.jsm",
|
||||
LoginCSVImport: "resource://gre/modules/LoginCSVImport.jsm",
|
||||
MigrationUtils: "resource:///modules/MigrationUtils.jsm",
|
||||
OSKeyStore: "resource://gre/modules/OSKeyStore.jsm",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
|
@ -517,7 +516,7 @@ class AboutLoginsParent extends JSWindowActorParent {
|
|||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(
|
||||
Ci.nsIFilePicker
|
||||
);
|
||||
function fpCallback(aResult) {
|
||||
let fpCallback = function fpCallback_done(aResult) {
|
||||
if (aResult != Ci.nsIFilePicker.returnCancel) {
|
||||
LoginExport.exportAsCSV(fp.file.path);
|
||||
Services.telemetry.recordEvent(
|
||||
|
@ -526,7 +525,7 @@ class AboutLoginsParent extends JSWindowActorParent {
|
|||
"export_complete"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
let [
|
||||
title,
|
||||
defaultFilename,
|
||||
|
@ -556,43 +555,6 @@ class AboutLoginsParent extends JSWindowActorParent {
|
|||
fp.open(fpCallback);
|
||||
break;
|
||||
}
|
||||
case "AboutLogins:ImportPasswords": {
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(
|
||||
Ci.nsIFilePicker
|
||||
);
|
||||
async function fpCallback(aResult) {
|
||||
if (aResult != Ci.nsIFilePicker.returnCancel) {
|
||||
await LoginCSVImport.importFromCSV(fp.file.path);
|
||||
Services.telemetry.recordEvent(
|
||||
"pwmgr",
|
||||
"mgmt_menu_item_used",
|
||||
"import_csv_complete"
|
||||
);
|
||||
}
|
||||
}
|
||||
let [
|
||||
title,
|
||||
okButtonLabel,
|
||||
csvFilterTitle,
|
||||
] = await AboutLoginsL10n.formatValues([
|
||||
{
|
||||
id: "about-logins-import-file-picker-title",
|
||||
},
|
||||
{
|
||||
id: "about-logins-import-file-picker-import-button",
|
||||
},
|
||||
{
|
||||
id: "about-logins-import-file-picker-csv-filter-title",
|
||||
},
|
||||
]);
|
||||
|
||||
fp.init(ownerGlobal, title, Ci.nsIFilePicker.modeOpen);
|
||||
fp.appendFilter(csvFilterTitle, "*.csv");
|
||||
fp.appendFilters(Ci.nsIFilePicker.filterAll);
|
||||
fp.okButtonLabel = okButtonLabel;
|
||||
fp.open(fpCallback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-intro.css">
|
||||
|
||||
<img class="illustration" src="chrome://browser/content/aboutlogins/icons/intro-illustration.svg"/>
|
||||
<h1 class="heading" data-l10n-id="about-logins-login-intro-heading-logged-out"></h1>
|
||||
<h1 class="heading" data-l10n-id="login-intro-heading"></h1>
|
||||
<section>
|
||||
<p class="description" data-l10n-id="login-intro-description"></p>
|
||||
<ul>
|
||||
|
@ -141,9 +141,8 @@
|
|||
<a data-l10n-name="help-link" class="intro-help-link" target="_blank" rel="noreferrer"></a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="description intro-import-text" hidden data-l10n-id="about-logins-intro-import2">
|
||||
<a data-l10n-name="import-browser-link" href="#"></a>
|
||||
<a data-l10n-name="import-file-link" href="#"></a>
|
||||
<p class="description intro-import-text" hidden data-l10n-id="about-logins-intro-import">
|
||||
<a data-l10n-name="import-link" href="#"></a>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -271,8 +270,7 @@
|
|||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/menu-button.css">
|
||||
<button class="menu-button ghost-button" data-l10n-id="menu"></button>
|
||||
<ul class="menu" role="menu" hidden>
|
||||
<button role="menuitem" class="menuitem-button menuitem-import-browser ghost-button" hidden data-supported-platforms="Win32,MacIntel" data-event-name="AboutLoginsImportFromBrowser" data-l10n-id="about-logins-menu-menuitem-import-from-another-browser"></button>
|
||||
<button role="menuitem" class="menuitem-button menuitem-import-file ghost-button" data-event-name="AboutLoginsImportFromFile" data-l10n-id="about-logins-menu-menuitem-import-from-a-file"></button>
|
||||
<button role="menuitem" class="menuitem-button menuitem-import ghost-button" hidden data-supported-platforms="Win32,MacIntel" data-event-name="AboutLoginsImport" data-l10n-id="about-logins-menu-menuitem-import-from-another-browser"></button>
|
||||
<button role="menuitem" class="menuitem-button menuitem-export ghost-button" data-event-name="AboutLoginsExportPasswordsDialog" data-l10n-id="about-logins-menu-menuitem-export-logins"></button>
|
||||
<hr role="separator" class="menuitem-separator"></hr>
|
||||
<button role="menuitem" class="menuitem-button menuitem-preferences ghost-button" data-event-name="AboutLoginsOpenPreferences" data-l10n-id="menu-menuitem-preferences"></button>
|
||||
|
|
|
@ -35,12 +35,8 @@ export default class LoginIntro extends HTMLElement {
|
|||
event.currentTarget.classList.contains("intro-import-text") &&
|
||||
event.target.localName == "a"
|
||||
) {
|
||||
let eventName =
|
||||
event.target.dataset.l10nName == "import-file-link"
|
||||
? "AboutLoginsImportFromFile"
|
||||
: "AboutLoginsImportFromBrowser";
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(eventName, {
|
||||
new CustomEvent("AboutLoginsImport", {
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
|
@ -51,7 +47,7 @@ export default class LoginIntro extends HTMLElement {
|
|||
updateState(syncState) {
|
||||
let l10nId = syncState.loggedIn
|
||||
? "about-logins-login-intro-heading-logged-in"
|
||||
: "about-logins-login-intro-heading-logged-out";
|
||||
: "login-intro-heading";
|
||||
document.l10n.setAttributes(
|
||||
this.shadowRoot.querySelector(".heading"),
|
||||
l10nId
|
||||
|
|
|
@ -66,14 +66,10 @@
|
|||
background-image: url("chrome://global/skin/icons/help.svg");
|
||||
}
|
||||
|
||||
.menuitem-import-browser {
|
||||
.menuitem-import {
|
||||
background-image: url("chrome://browser/skin/import.svg");
|
||||
}
|
||||
|
||||
.menuitem-import-file {
|
||||
background-image: url("chrome://browser/skin/open.svg");
|
||||
}
|
||||
|
||||
.menuitem-export {
|
||||
background-image: url("chrome://browser/skin/save.svg");
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ skip-if = (os == 'linux') # bug 1569789
|
|||
[browser_openFiltered.js]
|
||||
[browser_openImport.js]
|
||||
skip-if = (os != "win" && os != "mac") # import is only available on Windows and macOS
|
||||
[browser_openImportCSV.js]
|
||||
[browser_openPreferences.js]
|
||||
[browser_openPreferencesExternal.js]
|
||||
[browser_openSite.js]
|
||||
|
|
|
@ -56,7 +56,7 @@ add_task(async function test_no_logins_class() {
|
|||
content.document.l10n.getAttributes(
|
||||
loginIntro.shadowRoot.querySelector(".heading")
|
||||
).id,
|
||||
"about-logins-login-intro-heading-logged-out",
|
||||
"login-intro-heading",
|
||||
"The default message should be the non-logged-in message"
|
||||
);
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ add_task(async function test_open_import() {
|
|||
|
||||
function getImportItem() {
|
||||
let menuButton = window.document.querySelector("menu-button");
|
||||
return menuButton.shadowRoot.querySelector(".menuitem-import-browser");
|
||||
return menuButton.shadowRoot.querySelector(".menuitem-import");
|
||||
}
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(getImportItem, {}, browser);
|
||||
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
let { TelemetryTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/TelemetryTestUtils.jsm"
|
||||
);
|
||||
|
||||
const { FileTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/FileTestUtils.jsm"
|
||||
);
|
||||
|
||||
let { MockFilePicker } = SpecialPowers;
|
||||
|
||||
/**
|
||||
* Waits until the mock file picker is opened and sets the destFilePath as it's selected file.
|
||||
*
|
||||
* @param {nsIFile} destFile
|
||||
* The file being passed to the picker.
|
||||
* @returns {string} A promise that is resolved when the picker selects the file.
|
||||
*/
|
||||
function waitForOpenFilePicker(destFile) {
|
||||
return new Promise(resolve => {
|
||||
MockFilePicker.showCallback = function(fp) {
|
||||
info("showCallback");
|
||||
info("fileName: " + destFile.path);
|
||||
MockFilePicker.setFiles([destFile]);
|
||||
MockFilePicker.filterIndex = 1;
|
||||
info("done showCallback");
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function test_open_import_from_csv() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{ gBrowser, url: "about:logins" },
|
||||
async function(browser) {
|
||||
MockFilePicker.init(window);
|
||||
MockFilePicker.returnValue = MockFilePicker.returnOK;
|
||||
|
||||
let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://example.com,joe@example.com,qwerty,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
"menu-button",
|
||||
{},
|
||||
browser
|
||||
);
|
||||
|
||||
await SpecialPowers.spawn(browser, [], async () => {
|
||||
let menuButton = content.document.querySelector("menu-button");
|
||||
return ContentTaskUtils.waitForCondition(function waitForMenu() {
|
||||
return !menuButton.shadowRoot.querySelector(".menu").hidden;
|
||||
}, "waiting for menu to open");
|
||||
});
|
||||
|
||||
function getImportMenuItem() {
|
||||
let menuButton = window.document.querySelector("menu-button");
|
||||
let importButton = menuButton.shadowRoot.querySelector(
|
||||
".menuitem-import-file"
|
||||
);
|
||||
// Force the menu item to be visible for the test.
|
||||
importButton.hidden = false;
|
||||
return importButton;
|
||||
}
|
||||
|
||||
EXPECTED_ERROR_MESSAGE = "Couldn't parse origin for";
|
||||
|
||||
let filePicker = waitForOpenFilePicker(csvFile);
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
getImportMenuItem,
|
||||
{},
|
||||
browser
|
||||
);
|
||||
|
||||
// First event is for opening about:logins
|
||||
await LoginTestUtils.telemetry.waitForEventCount(2);
|
||||
TelemetryTestUtils.assertEvents(
|
||||
[["pwmgr", "mgmt_menu_item_used", "import_from_csv"]],
|
||||
{ category: "pwmgr", method: "mgmt_menu_item_used" },
|
||||
{ process: "content", clear: false }
|
||||
);
|
||||
|
||||
info("waiting for Import file picker to get opened");
|
||||
await filePicker;
|
||||
ok(true, "Import file picker opened");
|
||||
info("Waiting for the import to complete");
|
||||
await LoginTestUtils.telemetry.waitForEventCount(1, "parent");
|
||||
TelemetryTestUtils.assertEvents(
|
||||
[["pwmgr", "mgmt_menu_item_used", "import_csv_complete"]],
|
||||
{ category: "pwmgr", method: "mgmt_menu_item_used" },
|
||||
{ process: "parent" }
|
||||
);
|
||||
|
||||
EXPECTED_ERROR_MESSAGE = null;
|
||||
}
|
||||
);
|
||||
});
|
|
@ -73,7 +73,6 @@ add_task(async function test_menu_open_close() {
|
|||
await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
|
||||
"waiting for firstVisibleItem to get focus again");
|
||||
ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it again");
|
||||
sendKey("TAB"); // Import from file
|
||||
sendKey("TAB"); // Export
|
||||
|
||||
if (navigator.platform == "Win32" || navigator.platform == "MacIntel") {
|
||||
|
@ -124,7 +123,6 @@ add_task(async function test_menu_keyboard_cycling() {
|
|||
}
|
||||
|
||||
let allItems = [
|
||||
"menuitem-import-file",
|
||||
"menuitem-export",
|
||||
"menuitem-preferences",
|
||||
"menuitem-help",
|
||||
|
@ -132,7 +130,7 @@ add_task(async function test_menu_keyboard_cycling() {
|
|||
"menuitem-mobile-ios",
|
||||
];
|
||||
if (navigator.platform == "Win32" || navigator.platform == "MacIntel") {
|
||||
allItems = ["menuitem-import-browser", ...allItems];
|
||||
allItems = ["menuitem-import", ...allItems];
|
||||
}
|
||||
|
||||
let menu = gMenuButton.shadowRoot.querySelector(".menu");
|
||||
|
|
|
@ -30,7 +30,6 @@ menu =
|
|||
.title = Open menu
|
||||
# This menuitem is only visible on Windows and macOS
|
||||
about-logins-menu-menuitem-import-from-another-browser = Import from Another Browser…
|
||||
about-logins-menu-menuitem-import-from-a-file = Import from a File…
|
||||
about-logins-menu-menuitem-export-logins = Export Logins…
|
||||
menu-menuitem-preferences =
|
||||
{ PLATFORM() ->
|
||||
|
@ -70,13 +69,13 @@ about-logins-list-item-vulnerable-password-icon =
|
|||
|
||||
## Introduction screen
|
||||
|
||||
about-logins-login-intro-heading-logged-out = Looking for your saved logins? Set up { -sync-brand-short-name } or Import Them.
|
||||
login-intro-heading = Looking for your saved logins? Set up { -sync-brand-short-name }.
|
||||
about-logins-login-intro-heading-logged-in = No synced logins found.
|
||||
login-intro-description = If you saved your logins to { -brand-product-name } on a different device, here’s how to get them here:
|
||||
login-intro-instruction-fxa = Create or sign in to your { -fxaccount-brand-name } on the device where your logins are saved
|
||||
login-intro-instruction-fxa-settings = Make sure you’ve selected the Logins checkbox in { -sync-brand-short-name } Settings
|
||||
about-logins-intro-instruction-help = Visit <a data-l10n-name="help-link">{ -lockwise-brand-short-name } Support</a> for more help
|
||||
about-logins-intro-import2 = If your logins are saved outside of { -brand-product-name }, you can <a data-l10n-name="import-browser-link">import them from another browser</a> or <a data-l10n-name="import-file-link">from a file</a>
|
||||
about-logins-intro-import = If your logins are saved in another browser, you can <a data-l10n-name="import-link">import them into { -lockwise-brand-short-name }</a>
|
||||
|
||||
## Login
|
||||
|
||||
|
@ -224,16 +223,3 @@ about-logins-export-file-picker-csv-filter-title =
|
|||
[macos] CSV Document
|
||||
*[other] CSV File
|
||||
}
|
||||
|
||||
## Login Import Dialog
|
||||
|
||||
# Title of the file picker dialog
|
||||
about-logins-import-file-picker-title = Import Logins File
|
||||
about-logins-import-file-picker-import-button = Import
|
||||
# A description for the .csv file format that may be shown as the file type
|
||||
# filter by the operating system.
|
||||
about-logins-import-file-picker-csv-filter-title =
|
||||
{ PLATFORM() ->
|
||||
[macos] CSV Document
|
||||
*[other] CSV File
|
||||
}
|
||||
|
|
|
@ -10,9 +10,6 @@ with Files('aom/**'):
|
|||
with Files('dav1d/**'):
|
||||
BUG_COMPONENT = ('Core', 'Audio/Video: Playback')
|
||||
|
||||
with Files('js/d3/**'):
|
||||
BUG_COMPONENT = ('Toolkit', 'General')
|
||||
|
||||
with Files('rust/**'):
|
||||
BUG_COMPONENT = ('Firefox Build System', 'General')
|
||||
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Provides a class to import login-related data CSV files.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["LoginCSVImport"];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
OS: "resource://gre/modules/osfile.jsm",
|
||||
ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.jsm",
|
||||
Services: "resource://gre/modules/Services.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "d3", () => {
|
||||
let d3Scope = Cu.Sandbox(null);
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://global/content/third_party/d3/d3.js",
|
||||
d3Scope
|
||||
);
|
||||
return Cu.waiveXrays(d3Scope.d3);
|
||||
});
|
||||
|
||||
const FIELD_TO_CSV_COLUMNS = {
|
||||
origin: ["url", "login_uri"],
|
||||
username: ["username", "login_username"],
|
||||
password: ["password", "login_password"],
|
||||
httpRealm: ["httpRealm"],
|
||||
formActionOrigin: ["formActionOrigin"],
|
||||
guid: ["guid"],
|
||||
timeCreated: ["timeCreated"],
|
||||
timeLastUsed: ["timeLastUsed"],
|
||||
timePasswordChanged: ["timePasswordChanged"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides an object that has a method to import login-related data CSV files
|
||||
*/
|
||||
class LoginCSVImport {
|
||||
static get MIGRATION_HISTOGRAM_KEY() {
|
||||
return "login_csv";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map that has the csv column name as key and the value the field name.
|
||||
*
|
||||
* @returns {Map} A map that has the csv column name as key and the value the field name.
|
||||
*/
|
||||
static _getCSVColumnToFieldMap() {
|
||||
let csvColumnToField = new Map();
|
||||
for (let [field, columns] of Object.entries(FIELD_TO_CSV_COLUMNS)) {
|
||||
for (let column of columns) {
|
||||
csvColumnToField.set(column, field);
|
||||
}
|
||||
}
|
||||
return csvColumnToField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a vanilla JS object containing all the login fields from a row of CSV cells.
|
||||
*
|
||||
* @param {object} csvObject
|
||||
* An object created from a csv row. The keys are the csv column names, the values are the cells.
|
||||
* @param {Map} csvColumnToFieldMap
|
||||
* A map where the keys are the csv properties and the values are the object keys.
|
||||
* @returns {object} Representing login object with only properties, not functions.
|
||||
*/
|
||||
static _getVanillaLoginFromCSVObject(csvObject, csvColumnToFieldMap) {
|
||||
let vanillaLogin = Object.create(null);
|
||||
for (let columnName of Object.keys(csvObject)) {
|
||||
let fieldName = csvColumnToFieldMap.get(columnName);
|
||||
if (!fieldName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof vanillaLogin[fieldName] != "undefined" &&
|
||||
vanillaLogin[fieldName] !== csvObject[columnName]
|
||||
) {
|
||||
// Differing column values map to one property.
|
||||
// e.g. if two headings map to `origin` we won't know which to use.
|
||||
return {};
|
||||
}
|
||||
|
||||
vanillaLogin[fieldName] = csvObject[columnName];
|
||||
}
|
||||
|
||||
// Since `null` can't be represented in a CSV file and the httpRealm header
|
||||
// cannot be an empty string, assume that an empty httpRealm means this is
|
||||
// a form login and therefore null-out httpRealm.
|
||||
if (vanillaLogin.httpRealm === "") {
|
||||
vanillaLogin.httpRealm = null;
|
||||
}
|
||||
|
||||
return vanillaLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports logins from a CSV file (comma-separated values file).
|
||||
* Existing logins may be updated in the process.
|
||||
*
|
||||
* @param {string} filePath
|
||||
*/
|
||||
static async importFromCSV(filePath) {
|
||||
TelemetryStopwatch.startKeyed(
|
||||
"FX_MIGRATION_LOGINS_IMPORT_MS",
|
||||
LoginCSVImport.MIGRATION_HISTOGRAM_KEY
|
||||
);
|
||||
let responsivenessMonitor = new ResponsivenessMonitor();
|
||||
let csvColumnToFieldMap = LoginCSVImport._getCSVColumnToFieldMap();
|
||||
let csvString = await OS.File.read(filePath, { encoding: "utf-8" });
|
||||
let parsedLines = d3.csv.parse(csvString);
|
||||
let fieldsInFile = new Set(
|
||||
Object.keys(parsedLines[0] || {}).map(col => csvColumnToFieldMap.get(col))
|
||||
);
|
||||
if (
|
||||
parsedLines[0] &&
|
||||
(!fieldsInFile.has("origin") ||
|
||||
!fieldsInFile.has("username") ||
|
||||
!fieldsInFile.has("password"))
|
||||
) {
|
||||
// The username *value* can be empty but we require a username column to
|
||||
// ensure that we don't import logins without their usernames due to the
|
||||
// username column not being recognized.
|
||||
TelemetryStopwatch.cancelKeyed(
|
||||
"FX_MIGRATION_LOGINS_IMPORT_MS",
|
||||
LoginCSVImport.MIGRATION_HISTOGRAM_KEY
|
||||
);
|
||||
throw new Error(
|
||||
"CSV file must contain origin, username, and password columns"
|
||||
);
|
||||
}
|
||||
|
||||
let loginsToImport = parsedLines.map(csvObject => {
|
||||
return LoginCSVImport._getVanillaLoginFromCSVObject(
|
||||
csvObject,
|
||||
csvColumnToFieldMap
|
||||
);
|
||||
});
|
||||
|
||||
await LoginHelper.maybeImportLogins(loginsToImport);
|
||||
|
||||
// Record quantity, jank, and duration telemetry.
|
||||
try {
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById("FX_MIGRATION_LOGINS_QUANTITY")
|
||||
.add(LoginCSVImport.MIGRATION_HISTOGRAM_KEY, parsedLines.length);
|
||||
let accumulatedDelay = responsivenessMonitor.finish();
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById("FX_MIGRATION_LOGINS_JANK_MS")
|
||||
.add(LoginCSVImport.MIGRATION_HISTOGRAM_KEY, accumulatedDelay);
|
||||
TelemetryStopwatch.finishKeyed(
|
||||
"FX_MIGRATION_LOGINS_IMPORT_MS",
|
||||
LoginCSVImport.MIGRATION_HISTOGRAM_KEY
|
||||
);
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,13 +50,8 @@ class LoginExport {
|
|||
*
|
||||
* @param {string} path
|
||||
* The file path to save the login to.
|
||||
* @param {nsILoginInfo[]} [logins = null]
|
||||
* An optional list of logins.
|
||||
*/
|
||||
static async exportAsCSV(path, logins = null) {
|
||||
if (!logins) {
|
||||
logins = await Services.logins.getAllLoginsAsync();
|
||||
}
|
||||
static async exportAsCSV(path) {
|
||||
let columns = [
|
||||
"origin",
|
||||
"username",
|
||||
|
@ -77,6 +72,7 @@ class LoginExport {
|
|||
|
||||
let rows = [];
|
||||
rows.push(csvHeader);
|
||||
let logins = await Services.logins.getAllLoginsAsync();
|
||||
for (let login of logins) {
|
||||
rows.push(LoginExport._buildCSVRow(login, columns));
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
const EXPORTED_SYMBOLS = ["LoginHelper"];
|
||||
|
||||
// Globals
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
|
@ -175,10 +177,9 @@ this.LoginHelper = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Due to the way the signons2.txt file was formatted, we needed to make
|
||||
* Due to the way the signons2.txt file is formatted, we need to make
|
||||
* sure certain field values or characters do not cause the file to
|
||||
* be parsed incorrectly. These characters can cause problems in other
|
||||
* formats/languages too so reject logins that may not be stored correctly.
|
||||
* be parsed incorrectly. Reject logins that we can't store correctly.
|
||||
*
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
|
@ -199,10 +200,6 @@ this.LoginHelper = {
|
|||
throw new Error("login values can't contain nulls");
|
||||
}
|
||||
|
||||
if (!aLogin.password || typeof aLogin.password != "string") {
|
||||
throw new Error("passwords must be non-empty strings");
|
||||
}
|
||||
|
||||
// In theory these nulls should just be rolled up into the encrypted
|
||||
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
|
||||
// nulls cause truncation. Check for them here just to avoid
|
||||
|
@ -288,7 +285,7 @@ this.LoginHelper = {
|
|||
* Get the parts of the URL we want for identification.
|
||||
* Strip out things like the userPass portion and handle javascript:.
|
||||
*/
|
||||
getLoginOrigin(uriString, allowJS = false) {
|
||||
getLoginOrigin(uriString, allowJS) {
|
||||
let realm = "";
|
||||
try {
|
||||
let uri = Services.io.newURI(uriString);
|
||||
|
@ -450,14 +447,14 @@ this.LoginHelper = {
|
|||
* Creates a new login object that results by modifying the given object with
|
||||
* the provided data.
|
||||
*
|
||||
* @param {nsILoginInfo} aOldStoredLogin
|
||||
* Existing login object to modify.
|
||||
* @param {nsILoginInfo|nsIProperyBag} aNewLoginData
|
||||
* The new login values, either as an nsILoginInfo or nsIProperyBag.
|
||||
* @param aOldStoredLogin
|
||||
* Existing nsILoginInfo object to modify.
|
||||
* @param aNewLoginData
|
||||
* The new login values, either as nsILoginInfo or nsIProperyBag.
|
||||
*
|
||||
* @return {nsILoginInfo} The newly created nsILoginInfo object.
|
||||
* @return The newly created nsILoginInfo object.
|
||||
*
|
||||
* @throws {Error} With English message in case validation failed.
|
||||
* @throws String with English message in case validation failed.
|
||||
*/
|
||||
buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
|
||||
function bagHasProperty(aPropName) {
|
||||
|
@ -966,55 +963,16 @@ this.LoginHelper = {
|
|||
async maybeImportLogins(loginDatas) {
|
||||
let loginsToAdd = [];
|
||||
let loginMap = new Map();
|
||||
for (let rawLoginData of loginDatas) {
|
||||
// Do some sanitization on a clone of the loginData.
|
||||
let loginData = ChromeUtils.shallowClone(rawLoginData);
|
||||
loginData.origin = this.getLoginOrigin(loginData.origin);
|
||||
if (!loginData.origin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
loginData.formActionOrigin =
|
||||
this.getLoginOrigin(loginData.formActionOrigin, true) ||
|
||||
(typeof loginData.httpRealm == "string" ? null : "");
|
||||
|
||||
loginData.httpRealm =
|
||||
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null;
|
||||
|
||||
if (loginData.guid) {
|
||||
// First check for `guid` matches if it's set.
|
||||
// `guid` matches will allow every kind of update, including reverting
|
||||
// to older passwords which can be useful if the user wants to recover
|
||||
// an old password.
|
||||
let existingLogins = await Services.logins.searchLoginsAsync({
|
||||
guid: loginData.guid,
|
||||
origin: loginData.origin, // Ignored outside of GV.
|
||||
});
|
||||
|
||||
if (existingLogins.length) {
|
||||
log.debug("maybeImportLogins: Found existing login with GUID");
|
||||
// There should only be one `guid` match.
|
||||
let existingLogin = existingLogins[0].QueryInterface(
|
||||
Ci.nsILoginMetaInfo
|
||||
);
|
||||
|
||||
// Use a property bag rather than an nsILoginInfo so we don't clobber
|
||||
// properties that the import source doesn't provide.
|
||||
let propBag = this.newPropertyBag(loginData);
|
||||
Services.logins.modifyLogin(existingLogin, propBag);
|
||||
// Updated a login so we're done.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (let loginData of loginDatas) {
|
||||
// create a new login
|
||||
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
|
||||
Ci.nsILoginInfo
|
||||
);
|
||||
login.init(
|
||||
loginData.origin,
|
||||
loginData.formActionOrigin,
|
||||
loginData.httpRealm,
|
||||
loginData.formActionOrigin ||
|
||||
(typeof loginData.httpRealm == "string" ? null : ""),
|
||||
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null,
|
||||
loginData.username,
|
||||
loginData.password,
|
||||
loginData.usernameElement || "",
|
||||
|
@ -1027,7 +985,6 @@ this.LoginHelper = {
|
|||
login.timePasswordChanged =
|
||||
loginData.timePasswordChanged || loginData.timeCreated;
|
||||
login.timesUsed = loginData.timesUsed || 1;
|
||||
login.guid = loginData.guid || null;
|
||||
|
||||
try {
|
||||
// Ensure we only send checked logins through, since the validation is optimized
|
||||
|
@ -1125,9 +1082,9 @@ this.LoginHelper = {
|
|||
|
||||
/**
|
||||
* Convert an array of nsILoginInfo to vanilla JS objects suitable for
|
||||
* sending over IPC. Avoid using this in other cases.
|
||||
* sending over IPC.
|
||||
*
|
||||
* NB: All members of nsILoginInfo (not nsILoginMetaInfo) are strings.
|
||||
* NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
|
||||
*/
|
||||
loginsToVanillaObjects(logins) {
|
||||
return logins.map(this.loginToVanillaObject);
|
||||
|
|
|
@ -724,7 +724,7 @@ class LoginManagerParent extends JSWindowActorParent {
|
|||
if (autoFilledLoginGuid) {
|
||||
let loginsForGuid = await Services.logins.searchLoginsAsync({
|
||||
guid: autoFilledLoginGuid,
|
||||
origin: formOrigin, // Ignored outside of GV.
|
||||
origin: formOrigin,
|
||||
});
|
||||
if (
|
||||
loginsForGuid.length == 1 &&
|
||||
|
@ -971,7 +971,7 @@ class LoginManagerParent extends JSWindowActorParent {
|
|||
if (autoFilledLoginGuid) {
|
||||
let [matchedLogin] = await Services.logins.searchLoginsAsync({
|
||||
guid: autoFilledLoginGuid,
|
||||
origin: formOrigin, // Ignored outside of GV.
|
||||
origin: formOrigin,
|
||||
});
|
||||
if (
|
||||
matchedLogin &&
|
||||
|
@ -1045,7 +1045,7 @@ class LoginManagerParent extends JSWindowActorParent {
|
|||
if (generatedPW.storageGUID) {
|
||||
[autoSavedLogin] = await Services.logins.searchLoginsAsync({
|
||||
guid: generatedPW.storageGUID,
|
||||
origin: formOrigin, // Ignored outside of GV.
|
||||
origin: formOrigin,
|
||||
});
|
||||
|
||||
if (autoSavedLogin) {
|
||||
|
|
|
@ -53,7 +53,6 @@ if CONFIG['OS_TARGET'] == 'Android':
|
|||
]
|
||||
else:
|
||||
EXTRA_JS_MODULES += [
|
||||
'LoginCSVImport.jsm',
|
||||
'LoginExport.jsm',
|
||||
'LoginStore.jsm',
|
||||
]
|
||||
|
|
|
@ -22,8 +22,7 @@ interface nsILoginInfo : nsISupports {
|
|||
* The origin the login applies to.
|
||||
*
|
||||
* For example,
|
||||
* "https://site.com", "http://site.com:1234", "ftp://ftp.site.com",
|
||||
* "moz-proxy://127.0.0.1:8888, "chrome://FirefoxAccounts", "file://".
|
||||
* "https://site.com", "http://site.com:1234", "ftp://ftp.site.com".
|
||||
*/
|
||||
attribute AString origin;
|
||||
|
||||
|
|
|
@ -113,8 +113,6 @@ interface nsILoginManager : nsISupports {
|
|||
* Fetch all logins in the login manager. An array is always returned;
|
||||
* if there are no logins the array is empty.
|
||||
*
|
||||
* @deprecated Use `getAllLoginsAsync` instead.
|
||||
*
|
||||
* @return An array of nsILoginInfo objects.
|
||||
*/
|
||||
Array<nsILoginInfo> getAllLogins();
|
||||
|
@ -159,8 +157,6 @@ interface nsILoginManager : nsISupports {
|
|||
* Search for logins matching the specified criteria. Called when looking
|
||||
* for logins that might be applicable to a form or authentication request.
|
||||
*
|
||||
* @deprecated Use `searchLoginsAsync` instead.
|
||||
*
|
||||
* @param aOrigin
|
||||
* The origin to restrict searches to. For example: "http://www.site.com".
|
||||
* To find logins for a given nsIURI, you would typically pass in
|
||||
|
@ -211,8 +207,7 @@ interface nsILoginManager : nsISupports {
|
|||
* @param {object} matchData
|
||||
* The data used to search as a JS object. This does not follow the same
|
||||
* requirements as findLogins for those fields. Wildcard matches are
|
||||
* simply not specified. If a `guid` is specified then no other properties
|
||||
* are used (outside of GeckoView).
|
||||
* simply not specified.
|
||||
* @return A promise resolving to an array of nsILoginInfo objects.
|
||||
*/
|
||||
Promise searchLoginsAsync(in jsval matchData);
|
||||
|
@ -226,8 +221,7 @@ interface nsILoginManager : nsISupports {
|
|||
* @param matchData
|
||||
* The data used to search. This does not follow the same
|
||||
* requirements as findLogins for those fields. Wildcard matches are
|
||||
* simply not specified. If a `guid` is specified then no other properties
|
||||
* are used (outside of GeckoView).
|
||||
* simply not specified.
|
||||
* @return An array of nsILoginInfo objects.
|
||||
*/
|
||||
Array<nsILoginInfo> searchLogins(in nsIPropertyBag matchData);
|
||||
|
|
|
@ -30,6 +30,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
Promise initialize();
|
||||
|
||||
|
||||
/**
|
||||
* Ensures that all data has been written to disk and all files are closed.
|
||||
*
|
||||
|
@ -42,6 +43,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
Promise terminate();
|
||||
|
||||
|
||||
/**
|
||||
* Store a new login in the storage module.
|
||||
*
|
||||
|
@ -61,6 +63,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
nsILoginInfo addLogin(in nsILoginInfo aLogin, [optional] in boolean aPreEncrypted, [optional] in jsval aPlaintextUsername, [optional] in jsval aPlaintextPassword);
|
||||
|
||||
|
||||
/**
|
||||
* Remove a login from the storage module.
|
||||
*
|
||||
|
@ -72,6 +75,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
void removeLogin(in nsILoginInfo aLogin);
|
||||
|
||||
|
||||
/**
|
||||
* Modify an existing login in the storage module.
|
||||
*
|
||||
|
@ -93,6 +97,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
void modifyLogin(in nsILoginInfo oldLogin, in nsISupports newLoginData);
|
||||
|
||||
|
||||
/**
|
||||
* Record that the password of a saved login was used (e.g. submitted or copied).
|
||||
*
|
||||
|
@ -105,6 +110,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
void recordPasswordUse(in nsILoginInfo aLogin);
|
||||
|
||||
|
||||
/**
|
||||
* Remove all stored logins.
|
||||
*
|
||||
|
@ -114,12 +120,11 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
*/
|
||||
void removeAllLogins();
|
||||
|
||||
|
||||
/**
|
||||
* Fetch all logins in the login manager. An array is always returned;
|
||||
* if there are no logins the array is empty.
|
||||
*
|
||||
* @deprecated Use `getAllLoginsAsync` instead.
|
||||
*
|
||||
* @return An array of nsILoginInfo objects.
|
||||
*/
|
||||
Array<nsILoginInfo> getAllLogins();
|
||||
|
@ -147,7 +152,6 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
/**
|
||||
* Search for logins in the login manager. An array is always returned;
|
||||
* if there are no logins the array is empty.
|
||||
*
|
||||
* @deprecated New code should use `searchLoginsAsync`.
|
||||
* Only autocomplete, prompt, and test code still use this.
|
||||
*
|
||||
|
@ -163,8 +167,6 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
* Search for logins matching the specified criteria. Called when looking
|
||||
* for logins that might be applicable to a form or authentication request.
|
||||
*
|
||||
* @deprecated Use `searchLoginsAsync` instead.
|
||||
*
|
||||
* @param aOrigin
|
||||
* The origin to restrict searches to. For example: "http://www.site.com".
|
||||
* @param aActionURL
|
||||
|
@ -180,6 +182,7 @@ interface nsILoginManagerStorage : nsISupports {
|
|||
Array<nsILoginInfo> findLogins(in AString aOrigin, in AString aActionOrigin,
|
||||
in AString aHttpRealm);
|
||||
|
||||
|
||||
/**
|
||||
* Search for logins matching the specified criteria, as with
|
||||
* findLogins(). This interface only returns the number of matching
|
||||
|
|
|
@ -20,12 +20,6 @@ const { TestUtils } = ChromeUtils.import(
|
|||
"resource://testing-common/TestUtils.jsm"
|
||||
);
|
||||
|
||||
const { FileTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/FileTestUtils.jsm"
|
||||
);
|
||||
|
||||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
const LoginInfo = Components.Constructor(
|
||||
"@mozilla.org/login-manager/loginInfo;1",
|
||||
"nsILoginInfo",
|
||||
|
@ -89,39 +83,22 @@ this.LoginTestUtils = {
|
|||
|
||||
/**
|
||||
* Checks that the currently stored list of nsILoginInfo matches the provided
|
||||
* array. If no `checkFn` is provided, the comparison uses the "equals"
|
||||
* method of nsILoginInfo, that does not include nsILoginMetaInfo properties in the test.
|
||||
* array. The comparison uses the "equals" method of nsILoginInfo, that does
|
||||
* not include nsILoginMetaInfo properties in the test.
|
||||
*/
|
||||
checkLogins(expectedLogins, msg = "checkLogins", checkFn = undefined) {
|
||||
this.assertLoginListsEqual(
|
||||
Services.logins.getAllLogins(),
|
||||
expectedLogins,
|
||||
msg,
|
||||
checkFn
|
||||
);
|
||||
checkLogins(expectedLogins) {
|
||||
this.assertLoginListsEqual(Services.logins.getAllLogins(), expectedLogins);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the two provided arrays of nsILoginInfo have the same length,
|
||||
* and every login in "expected" is also found in "actual". If no `checkFn`
|
||||
* is provided, the comparison uses the "equals" method of nsILoginInfo, that
|
||||
* does not include nsILoginMetaInfo properties in the test.
|
||||
* and every login in "expected" is also found in "actual". The comparison
|
||||
* uses the "equals" method of nsILoginInfo, that does not include
|
||||
* nsILoginMetaInfo properties in the test.
|
||||
*/
|
||||
assertLoginListsEqual(
|
||||
actual,
|
||||
expected,
|
||||
msg = "assertLoginListsEqual",
|
||||
checkFn = undefined
|
||||
) {
|
||||
Assert.equal(expected.length, actual.length, msg);
|
||||
Assert.ok(
|
||||
expected.every(e =>
|
||||
actual.some(a => {
|
||||
return checkFn ? checkFn(a, e) : a.equals(e);
|
||||
})
|
||||
),
|
||||
msg
|
||||
);
|
||||
assertLoginListsEqual(actual, expected) {
|
||||
Assert.equal(expected.length, actual.length);
|
||||
Assert.ok(expected.every(e => actual.some(a => a.equals(e))));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -455,11 +432,11 @@ LoginTestUtils.testData = {
|
|||
"the password two"
|
||||
),
|
||||
|
||||
// -- file:// URIs throw accessing nsIURI.host
|
||||
// -- file:/// URIs throw accessing nsIURI.host
|
||||
|
||||
new LoginInfo(
|
||||
"file://",
|
||||
"file://",
|
||||
"file:///",
|
||||
"file:///",
|
||||
null,
|
||||
"file: username",
|
||||
"file: password"
|
||||
|
@ -582,21 +559,3 @@ LoginTestUtils.telemetry = {
|
|||
return events;
|
||||
},
|
||||
};
|
||||
|
||||
LoginTestUtils.file = {
|
||||
/**
|
||||
* Given an array of strings it creates a temporary CSV file that has them as content.
|
||||
*
|
||||
* @param {string[]} csvLines
|
||||
* The lines that make up the CSV file.
|
||||
* @returns {window.File} The File to the CSV file that was created.
|
||||
*/
|
||||
async setupCsvFileWithLines(csvLines) {
|
||||
let tmpFile = FileTestUtils.getTempFile("firefox_logins.csv");
|
||||
await OS.File.writeAtomic(
|
||||
tmpFile.path,
|
||||
new TextEncoder().encode(csvLines.join("\r\n"))
|
||||
);
|
||||
return tmpFile;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ add_task(function test_displayOrigin() {
|
|||
Assert.greater(displayOrigin.length, 0, "Check length");
|
||||
if (
|
||||
loginInfo.origin.startsWith("chrome://") ||
|
||||
loginInfo.origin.startsWith("file://")
|
||||
loginInfo.origin.startsWith("file:///")
|
||||
) {
|
||||
Assert.ok(displayOrigin.startsWith(loginInfo.origin), "Contains origin");
|
||||
} else {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const HOST1 = "https://www.example.com";
|
||||
const HOST2 = "https://www.mozilla.org";
|
||||
const HOST1 = "https://www.example.com/";
|
||||
const HOST2 = "https://www.mozilla.org/";
|
||||
|
||||
const USER1 = "myuser";
|
||||
const USER2 = "anotheruser";
|
||||
|
@ -10,52 +10,13 @@ const PASS1 = "mypass";
|
|||
const PASS2 = "anotherpass";
|
||||
const PASS3 = "yetanotherpass";
|
||||
|
||||
add_task(async function test_invalid_logins() {
|
||||
let importedLogins = await LoginHelper.maybeImportLogins([
|
||||
{
|
||||
username: USER1,
|
||||
password: PASS1,
|
||||
origin: "example.com", // Not an origin
|
||||
formActionOrigin: HOST1,
|
||||
},
|
||||
{
|
||||
username: USER1,
|
||||
// no password
|
||||
origin: HOST1,
|
||||
formActionOrigin: HOST1,
|
||||
},
|
||||
{
|
||||
username: USER2,
|
||||
password: "", // Empty password
|
||||
origin: HOST1,
|
||||
formActionOrigin: HOST1,
|
||||
},
|
||||
]);
|
||||
Assert.equal(
|
||||
importedLogins.length,
|
||||
0,
|
||||
`Return value should indicate no imported login: ${JSON.stringify(
|
||||
importedLogins,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
let savedLogins = Services.logins.getAllLogins();
|
||||
Assert.equal(
|
||||
savedLogins.length,
|
||||
0,
|
||||
`Should have no logins in storage: ${JSON.stringify(savedLogins, null, 2)}`
|
||||
);
|
||||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
add_task(async function test_new_logins() {
|
||||
let [importedLogin] = await LoginHelper.maybeImportLogins([
|
||||
{
|
||||
username: USER1,
|
||||
password: PASS1,
|
||||
origin: HOST1 + "/",
|
||||
formActionOrigin: HOST1 + "/",
|
||||
origin: HOST1,
|
||||
formActionOrigin: HOST1,
|
||||
},
|
||||
]);
|
||||
Assert.ok(importedLogin, "Return value should indicate imported login.");
|
||||
|
@ -211,7 +172,7 @@ add_task(async function test_different_passwords() {
|
|||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
add_task(async function test_different_usernames_without_guid() {
|
||||
add_task(async function test_different_usernames() {
|
||||
let [importedLogin] = await LoginHelper.maybeImportLogins([
|
||||
{
|
||||
username: USER1,
|
||||
|
@ -250,48 +211,6 @@ add_task(async function test_different_usernames_without_guid() {
|
|||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
add_task(async function test_different_usernames_with_guid() {
|
||||
let [importedLogin] = await LoginHelper.maybeImportLogins([
|
||||
{
|
||||
username: USER1,
|
||||
password: PASS1,
|
||||
origin: HOST1,
|
||||
formActionOrigin: HOST1,
|
||||
},
|
||||
]);
|
||||
Assert.ok(importedLogin, "Return value should indicate imported login.");
|
||||
let matchingLogins = LoginHelper.searchLoginsWithObject({ origin: HOST1 });
|
||||
Assert.equal(
|
||||
matchingLogins.length,
|
||||
1,
|
||||
`There should be 1 login for ${HOST1}`
|
||||
);
|
||||
|
||||
info("Changing both the origin and username using the GUID");
|
||||
let importedLogins = await LoginHelper.maybeImportLogins([
|
||||
{
|
||||
username: USER2,
|
||||
password: PASS1,
|
||||
origin: HOST2,
|
||||
formActionOrigin: HOST1,
|
||||
guid: importedLogin.guid,
|
||||
},
|
||||
]);
|
||||
Assert.ok(!importedLogins.length, "Return value should indicate an update");
|
||||
matchingLogins = LoginHelper.searchLoginsWithObject({ origin: HOST2 });
|
||||
Assert.equal(
|
||||
matchingLogins.length,
|
||||
1,
|
||||
`The 1 login for ${HOST1} should have been updated`
|
||||
);
|
||||
let storageLogin = matchingLogins[0];
|
||||
Assert.equal(storageLogin.guid, importedLogin.guid, "Check same guid");
|
||||
Assert.equal(storageLogin.username, USER2, "Check username updated");
|
||||
Assert.equal(storageLogin.origin, HOST2, "Check origin updated");
|
||||
|
||||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
add_task(async function test_different_targets() {
|
||||
let [importedLogin] = await LoginHelper.maybeImportLogins([
|
||||
{
|
||||
|
|
|
@ -1,439 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Tests the LoginCSVImport module.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { LoginCSVImport } = ChromeUtils.import(
|
||||
"resource://gre/modules/LoginCSVImport.jsm"
|
||||
);
|
||||
const { LoginExport } = ChromeUtils.import(
|
||||
"resource://gre/modules/LoginExport.jsm"
|
||||
);
|
||||
const { TelemetryTestUtils: TTU } = ChromeUtils.import(
|
||||
"resource://testing-common/TelemetryTestUtils.jsm"
|
||||
);
|
||||
|
||||
// Enable the collection (during test) for all products so even products
|
||||
// that don't collect the data will be able to run the test without failure.
|
||||
Services.prefs.setBoolPref(
|
||||
"toolkit.telemetry.testing.overrideProductsCheck",
|
||||
true
|
||||
);
|
||||
|
||||
/**
|
||||
* Given an array of strings it creates a temporary CSV file that has them as content.
|
||||
*
|
||||
* @param {string[]} csvLines
|
||||
* The lines that make up the CSV file.
|
||||
* @returns {string} The path to the CSV file that was created.
|
||||
*/
|
||||
async function setupCsv(csvLines) {
|
||||
// Cleanup state.
|
||||
TTU.getAndClearKeyedHistogram("FX_MIGRATION_LOGINS_QUANTITY");
|
||||
TTU.getAndClearKeyedHistogram("FX_MIGRATION_LOGINS_IMPORT_MS");
|
||||
TTU.getAndClearKeyedHistogram("FX_MIGRATION_LOGINS_JANK_MS");
|
||||
Services.logins.removeAllLogins();
|
||||
|
||||
let tmpFile = await LoginTestUtils.file.setupCsvFileWithLines(csvLines);
|
||||
return tmpFile.path;
|
||||
}
|
||||
|
||||
function checkMetaInfo(
|
||||
actual,
|
||||
expected,
|
||||
props = ["timesUsed", "timeCreated", "timePasswordChanged", "timeLastUsed"]
|
||||
) {
|
||||
for (let prop of props) {
|
||||
// This will throw if not equal.
|
||||
equal(actual[prop], expected[prop], `Check ${prop}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkLoginNewlyCreated(login) {
|
||||
// These will throw if not equal.
|
||||
LoginTestUtils.assertTimeIsAboutNow(login.timeCreated);
|
||||
LoginTestUtils.assertTimeIsAboutNow(login.timePasswordChanged);
|
||||
LoginTestUtils.assertTimeIsAboutNow(login.timeLastUsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an import fails if there is no username column. We don't want
|
||||
*/
|
||||
add_task(async function test_import_tsv() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url\tusernameTypo\tpassword\thttpRealm\tformActionOrigin\tguid\ttimeCreated\ttimeLastUsed\ttimePasswordChanged",
|
||||
"https://example.com\tjoe@example.com\tqwerty\tMy realm\t\t{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}\t1589617814635\t1589710449871\t1589617846802",
|
||||
]);
|
||||
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/must contain origin, username, and password columns/,
|
||||
"Ensure non-CSV throws"
|
||||
);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[],
|
||||
"Check that no login was added without finding columns"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that an import fails if there is no username column. We don't want
|
||||
* to accidentally import duplicates due to a name mismatch for the username column.
|
||||
*/
|
||||
add_task(async function test_import_lacking_username_column() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,usernameTypo,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
`https://example.com,joe@example.com,qwerty,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802`,
|
||||
]);
|
||||
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/must contain origin, username, and password columns/,
|
||||
"Ensure missing username throws"
|
||||
);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[],
|
||||
"Check that no login was added without finding a username column"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that an import fails if there are two headings that map to one login field.
|
||||
*/
|
||||
add_task(async function test_import_with_duplicate_columns() {
|
||||
// Two origin columns (url & login_uri).
|
||||
// One row has different values and the other has the same.
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,login_uri,username,login_password",
|
||||
"https://example.com/path,https://example.org,john@example.com,azerty",
|
||||
"https://mozilla.org,https://mozilla.org,jdoe@example.com,qwerty",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "",
|
||||
httpRealm: null,
|
||||
origin: "https://mozilla.org",
|
||||
password: "qwerty",
|
||||
passwordField: "",
|
||||
timesUsed: 1,
|
||||
username: "jdoe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that no login was added with duplicate columns of differing values"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that an import doesn't throw with only a header row.
|
||||
*/
|
||||
add_task(async function test_import_only_header_row() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,usernameTypo,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
]);
|
||||
|
||||
// Shouldn't throw
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[],
|
||||
"Check that no login was added without non-header rows."
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that import is allowed with only origin, username, password and that
|
||||
* one can mix and match column naming between conventions from different
|
||||
* password managers (so that we better support new/unknown password managers).
|
||||
*/
|
||||
add_task(async function test_import_minimal_with_mixed_naming() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,login_password",
|
||||
"ftp://example.com,john@example.com,azerty",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "",
|
||||
httpRealm: null,
|
||||
origin: "ftp://example.com",
|
||||
password: "azerty",
|
||||
passwordField: "",
|
||||
timesUsed: 1,
|
||||
username: "john@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new login was added with the correct fields",
|
||||
(a, e) =>
|
||||
a.equals(e) &&
|
||||
checkMetaInfo(a, e, ["timesUsed"]) &&
|
||||
checkLoginNewlyCreated(a)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from the latest Firefox CSV file for various logins from
|
||||
* LoginTestUtils.testData.loginList().
|
||||
*/
|
||||
add_task(async function test_import_from_firefox_various_latest() {
|
||||
await setupCsv([]);
|
||||
info("Populate the login list for export");
|
||||
let logins = LoginTestUtils.testData.loginList();
|
||||
for (let loginInfo of logins) {
|
||||
Services.logins.addLogin(loginInfo);
|
||||
}
|
||||
|
||||
let tmpFilePath = FileTestUtils.getTempFile("logins.csv").path;
|
||||
await LoginExport.exportAsCSV(tmpFilePath);
|
||||
|
||||
await LoginCSVImport.importFromCSV(tmpFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
logins,
|
||||
"Check that all of LoginTestUtils.testData.loginList can be re-imported"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Firefox CSV file without quotes.
|
||||
*/
|
||||
add_task(async function test_import_from_firefox_auth() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
`https://example.com:8080,joe@example.com,qwerty,My realm,"",{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802`,
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.authLogin({
|
||||
formActionOrigin: null,
|
||||
guid: "{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}",
|
||||
httpRealm: "My realm",
|
||||
origin: "https://example.com:8080",
|
||||
password: "qwerty",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814635,
|
||||
timeLastUsed: 1589710449871,
|
||||
timePasswordChanged: 1589617846802,
|
||||
timesUsed: 1,
|
||||
username: "joe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new login was added with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Firefox CSV file with quotes.
|
||||
*/
|
||||
add_task(async function test_import_from_firefox_auth_with_quotes() {
|
||||
let csvFilePath = await setupCsv([
|
||||
'"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"',
|
||||
'"https://example.com","joe@example.com","qwerty2","My realm",,"{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}","1589617814635","1589710449871","1589617846802"',
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.authLogin({
|
||||
formActionOrigin: null,
|
||||
httpRealm: "My realm",
|
||||
origin: "https://example.com",
|
||||
password: "qwerty2",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814635,
|
||||
timeLastUsed: 1589710449871,
|
||||
timePasswordChanged: 1589617846802,
|
||||
timesUsed: 1,
|
||||
username: "joe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new login was added with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Firefox CSV file where only cells containing a comma are quoted.
|
||||
*/
|
||||
add_task(async function test_import_from_firefox_auth_some_quoted_fields() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
'https://example.com,joe@example.com,"one,two,tree","My realm",,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814635,1589710449871,1589617846802',
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.authLogin({
|
||||
formActionOrigin: null,
|
||||
httpRealm: "My realm",
|
||||
origin: "https://example.com",
|
||||
password: "one,two,tree",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814635,
|
||||
timePasswordChanged: 1589617846802,
|
||||
timeLastUsed: 1589710449871,
|
||||
timesUsed: 1,
|
||||
username: "joe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new login was added with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Firefox CSV file with an empty formActionOrigin and null httpRealm
|
||||
*/
|
||||
add_task(async function test_import_from_firefox_form_empty_formActionOrigin() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://example.com,joe@example.com,s3cret1,,,{5ec0d12f-e194-4279-ae1b-d7d281bb46f0},1589617814636,1589710449872,1589617846803",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "",
|
||||
httpRealm: null,
|
||||
origin: "https://example.com",
|
||||
password: "s3cret1",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814636,
|
||||
timePasswordChanged: 1589617846803,
|
||||
timeLastUsed: 1589710449872,
|
||||
timesUsed: 1,
|
||||
username: "joe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new login was added with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Firefox CSV file with a non-empty formActionOrigin and null httpRealm.
|
||||
*/
|
||||
add_task(async function test_import_from_firefox_form_with_formActionOrigin() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"http://example.com,joe@example.com,s3cret1,,https://other.example.org,{5ec0d12f-e194-4279-ae1b-d7d281bb46f1},1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "https://other.example.org",
|
||||
httpRealm: null,
|
||||
origin: "http://example.com",
|
||||
password: "s3cret1",
|
||||
passwordField: "",
|
||||
timeCreated: 1589617814635,
|
||||
timePasswordChanged: 1589617846802,
|
||||
timeLastUsed: 1589710449871,
|
||||
timesUsed: 1,
|
||||
username: "joe@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new login was added with the correct fields",
|
||||
(a, e) => a.equals(e) && checkMetaInfo(a, e)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Bitwarden CSV file.
|
||||
* `name` is ignored until bug 1433770.
|
||||
*/
|
||||
add_task(async function test_import_from_bitwarden_csv() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"folder,favorite,type,name,notes,fields,login_uri,login_username,login_password,login_totp",
|
||||
`,,note,jane's note,"secret note, ignore me!",,,,,`,
|
||||
",,login,example.com,,,https://example.com/login,jane@example.com,secret_password",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "",
|
||||
httpRealm: null,
|
||||
origin: "https://example.com",
|
||||
password: "secret_password",
|
||||
passwordField: "",
|
||||
timesUsed: 1,
|
||||
username: "jane@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new Bitwarden login was added with the correct fields",
|
||||
(a, e) =>
|
||||
a.equals(e) &&
|
||||
checkMetaInfo(a, e, ["timesUsed"]) &&
|
||||
checkLoginNewlyCreated(a)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login data from a Chrome CSV file.
|
||||
* `name` is ignored until bug 1433770.
|
||||
*/
|
||||
add_task(async function test_import_from_chrome_csv() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"name,url,username,password",
|
||||
"example.com,https://example.com/login,jane@example.com,secret_chrome_password",
|
||||
]);
|
||||
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[
|
||||
TestData.formLogin({
|
||||
formActionOrigin: "",
|
||||
httpRealm: null,
|
||||
origin: "https://example.com",
|
||||
password: "secret_chrome_password",
|
||||
passwordField: "",
|
||||
timesUsed: 1,
|
||||
username: "jane@example.com",
|
||||
usernameField: "",
|
||||
}),
|
||||
],
|
||||
"Check that a new Chrome login was added with the correct fields",
|
||||
(a, e) =>
|
||||
a.equals(e) &&
|
||||
checkMetaInfo(a, e, ["timesUsed"]) &&
|
||||
checkLoginNewlyCreated(a)
|
||||
);
|
||||
});
|
|
@ -197,7 +197,7 @@ add_task(async function test_export_multiple_rows() {
|
|||
'"ftp://example.net","the username","the password","ftp://example.net",,,,,',
|
||||
'"chrome://example_extension","the username","the password one","Example Login One",,,,,',
|
||||
'"chrome://example_extension","the username","the password two","Example Login Two",,,,,',
|
||||
'"file://","file: username","file: password",,"file://",,,,',
|
||||
'"file:///","file: username","file: password",,"file:///",,,,',
|
||||
'"https://js.example.com","javascript: username","javascript: password",,"javascript:",,,,',
|
||||
];
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ skip-if = os == "android" # Bug 1171687: Needs fixing on Android
|
|||
[test_logins_search.js]
|
||||
[test_maybeImportLogin.js]
|
||||
skip-if = os == "android" # Only used by migrator, which isn't on Android
|
||||
[test_module_LoginCSVImport.js]
|
||||
[test_module_LoginExport.js]
|
||||
skip-if = os == "android" # there is no export for android
|
||||
[test_notifications.js]
|
||||
|
|
|
@ -768,7 +768,6 @@ pwmgr:
|
|||
objects: [
|
||||
"import_from_browser",
|
||||
"import_from_csv",
|
||||
"import_csv_complete",
|
||||
"export",
|
||||
"export_complete",
|
||||
"preferences",
|
||||
|
|
Загрузка…
Ссылка в новой задаче