Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new browser import sources and bitwarden #1735

Closed
wants to merge 11 commits into from
66 changes: 53 additions & 13 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,10 @@ struct UserText {

static let csvImportDescription = NSLocalizedString("import.logins.csv.description", value: "The CSV importer will try to match column headers to their position.\nIf there is no header, it supports two formats:\n\n1. URL, Username, Password\n2. Title, URL, Username, Password", comment: "Description text for the CSV importer")
static let importLoginsSelectCSVFile = NSLocalizedString("import.logins.select-csv-file", value: "Select CSV File…", comment: "Button text for selecting a CSV file")
static let importLoginsSelectSafariCSVFile = NSLocalizedString("import.logins.select-safari-csv-file", value: "Select Passwords CSV File…", comment: "Button text for selecting a Safari CSV file")
static let importLoginsSelectBrowserCSVFile = NSLocalizedString("import.logins.select-browser-csv-file", value: "Select Passwords CSV File…", comment: "Button text for selecting a browser CSV file")
static let importLoginsSelect1PasswordCSVFile = NSLocalizedString("import.logins.select-1password-csv-file", value: "Select 1Password CSV File…", comment: "Button text for selecting a 1Password CSV file")
static let importLoginsSelectLastPassCSVFile = NSLocalizedString("import.logins.select-lastpass-csv-file", value: "Select LastPass CSV File…", comment: "Button text for selecting a LastPass CSV file")
static let importLoginsSelectBitwardenCSVFile = NSLocalizedString("import.logins.select-bitwarden-csv-file", value: "Select Bitwarden CSV File…", comment: "Button text for selecting a Bitwarden CSV file")

static let importLoginsSelectAnotherFile = NSLocalizedString("import.logins.select-another-file", value: "Select Another CSV File…", comment: "Button text for selecting another file")
static let importLoginsFailedToReadCSVFile = NSLocalizedString("import.logins.failed-to-read-file", value: "Failed to get CSV file URL", comment: "Error text when importing a CSV file")
Expand Down Expand Up @@ -687,7 +688,19 @@ struct UserText {
Imported passwords are stored securely using encryption.
""", comment: "More info when importing from Chromium")

static let importFromFirefoxMoreInfo = NSLocalizedString("import.from.firefox.info", value: "You'll be asked to enter your Primary Password for Firefox.\n\nImported passwords are encrypted and only stored on this computer.", comment: "More info when importing from Firefox")
DuckDuckGo won’t see your Keychain password, but macOS needs it to access and import passwords into DuckDuckGo.

Imported passwords are encrypted and only stored on this computer.
""", comment: "More info when importing from Chromium")

static func importMoreInfo(fromFirefoxBasedBrowserNamed sourceName: String) -> String {
let localized = NSLocalizedString("import.from.firefox.info", value: """
You'll be asked to enter your Primary Password for %@.

Imported passwords are encrypted and only stored on this computer.
""", comment: "More info when importing from Firefox")
return String(format: localized, sourceName)
}

static let moreOrLessCollapse = NSLocalizedString("more.or.less.collapse", value: "Show Less", comment: "For collapsing views to show less.")
static let moreOrLessExpand = NSLocalizedString("more.or.less.expand", value: "Show More", comment: "For expanding views to show more.")
Expand Down
8 changes: 8 additions & 0 deletions DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,16 @@ private extension BookmarkImportSource {

case .thirdPartyBrowser(.brave),
.thirdPartyBrowser(.chrome),
.thirdPartyBrowser(.chromium),
.thirdPartyBrowser(.coccoc),
.thirdPartyBrowser(.edge),
.thirdPartyBrowser(.firefox),
.thirdPartyBrowser(.opera),
.thirdPartyBrowser(.operaGX),
.thirdPartyBrowser(.tor),
.thirdPartyBrowser(.vivaldi),
.thirdPartyBrowser(.yandex),
.thirdPartyBrowser(.bitwarden),
.thirdPartyBrowser(.onePassword8),
.thirdPartyBrowser(.onePassword7),
.thirdPartyBrowser(.lastPass),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// ChromePreferences.swift
// ChromiumPreferences.swift
//
// Copyright © 2022 DuckDuckGo. All rights reserved.
//
Expand All @@ -18,28 +18,28 @@

import AppKit

struct ChromePreferences: Decodable {
struct ChromiumPreferences: Decodable {

struct AccountInfo: Decodable {
let email: String?
let fullName: String?
}
struct Profile: Decodable {
let name: String
let name: String?
let createdByVersion: String?
}

let accountInfo: [AccountInfo]?
let profile: Profile

init(from data: Data) throws {
var decoder = JSONDecoder()
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

self = try decoder.decode(Self.self, from: data)
}

var profileName: String {
var profileName: String? {
for account in accountInfo ?? [] {
switch (account.fullName, account.email) {
case (.some(let fullName), .some(let email)):
Expand Down
89 changes: 56 additions & 33 deletions DuckDuckGo/DataImport/DataImport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@ import PixelKit
enum DataImport {

enum Source: CaseIterable, Equatable {

case brave
case chrome
case chromium
case coccoc
case edge
case firefox
case opera
case operaGX
case safari
case safariTechnologyPreview
case tor
case vivaldi
case yandex
case onePassword8
case onePassword7
case bitwarden
case lastPass
case csv
case bookmarksHTML
Expand All @@ -44,14 +51,30 @@ enum DataImport {
return "Brave"
case .chrome:
return "Chrome"
case .chromium:
return "Chromium"
case .coccoc:
return "Cốc Cốc"
case .edge:
return "Edge"
case .firefox:
return "Firefox"
case .opera:
return "Opera"
case .operaGX:
return "OperaGX"
case .safari:
return "Safari"
case .safariTechnologyPreview:
return "Safari Technology Preview"
case .tor:
return "Tor Browser"
case .vivaldi:
return "Vivaldi"
case .yandex:
return "Yandex"
case .bitwarden:
return "Bitwarden"
case .lastPass:
return "LastPass"
case .onePassword7:
Expand All @@ -75,10 +98,10 @@ enum DataImport {
}

switch self {
case .csv, .onePassword8, .onePassword7, .lastPass, .bookmarksHTML:
case .csv, .bitwarden, .onePassword8, .onePassword7, .lastPass, .bookmarksHTML:
// Users can always import from exported files
return true
case .brave, .chrome, .edge, .firefox, .safari, .safariTechnologyPreview:
case .brave, .chrome, .chromium, .coccoc, .edge, .firefox, .opera, .operaGX, .safari, .safariTechnologyPreview, .tor, .vivaldi, .yandex:
// Users can't import from browsers unless they're installed
return false
}
Expand Down Expand Up @@ -112,6 +135,13 @@ enum DataImport {
}

struct BrowserProfileList {

enum Constants {
static let chromiumDefaultProfileName = "Default"
static let chromiumProfilePrefix = "Profile "
static let firefoxDefaultProfileName = "default-release"
}

let browser: ThirdPartyBrowser
let profiles: [BrowserProfile]

Expand All @@ -123,26 +153,26 @@ enum DataImport {
self.browser = browser

switch browser {
case .brave, .chrome, .edge:
case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi, .yandex:
// Chromium profiles are either named "Default", or a series of incrementing profile names, i.e. "Profile 1", "Profile 2", etc.
let potentialProfiles = profileURLs.map({
BrowserProfile.for(browser: browser, profileURL: $0)
BrowserProfile(browser: browser, profileURL: $0)
})

let filteredProfiles = potentialProfiles.filter {
$0.hasNonDefaultProfileName ||
$0.profileName == "Default" ||
$0.profileName.hasPrefix("Profile ")
$0.chromiumPreferences != nil
|| $0.profileName == Constants.chromiumDefaultProfileName
|| $0.profileName.hasPrefix(Constants.chromiumProfilePrefix)
}

let sortedProfiles = filteredProfiles.sorted()

self.profiles = sortedProfiles
case .firefox, .safari, .safariTechnologyPreview:
case .firefox, .safari, .safariTechnologyPreview, .tor:
self.profiles = profileURLs.map {
BrowserProfile.for(browser: browser, profileURL: $0)
BrowserProfile(browser: browser, profileURL: $0)
}.sorted()
case .lastPass, .onePassword7, .onePassword8:
case .bitwarden, .lastPass, .onePassword7, .onePassword8:
self.profiles = []
}
}
Expand All @@ -153,11 +183,11 @@ enum DataImport {

var defaultProfile: BrowserProfile? {
switch browser {
case .brave, .chrome, .edge:
return profiles.first { $0.profileName == "Default" } ?? profiles.first
case .firefox:
return profiles.first { $0.profileName == "default-release" } ?? profiles.first
case .safari, .safariTechnologyPreview, .lastPass, .onePassword7, .onePassword8:
case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi, .yandex:
return profiles.first { $0.profileName == Constants.chromiumDefaultProfileName } ?? profiles.first
case .firefox, .tor:
return profiles.first { $0.profileName == Constants.firefoxDefaultProfileName } ?? profiles.first
case .safari, .safariTechnologyPreview, .bitwarden, .lastPass, .onePassword7, .onePassword8:
return profiles.first
}
}
Expand All @@ -172,29 +202,21 @@ enum DataImport {

let profileURL: URL
var profileName: String {
return detectedChromePreferencesProfileName ?? fallbackProfileName
}

var hasNonDefaultProfileName: Bool {
return detectedChromePreferencesProfileName != nil
return chromiumPreferences?.profileName ?? fallbackProfileName
}

private let browser: ThirdPartyBrowser
private let fileStore: FileStore
private let fallbackProfileName: String
private let detectedChromePreferencesProfileName: String?

static func `for`(browser: ThirdPartyBrowser, profileURL: URL) -> BrowserProfile {
return BrowserProfile(browser: browser, profileURL: profileURL)
}
let chromiumPreferences: ChromiumPreferences?

init(browser: ThirdPartyBrowser, profileURL: URL, fileStore: FileStore = FileManager.default) {
self.browser = browser
self.fileStore = fileStore
self.profileURL = profileURL

self.fallbackProfileName = Self.getDefaultProfileName(at: profileURL)
self.detectedChromePreferencesProfileName = Self.getChromeProfileName(at: profileURL, fileStore: fileStore)
self.chromiumPreferences = Self.getChromiumProfilePreferences(at: profileURL, fileStore: fileStore)
}

var hasBrowserData: Bool {
Expand All @@ -205,15 +227,15 @@ enum DataImport {
let profileDirectoryContentsSet = Set(profileDirectoryContents)

switch browser {
case .brave, .chrome, .edge:
case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi:
let hasChromiumLogins = ChromiumLoginReader.LoginDataFileName.allCases.contains { loginFileName in
return profileDirectoryContentsSet.contains(loginFileName.rawValue)
}

let hasChromiumBookmarks = profileDirectoryContentsSet.contains(ChromiumBookmarksReader.Constants.defaultBookmarksFileName)

return hasChromiumLogins || hasChromiumBookmarks
case .firefox:
case .firefox, .tor:
let hasFirefoxLogins = FirefoxLoginReader.DataFormat.allCases.contains { dataFormat in
let (databaseName, loginFileName) = dataFormat.formatFileNames

Expand All @@ -232,7 +254,7 @@ enum DataImport {
return profileURL.lastPathComponent.components(separatedBy: ".").last ?? profileURL.lastPathComponent
}

private static func getChromeProfileName(at profileURL: URL, fileStore: FileStore) -> String? {
private static func getChromiumProfilePreferences(at profileURL: URL, fileStore: FileStore) -> ChromiumPreferences? {
guard let profileDirectoryContents = try? fileStore.directoryContents(at: profileURL.path) else {
return nil
}
Expand All @@ -242,9 +264,9 @@ enum DataImport {
}

if profileDirectoryContents.contains(Constants.chromiumPreferencesFileName),
let chromePreferenceData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.chromiumPreferencesFileName)),
let chromePreferences = try? ChromePreferences(from: chromePreferenceData) {
return chromePreferences.profileName
let preferencesData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.chromiumPreferencesFileName)),
let preferences = try? ChromiumPreferences(from: preferencesData) {
return preferences
}

return nil
Expand Down Expand Up @@ -347,6 +369,7 @@ struct LoginImporterError: DataImportError {
let rawValue: Int

static let defaultFirefoxProfilePathNotFound = OperationType(rawValue: -1)
static let malformedCSV = OperationType(rawValue: -2)
}

var type: OperationType {
Expand Down
Loading
Loading