From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../components/passwordmgr/LoginCSVImport.sys.mjs | 199 +++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 toolkit/components/passwordmgr/LoginCSVImport.sys.mjs (limited to 'toolkit/components/passwordmgr/LoginCSVImport.sys.mjs') diff --git a/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs b/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs new file mode 100644 index 0000000000..2e9db07014 --- /dev/null +++ b/toolkit/components/passwordmgr/LoginCSVImport.sys.mjs @@ -0,0 +1,199 @@ +/* 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", +}); + +/** + * 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) { + 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 and duration telemetry. + try { + let histogram = Services.telemetry.getHistogramById( + "PWMGR_IMPORT_LOGINS_FROM_FILE_CATEGORICAL" + ); + this._recordHistogramTelemetry(histogram, report); + } catch (ex) { + console.error(ex); + } + LoginCSVImport.lastImportReport = report; + return report; + } +} -- cgit v1.2.3