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; } /** * 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 { for (let reportRow of report) { let { result } = reportRow; Glean.pwmgr.importLoginsFromFileCategorical[ result.includes("error") ? "error" : result ].add(1); } } catch (ex) { console.error(ex); } LoginCSVImport.lastImportReport = report; return report; } } PK