Skip to content

Commit

Permalink
Autofill "Never Save for this Site" (#555)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1205783642285427/f
iOS PR: duckduckgo/iOS#2104
macOS PR: duckduckgo/macos-browser#1827
What kind of version bump will this require?: Minor

Description:
Adds Autofill "Never Save for this Site" support for iOS
  • Loading branch information
amddg44 authored Nov 16, 2023
1 parent f22eb40 commit 641018c
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 30 deletions.
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"),
.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

0 comments on commit 641018c

Please sign in to comment.