Bug 1650675 Import CSV error dialog r=sfoster

Differential Revision: https://phabricator.services.mozilla.com/D96101
This commit is contained in:
Andrei Cristian Petcu 2021-01-21 02:06:24 +00:00
Родитель 1f52ebc03b
Коммит c67eaa1b46
18 изменённых файлов: 705 добавлений и 262 удалений

Просмотреть файл

@ -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();