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

Autofill "Never Save for this Site" #555

Merged
merged 14 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/duckduckgo-autofill.git",
"state" : {
"revision" : "c8e895c8fd50dc76e8d8dc827a636ad77b7f46ff",
"version" : "9.0.0"
"revision" : "93677cc02cfe650ce7f417246afd0e8e972cd83e",
"version" : "10.0.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let package = Package(
.library(name: "SecureStorage", targets: ["SecureStorage"])
],
dependencies: [
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "9.0.0"),
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.0"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this update is what’s causing the inline signup for email protection to fail?

.package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.2.0"),
.package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.1"),
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public protocol AutofillSecureVaultDelegate: AnyObject {
func autofillUserScript(_: AutofillUserScript, didRequestCredentialsForDomain domain: String,
completionHandler: @escaping ([SecureVaultModels.WebsiteCredentials], SecureVaultModels.CredentialsProvider) -> Void)

func autofillUserScript(_: AutofillUserScript, didRequestRuntimeConfigurationForDomain domain: String,
completionHandler: @escaping (String?) -> Void)

func autofillUserScriptDidOfferGeneratedPassword(_: AutofillUserScript,
password: String,
completionHandler: @escaping (Bool) -> Void)
Expand Down Expand Up @@ -429,6 +432,14 @@ extension AutofillUserScript {

// MARK: - Message Handlers

func getRuntimeConfiguration(_ message: UserScriptMessage, _ replyHandler: @escaping MessageReplyHandler) {
let domain = hostForMessage(message)

vaultDelegate?.autofillUserScript(self, didRequestRuntimeConfigurationForDomain: domain, completionHandler: { response in
replyHandler(response)
})
}

func getAvailableInputTypes(_ message: UserScriptMessage, _ replyHandler: @escaping MessageReplyHandler) {
let domain = hostForMessage(message)
let email = emailDelegate?.autofillUserScriptDidRequestSignedInStatus(self) ?? false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,114 @@ public protocol AutofillUserScriptSourceProvider {
}

public class DefaultAutofillSourceProvider: AutofillUserScriptSourceProvider {

private var sourceStr: String


private struct ProviderData {
var privacyConfig: Data
var userUnprotectedDomains: Data
var userPreferences: Data
}

let privacyConfigurationManager: PrivacyConfigurationManaging
let properties: ContentScopeProperties
private var sourceStr: String = ""

public var source: String {
return sourceStr
}

public init(privacyConfigurationManager: PrivacyConfigurationManaging, properties: ContentScopeProperties) {
self.privacyConfigurationManager = privacyConfigurationManager
self.properties = properties
}

public func loadJS() {
guard let replacements = buildReplacementsString() else {
sourceStr = ""
return
}
sourceStr = AutofillUserScript.loadJS("assets/autofill", from: Autofill.bundle, withReplacements: replacements)
}

public func buildRuntimeConfigResponse() -> String? {
guard let providerData = buildReplacementsData(),
let privacyConfigJson = String(data: providerData.privacyConfig, encoding: .utf8),
let userUnprotectedDomainsString = String(data: providerData.userUnprotectedDomains, encoding: .utf8),
let userPreferencesString = String(data: providerData.userPreferences, encoding: .utf8) else {
return nil
}

return """
{
"success": {
"contentScope": \(privacyConfigJson),
"userUnprotectedDomains": \(userUnprotectedDomainsString),
"userPreferences": \(userPreferencesString)
}
}
"""
}

private func buildReplacementsString() -> [String: String]? {
var replacements: [String: String] = [:]
#if os(macOS)
replacements["// INJECT isApp HERE"] = "isApp = true;"
#endif
#if os(macOS)
replacements["// INJECT isApp HERE"] = "isApp = true;"
#endif

if #available(iOS 14, macOS 11, *) {
replacements["// INJECT hasModernWebkitAPI HERE"] = "hasModernWebkitAPI = true;"
#if os(macOS)
replacements["// INJECT supportsTopFrame HERE"] = "supportsTopFrame = true;"
#endif

#if os(macOS)
replacements["// INJECT supportsTopFrame HERE"] = "supportsTopFrame = true;"
#endif
}

guard let privacyConfigJson = String(data: privacyConfigurationManager.currentConfig, encoding: .utf8),
let userUnprotectedDomains = try? JSONEncoder().encode(privacyConfigurationManager.privacyConfig.userUnprotectedDomains),
let userUnprotectedDomainsString = String(data: userUnprotectedDomains, encoding: .utf8),
let jsonProperties = try? JSONEncoder().encode(properties),
let jsonPropertiesString = String(data: jsonProperties, encoding: .utf8)
else {
sourceStr = ""
return

guard let providerData = buildReplacementsData(),
let privacyConfigJson = String(data: providerData.privacyConfig, encoding: .utf8),
let userUnprotectedDomainsString = String(data: providerData.userUnprotectedDomains, encoding: .utf8),
let userPreferencesString = String(data: providerData.userPreferences, encoding: .utf8) else {
return nil
}

replacements["// INJECT contentScope HERE"] = "contentScope = " + privacyConfigJson + ";"
replacements["// INJECT userUnprotectedDomains HERE"] = "userUnprotectedDomains = " + userUnprotectedDomainsString + ";"
replacements["// INJECT userPreferences HERE"] = "userPreferences = " + jsonPropertiesString + ";"
replacements["// INJECT userPreferences HERE"] = "userPreferences = " + userPreferencesString + ";"
return replacements
}

sourceStr = AutofillUserScript.loadJS("assets/autofill", from: Autofill.bundle, withReplacements: replacements)
private func buildReplacementsData() -> ProviderData? {
guard let userUnprotectedDomains = try? JSONEncoder().encode(privacyConfigurationManager.privacyConfig.userUnprotectedDomains),
let jsonProperties = try? JSONEncoder().encode(properties) else {
return nil
}
return ProviderData(privacyConfig: privacyConfigurationManager.currentConfig,
userUnprotectedDomains: userUnprotectedDomains,
userPreferences: jsonProperties)
}

public class Builder {
private var privacyConfigurationManager: PrivacyConfigurationManaging
private var properties: ContentScopeProperties
private var sourceStr: String = ""
private var shouldLoadJS: Bool = false

public init(privacyConfigurationManager: PrivacyConfigurationManaging, properties: ContentScopeProperties) {
self.privacyConfigurationManager = privacyConfigurationManager
self.properties = properties
}

public func build() -> DefaultAutofillSourceProvider {
let provider = DefaultAutofillSourceProvider(privacyConfigurationManager: privacyConfigurationManager, properties: properties)

if shouldLoadJS {
provider.loadJS()
}

return provider
}

public func withJSLoading() -> Builder {
self.shouldLoadJS = true
return self
}
}
}
6 changes: 5 additions & 1 deletion Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti
case pmHandlerOpenManageIdentities
case pmHandlerOpenManagePasswords

case getRuntimeConfiguration
case getAvailableInputTypes
case getAutofillData
case storeFormData
Expand All @@ -68,6 +69,8 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti
/// once the user selects a field to open, we store field type and other contextual information to be initialized into the top autofill.
public var serializedInputContext: String?

public var sessionKey: String?

public weak var emailDelegate: AutofillEmailDelegate?
public weak var vaultDelegate: AutofillSecureVaultDelegate?

Expand Down Expand Up @@ -131,7 +134,8 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti
case .emailHandlerCheckAppSignedInStatus: return emailCheckSignedInStatus

case .pmHandlerGetAutofillInitData: return pmGetAutoFillInitData


case .getRuntimeConfiguration: return getRuntimeConfiguration
case .getAvailableInputTypes: return getAvailableInputTypes
case .getAutofillData: return getAutofillData
case .storeFormData: return pmStoreData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public struct ContentScopeFeatureToggles: Encodable {

public let credentialsSaving: Bool

public let passwordGeneration: Bool
public var passwordGeneration: Bool

public let inlineIconCredentials: Bool
public let thirdPartyCredentialsProvider: Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ public protocol AutofillDatabaseProvider: SecureStorageDatabaseProvider {
func websiteAccountsForTopLevelDomain(_ eTLDplus1: String) throws -> [SecureVaultModels.WebsiteAccount]
func deleteWebsiteCredentialsForAccountId(_ accountId: Int64) throws

func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites]
func hasNeverPromptWebsitesFor(domain: String) throws -> Bool
@discardableResult
func storeNeverPromptWebsite(_ neverPromptWebsite: SecureVaultModels.NeverPromptWebsites) throws -> Int64
func deleteAllNeverPromptWebsites() throws

func notes() throws -> [SecureVaultModels.Note]
func noteForNoteId(_ noteId: Int64) throws -> SecureVaultModels.Note?
@discardableResult
Expand Down Expand Up @@ -93,6 +99,7 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro
migrator.registerMigration("v9", migrate: Self.migrateV9(database:))
migrator.registerMigration("v10", migrate: Self.migrateV10(database:))
migrator.registerMigration("v11", migrate: Self.migrateV11(database:))
migrator.registerMigration("v12", migrate: Self.migrateV12(database:))
}
}
}
Expand Down Expand Up @@ -335,6 +342,54 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro
}
}

// MARK: NeverPromptWebsites

public func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites] {
try db.read {
try SecureVaultModels.NeverPromptWebsites.fetchAll($0)
}
}

public func hasNeverPromptWebsitesFor(domain: String) throws -> Bool {
let neverPromptWebsite = try db.read {
try SecureVaultModels.NeverPromptWebsites
.filter(SecureVaultModels.NeverPromptWebsites.Columns.domain.like(domain))
.fetchOne($0)
}
return neverPromptWebsite != nil
}

public func storeNeverPromptWebsite(_ neverPromptWebsite: SecureVaultModels.NeverPromptWebsites) throws -> Int64 {
if let id = neverPromptWebsite.id {
try updateNeverPromptWebsite(neverPromptWebsite, usingId: id)
return id
} else {
return try insertNeverPromptWebsite(neverPromptWebsite)
}
}

public func deleteAllNeverPromptWebsites() throws {
try db.write {
try $0.execute(sql: """
DELETE FROM
\(SecureVaultModels.NeverPromptWebsites.databaseTableName)
""")
}
}

func updateNeverPromptWebsite(_ neverPromptWebsite: SecureVaultModels.NeverPromptWebsites, usingId id: Int64) throws {
try db.write {
try neverPromptWebsite.update($0)
}
}

func insertNeverPromptWebsite(_ neverPromptWebsite: SecureVaultModels.NeverPromptWebsites) throws -> Int64 {
try db.write {
try neverPromptWebsite.insert($0)
return $0.lastInsertedRowID
}
}

// MARK: Notes

public func notes() throws -> [SecureVaultModels.Note] {
Expand Down Expand Up @@ -886,6 +941,15 @@ extension DefaultAutofillDatabaseProvider {
}
}

static func migrateV12(database: Database) throws {

try database.create(table: SecureVaultModels.NeverPromptWebsites.databaseTableName) {
$0.autoIncrementedPrimaryKey(SecureVaultModels.NeverPromptWebsites.Columns.id.name)

$0.column(SecureVaultModels.NeverPromptWebsites.Columns.domain.name, .text)
}
}

// Refresh password comparison hashes
static private func updatePasswordHashes(database: Database) throws {
let accountRows = try Row.fetchCursor(database, sql: "SELECT * FROM \(SecureVaultModels.WebsiteAccount.databaseTableName)")
Expand Down Expand Up @@ -1027,6 +1091,26 @@ extension SecureVaultModels.WebsiteCredentials {

}

extension SecureVaultModels.NeverPromptWebsites: PersistableRecord, FetchableRecord {

public enum Columns: String, ColumnExpression {
case id, domain
}

public init(row: Row) {
id = row[Columns.id]
domain = row[Columns.domain]
}

public func encode(to container: inout PersistenceContainer) {
container[Columns.id] = id
container[Columns.domain] = domain
}

public static var databaseTableName: String = "never_prompt_websites"

}

extension SecureVaultModels.CreditCard: PersistableRecord, FetchableRecord {

enum Columns: String, ColumnExpression {
Expand Down
45 changes: 45 additions & 0 deletions Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public protocol AutofillSecureVault: SecureVault {
func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64
func deleteWebsiteCredentialsFor(accountId: Int64) throws

func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites]
func hasNeverPromptWebsitesFor(domain: String) throws -> Bool
@discardableResult
func storeNeverPromptWebsites(_ neverPromptWebsite: SecureVaultModels.NeverPromptWebsites) throws -> Int64
func deleteAllNeverPromptWebsites() throws

func notes() throws -> [SecureVaultModels.Note]
func noteFor(id: Int64) throws -> SecureVaultModels.Note?
@discardableResult
Expand Down Expand Up @@ -361,6 +367,45 @@ public class DefaultAutofillSecureVault<T: AutofillDatabaseProvider>: AutofillSe
}
}

// MARK: NeverPromptWebsites

public func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites] {
lock.lock()
defer {
lock.unlock()
}

do {
return try self.providers.database.neverPromptWebsites()
} catch {
throw SecureStorageError.databaseError(cause: error)
}
}

public func hasNeverPromptWebsitesFor(domain: String) throws -> Bool {
lock.lock()
defer {
lock.unlock()
}
do {
return try self.providers.database.hasNeverPromptWebsitesFor(domain: domain)
} catch {
throw SecureStorageError.databaseError(cause: error)
}
}

public func storeNeverPromptWebsites(_ neverPromptWebsite: SecureVaultModels.NeverPromptWebsites) throws -> Int64 {
return try executeThrowingDatabaseOperation {
return try self.providers.database.storeNeverPromptWebsite(neverPromptWebsite)
}
}

public func deleteAllNeverPromptWebsites() throws {
try executeThrowingDatabaseOperation {
try self.providers.database.deleteAllNeverPromptWebsites()
}
}

// MARK: - Notes

public func notes() throws -> [SecureVaultModels.Note] {
Expand Down
Loading