mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-17 07:15:46 +00:00
Bug 1650675 Import CSV error dialog r=sfoster
Differential Revision: https://phabricator.services.mozilla.com/D96101
This commit is contained in:
parent
1f52ebc03b
commit
c67eaa1b46
@ -401,7 +401,8 @@ class AboutLoginsParent extends JSWindowActorParent {
|
||||
let [
|
||||
title,
|
||||
okButtonLabel,
|
||||
filterTitle,
|
||||
csvFilterTitle,
|
||||
tsvFilterTitle,
|
||||
] = await AboutLoginsL10n.formatValues([
|
||||
{
|
||||
id: "about-logins-import-file-picker-title",
|
||||
@ -412,23 +413,45 @@ class AboutLoginsParent extends JSWindowActorParent {
|
||||
{
|
||||
id: "about-logins-import-file-picker-csv-filter-title",
|
||||
},
|
||||
{
|
||||
id: "about-logins-import-file-picker-tsv-filter-title",
|
||||
},
|
||||
]);
|
||||
let { result, path } = await this.openFilePickerDialog(
|
||||
title,
|
||||
okButtonLabel,
|
||||
filterTitle,
|
||||
"*.csv",
|
||||
[
|
||||
{
|
||||
title: csvFilterTitle,
|
||||
extensionPattern: "*.csv",
|
||||
},
|
||||
{
|
||||
title: tsvFilterTitle,
|
||||
extensionPattern: "*.tsv",
|
||||
},
|
||||
],
|
||||
ownerGlobal
|
||||
);
|
||||
|
||||
if (result != Ci.nsIFilePicker.returnCancel) {
|
||||
let summary = await LoginCSVImport.importFromCSV(path);
|
||||
this.sendAsyncMessage("AboutLogins:ImportPasswordsDialog", summary);
|
||||
Services.telemetry.recordEvent(
|
||||
"pwmgr",
|
||||
"mgmt_menu_item_used",
|
||||
"import_csv_complete"
|
||||
);
|
||||
let summary;
|
||||
try {
|
||||
summary = await LoginCSVImport.importFromCSV(path);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
this.sendAsyncMessage(
|
||||
"AboutLogins:ImportPasswordsErrorDialog",
|
||||
e.errorType
|
||||
);
|
||||
}
|
||||
if (summary) {
|
||||
this.sendAsyncMessage("AboutLogins:ImportPasswordsDialog", summary);
|
||||
Services.telemetry.recordEvent(
|
||||
"pwmgr",
|
||||
"mgmt_menu_item_used",
|
||||
"import_csv_complete"
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -454,17 +477,13 @@ class AboutLoginsParent extends JSWindowActorParent {
|
||||
this.sendAsyncMessage("AboutLogins:ShowLoginItemError", messageObject);
|
||||
}
|
||||
|
||||
async openFilePickerDialog(
|
||||
title,
|
||||
okButtonLabel,
|
||||
filterTitle,
|
||||
filterExtension,
|
||||
ownerGlobal
|
||||
) {
|
||||
async openFilePickerDialog(title, okButtonLabel, appendFilters, ownerGlobal) {
|
||||
return new Promise(resolve => {
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
fp.init(ownerGlobal, title, Ci.nsIFilePicker.modeOpen);
|
||||
fp.appendFilter(filterTitle, filterExtension);
|
||||
for (const appendFilter of appendFilters) {
|
||||
fp.appendFilter(appendFilter.title, appendFilter.extensionPattern);
|
||||
}
|
||||
fp.appendFilters(Ci.nsIFilePicker.filterAll);
|
||||
fp.okButtonLabel = okButtonLabel;
|
||||
fp.open(async result => {
|
||||
|
@ -15,6 +15,8 @@
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/confirmation-dialog.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/remove-logins-dialog.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/import-summary-dialog.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/import-error-dialog.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/generic-dialog.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/fxaccounts-button.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/login-filter.js"></script>
|
||||
<script type="module" src="chrome://browser/content/aboutlogins/components/login-intro.js"></script>
|
||||
@ -41,6 +43,7 @@
|
||||
<confirmation-dialog hidden></confirmation-dialog>
|
||||
<remove-logins-dialog hidden></remove-logins-dialog>
|
||||
<import-summary-dialog hidden></import-summary-dialog>
|
||||
<import-error-dialog hidden></import-error-dialog>
|
||||
<div id="master-password-required-overlay"></div>
|
||||
|
||||
<template id="confirmation-dialog-template">
|
||||
@ -65,38 +68,70 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="import-summary-dialog-template">
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/import-summary-dialog.css">
|
||||
<template id="generic-dialog-template">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/generic-dialog.css">
|
||||
<div class="overlay">
|
||||
<div class="container" role="dialog" aria-labelledby="title">
|
||||
<img class="import-icon" src="chrome://browser/skin/import.svg"/>
|
||||
<h1 class="title" id="title" data-l10n-id="about-logins-import-dialog-title"></h1>
|
||||
<div class="content">
|
||||
<div class="import-summary">
|
||||
<div class="import-items-added import-items-row" data-l10n-id="about-logins-import-dialog-items-added" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
</div>
|
||||
<div class="import-items-modified import-items-row" data-l10n-id="about-logins-import-dialog-items-modified" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
</div>
|
||||
<div class="import-items-no-change import-items-row" data-l10n-id="about-logins-import-dialog-items-no-change" data-l10n-name="no-change" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
<span data-l10n-name="meta" class="result-meta"></span>
|
||||
</div>
|
||||
<div class="import-items-errors import-items-row" data-l10n-id="about-logins-import-dialog-items-error" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
<span data-l10n-name="meta" class="result-meta"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-separator"></div>
|
||||
<button class="import-done-button primary" data-l10n-id="about-logins-import-dialog-done"></button>
|
||||
<slot name="dialog-icon" part="dialog-icon"></slot>
|
||||
<slot name="dialog-title"></slot>
|
||||
<slot name="content"></slot>
|
||||
<slot name="buttons"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="import-summary-dialog-template">
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/generic-dialog.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/import-summary-dialog.css">
|
||||
<generic-dialog>
|
||||
<span slot="dialog-title" data-l10n-id="about-logins-import-dialog-title"></span>
|
||||
<img slot="dialog-icon" part="dialog-icon" src="chrome://browser/skin/import.svg"/>
|
||||
<div slot="content">
|
||||
<div class="import-summary">
|
||||
<div class="import-items-added import-items-row" data-l10n-id="about-logins-import-dialog-items-added" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
</div>
|
||||
<div class="import-items-modified import-items-row" data-l10n-id="about-logins-import-dialog-items-modified" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
</div>
|
||||
<div class="import-items-no-change import-items-row" data-l10n-id="about-logins-import-dialog-items-no-change" data-l10n-name="no-change" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
<span data-l10n-name="meta" class="result-meta"></span>
|
||||
</div>
|
||||
<div class="import-items-errors import-items-row" data-l10n-id="about-logins-import-dialog-items-error" data-l10n-args='{"count": 0}'>
|
||||
<span data-l10n-name="count" class="result-count"></span>
|
||||
<span data-l10n-name="meta" class="result-meta"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="buttons">
|
||||
<button class="dismiss-button primary" data-l10n-id="about-logins-import-dialog-done"></button>
|
||||
</div>
|
||||
</generic-dialog>
|
||||
</template>
|
||||
|
||||
<template id="import-error-dialog-template">
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/generic-dialog.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/import-error-dialog.css">
|
||||
<generic-dialog>
|
||||
<span slot="dialog-title" data-l10n-id="about-logins-import-dialog-error-title"></span>
|
||||
<img slot="dialog-icon" part="dialog-icon" src="chrome://browser/skin/warning.svg"/>
|
||||
<div slot="content" class="content">
|
||||
<span class="error-title" data-l10n-id="about-logins-import-dialog-error-unable-to-read-title"></span>
|
||||
<span class="error-description" data-l10n-id="about-logins-import-dialog-error-unable-to-read-description"></span>
|
||||
<span class="no-logins" data-l10n-id="about-logins-import-dialog-error-no-logins-imported"></span>
|
||||
<a href="https://support.mozilla.org/kb/import-login-data-file"
|
||||
data-l10n-id="about-logins-import-dialog-error-learn-more" target="_blank" rel="noreferrer"></a>
|
||||
</div>
|
||||
<div slot="buttons" class="buttons">
|
||||
<button class="dismiss-button" data-l10n-id="about-logins-import-dialog-error-cancel"></button>
|
||||
<button class="try-import-again primary" data-l10n-id="about-logins-import-dialog-error-try-again"></button>
|
||||
</div>
|
||||
</generic-dialog>
|
||||
</template>
|
||||
|
||||
<template id="remove-logins-dialog-template">
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
|
||||
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
|
||||
|
@ -138,6 +138,11 @@ window.addEventListener("AboutLoginsChromeToContent", event => {
|
||||
window.dispatchEvent(new CustomEvent("AboutLoginsRemaskPassword"));
|
||||
break;
|
||||
}
|
||||
case "ImportPasswordsErrorDialog": {
|
||||
let dialog = document.querySelector("import-error-dialog");
|
||||
dialog.show(event.detail.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -56,3 +56,17 @@ export function promptForMasterPassword(messageId) {
|
||||
window.AboutLoginsUtils.promptForMasterPassword(resolve, messageId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a dialog based on a template using shadow dom.
|
||||
* @param {HTMLElement} element The element to attach the shadow dom to.
|
||||
* @param {string} templateSelector The selector of the template to be used.
|
||||
* @returns {object} The shadow dom that is attached.
|
||||
*/
|
||||
export function initDialog(element, templateSelector) {
|
||||
let template = document.querySelector(templateSelector);
|
||||
let shadowRoot = element.attachShadow({ mode: "open" });
|
||||
document.l10n.connectRoot(shadowRoot);
|
||||
shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
return shadowRoot;
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
/* 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/. */
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
/* TODO: this color is used in the about:preferences overlay, but
|
||||
why isn't it declared as a variable? */
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 37px auto;
|
||||
grid-template-rows: 32px auto 50px;
|
||||
grid-gap: 5px;
|
||||
align-items: center;
|
||||
width: 580px;
|
||||
height: 290px;
|
||||
padding: 50px 50px 20px;
|
||||
margin: auto;
|
||||
background-color: var(--in-content-page-background);
|
||||
color: var(--in-content-page-color);
|
||||
box-shadow: var(--shadow-30);
|
||||
/* show a border in high contrast mode */
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
|
||||
::slotted([slot="dialog-icon"]) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
::slotted([slot="dialog-title"]) {
|
||||
font-size: 2.2em;
|
||||
font-weight: 300;
|
||||
user-select: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="content"]) {
|
||||
grid-column-start: 2;
|
||||
align-self: baseline;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
::slotted([slot="buttons"]) {
|
||||
grid-column: 1 / 4;
|
||||
grid-row-start: 3;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--in-content-border-color);
|
||||
padding-top: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 140px;
|
||||
width: 170px;
|
||||
height: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding-block: 40px 16px;
|
||||
padding-inline: 45px 32px;
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/* 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/. */
|
||||
|
||||
import {
|
||||
setKeyboardAccessForNonDialogElements,
|
||||
initDialog,
|
||||
} from "../aboutLoginsUtils.js";
|
||||
|
||||
export default class GenericDialog extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._promise = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
const shadowRoot = initDialog(this, "#generic-dialog-template");
|
||||
this._dismissButton = this.querySelector(".dismiss-button");
|
||||
this._overlay = shadowRoot.querySelector(".overlay");
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "keydown":
|
||||
if (event.key === "Escape" && !event.defaultPrevented) {
|
||||
this.hide();
|
||||
}
|
||||
break;
|
||||
case "click":
|
||||
if (
|
||||
event.currentTarget.classList.contains("dismiss-button") ||
|
||||
event.target.classList.contains("overlay")
|
||||
) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
setKeyboardAccessForNonDialogElements(false);
|
||||
this.hidden = false;
|
||||
this.parentNode.host.hidden = false;
|
||||
|
||||
this._dismissButton.addEventListener("click", this);
|
||||
this._overlay.addEventListener("click", this);
|
||||
window.addEventListener("keydown", this);
|
||||
}
|
||||
|
||||
hide() {
|
||||
setKeyboardAccessForNonDialogElements(true);
|
||||
this._dismissButton.removeEventListener("click", this);
|
||||
this._overlay.removeEventListener("click", this);
|
||||
window.removeEventListener("keydown", this);
|
||||
|
||||
this.hidden = true;
|
||||
this.parentNode.host.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("generic-dialog", GenericDialog);
|
@ -0,0 +1,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/. */
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.no-logins {
|
||||
margin-top: 25px;
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* 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/. */
|
||||
|
||||
import { initDialog } from "../aboutLoginsUtils.js";
|
||||
|
||||
export default class ImportErrorDialog extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._promise = null;
|
||||
this._errorMessages = {};
|
||||
this._errorMessages.CONFLICTING_VALUES_ERROR = {
|
||||
title: "about-logins-import-dialog-error-conflicting-values-title",
|
||||
description:
|
||||
"about-logins-import-dialog-error-conflicting-values-description",
|
||||
};
|
||||
this._errorMessages.FILE_FORMAT_ERROR = {
|
||||
title: "about-logins-import-dialog-error-file-format-title",
|
||||
description: "about-logins-import-dialog-error-file-format-description",
|
||||
};
|
||||
this._errorMessages.FILE_PERMISSIONS_ERROR = {
|
||||
title: "about-logins-import-dialog-error-file-permission-title",
|
||||
description:
|
||||
"about-logins-import-dialog-error-file-permission-description",
|
||||
};
|
||||
this._errorMessages.UNABLE_TO_READ_ERROR = {
|
||||
title: "about-logins-import-dialog-error-unable-to-read-title",
|
||||
description:
|
||||
"about-logins-import-dialog-error-unable-to-read-description",
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
const shadowRoot = initDialog(this, "#import-error-dialog-template");
|
||||
this._titleElement = shadowRoot.querySelector(".error-title");
|
||||
this._descriptionElement = shadowRoot.querySelector(".error-description");
|
||||
this._genericDialog = this.shadowRoot.querySelector("generic-dialog");
|
||||
const tryImportAgain = this.shadowRoot.querySelector(".try-import-again");
|
||||
tryImportAgain.addEventListener("click", () => {
|
||||
this._genericDialog.hide();
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("AboutLoginsImportFromFile", { bubbles: true })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
show(errorType) {
|
||||
const { title, description } = this._errorMessages[errorType];
|
||||
document.l10n.setAttributes(this._titleElement, title);
|
||||
document.l10n.setAttributes(this._descriptionElement, description);
|
||||
return this._genericDialog.show();
|
||||
}
|
||||
}
|
||||
customElements.define("import-error-dialog", ImportErrorDialog);
|
@ -2,64 +2,6 @@
|
||||
* 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/. */
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
/* TODO: this color is used in the about:preferences overlay, but
|
||||
why isn't it declared as a variable? */
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 30px auto 170px;
|
||||
grid-template-rows: 30px auto 20px 35px;
|
||||
grid-gap: 5px;
|
||||
align-items: center;
|
||||
width: 580px;
|
||||
height: 290px;
|
||||
padding: 50px 50px 20px;
|
||||
margin: auto;
|
||||
background-color: var(--in-content-page-background);
|
||||
color: var(--in-content-page-color);
|
||||
box-shadow: var(--shadow-30);
|
||||
/* show a border in high contrast mode */
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.2em;
|
||||
font-weight: 300;
|
||||
user-select: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 16px 32px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.buttons.macosx > .confirm-button {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.buttons > button {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.import-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.import-summary {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content max-content;
|
||||
@ -70,31 +12,6 @@
|
||||
margin-inline: 0 10px;
|
||||
}
|
||||
|
||||
.import-done-button {
|
||||
width: 170px;
|
||||
height: 30px;
|
||||
grid-column-start: 3;
|
||||
grid-row-start: 4;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column-start: 2;
|
||||
align-self: baseline;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding-block: 40px 16px;
|
||||
padding-inline: 45px 32px;
|
||||
}
|
||||
|
||||
.import-separator {
|
||||
grid-column: 1 / 4;
|
||||
grid-row-start: 3;
|
||||
border-top: 1px solid var(--in-content-border-color);
|
||||
}
|
||||
|
||||
.import-items-row {
|
||||
grid-column: 1 / 4;
|
||||
display: grid;
|
||||
@ -109,7 +26,6 @@
|
||||
.result-meta {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.import-items-errors .result-meta {
|
||||
color: var(--dialog-warning-text-color);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* 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/. */
|
||||
|
||||
import { setKeyboardAccessForNonDialogElements } from "../aboutLoginsUtils.js";
|
||||
import { initDialog } from "../aboutLoginsUtils.js";
|
||||
|
||||
export default class ImportSummaryDialog extends HTMLElement {
|
||||
constructor() {
|
||||
@ -14,50 +14,12 @@ export default class ImportSummaryDialog extends HTMLElement {
|
||||
if (this.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
let template = document.querySelector("#import-summary-dialog-template");
|
||||
let shadowRoot = this.attachShadow({ mode: "open" });
|
||||
document.l10n.connectRoot(shadowRoot);
|
||||
shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
|
||||
this._dismissButton = this.shadowRoot.querySelector(".import-done-button");
|
||||
this._overlay = this.shadowRoot.querySelector(".overlay");
|
||||
|
||||
initDialog(this, "#import-summary-dialog-template");
|
||||
this._added = this.shadowRoot.querySelector(".import-items-added");
|
||||
this._modified = this.shadowRoot.querySelector(".import-items-modified");
|
||||
this._noChange = this.shadowRoot.querySelector(".import-items-no-change");
|
||||
this._error = this.shadowRoot.querySelector(".import-items-errors");
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "keydown":
|
||||
if (event.repeat) {
|
||||
// Prevent repeat keypresses from accidentally confirming the
|
||||
// dialog since the confirmation button is focused by default.
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape" && !event.defaultPrevented) {
|
||||
this.onCancel();
|
||||
}
|
||||
break;
|
||||
case "click":
|
||||
if (
|
||||
event.currentTarget.classList.contains("import-done-button") ||
|
||||
event.target.classList.contains("overlay")
|
||||
) {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
setKeyboardAccessForNonDialogElements(true);
|
||||
this._dismissButton.removeEventListener("click", this);
|
||||
this._overlay.removeEventListener("click", this);
|
||||
window.removeEventListener("keydown", this);
|
||||
|
||||
this.hidden = true;
|
||||
this._genericDialog = this.shadowRoot.querySelector("generic-dialog");
|
||||
}
|
||||
|
||||
show({ logins }) {
|
||||
@ -94,24 +56,7 @@ export default class ImportSummaryDialog extends HTMLElement {
|
||||
this._error,
|
||||
"about-logins-import-dialog-items-error"
|
||||
);
|
||||
setKeyboardAccessForNonDialogElements(false);
|
||||
this.hidden = false;
|
||||
|
||||
this._dismissButton.addEventListener("click", this);
|
||||
this._overlay.addEventListener("click", this);
|
||||
window.addEventListener("keydown", this);
|
||||
|
||||
this._promise = new Promise((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
});
|
||||
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this._reject();
|
||||
this.hide();
|
||||
return this._genericDialog.show();
|
||||
}
|
||||
|
||||
_updateCount(count, component, message) {
|
||||
|
@ -3,33 +3,37 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
browser.jar:
|
||||
content/browser/aboutlogins/components/confirmation-dialog.css (content/components/confirmation-dialog.css)
|
||||
content/browser/aboutlogins/components/confirmation-dialog.js (content/components/confirmation-dialog.js)
|
||||
content/browser/aboutlogins/components/remove-logins-dialog.css (content/components/remove-logins-dialog.css)
|
||||
content/browser/aboutlogins/components/remove-logins-dialog.js (content/components/remove-logins-dialog.js)
|
||||
content/browser/aboutlogins/components/import-summary-dialog.css (content/components/import-summary-dialog.css)
|
||||
content/browser/aboutlogins/components/import-summary-dialog.js (content/components/import-summary-dialog.js)
|
||||
content/browser/aboutlogins/components/fxaccounts-button.css (content/components/fxaccounts-button.css)
|
||||
content/browser/aboutlogins/components/fxaccounts-button.js (content/components/fxaccounts-button.js)
|
||||
content/browser/aboutlogins/components/login-filter.css (content/components/login-filter.css)
|
||||
content/browser/aboutlogins/components/login-filter.js (content/components/login-filter.js)
|
||||
content/browser/aboutlogins/components/login-intro.css (content/components/login-intro.css)
|
||||
content/browser/aboutlogins/components/login-intro.js (content/components/login-intro.js)
|
||||
content/browser/aboutlogins/components/login-item.css (content/components/login-item.css)
|
||||
content/browser/aboutlogins/components/login-item.js (content/components/login-item.js)
|
||||
content/browser/aboutlogins/components/login-list.css (content/components/login-list.css)
|
||||
content/browser/aboutlogins/components/login-list.js (content/components/login-list.js)
|
||||
content/browser/aboutlogins/components/login-list-item.js (content/components/login-list-item.js)
|
||||
content/browser/aboutlogins/components/menu-button.css (content/components/menu-button.css)
|
||||
content/browser/aboutlogins/components/menu-button.js (content/components/menu-button.js)
|
||||
content/browser/aboutlogins/icons/breached-website.svg (content/icons/breached-website.svg)
|
||||
content/browser/aboutlogins/icons/favicon.svg (content/icons/favicon.svg)
|
||||
content/browser/aboutlogins/icons/hide-password.svg (content/icons/hide-password.svg)
|
||||
content/browser/aboutlogins/icons/vulnerable-password.svg (content/icons/vulnerable-password.svg)
|
||||
content/browser/aboutlogins/icons/show-password.svg (content/icons/show-password.svg)
|
||||
content/browser/aboutlogins/icons/intro-illustration.svg (content/icons/intro-illustration.svg)
|
||||
content/browser/aboutlogins/aboutLogins.css (content/aboutLogins.css)
|
||||
content/browser/aboutlogins/aboutLogins.js (content/aboutLogins.js)
|
||||
content/browser/aboutlogins/aboutLogins.html (content/aboutLogins.html)
|
||||
content/browser/aboutlogins/aboutLoginsUtils.js (content/aboutLoginsUtils.js)
|
||||
content/browser/aboutlogins/common.css (content/common.css)
|
||||
content/browser/aboutlogins/components/confirmation-dialog.css (content/components/confirmation-dialog.css)
|
||||
content/browser/aboutlogins/components/confirmation-dialog.js (content/components/confirmation-dialog.js)
|
||||
content/browser/aboutlogins/components/remove-logins-dialog.css (content/components/remove-logins-dialog.css)
|
||||
content/browser/aboutlogins/components/remove-logins-dialog.js (content/components/remove-logins-dialog.js)
|
||||
content/browser/aboutlogins/components/import-summary-dialog.css (content/components/import-summary-dialog.css)
|
||||
content/browser/aboutlogins/components/import-summary-dialog.js (content/components/import-summary-dialog.js)
|
||||
content/browser/aboutlogins/components/import-error-dialog.css (content/components/import-error-dialog.css)
|
||||
content/browser/aboutlogins/components/import-error-dialog.js (content/components/import-error-dialog.js)
|
||||
content/browser/aboutlogins/components/generic-dialog.css (content/components/generic-dialog.css)
|
||||
content/browser/aboutlogins/components/generic-dialog.js (content/components/generic-dialog.js)
|
||||
content/browser/aboutlogins/components/fxaccounts-button.css (content/components/fxaccounts-button.css)
|
||||
content/browser/aboutlogins/components/fxaccounts-button.js (content/components/fxaccounts-button.js)
|
||||
content/browser/aboutlogins/components/login-filter.css (content/components/login-filter.css)
|
||||
content/browser/aboutlogins/components/login-filter.js (content/components/login-filter.js)
|
||||
content/browser/aboutlogins/components/login-intro.css (content/components/login-intro.css)
|
||||
content/browser/aboutlogins/components/login-intro.js (content/components/login-intro.js)
|
||||
content/browser/aboutlogins/components/login-item.css (content/components/login-item.css)
|
||||
content/browser/aboutlogins/components/login-item.js (content/components/login-item.js)
|
||||
content/browser/aboutlogins/components/login-list.css (content/components/login-list.css)
|
||||
content/browser/aboutlogins/components/login-list.js (content/components/login-list.js)
|
||||
content/browser/aboutlogins/components/login-list-item.js (content/components/login-list-item.js)
|
||||
content/browser/aboutlogins/components/menu-button.css (content/components/menu-button.css)
|
||||
content/browser/aboutlogins/components/menu-button.js (content/components/menu-button.js)
|
||||
content/browser/aboutlogins/icons/breached-website.svg (content/icons/breached-website.svg)
|
||||
content/browser/aboutlogins/icons/favicon.svg (content/icons/favicon.svg)
|
||||
content/browser/aboutlogins/icons/hide-password.svg (content/icons/hide-password.svg)
|
||||
content/browser/aboutlogins/icons/vulnerable-password.svg (content/icons/vulnerable-password.svg)
|
||||
content/browser/aboutlogins/icons/show-password.svg (content/icons/show-password.svg)
|
||||
content/browser/aboutlogins/icons/intro-illustration.svg (content/icons/intro-illustration.svg)
|
||||
content/browser/aboutlogins/aboutLogins.css (content/aboutLogins.css)
|
||||
content/browser/aboutlogins/aboutLogins.js (content/aboutLogins.js)
|
||||
content/browser/aboutlogins/aboutLogins.html (content/aboutLogins.html)
|
||||
content/browser/aboutlogins/aboutLoginsUtils.js (content/aboutLoginsUtils.js)
|
||||
content/browser/aboutlogins/common.css (content/common.css)
|
||||
|
@ -104,3 +104,94 @@ add_task(async function test_open_import_from_csv() {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_open_import_from_csv_with_invalid_file() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{ gBrowser, url: "about:logins" },
|
||||
async function(browser) {
|
||||
MockFilePicker.init(window);
|
||||
MockFilePicker.returnValue = MockFilePicker.returnOK;
|
||||
|
||||
let csvFile = await LoginTestUtils.file.setupCsvFileWithLines([
|
||||
"invalid csv file",
|
||||
]);
|
||||
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";
|
||||
Services.telemetry.clearEvents();
|
||||
|
||||
let filePicker = waitForOpenFilePicker(csvFile);
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
getImportMenuItem,
|
||||
{},
|
||||
browser
|
||||
);
|
||||
|
||||
// First event is for opening about:logins
|
||||
await LoginTestUtils.telemetry.waitForEventCount(
|
||||
1,
|
||||
"content",
|
||||
"pwmgr",
|
||||
"mgmt_menu_item_used"
|
||||
);
|
||||
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 error dialog");
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
|
||||
const dialog = Cu.waiveXrays(
|
||||
content.document.querySelector("import-error-dialog")
|
||||
);
|
||||
info("Dialog hidden=" + dialog.hidden);
|
||||
info(
|
||||
"Generic dialog error title " +
|
||||
dialog._genericDialog
|
||||
.querySelector(".error-title")
|
||||
.getAttribute("data-l10n-id")
|
||||
);
|
||||
is(dialog.hidden, false, "Dialog should not be hidden");
|
||||
is(
|
||||
dialog._genericDialog
|
||||
.querySelector(".error-title")
|
||||
.getAttribute("data-l10n-id"),
|
||||
"about-logins-import-dialog-error-file-format-title",
|
||||
"Dialog error title should be correct"
|
||||
);
|
||||
is(
|
||||
dialog._genericDialog
|
||||
.querySelector(".error-description")
|
||||
.getAttribute("data-l10n-id"),
|
||||
"about-logins-import-dialog-error-file-format-description",
|
||||
"Dialog error description should be correct"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -147,6 +147,10 @@ add_task(async function setup_head() {
|
||||
if (msg.errorMessage.includes(EXPECTED_ERROR_MESSAGE)) {
|
||||
return;
|
||||
}
|
||||
if (msg.errorMessage == "FILE_FORMAT_ERROR") {
|
||||
// Ignore errors handled by the error message dialog.
|
||||
return;
|
||||
}
|
||||
ok(false, msg.message || msg.errorMessage);
|
||||
});
|
||||
|
||||
|
@ -266,6 +266,13 @@ about-logins-import-file-picker-csv-filter-title =
|
||||
[macos] CSV Document
|
||||
*[other] CSV File
|
||||
}
|
||||
# A description for the .tsv file format that may be shown as the file type
|
||||
# filter by the operating system. TSV is short for 'tab separated values'.
|
||||
about-logins-import-file-picker-tsv-filter-title =
|
||||
{ PLATFORM() ->
|
||||
[macos] TSV Document
|
||||
*[other] TSV File
|
||||
}
|
||||
|
||||
##
|
||||
## Variables:
|
||||
@ -291,3 +298,17 @@ about-logins-import-dialog-items-error =
|
||||
*[other] <span>Errors:</span> <span data-l10n-name="count">{ $count }</span> <span data-l10n-name="meta">(not imported)</span>
|
||||
}
|
||||
about-logins-import-dialog-done = Done
|
||||
|
||||
about-logins-import-dialog-error-title = Import Error
|
||||
about-logins-import-dialog-error-conflicting-values-title = Multiple Conflicting Values for One Login
|
||||
about-logins-import-dialog-error-conflicting-values-description = For example: multiple usernames, passwords, URLs, etc. for one login.
|
||||
about-logins-import-dialog-error-file-format-title = File Format Issue
|
||||
about-logins-import-dialog-error-file-format-description = Incorrect or missing column headers. Make sure the file includes columns for username, password and URL.
|
||||
about-logins-import-dialog-error-file-permission-title = Unable to Read File
|
||||
about-logins-import-dialog-error-file-permission-description = { -brand-short-name } does not have permission to read the file. Try changing the file permissions.
|
||||
about-logins-import-dialog-error-unable-to-read-title = Unable to Parse File
|
||||
about-logins-import-dialog-error-unable-to-read-description = Make sure you selected a CSV or TSV file.
|
||||
about-logins-import-dialog-error-no-logins-imported = No logins have been imported
|
||||
about-logins-import-dialog-error-learn-more = Learn more
|
||||
about-logins-import-dialog-error-try-again = Try Again…
|
||||
about-logins-import-dialog-error-cancel = Cancel
|
||||
|
@ -8,7 +8,11 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["LoginCSVImport"];
|
||||
const EXPORTED_SYMBOLS = [
|
||||
"LoginCSVImport",
|
||||
"ImportFailedException",
|
||||
"ImportFailedErrorType",
|
||||
];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
@ -46,6 +50,20 @@ const FIELD_TO_CSV_COLUMNS = {
|
||||
timePasswordChanged: ["timepasswordchanged"],
|
||||
};
|
||||
|
||||
const ImportFailedErrorType = Object.freeze({
|
||||
CONFLICTING_VALUES_ERROR: "CONFLICTING_VALUES_ERROR",
|
||||
FILE_FORMAT_ERROR: "FILE_FORMAT_ERROR",
|
||||
FILE_PERMISSIONS_ERROR: "FILE_PERMISSIONS_ERROR",
|
||||
UNABLE_TO_READ_ERROR: "UNABLE_TO_READ_ERROR",
|
||||
});
|
||||
|
||||
class ImportFailedException extends Error {
|
||||
constructor(errorType, message) {
|
||||
super(message != null ? message : errorType);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an object that has a method to import login-related data CSV files
|
||||
*/
|
||||
@ -121,13 +139,36 @@ class LoginCSVImport {
|
||||
);
|
||||
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 => {
|
||||
return csvColumnToFieldMap.get(col.toLowerCase());
|
||||
})
|
||||
);
|
||||
let csvString;
|
||||
try {
|
||||
csvString = await OS.File.read(filePath, { encoding: "utf-8" });
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
throw new ImportFailedException(
|
||||
ImportFailedErrorType.FILE_PERMISSIONS_ERROR
|
||||
);
|
||||
}
|
||||
let parsedLines;
|
||||
if (filePath.endsWith(".csv")) {
|
||||
parsedLines = d3.csv.parse(csvString);
|
||||
} else if (filePath.endsWith(".tsv")) {
|
||||
parsedLines = d3.tsv.parse(csvString);
|
||||
}
|
||||
|
||||
let fieldsInFile = new Set();
|
||||
if (parsedLines && parsedLines[0]) {
|
||||
for (const columnName in parsedLines[0]) {
|
||||
const fieldName = csvColumnToFieldMap.get(
|
||||
columnName.toLocaleLowerCase()
|
||||
);
|
||||
if (fieldName) {
|
||||
fieldsInFile.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fieldsInFile.size === 0) {
|
||||
throw new ImportFailedException(ImportFailedErrorType.FILE_FORMAT_ERROR);
|
||||
}
|
||||
if (
|
||||
parsedLines[0] &&
|
||||
(!fieldsInFile.has("origin") ||
|
||||
@ -141,9 +182,23 @@ class LoginCSVImport {
|
||||
"FX_MIGRATION_LOGINS_IMPORT_MS",
|
||||
LoginCSVImport.MIGRATION_HISTOGRAM_KEY
|
||||
);
|
||||
throw new Error(
|
||||
"CSV file must contain origin, username, and password columns"
|
||||
);
|
||||
throw new ImportFailedException(ImportFailedErrorType.FILE_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
const uniqueLoginIdentifiers = new Set();
|
||||
for (const csvObject of parsedLines) {
|
||||
// TODO: handle duplicates without guid column. Bug 1687852
|
||||
|
||||
if (csvObject.guid) {
|
||||
if (uniqueLoginIdentifiers.has(csvObject.guid)) {
|
||||
throw new ImportFailedException(
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
|
||||
csvObject.guid
|
||||
);
|
||||
} else {
|
||||
uniqueLoginIdentifiers.add(csvObject.guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let loginsToImport = parsedLines.map(csvObject => {
|
||||
@ -159,7 +214,7 @@ class LoginCSVImport {
|
||||
try {
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById("FX_MIGRATION_LOGINS_QUANTITY")
|
||||
.add(LoginCSVImport.MIGRATION_HISTOGRAM_KEY, parsedLines.length);
|
||||
.add(LoginCSVImport.MIGRATION_HISTOGRAM_KEY, summary.length);
|
||||
let accumulatedDelay = responsivenessMonitor.finish();
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById("FX_MIGRATION_LOGINS_JANK_MS")
|
||||
|
@ -596,10 +596,12 @@ LoginTestUtils.file = {
|
||||
*
|
||||
* @param {string[]} csvLines
|
||||
* The lines that make up the CSV file.
|
||||
* @param {string} extension
|
||||
* Optional parameter. Either 'csv' or 'tsv'. Default is 'csv'.
|
||||
* @returns {window.File} The File to the CSV file that was created.
|
||||
*/
|
||||
async setupCsvFileWithLines(csvLines) {
|
||||
let tmpFile = FileTestUtils.getTempFile("firefox_logins.csv");
|
||||
async setupCsvFileWithLines(csvLines, extension = "csv") {
|
||||
let tmpFile = FileTestUtils.getTempFile(`firefox_logins.${extension}`);
|
||||
await OS.File.writeAtomic(
|
||||
tmpFile.path,
|
||||
new TextEncoder().encode(csvLines.join("\r\n"))
|
||||
|
@ -8,9 +8,11 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const { LoginCSVImport } = ChromeUtils.import(
|
||||
"resource://gre/modules/LoginCSVImport.jsm"
|
||||
);
|
||||
const {
|
||||
LoginCSVImport,
|
||||
ImportFailedException,
|
||||
ImportFailedErrorType,
|
||||
} = ChromeUtils.import("resource://gre/modules/LoginCSVImport.jsm");
|
||||
const { LoginExport } = ChromeUtils.import(
|
||||
"resource://gre/modules/LoginExport.jsm"
|
||||
);
|
||||
@ -30,16 +32,21 @@ Services.prefs.setBoolPref(
|
||||
*
|
||||
* @param {string[]} csvLines
|
||||
* The lines that make up the CSV file.
|
||||
* @param {string} extension
|
||||
* Optional parameter. Either 'csv' or 'tsv'. Default is 'csv'.
|
||||
* @returns {string} The path to the CSV file that was created.
|
||||
*/
|
||||
async function setupCsv(csvLines) {
|
||||
async function setupCsv(csvLines, extension) {
|
||||
// Cleanup state.
|
||||
TTU.getAndClearKeyedHistogram("FX_MIGRATION_LOGINS_QUANTITY");
|
||||
TTU.getAndClearKeyedHistogram("FX_MIGRATION_LOGINS_IMPORT_MS");
|
||||
TTU.getAndClearKeyedHistogram("FX_MIGRATION_LOGINS_JANK_MS");
|
||||
Services.logins.removeAllUserFacingLogins();
|
||||
|
||||
let tmpFile = await LoginTestUtils.file.setupCsvFileWithLines(csvLines);
|
||||
let tmpFile = await LoginTestUtils.file.setupCsvFileWithLines(
|
||||
csvLines,
|
||||
extension
|
||||
);
|
||||
return tmpFile.path;
|
||||
}
|
||||
|
||||
@ -64,18 +71,57 @@ function checkLoginNewlyCreated(login) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an import fails if there is no username column. We don't want
|
||||
* Ensure that an import works with TSV.
|
||||
*/
|
||||
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",
|
||||
]);
|
||||
let tsvFilePath = await setupCsv(
|
||||
[
|
||||
"url\tusername\tpassword\thttpRealm\tformActionOrigin\tguid\ttimeCreated\ttimeLastUsed\ttimePasswordChanged",
|
||||
`https://example.com:8080\tjoe@example.com\tqwerty\tMy realm\t""\t{5ec0d12f-e194-4279-ae1b-d7d281bb46f0}\t1589617814635\t1589710449871\t1589617846802`,
|
||||
],
|
||||
"tsv"
|
||||
);
|
||||
|
||||
await LoginCSVImport.importFromCSV(tsvFilePath);
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that an import fails if there is no username column in a TSV file.
|
||||
*/
|
||||
add_task(async function test_import_tsv_with_missing_columns() {
|
||||
let csvFilePath = await setupCsv(
|
||||
[
|
||||
"url\tusernameTypo\tpassword\thttpRealm\tformActionOrigin\tguid\ttimeCreated\ttimeLastUsed\ttimePasswordChanged",
|
||||
"https://example.com\tkramer@example.com\tqwerty\tMy realm\t\t{5ec0d12f-e194-4279-ae1b-d7d281bb46f7}\t1589617814635\t1589710449871\t1589617846802",
|
||||
],
|
||||
"tsv"
|
||||
);
|
||||
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/must contain origin, username, and password columns/,
|
||||
"Ensure non-CSV throws"
|
||||
/FILE_FORMAT_ERROR/,
|
||||
"Ensure missing username throws"
|
||||
);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
@ -96,7 +142,7 @@ add_task(async function test_import_lacking_username_column() {
|
||||
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/must contain origin, username, and password columns/,
|
||||
/FILE_FORMAT_ERROR/,
|
||||
"Ensure missing username throws"
|
||||
);
|
||||
|
||||
@ -137,23 +183,6 @@ add_task(async function test_import_with_duplicate_columns() {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -546,7 +575,7 @@ add_task(async function test_import_summary_contains_logins_with_errors() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://invalid.password.example.com,jane@example.com,,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0002},1589617814635,1589710449871,1589617846802",
|
||||
",jane@example.com,invalid_origin,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0002},1589617814635,1589710449871,1589617846802",
|
||||
",jane@example.com,invalid_origin,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0005},1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
let [invalidPassword, invalidOrigin] = await LoginCSVImport.importFromCSV(
|
||||
csvFilePath
|
||||
@ -563,3 +592,93 @@ add_task(async function test_import_summary_contains_logins_with_errors() {
|
||||
`Check that the invalid origin error is reported`
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login with wrong file format will have correct errorType.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_bad_format() {
|
||||
let csvFilePath = await setupCsv(["password", "123qwe!@#QWE"]);
|
||||
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/FILE_FORMAT_ERROR/,
|
||||
"Check that the errorType is file format error"
|
||||
);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[],
|
||||
"Check that no login was added with bad format"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login with wrong file type will have correct errorType.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_non_csv_file() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"<body>this is totally not a csv file</body>",
|
||||
]);
|
||||
|
||||
await Assert.rejects(
|
||||
LoginCSVImport.importFromCSV(csvFilePath),
|
||||
/FILE_FORMAT_ERROR/,
|
||||
"Check that the errorType is file format error"
|
||||
);
|
||||
|
||||
LoginTestUtils.checkLogins(
|
||||
[],
|
||||
"Check that no login was added with file of different format"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Imports login with wrong file type will have correct errorType.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_url_user_multiple_values() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://example.com,jane@example.com,password1,My realm",
|
||||
"https://example.com,jane@example.com,password2,My realm",
|
||||
]);
|
||||
|
||||
let errorType;
|
||||
try {
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
} catch (e) {
|
||||
if (e instanceof ImportFailedException) {
|
||||
errorType = e.errorType;
|
||||
}
|
||||
}
|
||||
|
||||
equal(
|
||||
errorType,
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
|
||||
`Check that the errorType is file format error in case of duplicate entries`
|
||||
);
|
||||
}).skip(); // TODO: Bug 1687852, resolve duplicates when importing
|
||||
|
||||
/**
|
||||
* Imports login with wrong file type will have correct errorType.
|
||||
*/
|
||||
add_task(async function test_import_summary_with_multiple_guid_values() {
|
||||
let csvFilePath = await setupCsv([
|
||||
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged",
|
||||
"https://example1.com,jane1@example.com,password1,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0004},1589617814635,1589710449871,1589617846802",
|
||||
"https://example2.com,jane2@example.com,password2,My realm,,{5ec0d12f-e194-4279-ae1b-d7d281bb0004},1589617814635,1589710449871,1589617846802",
|
||||
]);
|
||||
|
||||
let errorType;
|
||||
try {
|
||||
await LoginCSVImport.importFromCSV(csvFilePath);
|
||||
} catch (e) {
|
||||
if (e instanceof ImportFailedException) {
|
||||
errorType = e.errorType;
|
||||
}
|
||||
}
|
||||
|
||||
equal(
|
||||
errorType,
|
||||
ImportFailedErrorType.CONFLICTING_VALUES_ERROR,
|
||||
`Check that the errorType is file format error in case of duplicate entries`
|
||||
);
|
||||
});
|
||||
|
@ -167,6 +167,8 @@ add_task(async function test_export_escapes_values() {
|
||||
|
||||
add_task(async function test_export_multiple_rows() {
|
||||
let logins = await LoginTestUtils.testData.loginList();
|
||||
// Note, because we're stubbing this method and avoiding the actual login manager logic,
|
||||
// login de-duplication does not occur
|
||||
Services.logins.getAllLoginsAsync.returns(logins);
|
||||
|
||||
let actualRows = await exportAsCSVInTmpFile();
|
||||
|
Loading…
Reference in New Issue
Block a user