summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/passwordmgr/LoginCSVImport.sys.mjs
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/passwordmgr/LoginCSVImport.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/LoginCSVImport.sys.mjs221
1 files changed, 221 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs b/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs
new file mode 100644
index 0000000000..247ed80a3a
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Provides a class to import login-related data CSV files.
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CSV: "resource://gre/modules/CSV.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ ResponsivenessMonitor: "resource://gre/modules/ResponsivenessMonitor.sys.mjs",
+});
+
+/**
+ * All the CSV column names will be converted to lower case before lookup
+ * so they must be specified here in lower case.
+ */
+const FIELD_TO_CSV_COLUMNS = {
+ origin: ["url", "login_uri"],
+ username: ["username", "login_username"],
+ password: ["password", "login_password"],
+ httpRealm: ["httprealm"],
+ formActionOrigin: ["formactionorigin"],
+ guid: ["guid"],
+ timeCreated: ["timecreated"],
+ timeLastUsed: ["timelastused"],
+ timePasswordChanged: ["timepasswordchanged"],
+};
+
+export 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",
+});
+
+export 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
+ */
+export class LoginCSVImport {
+ /**
+ * Returns a map that has the csv column name as key and the value the field name.
+ *
+ * @returns {Map} A map that has the csv column name as key and the value the field name.
+ */
+ static _getCSVColumnToFieldMap() {
+ let csvColumnToField = new Map();
+ for (let [field, columns] of Object.entries(FIELD_TO_CSV_COLUMNS)) {
+ for (let column of columns) {
+ csvColumnToField.set(column.toLowerCase(), field);
+ }
+ }
+ return csvColumnToField;
+ }
+
+ /**
+ * Builds a vanilla JS object containing all the login fields from a row of CSV cells.
+ *
+ * @param {object} csvObject
+ * An object created from a csv row. The keys are the csv column names, the values are the cells.
+ * @param {Map} csvColumnToFieldMap
+ * A map where the keys are the csv properties and the values are the object keys.
+ * @returns {object} Representing login object with only properties, not functions.
+ */
+ static _getVanillaLoginFromCSVObject(csvObject, csvColumnToFieldMap) {
+ let vanillaLogin = Object.create(null);
+ for (let columnName of Object.keys(csvObject)) {
+ let fieldName = csvColumnToFieldMap.get(columnName.toLowerCase());
+ if (!fieldName) {
+ continue;
+ }
+
+ if (
+ typeof vanillaLogin[fieldName] != "undefined" &&
+ vanillaLogin[fieldName] !== csvObject[columnName]
+ ) {
+ // Differing column values map to one property.
+ // e.g. if two headings map to `origin` we won't know which to use.
+ return {};
+ }
+
+ vanillaLogin[fieldName] = csvObject[columnName];
+ }
+
+ // Since `null` can't be represented in a CSV file and the httpRealm header
+ // cannot be an empty string, assume that an empty httpRealm means this is
+ // a form login and therefore null-out httpRealm.
+ if (vanillaLogin.httpRealm === "") {
+ vanillaLogin.httpRealm = null;
+ }
+
+ return vanillaLogin;
+ }
+ static _recordHistogramTelemetry(histogram, report) {
+ for (let reportRow of report) {
+ let { result } = reportRow;
+ if (result.includes("error")) {
+ histogram.add("error");
+ } else {
+ histogram.add(result);
+ }
+ }
+ }
+ /**
+ * Imports logins from a CSV file (comma-separated values file).
+ * Existing logins may be updated in the process.
+ *
+ * @param {string} filePath
+ * @returns {Object[]} An array of rows where each is mapped to a row in the CSV and it's import information.
+ */
+ static async importFromCSV(filePath) {
+ TelemetryStopwatch.start("PWMGR_IMPORT_LOGINS_FROM_FILE_MS");
+ let responsivenessMonitor;
+ try {
+ responsivenessMonitor = new lazy.ResponsivenessMonitor();
+ let csvColumnToFieldMap = LoginCSVImport._getCSVColumnToFieldMap();
+ let csvFieldToColumnMap = new Map();
+
+ let csvString;
+ try {
+ csvString = await IOUtils.readUTF8(filePath, { encoding: "utf-8" });
+ } catch (ex) {
+ console.error(ex);
+ throw new ImportFailedException(
+ ImportFailedErrorType.FILE_PERMISSIONS_ERROR
+ );
+ }
+ let headerLine;
+ let parsedLines;
+ try {
+ let delimiter = filePath.toUpperCase().endsWith(".CSV") ? "," : "\t";
+ [headerLine, parsedLines] = lazy.CSV.parse(csvString, delimiter);
+ } catch {
+ throw new ImportFailedException(
+ ImportFailedErrorType.FILE_FORMAT_ERROR
+ );
+ }
+ if (parsedLines && headerLine) {
+ for (const columnName of headerLine) {
+ const fieldName = csvColumnToFieldMap.get(
+ columnName.toLocaleLowerCase()
+ );
+ if (fieldName) {
+ if (!csvFieldToColumnMap.has(fieldName)) {
+ csvFieldToColumnMap.set(fieldName, columnName);
+ } else {
+ throw new ImportFailedException(
+ ImportFailedErrorType.CONFLICTING_VALUES_ERROR
+ );
+ }
+ }
+ }
+ }
+ if (csvFieldToColumnMap.size === 0) {
+ throw new ImportFailedException(
+ ImportFailedErrorType.FILE_FORMAT_ERROR
+ );
+ }
+ if (
+ parsedLines[0] &&
+ (!csvFieldToColumnMap.has("origin") ||
+ !csvFieldToColumnMap.has("username") ||
+ !csvFieldToColumnMap.has("password"))
+ ) {
+ // The username *value* can be empty but we require a username column to
+ // ensure that we don't import logins without their usernames due to the
+ // username column not being recognized.
+ throw new ImportFailedException(
+ ImportFailedErrorType.FILE_FORMAT_ERROR
+ );
+ }
+
+ let loginsToImport = parsedLines.map(csvObject => {
+ return LoginCSVImport._getVanillaLoginFromCSVObject(
+ csvObject,
+ csvColumnToFieldMap
+ );
+ });
+
+ let report = await lazy.LoginHelper.maybeImportLogins(loginsToImport);
+
+ for (const reportRow of report) {
+ if (reportRow.result === "error_missing_field") {
+ reportRow.field_name = csvFieldToColumnMap.get(reportRow.field_name);
+ }
+ }
+
+ // Record quantity, jank, and duration telemetry.
+ try {
+ let histogram = Services.telemetry.getHistogramById(
+ "PWMGR_IMPORT_LOGINS_FROM_FILE_CATEGORICAL"
+ );
+ this._recordHistogramTelemetry(histogram, report);
+ let accumulatedDelay = responsivenessMonitor.finish();
+ Services.telemetry
+ .getHistogramById("PWMGR_IMPORT_LOGINS_FROM_FILE_JANK_MS")
+ .add(accumulatedDelay);
+ TelemetryStopwatch.finish("PWMGR_IMPORT_LOGINS_FROM_FILE_MS");
+ } catch (ex) {
+ console.error(ex);
+ }
+ LoginCSVImport.lastImportReport = report;
+ return report;
+ } finally {
+ if (TelemetryStopwatch.running("PWMGR_IMPORT_LOGINS_FROM_FILE_MS")) {
+ TelemetryStopwatch.cancel("PWMGR_IMPORT_LOGINS_FROM_FILE_MS");
+ }
+ responsivenessMonitor.abort();
+ }
+ }
+}