From 7e360588ecc30ba1c1ae21c2a2e56283b86cc3e1 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 29 Nov 2024 15:52:27 -0800 Subject: [PATCH 1/8] Add support for RMF survey `locale` parameter (#3648) Task/Issue URL: https://app.asana.com/0/1199333091098016/1208868981778301/f Tech Design URL: CC: Description: This PR updates BSK to pull in the survey builder's new locale support. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 826c5ccf05..6ba7046c3e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11247,7 +11247,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 215.0.0; + version = 215.0.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f150a7d158..d6698fc14d 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "dfd266ab6550902dc2511714f85e2cc03009c057", - "version" : "215.0.0" + "revision" : "837dfbfe7a1b2a5e0ec2fb24a47a53dec53444b0", + "version" : "215.0.1" } }, { From c82e1c898bc881af05e34aec9d22ef082ad374e7 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 29 Nov 2024 18:03:18 -0800 Subject: [PATCH 2/8] Remove malformed error pixel (#3647) Task/Issue URL: https://app.asana.com/0/414235014887631/1208683003138954/f Tech Design URL: CC: Description: This PR removes the malformed error pixel. --- Core/PixelEvent.swift | 4 ---- DuckDuckGo.xcodeproj/project.pbxproj | 6 +----- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../NetworkProtectionPacketTunnelProvider.swift | 4 ---- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index d7433366d2..5fc44638c9 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -474,8 +474,6 @@ extension Pixel { case networkProtectionConfigurationInvalidPayload(configuration: Configuration) - case networkProtectionMalformedErrorDetected - // MARK: - VPN Tips case networkProtectionGeoswitchingTipShown @@ -1336,8 +1334,6 @@ extension Pixel.Event { case .networkProtectionConfigurationInvalidPayload(let config): return "m_netp_vpn_configuration_\(config.rawValue)_invalid_payload" - case .networkProtectionMalformedErrorDetected: return "m_netp_vpn_malformed_error_detected" - // MARK: VPN tips case .networkProtectionGeoswitchingTipShown: return "m_vpn_tip_geoswitching_shown" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6ba7046c3e..e0b5d4e5a1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2025,7 +2025,6 @@ 983C52E32C2C050B007B5747 /* BookmarksStateRepair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksStateRepair.swift; sourceTree = ""; }; 983C52E52C2C0ABA007B5747 /* BookmarkStateRepairTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkStateRepairTests.swift; sourceTree = ""; }; 983D71B02A286E810072E26D /* SyncDebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDebugViewController.swift; sourceTree = ""; }; - 983D88D52CED006000E99B46 /* UnitTests copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "UnitTests copy-Info.plist"; path = "/Users/bartek/Develop/iOS/UnitTests copy-Info.plist"; sourceTree = ""; }; 983E1349251EABF200149BD9 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; 983E134A251EABF200149BD9 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; 983E134C251EABF200149BD9 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2040,7 +2039,6 @@ 984147C424F026C800362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/HomeRow.storyboard; sourceTree = ""; }; 984147CA24F02E9E00362052 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/DaxOnboarding.storyboard; sourceTree = ""; }; 98424AA92CED4F430071C7DB /* WebViewUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebViewUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 98424AAA2CED4F430071C7DB /* UnitTests copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "UnitTests copy-Info.plist"; path = "/Users/bartek/Develop/iOS/UnitTests copy-Info.plist"; sourceTree = ""; }; 9846AA6622BD3BBF007DE48E /* InitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitHelpers.swift; sourceTree = ""; }; 9847BFFD27A2DDB400DB07AA /* ContentBlocking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlocking.swift; sourceTree = ""; }; 9847BFFF27A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppPrivacyConfigurationDataProvider.swift; sourceTree = ""; }; @@ -4257,8 +4255,6 @@ 83ED3B8D1FA8E63700B47556 /* README.md */, 83ED3B8C1FA8E61D00B47556 /* ManualTestsScript.md */, 85A313962028E78A00327D00 /* release_notes.txt */, - 983D88D52CED006000E99B46 /* UnitTests copy-Info.plist */, - 98424AAA2CED4F430071C7DB /* UnitTests copy-Info.plist */, ); sourceTree = ""; }; @@ -11247,7 +11243,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 215.0.1; + version = 216.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d6698fc14d..0268a2f9d8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "837dfbfe7a1b2a5e0ec2fb24a47a53dec53444b0", - "version" : "215.0.1" + "revision" : "1f1a9f979671e886699ef9b5edd572bc6b490875", + "version" : "216.0.0" } }, { diff --git a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 1f485fb540..30c02830ee 100644 --- a/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -295,10 +295,6 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { vpnLogger.logStartingWithoutAuthToken() DailyPixel.fireDailyAndCount(pixel: .networkProtectionTunnelStartAttemptOnDemandWithoutAccessToken, pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes) - case .malformedErrorDetected(let error): - DailyPixel.fireDailyAndCount(pixel: .networkProtectionMalformedErrorDetected, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error) } } From c5b988699291314bd4a207cf4a52e5d7a82f0de4 Mon Sep 17 00:00:00 2001 From: Pete Smith <5278441+aataraxiaa@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:28:38 +0000 Subject: [PATCH 3/8] Bump BSK, add Free Trials Feature Flag (#3655) Task/Issue URL: https://app.asana.com/0/0/1208767141940869/f **Description**: Bump BSK and add Free Trials Feature Flag --- Core/FeatureFlag.swift | 5 +++++ DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 3b2a79bbc3..15263d9e7b 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -58,6 +58,9 @@ public enum FeatureFlag: String { case crashReportOptInStatusResetting case isPrivacyProLaunchedROW case isPrivacyProLaunchedROWOverride + + /// https://app.asana.com/0/0/1208767141940869/f + case freeTrials } extension FeatureFlag: FeatureFlagDescribing { @@ -137,6 +140,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROW)) case .isPrivacyProLaunchedROWOverride: return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROWOverride)) + case .freeTrials: + return .remoteDevelopment(.subfeature(PrivacyProSubfeature.freeTrials)) } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e0b5d4e5a1..bf273e30fe 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -11243,7 +11243,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 216.0.0; + version = 216.0.1; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0268a2f9d8..fbf8a01291 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "1f1a9f979671e886699ef9b5edd572bc6b490875", - "version" : "216.0.0" + "revision" : "59f8fb2f850f8a0482c604d07396e01e8de59d21", + "version" : "216.0.1" } }, { From 518d5276b4ced8499bd255f79daecb172f13d846 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 2 Dec 2024 18:47:15 +0100 Subject: [PATCH 4/8] Release 7.148.0-0 (#3659) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- Configuration/Version.xcconfig | 2 +- .../AppPrivacyConfigurationDataProvider.swift | 4 +- Core/ios-config.json | 77 +++++++++++-------- DuckDuckGo.xcodeproj/project.pbxproj | 56 +++++++------- DuckDuckGo/Settings.bundle/Root.plist | 2 +- fastlane/metadata/default/release_notes.txt | 1 - 6 files changed, 76 insertions(+), 66 deletions(-) diff --git a/Configuration/Version.xcconfig b/Configuration/Version.xcconfig index e34ef074c3..2f435d72f7 100644 --- a/Configuration/Version.xcconfig +++ b/Configuration/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 7.147.0 +MARKETING_VERSION = 7.148.0 diff --git a/Core/AppPrivacyConfigurationDataProvider.swift b/Core/AppPrivacyConfigurationDataProvider.swift index cf9518d6e3..3d1a36860f 100644 --- a/Core/AppPrivacyConfigurationDataProvider.swift +++ b/Core/AppPrivacyConfigurationDataProvider.swift @@ -23,8 +23,8 @@ import BrowserServicesKit final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"7ad37aaf57f883471cf65c00a4317a45\"" - public static let embeddedDataSHA = "bf1e81c46e53f75c7c90d343886183b99793675f223f5016af998dfe1cdedbae" + public static let embeddedDataETag = "\"a913df909743eadb5bd381c7fddf4902\"" + public static let embeddedDataSHA = "d1fb789ab4def06b6cf359d5e9971c840668ab5307f5690bc1483bb2266a2558" } public var embeddedDataEtag: String { diff --git a/Core/ios-config.json b/Core/ios-config.json index 5d11b1dde6..188f9299e7 100644 --- a/Core/ios-config.json +++ b/Core/ios-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1732540137973, + "version": 1733157661102, "features": { "adAttributionReporting": { "state": "disabled", @@ -69,11 +69,27 @@ }, "hash": "b813ade8472a097ffbd43a3331116fe1" }, - "aiChat": { + "additionalCampaignPixelParams": { "state": "disabled", "exceptions": [], "hash": "c292bb627849854515cebbded288ef5a" }, + "aiChat": { + "state": "enabled", + "exceptions": [], + "features": { + "browsingToolbarShortcut": { + "state": "enabled" + }, + "addressBarShortcut": { + "state": "enabled" + } + }, + "settings": { + "aiChatURL": "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=4" + }, + "hash": "14908e76cd3a8b4919e03003fd201300" + }, "ampLinks": { "exceptions": [ { @@ -735,6 +751,11 @@ }, "hash": "91e54b0d57fbf1cf8668c9a929631432" }, + "blockList": { + "state": "disabled", + "exceptions": [], + "hash": "c292bb627849854515cebbded288ef5a" + }, "bookmarks": { "state": "enabled", "exceptions": [], @@ -5822,6 +5843,15 @@ "state": "disabled", "hash": "728493ef7a1488e4781656d3f9db84aa" }, + "maliciousSiteProtection": { + "state": "internal", + "exceptions": [], + "settings": { + "hashPrefixUpdateFrequency": 20, + "filterSetUpdateFrequency": 720 + }, + "hash": "ca9942379aea7619f878337a2debdee8" + }, "marketplaceAdPostback": { "state": "enabled", "exceptions": [], @@ -5944,33 +5974,9 @@ "hash": "6792064606a5a72c5cd44addb4d40bda" }, "phishingDetection": { - "state": "internal", - "exceptions": [ - { - "domain": "marvel.com" - }, - { - "domain": "sundancecatalog.com" - }, - { - "domain": "noaprints.com" - }, - { - "domain": "flexmls.com" - }, - { - "domain": "instructure.com" - } - ], - "features": { - "allowErrorPage": { - "state": "internal" - }, - "allowPreferencesToggle": { - "state": "internal" - } - }, - "hash": "9a9143022e6cc8976461b337abfa81a1" + "state": "disabled", + "exceptions": [], + "hash": "c292bb627849854515cebbded288ef5a" }, "pluginPointFocusedViewPlugin": { "state": "disabled", @@ -6046,7 +6052,7 @@ "minSupportedVersion": "7.136.0" }, "setAccessTokenCookieForSubscriptionDomains": { - "state": "enabled", + "state": "disabled", "rollout": { "steps": [ { @@ -6054,13 +6060,18 @@ }, { "percent": 50 + }, + { + "percent": 100 } ] - }, - "minSupportedVersion": "7.145.1" + } + }, + "freeTrials": { + "state": "internal" } }, - "hash": "2a9bad4e25e3a161fbbbc324a240c03e" + "hash": "c002a2f7b1299197657574c7328e8f88" }, "privacyProtectionsPopup": { "state": "disabled", diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bf273e30fe..44fd5ea024 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9361,7 +9361,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9398,7 +9398,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9488,7 +9488,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9515,7 +9515,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9661,7 +9661,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9685,7 +9685,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9752,7 +9752,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9786,7 +9786,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9819,7 +9819,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9849,7 +9849,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10238,7 +10238,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10269,7 +10269,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10297,7 +10297,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10330,7 +10330,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10360,7 +10360,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10393,11 +10393,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 6; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10627,7 +10627,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10655,7 +10655,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10687,7 +10687,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10724,7 +10724,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10759,7 +10759,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10794,11 +10794,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 6; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10970,11 +10970,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 6; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -11003,10 +11003,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 6; + DYLIB_CURRENT_VERSION = 0; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/Settings.bundle/Root.plist b/DuckDuckGo/Settings.bundle/Root.plist index 8b585c64f2..c23b94b9bc 100644 --- a/DuckDuckGo/Settings.bundle/Root.plist +++ b/DuckDuckGo/Settings.bundle/Root.plist @@ -6,7 +6,7 @@ DefaultValue - 7.147.0 + 7.148.0 Key version Title diff --git a/fastlane/metadata/default/release_notes.txt b/fastlane/metadata/default/release_notes.txt index 4647430b0f..098fd1666f 100644 --- a/fastlane/metadata/default/release_notes.txt +++ b/fastlane/metadata/default/release_notes.txt @@ -1,2 +1 @@ -- Improved text zoom! You can now set the zoom level of specific domains from the browsing menu. Use the Fire Button to reset zoom levels to the default. - Bug fixes and other improvements. \ No newline at end of file From d2e604d8560dfd54d413780e15bb3bfaea75ca63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 3 Dec 2024 11:01:15 +0100 Subject: [PATCH 5/8] Make NTP Grid layout adaptive for various screen sizes (#3657) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208868059663007/f Tech Design URL: CC: **Description**: Addresses NTP layout issues when it's too wide to fit 4 items. It mostly changes how the spacings and item sizes are being defined. **Steps to test this PR**: 1. Create a bunch of Favorites. 2. Test layout in various environments: iPod touch, iPhone, iPad, iPad with Split View, different orientations. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Orientation Testing**: * [ ] Portrait * [ ] Landscape **Device Testing**: * [ ] iPhone SE (1st Gen) * [ ] iPhone 8 * [ ] iPhone X * [ ] iPhone 14 Pro * [ ] iPad * [ ] iPod touch **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 * [ ] iOS 17 --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- DuckDuckGo/NewTabPageGridView.swift | 19 ++++++++++++++----- DuckDuckGo/SimpleNewTabPageView.swift | 5 +++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/NewTabPageGridView.swift b/DuckDuckGo/NewTabPageGridView.swift index ffcffc0e17..f7e4390815 100644 --- a/DuckDuckGo/NewTabPageGridView.swift +++ b/DuckDuckGo/NewTabPageGridView.swift @@ -32,7 +32,7 @@ struct NewTabPageGridView: View { var body: some View { let columnsCount = NewTabPageGrid.columnsCount(for: horizontalSizeClass, isLandscape: isLandscape, isDynamic: isUsingDynamicSpacing) - LazyVGrid(columns: createColumns(columnsCount), spacing: 24, content: { + LazyVGrid(columns: createColumns(columnsCount), alignment: .center, spacing: 24, content: { content(columnsCount) }) .frame(maxWidth: .infinity) @@ -67,10 +67,17 @@ struct NewTabPageGridView: View { } private func staticColumns(_ count: Int) -> [GridItem] { - return Array(repeating: GridItem(.fixed(NewTabPageGrid.Item.edgeSize), - spacing: NewTabPageGrid.Item.staticSpacing, - alignment: .top), + let isRegularSizeClassOnPad = UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .regular + + let spacing: CGFloat = isRegularSizeClassOnPad ? NewTabPageGrid.Item.staticSpacingPad : NewTabPageGrid.Item.staticSpacing + let maximumSize = NewTabPageGrid.Item.maximumWidth - spacing + let itemSize = GridItem.Size.flexible(minimum: NewTabPageGrid.Item.edgeSize, + // This causes automatic (larger) spacing, when spacing itself is small comparing to parent view width. + maximum: maximumSize) + + return Array(repeating: GridItem(itemSize, spacing: spacing, alignment: .top), count: count) + } private func createColumns(_ count: Int) -> [GridItem] { @@ -120,5 +127,7 @@ private extension NewTabPageGrid { } private extension NewTabPageGrid.Item { - static let staticSpacing = 32.0 + static let staticSpacing = 10.0 + static let staticSpacingPad = 32.0 + static let maximumWidth = 128.0 } diff --git a/DuckDuckGo/SimpleNewTabPageView.swift b/DuckDuckGo/SimpleNewTabPageView.swift index d674e59e32..a228686ef5 100644 --- a/DuckDuckGo/SimpleNewTabPageView.swift +++ b/DuckDuckGo/SimpleNewTabPageView.swift @@ -117,8 +117,7 @@ private extension SimpleNewTabPageView { } private func sectionsViewPadding(in geometry: GeometryProxy) -> CGFloat { - let requiredWidth = NewTabPageGrid.staticGridWidth(for: horizontalSizeClass) + Metrics.regularPadding - return geometry.frame(in: .local).width >= requiredWidth ? Metrics.regularPadding : Metrics.smallPadding + geometry.frame(in: .local).width > Metrics.verySmallScreenWidth ? Metrics.regularPadding : Metrics.smallPadding } } @@ -142,6 +141,8 @@ private struct Metrics { static let messageMaximumWidth: CGFloat = 380 static let messageMaximumWidthPad: CGFloat = 455 + + static let verySmallScreenWidth: CGFloat = 320 } // MARK: - Preview From d3a068a587fb9367fdee91159d67751c05d440d8 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Tue, 3 Dec 2024 10:07:07 +0000 Subject: [PATCH 6/8] support local storage fireproofing (#3612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/392891325557410/1204888209490842/f Tech Design URL: CC: @samsymons, @bwaresiak **Description**: Revert to using default persistence container for website data storage. Also various refactoring to make future singleton slaying quests a bit easier. **Steps to test this PR**: 1. Using an older version of the app (any before this PR should be fine). Open some tabs, sign into to some websites, and fire proof them. Check the fire proofing is working by using the fire button and leave the tabs open to simulate users receiving this update. Then update the app to this branch. 2. Use the fire button and ensure previously signed in sites still work as before. 3. Use the Fireproofing UI in settings to remove fireproofed sites and use the fire button to clear them, ensuring their data has been cleared. 4. Use the Cookies UI in debug to check there's no cookies. 5. Navigate to https://privacy-test-pages.site/features/local-storage.html and start the counter. 6. Use the fire button and navigate back to https://privacy-test-pages.site/features/local-storage.html - previous values should be removed. 7. Fireproof the site and start the counter. Wait for a while, then use the fire button. Navigate back to https://privacy-test-pages.site/features/local-storage.html and the previous values should be set. 8. Remove fireproofing for the site and use the fire button. Navigate to it once more and ensure the values are again unset. 9. Do similar smoke testing using the fire button on the tab switcher 10. Do so similar smoke testing using auto-clear setting on 11. Run the `release_tests/local-storage` UI test and validate it works **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? **Copy Testing**: * [ ] Use of correct apostrophes in new copy, ie `’` rather than `'` **Device Testing**: * [ ] iPhone * [ ] iPad (smoke testing) **OS Testing**: * [ ] iOS 15 * [ ] iOS 16 (option) * [ ] iOS 17 (option) * [ ] iOS 18 --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- .maestro/release_tests/local-storage.yaml | 72 +++++ .maestro/setup_ui_tests.sh | 2 +- Core/CookieStorage.swift | 157 ---------- Core/DataStoreIDManager.swift | 57 ++++ Core/Fireproofing.swift | 19 +- Core/HTTPCookieExtension.swift | 35 +++ Core/MigratableCookieStorage.swift | 86 ++++++ Core/ObservationsDataCleaning.swift | 74 +++++ Core/PixelEvent.swift | 4 +- Core/WKWebViewConfigurationExtension.swift | 39 +-- Core/WebCacheManager.swift | 262 ++++++++--------- Core/WebsiteDataStoreCleaning.swift | 73 +++++ DuckDuckGo.xcodeproj/project.pbxproj | 46 ++- DuckDuckGo/AppDelegate.swift | 12 +- DuckDuckGo/ContentBlockingUpdating.swift | 6 +- DuckDuckGo/Favicons.swift | 2 +- .../FireproofingSettingsViewController.swift | 10 +- .../ImageCacheDebugViewController.swift | 2 +- DuckDuckGo/MainViewController+Segues.swift | 7 +- DuckDuckGo/MainViewController.swift | 11 +- ...otDebugViewController+VanillaBrowser.swift | 2 +- DuckDuckGo/RootDebugViewController.swift | 50 ++-- DuckDuckGo/ScriptSourceProviding.swift | 2 +- DuckDuckGo/SettingsLegacyViewProvider.swift | 35 ++- .../HeadlessWebView.swift | 3 +- .../PrivacyProDataReporting.swift | 2 +- DuckDuckGo/TabManager.swift | 16 +- DuckDuckGo/TabViewController.swift | 15 +- ...ViewControllerLongPressMenuExtension.swift | 4 +- .../FireButtonReferenceTests.swift | 59 +--- DuckDuckGoTests/MockFireproofing.swift | 36 +++ DuckDuckGoTests/MockTabDelegate.swift | 4 +- DuckDuckGoTests/MockWebsiteDataManager.swift | 35 +++ .../OnboardingDaxFavouritesTests.swift | 4 + .../OnboardingNavigationDelegateTests.swift | 2 + .../PrivacyProDataReporterTests.swift | 4 +- WebViewUnitTests/CookieStorageTests.swift | 227 +++------------ .../DataStoreIDManagerTests.swift | 51 ++++ .../DataStoreIdManagerTests.swift | 70 ----- .../UserDefaultsFireproofingTests.swift | 20 +- WebViewUnitTests/WebCacheManagerTests.swift | 271 +++++++++--------- 41 files changed, 1016 insertions(+), 872 deletions(-) create mode 100644 .maestro/release_tests/local-storage.yaml delete mode 100644 Core/CookieStorage.swift create mode 100644 Core/DataStoreIDManager.swift create mode 100644 Core/HTTPCookieExtension.swift create mode 100644 Core/MigratableCookieStorage.swift create mode 100644 Core/ObservationsDataCleaning.swift create mode 100644 Core/WebsiteDataStoreCleaning.swift create mode 100644 DuckDuckGoTests/MockFireproofing.swift create mode 100644 DuckDuckGoTests/MockWebsiteDataManager.swift create mode 100644 WebViewUnitTests/DataStoreIDManagerTests.swift delete mode 100644 WebViewUnitTests/DataStoreIdManagerTests.swift diff --git a/.maestro/release_tests/local-storage.yaml b/.maestro/release_tests/local-storage.yaml new file mode 100644 index 0000000000..715c0d2c21 --- /dev/null +++ b/.maestro/release_tests/local-storage.yaml @@ -0,0 +1,72 @@ +# local-storage.yaml +appId: com.duckduckgo.mobile.ios +tags: + - release + +--- + +# Set up +- runFlow: + file: ../shared/setup.yaml + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter + +# Add a cookie +- assertVisible: "Storage Counter: undefined" +- assertVisible: "Cookie Counter:" +- assertNotVisible: "Cookie Counter: 1" +- assertNotVisible: "Storage Counter: 1" +- assertVisible: "Manual Increment" +- tapOn: "Manual Increment" +- tapOn: "Manual Increment" +- assertVisible: "Cookie Counter: 2" +- assertVisible: "Storage Counter: 2" + +# Fireproofing +- tapOn: "Browsing menu" +- tapOn: "Fireproof This Site" +- tapOn: "Fireproof" + +# Fire button +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" + +- assertNotVisible: "https://privacy-test-pages.site/features/local-storage.html" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" + +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter +- assertVisible: "Storage Counter: 2" +- assertVisible: "Cookie Counter: 2" + +# Remove Fireproofing +- tapOn: "Browsing menu" +- tapOn: "Remove Fireproofing" + +# Fire button +- tapOn: "Close Tabs and Clear Data" +- tapOn: "Close Tabs and Clear Data" + +- assertNotVisible: "https://privacy-test-pages.site/features/local-storage.html" + +# Load Site +- assertVisible: + id: "searchEntry" +- tapOn: + id: "searchEntry" + +- inputText: "https://privacy-test-pages.site/features/local-storage.html" +- pressKey: Enter +- assertVisible: "Storage Counter: undefined" +- assertVisible: "Cookie Counter:" diff --git a/.maestro/setup_ui_tests.sh b/.maestro/setup_ui_tests.sh index 2e2c0d3014..8da1584978 100755 --- a/.maestro/setup_ui_tests.sh +++ b/.maestro/setup_ui_tests.sh @@ -15,7 +15,7 @@ target_os="iOS-18-1" check_maestro() { local command_name="maestro" - local known_version="1.39.1" + local known_version="1.39.2" if command -v $command_name > /dev/null 2>&1; then local version_output=$($command_name -v 2>&1 | tail -n 1) diff --git a/Core/CookieStorage.swift b/Core/CookieStorage.swift deleted file mode 100644 index 978d69197a..0000000000 --- a/Core/CookieStorage.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// CookieStorage.swift -// DuckDuckGo -// -// Copyright © 2018 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Common -import Foundation -import os.log - -/// Class for persisting cookies for fire proofed sites to work around a WKWebView / DataStore bug which does not let data get persisted until the webview has loaded. -/// -/// Privacy information: -/// * The Fire Button does not delete the user's DuckDuckGo search settings, which are saved as cookies. Removing these cookies would reset them and have undesired consequences, i.e. changing the theme, default language, etc. -/// * The Fire Button also does not delete temporary cookies associated with 'surveys.duckduckgo.com'. When we launch surveys to help us understand issues that impact users over time, we use this cookie to temporarily store anonymous survey answers, before deleting the cookie. Cookie storage duration is communicated to users before they opt to submit survey answers. -/// * These cookies are not stored in a personally identifiable way. For example, the large size setting is stored as 's=l.' More info in https://duckduckgo.com/privacy -public class CookieStorage { - - struct Keys { - static let allowedCookies = "com.duckduckgo.allowedCookies" - static let consumed = "com.duckduckgo.consumedCookies" - } - - private var userDefaults: UserDefaults - - var isConsumed: Bool { - get { - return userDefaults.bool(forKey: Keys.consumed, defaultValue: false) - } - set { - userDefaults.set(newValue, forKey: Keys.consumed) - } - } - - /// Use the `updateCookies` function rather than the setter which is only visible for testing. - var cookies: [HTTPCookie] { - get { - var storedCookies = [HTTPCookie]() - if let cookies = userDefaults.object(forKey: Keys.allowedCookies) as? [[String: Any?]] { - for cookieData in cookies { - var properties = [HTTPCookiePropertyKey: Any]() - cookieData.forEach({ - properties[HTTPCookiePropertyKey(rawValue: $0.key)] = $0.value - }) - - if let cookie = HTTPCookie(properties: properties) { - Logger.general.debug("read cookie \(cookie.domain) \(cookie.name) \(cookie.value)") - storedCookies.append(cookie) - } - } - } - - return storedCookies - } - - set { - var cookies = [[String: Any?]]() - newValue.forEach { cookie in - var mappedCookie = [String: Any?]() - cookie.properties?.forEach { - mappedCookie[$0.key.rawValue] = $0.value - } - cookies.append(mappedCookie) - } - userDefaults.setValue(cookies, forKey: Keys.allowedCookies) - } - - } - - public init(userDefaults: UserDefaults = UserDefaults.app) { - self.userDefaults = userDefaults - } - - /// Used when debugging (e.g. on the simulator). - enum CookieDomainsOnUpdateDiagnostic { - case empty - case match - case missing - case different - case notConsumed - } - - /// Update ALL cookies. The absence of cookie domains here indicateds they have been removed by the website, so be sure to call this with all cookies that might need to be persisted even if those websites have not been visited yet. - @discardableResult - func updateCookies(_ cookies: [HTTPCookie], preservingFireproofedDomains fireproofing: Fireproofing) -> CookieDomainsOnUpdateDiagnostic { - guard isConsumed else { return .notConsumed } - - isConsumed = false - - let persisted = self.cookies - - func cookiesByDomain(_ cookies: [HTTPCookie]) -> [String: [HTTPCookie]] { - var byDomain = [String: [HTTPCookie]]() - cookies.forEach { cookie in - var cookies = byDomain[cookie.domain, default: []] - cookies.append(cookie) - byDomain[cookie.domain] = cookies - } - return byDomain - } - - let updatedCookiesByDomain = cookiesByDomain(cookies) - var persistedCookiesByDomain = cookiesByDomain(persisted) - - // Do the diagnostics before the dicts get changed. - let diagnosticResult = evaluateDomains( - updatedDomains: updatedCookiesByDomain.keys.sorted(), - persistedDomains: persistedCookiesByDomain.keys.sorted() - ) - - let cookieDomains = Set(updatedCookiesByDomain.keys.map { $0 } + persistedCookiesByDomain.keys.map { $0 }) - - cookieDomains.forEach { - persistedCookiesByDomain[$0] = updatedCookiesByDomain[$0] - } - - persistedCookiesByDomain.keys.forEach { - guard !URL.isDuckDuckGo(domain: $0) else { return } // DDG cookies are for SERP settings only - - if !fireproofing.isAllowed(cookieDomain: $0) { - persistedCookiesByDomain.removeValue(forKey: $0) - } - } - - let now = Date() - self.cookies = persistedCookiesByDomain.map { $0.value }.joined().compactMap { $0 } - .filter { $0.expiresDate == nil || $0.expiresDate! > now } - - return diagnosticResult - } - - private func evaluateDomains(updatedDomains: [String], persistedDomains: [String]) -> CookieDomainsOnUpdateDiagnostic { - if persistedDomains.isEmpty { - return .empty - } else if updatedDomains.count < persistedDomains.count { - return .missing - } else if updatedDomains == persistedDomains { - return .match - } else { - return .different - } - } - -} diff --git a/Core/DataStoreIDManager.swift b/Core/DataStoreIDManager.swift new file mode 100644 index 0000000000..3c9709c64b --- /dev/null +++ b/Core/DataStoreIDManager.swift @@ -0,0 +1,57 @@ +// +// DataStoreIDManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import WebKit +import Persistence + +/// Supports an existing ID set in previous versions of the app, but moving forward does not allocate an ID. We have gone back to using the default +/// peristence for the webview storage so that we can fireproof types that don't have an API for accessing their data. (e.g. localStorage) +public protocol DataStoreIDManaging { + + var currentID: UUID? { get } + + func invalidateCurrentID() +} + +public class DataStoreIDManager: DataStoreIDManaging { + + enum Constants: String { + case currentWebContainerID = "com.duckduckgo.ios.webcontainer.id" + } + + public static let shared = DataStoreIDManager() + + private let store: KeyValueStoring + init(store: KeyValueStoring = UserDefaults.app) { + self.store = store + } + + public var currentID: UUID? { + guard let uuidString = store.object(forKey: Constants.currentWebContainerID.rawValue) as? String else { + return nil + } + return UUID(uuidString: uuidString) + } + + public func invalidateCurrentID() { + store.removeObject(forKey: Constants.currentWebContainerID.rawValue) + } + +} diff --git a/Core/Fireproofing.swift b/Core/Fireproofing.swift index 166c04c562..d8f515bbb9 100644 --- a/Core/Fireproofing.swift +++ b/Core/Fireproofing.swift @@ -18,6 +18,7 @@ // import Foundation +import Subscription public protocol Fireproofing { @@ -35,7 +36,9 @@ public protocol Fireproofing { // This class is not final because we override allowed domains in WebCacheManagerTests public class UserDefaultsFireproofing: Fireproofing { - public static let shared: Fireproofing = UserDefaultsFireproofing() + /// This is only here because there are some places that don't support injection at this time. DO NOT USE IT. + /// If you find you really need to use it, ping Apple Devs channel first. + public static let xshared: Fireproofing = UserDefaultsFireproofing() public struct Notifications { public static let loginDetectionStateChanged = Foundation.Notification.Name("com.duckduckgo.ios.PreserveLogins.loginDetectionStateChanged") @@ -51,15 +54,19 @@ public class UserDefaultsFireproofing: Fireproofing { } } + private var allowedDomainsIncludingDuckDuckGo: [String] { + allowedDomains + [ + URL.ddg.host ?? "", + SubscriptionCookieManager.cookieDomain + ] + } + public func addToAllowed(domain: String) { allowedDomains += [domain] } public func isAllowed(cookieDomain: String) -> Bool { - - return allowedDomains.contains(where: { $0 == cookieDomain - || ".\($0)" == cookieDomain - || (cookieDomain.hasPrefix(".") && $0.hasSuffix(cookieDomain)) }) + return allowedDomainsIncludingDuckDuckGo.contains(where: { HTTPCookie.cookieDomain(cookieDomain, matchesTestDomain: $0) }) } public func remove(domain: String) { @@ -71,7 +78,7 @@ public class UserDefaultsFireproofing: Fireproofing { } public func isAllowed(fireproofDomain domain: String) -> Bool { - return allowedDomains.contains(domain) + return allowedDomainsIncludingDuckDuckGo.contains(domain) } } diff --git a/Core/HTTPCookieExtension.swift b/Core/HTTPCookieExtension.swift new file mode 100644 index 0000000000..5475c54b43 --- /dev/null +++ b/Core/HTTPCookieExtension.swift @@ -0,0 +1,35 @@ +// +// HTTPCookieExtension.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import WebKit + +extension HTTPCookie { + + /// Checks that the `cookieDomain` (provided by a cookie) matches a given domain. e.g. + /// * cookie domain example.com would match a domain called example.com + /// * cookie domain `.example.com` would also match a domain called example.com and also any subdomain, e.g. `docs.example.com` + /// + /// See `UserDefaultsFireproofingTests` for more examples. + static func cookieDomain(_ cookieDomain: String, matchesTestDomain testDomain: String) -> Bool { + return testDomain == cookieDomain + || ".\(testDomain)" == cookieDomain + || (cookieDomain.hasPrefix(".") && testDomain.hasSuffix(cookieDomain)) + } + +} diff --git a/Core/MigratableCookieStorage.swift b/Core/MigratableCookieStorage.swift new file mode 100644 index 0000000000..b850f4173e --- /dev/null +++ b/Core/MigratableCookieStorage.swift @@ -0,0 +1,86 @@ +// +// MigratableCookieStorage.swift +// DuckDuckGo +// +// Copyright © 2018 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import Foundation +import os.log +import Persistence + +/// This class was used to store cookies when moving between persistences containers, but now we are clearing the default container, this class is only used for storing cookies that existed previously and need to be migrated. +public class MigratableCookieStorage { + + struct Keys { + static let allowedCookies = "com.duckduckgo.allowedCookies" + static let consumed = "com.duckduckgo.consumedCookies" + } + + private var keyValueStore: KeyValueStoring + + var isConsumed: Bool { + // The default is now true because if this key does not exist then there are no + // cookies to consume as they have been migrated. + // Nothing sets this to false explicitly now. If the stored value is false then there + // are cookies from a previous version of the app which still need to be migrated. This + // could happen if the user hit the fire button and then an update happened + // before they browsed again. + return (keyValueStore.object(forKey: Keys.consumed) as? Bool) ?? true + } + + var cookies: [HTTPCookie] { + var storedCookies = [HTTPCookie]() + if let cookies = keyValueStore.object(forKey: Keys.allowedCookies) as? [[String: Any?]] { + for cookieData in cookies { + var properties = [HTTPCookiePropertyKey: Any]() + cookieData.forEach({ + properties[HTTPCookiePropertyKey(rawValue: $0.key)] = $0.value + }) + + if let cookie = HTTPCookie(properties: properties) { + Logger.general.debug("read cookie \(cookie.domain) \(cookie.name) \(cookie.value)") + storedCookies.append(cookie) + } + } + } + return storedCookies + } + + public init(store: KeyValueStoring = UserDefaults.app) { + self.keyValueStore = store + } + + /// Called after consuming cookies but migration has not happened. + /// This can happen in the following flow + /// * User presses fire button on container based app and then backgrounds it/switches to another app + /// * Installs update to this version of the app without using the browser + /// * Starts browsing + func setConsumed() { + resetStore() + } + + /// Called when migration is completed to clean up + func migrationComplete() { + resetStore() + } + + private func resetStore() { + keyValueStore.removeObject(forKey: Keys.allowedCookies) + keyValueStore.removeObject(forKey: Keys.consumed) + } + +} diff --git a/Core/ObservationsDataCleaning.swift b/Core/ObservationsDataCleaning.swift new file mode 100644 index 0000000000..a4cdb0915d --- /dev/null +++ b/Core/ObservationsDataCleaning.swift @@ -0,0 +1,74 @@ +// +// ObservationsDataCleaning.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import GRDB +import os.log + +public protocol ObservationsDataCleaning { + + func removeObservationsData() async + +} + +/// Used by data clearing. Has no unit tests just now because getting the observation data is flakey, so we inject this for testing. +public class DefaultObservationsDataCleaner: ObservationsDataCleaning { + + public init() { } + + public func removeObservationsData() async { + if let pool = getValidDatabasePool() { + removeObservationsData(from: pool) + } else { + Logger.general.debug("Could not find valid pool to clear observations data") + } + } + + func getValidDatabasePool() -> DatabasePool? { + let bundleID = Bundle.main.bundleIdentifier ?? "" + + let databaseURLs = [ + FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] + .appendingPathComponent("WebKit/WebsiteData/ResourceLoadStatistics/observations.db"), + FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] + .appendingPathComponent("WebKit/\(bundleID)/WebsiteData/ResourceLoadStatistics/observations.db") + ] + + guard let validURL = databaseURLs.first(where: { FileManager.default.fileExists(atPath: $0.path) }) else { return nil } + + return try? DatabasePool(path: validURL.absoluteString) + } + + private func removeObservationsData(from pool: DatabasePool) { + do { + try pool.write { database in + try database.execute(sql: "PRAGMA wal_checkpoint(TRUNCATE);") + + let tables = try String.fetchAll(database, sql: "SELECT name FROM sqlite_master WHERE type='table'") + + for table in tables { + try database.execute(sql: "DELETE FROM \(table)") + } + } + } catch { + Pixel.fire(pixel: .debugCannotClearObservationsDatabase, error: error) + } + } + +} diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 5fc44638c9..d45a37bd61 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -553,7 +553,7 @@ extension Pixel { case cookieDeletionTime(_ time: BucketAggregation) case cookieDeletionLeftovers - case legacyDataClearingTime(_ time: BucketAggregation) + case clearDataInDefaultPersistence(_ time: BucketAggregation) case webkitWarmupStart(appState: String) case webkitWarmupFinished(appState: String) @@ -1416,7 +1416,7 @@ extension Pixel.Event { case .cookieDeletionTime(let aggregation): return "m_debug_cookie-clearing-time-\(aggregation)" - case .legacyDataClearingTime(let aggregation): + case .clearDataInDefaultPersistence(let aggregation): return "m_debug_legacy-data-clearing-time-\(aggregation)" case .cookieDeletionLeftovers: return "m_cookie_deletion_leftovers" diff --git a/Core/WKWebViewConfigurationExtension.swift b/Core/WKWebViewConfigurationExtension.swift index 18de2c2dad..be557a68be 100644 --- a/Core/WKWebViewConfigurationExtension.swift +++ b/Core/WKWebViewConfigurationExtension.swift @@ -23,11 +23,11 @@ import Persistence extension WKWebViewConfiguration { @MainActor - public static func persistent(idManager: DataStoreIdManaging = DataStoreIdManager.shared) -> WKWebViewConfiguration { + public static func persistent(idManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WKWebViewConfiguration { let config = configuration(persistsData: true) - // Only use a container if there's an id which will be allocated next time the fire button is used. - if #available(iOS 17, *), let containerId = idManager.currentId { + // Only use a container if there's an id. We no longer allocate ids so this should not happen. + if #available(iOS 17, *), let containerId = idManager.currentID { config.websiteDataStore = WKWebsiteDataStore(forIdentifier: containerId) } return config @@ -56,36 +56,3 @@ extension WKWebViewConfiguration { } } - -public protocol DataStoreIdManaging { - - var currentId: UUID? { get } - - func invalidateCurrentIdAndAllocateNew() -} - -public class DataStoreIdManager: DataStoreIdManaging { - - enum Constants: String { - case currentWebContainerId = "com.duckduckgo.ios.webcontainer.id" - } - - public static let shared = DataStoreIdManager() - - private let store: KeyValueStoring - init(store: KeyValueStoring = UserDefaults.app) { - self.store = store - } - - public var currentId: UUID? { - guard let uuidString = store.object(forKey: Constants.currentWebContainerId.rawValue) as? String else { - return nil - } - return UUID(uuidString: uuidString) - } - - public func invalidateCurrentIdAndAllocateNew() { - store.set(UUID().uuidString, forKey: Constants.currentWebContainerId.rawValue) - } - -} diff --git a/Core/WebCacheManager.swift b/Core/WebCacheManager.swift index 13ce952ba8..7e1af854dd 100644 --- a/Core/WebCacheManager.swift +++ b/Core/WebCacheManager.swift @@ -19,13 +19,12 @@ import Common import WebKit -import GRDB import os.log extension WKWebsiteDataStore { - public static func current(dataStoreIdManager: DataStoreIdManaging = DataStoreIdManager.shared) -> WKWebsiteDataStore { - if #available(iOS 17, *), let id = dataStoreIdManager.currentId { + public static func current(dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WKWebsiteDataStore { + if #available(iOS 17, *), let id = dataStoreIDManager.currentID { return WKWebsiteDataStore(forIdentifier: id) } else { return WKWebsiteDataStore.default() @@ -34,26 +33,75 @@ extension WKWebsiteDataStore { } -extension HTTPCookie { +public protocol WebsiteDataManaging { - func matchesDomain(_ domain: String) -> Bool { - return self.domain == domain || (self.domain.hasPrefix(".") && domain.hasSuffix(self.domain)) - } + func removeCookies(forDomains domains: [String], fromDataStore: WKWebsiteDataStore) async + func consumeCookies(into httpCookieStore: WKHTTPCookieStore) async + func clear(dataStore: WKWebsiteDataStore) async } @MainActor -public class WebCacheManager { +public class WebCacheManager: WebsiteDataManaging { + + static let safelyRemovableWebsiteDataTypes: Set = { + var types = WKWebsiteDataStore.allWebsiteDataTypes() + + types.insert("_WKWebsiteDataTypeMediaKeys") + types.insert("_WKWebsiteDataTypeHSTSCache") + types.insert("_WKWebsiteDataTypeSearchFieldRecentSearches") + types.insert("_WKWebsiteDataTypeResourceLoadStatistics") + types.insert("_WKWebsiteDataTypeCredentials") + types.insert("_WKWebsiteDataTypeAdClickAttributions") + types.insert("_WKWebsiteDataTypePrivateClickMeasurements") + types.insert("_WKWebsiteDataTypeAlternativeServices") - public static var shared = WebCacheManager() + fireproofableDataTypes.forEach { + types.remove($0) + } - private init() { } + return types + }() + + static let fireproofableDataTypes: Set = { + Set([ + WKWebsiteDataTypeLocalStorage, + WKWebsiteDataTypeIndexedDBDatabases, + WKWebsiteDataTypeCookies, + ]) + }() + + static let fireproofableDataTypesExceptCookies: Set = { + var dataTypes = fireproofableDataTypes + dataTypes.remove(WKWebsiteDataTypeCookies) + return dataTypes + }() + + let cookieStorage: MigratableCookieStorage + let fireproofing: Fireproofing + let dataStoreIDManager: DataStoreIDManaging + let dataStoreCleaner: WebsiteDataStoreCleaning + let observationsCleaner: ObservationsDataCleaning + + public init(cookieStorage: MigratableCookieStorage, + fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging, + dataStoreCleaner: WebsiteDataStoreCleaning = DefaultWebsiteDataStoreCleaner(), + observationsCleaner: ObservationsDataCleaning = DefaultObservationsDataCleaner()) { + self.cookieStorage = cookieStorage + self.fireproofing = fireproofing + self.dataStoreIDManager = dataStoreIDManager + self.dataStoreCleaner = dataStoreCleaner + self.observationsCleaner = observationsCleaner + } - /// We save cookies from the current container rather than copying them to a new container because - /// the container only persists cookies to disk when the web view is used. If the user presses the fire button - /// twice then the fire proofed cookies will be lost and the user will be logged out any sites they're logged in to. - public func consumeCookies(cookieStorage: CookieStorage = CookieStorage(), - httpCookieStore: WKHTTPCookieStore) async { + /// The previous version saved cookies externally to the data so we can move them between containers. We now use + /// the default persistence so this only needs to happen once when the fire button is pressed. + /// + /// The migration code removes the key that is used to check for the isConsumed flag so will only be + /// true if the data needs to be migrated. + public func consumeCookies(into httpCookieStore: WKHTTPCookieStore) async { + // This can only be true if the data has not yet been migrated. guard !cookieStorage.isConsumed else { return } let cookies = cookieStorage.cookies @@ -62,177 +110,103 @@ public class WebCacheManager { consumedCookiesCount += 1 await httpCookieStore.setCookie(cookie) } - cookieStorage.isConsumed = true + + cookieStorage.setConsumed() } public func removeCookies(forDomains domains: [String], - dataStore: WKWebsiteDataStore) async { + fromDataStore dataStore: WKWebsiteDataStore) async { let startTime = CACurrentMediaTime() let cookieStore = dataStore.httpCookieStore let cookies = await cookieStore.allCookies() - for cookie in cookies where domains.contains(where: { cookie.matchesDomain($0) }) { + for cookie in cookies where domains.contains(where: { HTTPCookie.cookieDomain(cookie.domain, matchesTestDomain: $0) }) { await cookieStore.deleteCookie(cookie) } let totalTime = CACurrentMediaTime() - startTime Pixel.fire(pixel: .cookieDeletionTime(.init(number: totalTime))) } - public func clear(cookieStorage: CookieStorage = CookieStorage(), - fireproofing: Fireproofing = UserDefaultsFireproofing.shared, - dataStoreIdManager: DataStoreIdManaging = DataStoreIdManager.shared) async { + public func clear(dataStore: WKWebsiteDataStore) async { - var cookiesToUpdate = [HTTPCookie]() - var leftoverContainerIDs = [UUID]() - if #available(iOS 17, *) { - let result = await containerBasedClearing(storeIdManager: dataStoreIdManager) - cookiesToUpdate += result.cookies - leftoverContainerIDs = result.leftoverContainerIDs - } + let count = await dataStoreCleaner.countContainers() + await performMigrationIfNeeded(dataStoreIDManager: dataStoreIDManager, cookieStorage: cookieStorage, destinationStore: dataStore) + await clearData(inDataStore: dataStore, withFireproofing: fireproofing) + await dataStoreCleaner.removeAllContainersAfterDelay(previousCount: count) - // Perform legacy clearing to migrate to new container - cookiesToUpdate += await legacyDataClearing() ?? [] - - cookieStorage.updateCookies(cookiesToUpdate, preservingFireproofedDomains: fireproofing) - - // Attempt to clean up leftover stores again after a delay - // This should not be a problem as these containers are not supposed to be used anymore. - // If this fails, we are going to still clean them next time as WebKit keeps track of all stores for us. - if #available(iOS 17, *), !leftoverContainerIDs.isEmpty { - Task { - try? await Task.sleep(for: .seconds(3)) - for uuid in leftoverContainerIDs { - try? await WKWebsiteDataStore.remove(forIdentifier: uuid) - } - } - } } } extension WebCacheManager { - @available(iOS 17, *) - private func checkForLeftBehindDataStores(previousLeftOversCount: Int) async { - let params = [ - "left_overs_count": "\(previousLeftOversCount)" - ] - - let ids = await WKWebsiteDataStore.allDataStoreIdentifiers - if ids.count > 1 { - Pixel.fire(pixel: .debugWebsiteDataStoresNotClearedMultiple, withAdditionalParameters: params) - } else if ids.count > 0 { - Pixel.fire(pixel: .debugWebsiteDataStoresNotClearedOne, withAdditionalParameters: params) - } else if previousLeftOversCount > 0 { - Pixel.fire(pixel: .debugWebsiteDataStoresCleared, withAdditionalParameters: params) - } - } - - @available(iOS 17, *) - private func containerBasedClearing(storeIdManager: DataStoreIdManaging) async -> (cookies: [HTTPCookie], - leftoverContainerIDs: [UUID]) { - guard let containerId = storeIdManager.currentId else { - storeIdManager.invalidateCurrentIdAndAllocateNew() - return ([], []) - } - storeIdManager.invalidateCurrentIdAndAllocateNew() - - var leftoverContainerIDs = [UUID]() - - var dataStore: WKWebsiteDataStore? = WKWebsiteDataStore(forIdentifier: containerId) - let cookies = await dataStore?.httpCookieStore.allCookies() ?? [] - dataStore = nil + private func performMigrationIfNeeded(dataStoreIDManager: DataStoreIDManaging, + cookieStorage: MigratableCookieStorage, + destinationStore: WKWebsiteDataStore) async { - var uuids = await WKWebsiteDataStore.allDataStoreIdentifiers + // Check version here rather than on function so that we don't need complicated logic related to verison in the calling function. + // Also, migration will not be needed if we are on a version lower than this. + guard #available(iOS 17, *) else { return } - // There may be a timing issue related to fetching current container, so append previous UUID as a precaution - if uuids.firstIndex(of: containerId) == nil { - uuids.append(containerId) - } + // If there's no id, then migration has been done or isn't needed + guard dataStoreIDManager.currentID != nil else { return } - if let newContainerID = storeIdManager.currentId, - let newIdIndex = uuids.firstIndex(of: newContainerID) { - assertionFailure("Attempted to cleanup current Data Store") - uuids.remove(at: newIdIndex) - } + // Get all cookies, we'll clean them later to keep all that logic in the same place + let cookies = cookieStorage.cookies - let previousLeftOversCount = max(0, uuids.count - 1) // -1 because one store is expected to be cleared - for uuid in uuids { - do { - try await WKWebsiteDataStore.remove(forIdentifier: uuid) - } catch { - leftoverContainerIDs.append(uuid) - } + // The returned cookies should be kept so move them to the data store + for cookie in cookies { + await destinationStore.httpCookieStore.setCookie(cookie) } - await checkForLeftBehindDataStores(previousLeftOversCount: previousLeftOversCount) - return (cookies, leftoverContainerIDs) + cookieStorage.migrationComplete() + dataStoreIDManager.invalidateCurrentID() } - private func legacyDataClearing() async -> [HTTPCookie]? { + private func removeContainersIfNeeded(previousCount: Int) async { + await dataStoreCleaner.removeAllContainersAfterDelay(previousCount: previousCount) + } - let dataStore = WKWebsiteDataStore.default() + private func clearData(inDataStore dataStore: WKWebsiteDataStore, withFireproofing fireproofing: Fireproofing) async { let startTime = CACurrentMediaTime() - let cookies = await dataStore.httpCookieStore.allCookies() - var types = WKWebsiteDataStore.allWebsiteDataTypes() - types.insert("_WKWebsiteDataTypeMediaKeys") - types.insert("_WKWebsiteDataTypeHSTSCache") - types.insert("_WKWebsiteDataTypeSearchFieldRecentSearches") - types.insert("_WKWebsiteDataTypeResourceLoadStatistics") - types.insert("_WKWebsiteDataTypeCredentials") - types.insert("_WKWebsiteDataTypeAdClickAttributions") - types.insert("_WKWebsiteDataTypePrivateClickMeasurements") - types.insert("_WKWebsiteDataTypeAlternativeServices") - - await dataStore.removeData(ofTypes: types, modifiedSince: .distantPast) + await clearDataForSafelyRemovableDataTypes(fromStore: dataStore) + await clearFireproofableDataForNonFireproofDomains(fromStore: dataStore, usingFireproofing: fireproofing) + await clearCookiesForNonFireproofedDomains(fromStore: dataStore, usingFireproofing: fireproofing) + await observationsCleaner.removeObservationsData() - self.removeObservationsData() let totalTime = CACurrentMediaTime() - startTime - Pixel.fire(pixel: .legacyDataClearingTime(.init(number: totalTime))) + Pixel.fire(pixel: .clearDataInDefaultPersistence(.init(number: totalTime))) + } - return cookies + @MainActor + private func clearDataForSafelyRemovableDataTypes(fromStore dataStore: WKWebsiteDataStore) async { + await dataStore.removeData(ofTypes: Self.safelyRemovableWebsiteDataTypes, modifiedSince: Date.distantPast) } - private func removeObservationsData() { - if let pool = getValidDatabasePool() { - removeObservationsData(from: pool) - } else { - Logger.general.debug("Could not find valid pool to clear observations data") + @MainActor + private func clearFireproofableDataForNonFireproofDomains(fromStore dataStore: WKWebsiteDataStore, usingFireproofing fireproofing: Fireproofing) async { + let allRecords = await dataStore.dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) + let removableRecords = allRecords.filter { record in + !fireproofing.isAllowed(fireproofDomain: record.displayName) } - } - func getValidDatabasePool() -> DatabasePool? { - let bundleID = Bundle.main.bundleIdentifier ?? "" + var fireproofableTypesExceptCookies = Self.fireproofableDataTypesExceptCookies + fireproofableTypesExceptCookies.remove(WKWebsiteDataTypeCookies) + await dataStore.removeData(ofTypes: fireproofableTypesExceptCookies, for: removableRecords) + } - let databaseURLs = [ - FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] - .appendingPathComponent("WebKit/WebsiteData/ResourceLoadStatistics/observations.db"), - FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0] - .appendingPathComponent("WebKit/\(bundleID)/WebsiteData/ResourceLoadStatistics/observations.db") - ] + @MainActor + private func clearCookiesForNonFireproofedDomains(fromStore dataStore: WKWebsiteDataStore, usingFireproofing fireproofing: Fireproofing) async { + let cookieStore = dataStore.httpCookieStore + let cookies = await cookieStore.allCookies() - guard let validURL = databaseURLs.first(where: { FileManager.default.fileExists(atPath: $0.path) }), - let pool = try? DatabasePool(path: validURL.absoluteString) else { - return nil + let cookiesToRemove = cookies.filter { cookie in + !fireproofing.isAllowed(cookieDomain: cookie.domain) } - return pool + for cookie in cookiesToRemove { + await cookieStore.deleteCookie(cookie) + } } - private func removeObservationsData(from pool: DatabasePool) { - do { - try pool.write { database in - try database.execute(sql: "PRAGMA wal_checkpoint(TRUNCATE);") - - let tables = try String.fetchAll(database, sql: "SELECT name FROM sqlite_master WHERE type='table'") - - for table in tables { - try database.execute(sql: "DELETE FROM \(table)") - } - } - } catch { - Pixel.fire(pixel: .debugCannotClearObservationsDatabase, error: error) - } - } - } diff --git a/Core/WebsiteDataStoreCleaning.swift b/Core/WebsiteDataStoreCleaning.swift new file mode 100644 index 0000000000..d2b3d472a9 --- /dev/null +++ b/Core/WebsiteDataStoreCleaning.swift @@ -0,0 +1,73 @@ +// +// WebsiteDataStoreCleaning.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import WebKit + +public protocol WebsiteDataStoreCleaning { + + func countContainers() async -> Int + func removeAllContainersAfterDelay(previousCount: Int) async + +} + +public class DefaultWebsiteDataStoreCleaner: WebsiteDataStoreCleaning { + + public init() { } + + @MainActor + public func countContainers() async -> Int { + guard #available(iOS 17, *) else { return 0 } + return await WKWebsiteDataStore.allDataStoreIdentifiers.count + } + + @MainActor + public func removeAllContainersAfterDelay(previousCount: Int) async { + guard #available(iOS 17, *) else { return } + + // Attempt to clean up all previous stores, but wait for a few seconds. + // If this fails, we are going to still clean them next time as WebKit keeps track of all stores for us. + Task { + try? await Task.sleep(interval: 3.0) + for uuid in await WKWebsiteDataStore.allDataStoreIdentifiers { + try? await WKWebsiteDataStore.remove(forIdentifier: uuid) + } + + await checkForLeftBehindDataStores(previousLeftOversCount: previousCount) + } + } + + @MainActor + private func checkForLeftBehindDataStores(previousLeftOversCount: Int) async { + guard #available(iOS 17, *) else { return } + + let params = [ + "left_overs_count": "\(previousLeftOversCount)" + ] + + let ids = await WKWebsiteDataStore.allDataStoreIdentifiers + if ids.count > 1 { + Pixel.fire(pixel: .debugWebsiteDataStoresNotClearedMultiple, withAdditionalParameters: params) + } else if ids.count > 0 { + Pixel.fire(pixel: .debugWebsiteDataStoresNotClearedOne, withAdditionalParameters: params) + } else if previousLeftOversCount > 0 { + Pixel.fire(pixel: .debugWebsiteDataStoresCleared, withAdditionalParameters: params) + } + } + +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 44fd5ea024..3e7dec040f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -498,6 +498,7 @@ 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8563A03B1F9288D600F04442 /* BrowserChromeManager.swift */; }; 8565A34B1FC8D96B00239327 /* LaunchTabNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */; }; 8565A34D1FC8DFE400239327 /* LaunchTabNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */; }; + 8567E29D2CF77A700035926F /* MockFireproofing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C503FC2CF0E7B10075DF6F /* MockFireproofing.swift */; }; 857229882BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857229872BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift */; }; 8572298A2BBEF0C800E2E802 /* AppRatingPrompt_v1 in Resources */ = {isa = PBXBuildFile; fileRef = 857229892BBEF0C800E2E802 /* AppRatingPrompt_v1 */; }; 8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8577A1C4255D2C0D00D43FCD /* HitTestingToolbar.swift */; }; @@ -540,9 +541,11 @@ 859DB8182CE6263C001F7210 /* TextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB8122CE6263C001F7210 /* TextZoomCoordinator.swift */; }; 859DB81C2CE6268C001F7210 /* MockTextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB81A2CE6268C001F7210 /* MockTextZoomCoordinator.swift */; }; 859DB81E2CE62766001F7210 /* TextZoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859DB81B2CE6268C001F7210 /* TextZoomTests.swift */; }; - 85A1B3B220C6CD9900C18F15 /* CookieStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A1B3B120C6CD9900C18F15 /* CookieStorage.swift */; }; + 85A1B3B220C6CD9900C18F15 /* MigratableCookieStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A1B3B120C6CD9900C18F15 /* MigratableCookieStorage.swift */; }; 85A313972028E78A00327D00 /* release_notes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 85A313962028E78A00327D00 /* release_notes.txt */; }; 85A9C37920E0E00C00073340 /* HomeRow.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85A9C37820E0E00C00073340 /* HomeRow.xcassets */; }; + 85AB84C12CF5DDB4007E679F /* WebsiteDataStoreCleaning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AB84C02CF5DDAF007E679F /* WebsiteDataStoreCleaning.swift */; }; + 85AB84C32CF624D8007E679F /* HTTPCookieExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AB84C22CF624CE007E679F /* HTTPCookieExtension.swift */; }; 85AE668E2097206E0014CF04 /* NotificationView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 85AE668D2097206E0014CF04 /* NotificationView.xib */; }; 85AE6690209724120014CF04 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AE668F209724120014CF04 /* NotificationView.swift */; }; 85AFA1212B45D14F0028A504 /* BookmarksMigrationAssertionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AFA1202B45D14F0028A504 /* BookmarksMigrationAssertionTests.swift */; }; @@ -562,6 +565,10 @@ 85C29708247BDD060063A335 /* DaxDialogsBrowsingSpecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C29706247BDCFF0063A335 /* DaxDialogsBrowsingSpecTests.swift */; }; 85C2970A247EB7AA0063A335 /* Text.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85C29709247EB7AA0063A335 /* Text.xcassets */; }; 85C2971A248162CA0063A335 /* DaxOnboardingPadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C29719248162CA0063A335 /* DaxOnboardingPadViewController.swift */; }; + 85C503F92CEF78C10075DF6F /* DataStoreIDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C503F82CEF78C00075DF6F /* DataStoreIDManager.swift */; }; + 85C503FB2CF0ADCE0075DF6F /* ObservationsDataCleaning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C503FA2CF0ADBD0075DF6F /* ObservationsDataCleaning.swift */; }; + 85C503FD2CF0E7B10075DF6F /* MockFireproofing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C503FC2CF0E7B10075DF6F /* MockFireproofing.swift */; }; + 85C503FF2CF0F4DC0075DF6F /* MockWebsiteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C503FE2CF0F4D60075DF6F /* MockWebsiteDataManager.swift */; }; 85C8E61D2B0E47380029A6BD /* BookmarksDatabaseSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */; }; 85C91CA224671F4C00A11132 /* AppDeepLinkSchemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17D723B1E8BB374003E8B0E /* AppDeepLinkSchemes.swift */; }; 85D2187024BF24DB004373D2 /* FaviconRequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D2186F24BF24DB004373D2 /* FaviconRequestModifierTests.swift */; }; @@ -647,7 +654,7 @@ 98424A9C2CED4F430071C7DB /* NetworkProtectionTestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C32CED4F430071C7DB /* NetworkProtectionTestUtils */; }; 98424A9D2CED4F430071C7DB /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 984249C72CED4F430071C7DB /* Common */; }; 98424AAB2CED4FF10071C7DB /* CookieStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85AD49ED2B6149110085D2D1 /* CookieStorageTests.swift */; }; - 98424AAC2CED4FF10071C7DB /* DataStoreIdManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */; }; + 98424AAC2CED4FF10071C7DB /* DataStoreIDManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981C49AF2C8FA61D00DF11E8 /* DataStoreIDManagerTests.swift */; }; 98424AAD2CED4FF10071C7DB /* WKWebViewConfigurationExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */; }; 98424AAE2CED4FF10071C7DB /* WebCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850559D123CF710C0055C0D5 /* WebCacheManagerTests.swift */; }; 98424AAF2CED4FF10071C7DB /* UserAgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 834DF990248FDDF60075EA48 /* UserAgentTests.swift */; }; @@ -1874,10 +1881,12 @@ 859DB8122CE6263C001F7210 /* TextZoomCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomCoordinator.swift; sourceTree = ""; }; 859DB81A2CE6268C001F7210 /* MockTextZoomCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTextZoomCoordinator.swift; sourceTree = ""; }; 859DB81B2CE6268C001F7210 /* TextZoomTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextZoomTests.swift; sourceTree = ""; }; - 85A1B3B120C6CD9900C18F15 /* CookieStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieStorage.swift; sourceTree = ""; }; + 85A1B3B120C6CD9900C18F15 /* MigratableCookieStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratableCookieStorage.swift; sourceTree = ""; }; 85A313962028E78A00327D00 /* release_notes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = release_notes.txt; path = fastlane/metadata/default/release_notes.txt; sourceTree = ""; }; 85A53EC9200D1FA20010D13F /* FileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStore.swift; sourceTree = ""; }; 85A9C37820E0E00C00073340 /* HomeRow.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = HomeRow.xcassets; sourceTree = ""; }; + 85AB84C02CF5DDAF007E679F /* WebsiteDataStoreCleaning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteDataStoreCleaning.swift; sourceTree = ""; }; + 85AB84C22CF624CE007E679F /* HTTPCookieExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPCookieExtension.swift; sourceTree = ""; }; 85AD49ED2B6149110085D2D1 /* CookieStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieStorageTests.swift; sourceTree = ""; }; 85AE668D2097206E0014CF04 /* NotificationView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NotificationView.xib; sourceTree = ""; }; 85AE668F209724120014CF04 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; @@ -1899,6 +1908,10 @@ 85C29706247BDCFF0063A335 /* DaxDialogsBrowsingSpecTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxDialogsBrowsingSpecTests.swift; sourceTree = ""; }; 85C29709247EB7AA0063A335 /* Text.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Text.xcassets; sourceTree = ""; }; 85C29719248162CA0063A335 /* DaxOnboardingPadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaxOnboardingPadViewController.swift; sourceTree = ""; }; + 85C503F82CEF78C00075DF6F /* DataStoreIDManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreIDManager.swift; sourceTree = ""; }; + 85C503FA2CF0ADBD0075DF6F /* ObservationsDataCleaning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationsDataCleaning.swift; sourceTree = ""; }; + 85C503FC2CF0E7B10075DF6F /* MockFireproofing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFireproofing.swift; sourceTree = ""; }; + 85C503FE2CF0F4D60075DF6F /* MockWebsiteDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebsiteDataManager.swift; sourceTree = ""; }; 85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDatabaseSetup.swift; sourceTree = ""; }; 85CA53A324B9F2BD00A6288C /* Favicons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favicons.swift; sourceTree = ""; }; 85CA53A924BB376800A6288C /* NotFoundCachingDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NotFoundCachingDownloader.swift; path = ../Core/NotFoundCachingDownloader.swift; sourceTree = ""; }; @@ -1979,7 +1992,7 @@ 981685572521EEF600FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/MainInterface.strings; sourceTree = ""; }; 981685A825221ACF00FA91A1 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = ""; }; 9817C9C221EF594700884F65 /* AutoClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoClear.swift; sourceTree = ""; }; - 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreIdManagerTests.swift; sourceTree = ""; }; + 981C49AF2C8FA61D00DF11E8 /* DataStoreIDManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreIDManagerTests.swift; sourceTree = ""; }; 981CA7E92617797500E119D5 /* MainViewController+AddFavoriteFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+AddFavoriteFlow.swift"; sourceTree = ""; }; 981DCA922521EFAB00CD4C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; 981DCA932521EFAB00CD4C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -4782,7 +4795,7 @@ 834DF990248FDDF60075EA48 /* UserAgentTests.swift */, 8540BD5123D8C2220057FDD2 /* UserDefaultsFireproofingTests.swift */, 850559D123CF710C0055C0D5 /* WebCacheManagerTests.swift */, - 981C49AF2C8FA61D00DF11E8 /* DataStoreIdManagerTests.swift */, + 981C49AF2C8FA61D00DF11E8 /* DataStoreIDManagerTests.swift */, F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */, 85AD49ED2B6149110085D2D1 /* CookieStorageTests.swift */, ); @@ -6074,18 +6087,22 @@ F143C3311E4A9A6A00CFDE3A /* Web */ = { isa = PBXGroup; children = ( - 85A1B3B120C6CD9900C18F15 /* CookieStorage.swift */, + 85AB84C22CF624CE007E679F /* HTTPCookieExtension.swift */, + 85C503F82CEF78C00075DF6F /* DataStoreIDManager.swift */, + 8596C30C2B7EB1800058EF90 /* DataStoreWarmup.swift */, 85BDC3132434D8F80053DB07 /* DebugUserScript.swift */, + 850559CF23CF647C0055C0D5 /* Fireproofing.swift */, 4B60ACA0252EC0B100E8D219 /* FullScreenVideoUserScript.swift */, 85BDC3182436161C0053DB07 /* LoginFormDetectionUserScript.swift */, - 850559CF23CF647C0055C0D5 /* Fireproofing.swift */, + 85A1B3B120C6CD9900C18F15 /* MigratableCookieStorage.swift */, + 85C503FA2CF0ADBD0075DF6F /* ObservationsDataCleaning.swift */, 4B75EA9126A266CB00018634 /* PrintingUserScript.swift */, 988F3DCE237D5C0F00AEE34C /* SchemeHandler.swift */, 836A941C247F23C600BF8EF5 /* UserAgentManager.swift */, F1A886771F29394E0096251E /* WebCacheManager.swift */, + 85AB84C02CF5DDAF007E679F /* WebsiteDataStoreCleaning.swift */, 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */, 830381BF1F850AAF00863075 /* WKWebViewConfigurationExtension.swift */, - 8596C30C2B7EB1800058EF90 /* DataStoreWarmup.swift */, ); name = Web; sourceTree = ""; @@ -6133,6 +6150,7 @@ F17669A91E412A17003D3222 /* Mocks */ = { isa = PBXGroup; children = ( + 85C503FE2CF0F4D60075DF6F /* MockWebsiteDataManager.swift */, 8598D2E52CEBAA1B00C45685 /* MockFaviconStore.swift */, 9F4CC51A2C48C0C7006A96EB /* MockTabDelegate.swift */, C14882E927F20DD000D59F0C /* MockBookmarksCoreDataStorage.swift */, @@ -6152,6 +6170,7 @@ 9F69331C2C5A191400CD6A5D /* MockTutorialSettings.swift */, 852409302C78030D00CB28FC /* MockUsageSegmentation.swift */, 4B27FBB72C93F53B007E21A7 /* MockPersistentPixel.swift */, + 85C503FC2CF0E7B10075DF6F /* MockFireproofing.swift */, ); name = Mocks; sourceTree = ""; @@ -8224,6 +8243,7 @@ 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */, F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */, BBFF18B12C76448100C48D7D /* QuerySubmittedTests.swift in Sources */, + 85C503FF2CF0F4DC0075DF6F /* MockWebsiteDataManager.swift in Sources */, 854A8D812C7F4452001D62E5 /* AtbTests.swift in Sources */, 9F23B8092C2BE9B700950875 /* MockURLOpener.swift in Sources */, 9F8007262C5261AF003EDAF4 /* MockPrivacyDataReporter.swift in Sources */, @@ -8250,6 +8270,7 @@ 9F4CC51D2C48D240006A96EB /* CoreDataDatabaseTestUtilities.swift in Sources */, C185ED672BD43DA100BAE9DC /* ImportPasswordsStatusHandlerTests.swift in Sources */, 6FF9AD452CE766F700C5A406 /* NewTabPageControllerPixelTests.swift in Sources */, + 85C503FD2CF0E7B10075DF6F /* MockFireproofing.swift in Sources */, 6F3529FF2CDCEDFF00A59170 /* OmniBarLoadingStateBearerTests.swift in Sources */, 564DE45E2C45218500D23241 /* OnboardingNavigationDelegateTests.swift in Sources */, C14E2F7729DE14EA002AC515 /* AutofillInterfaceUsernameTruncatorTests.swift in Sources */, @@ -8381,8 +8402,9 @@ buildActionMask = 2147483647; files = ( 98424AAB2CED4FF10071C7DB /* CookieStorageTests.swift in Sources */, - 98424AAC2CED4FF10071C7DB /* DataStoreIdManagerTests.swift in Sources */, + 98424AAC2CED4FF10071C7DB /* DataStoreIDManagerTests.swift in Sources */, 98424AAD2CED4FF10071C7DB /* WKWebViewConfigurationExtensionTests.swift in Sources */, + 8567E29D2CF77A700035926F /* MockFireproofing.swift in Sources */, 98424AAE2CED4FF10071C7DB /* WebCacheManagerTests.swift in Sources */, 98424AAF2CED4FF10071C7DB /* UserAgentTests.swift in Sources */, 98424AB02CED4FF10071C7DB /* UserDefaultsFireproofingTests.swift in Sources */, @@ -8439,7 +8461,8 @@ 85E065BC2C73A54700D73E2A /* UsageSegmentationStorage.swift in Sources */, F143C3271E4A9A0E00CFDE3A /* Logger+Multiple.swift in Sources */, 85372447220DD103009D09CD /* UIKeyCommandExtension.swift in Sources */, - 85A1B3B220C6CD9900C18F15 /* CookieStorage.swift in Sources */, + 85AB84C12CF5DDB4007E679F /* WebsiteDataStoreCleaning.swift in Sources */, + 85A1B3B220C6CD9900C18F15 /* MigratableCookieStorage.swift in Sources */, 9856A1992933D2EB00ACB44F /* BookmarksModelsErrorHandling.swift in Sources */, 850559D023CF647C0055C0D5 /* Fireproofing.swift in Sources */, 4B27FBAE2C924EC6007E21A7 /* PersistentPixelStoring.swift in Sources */, @@ -8469,6 +8492,7 @@ 85528AA72C7CA95D0017BCCA /* UsageSegmentationCalculator.swift in Sources */, 8598D2DC2CEB93AD00C45685 /* FaviconsCacheType.swift in Sources */, 1E6A4D692984208800A371D3 /* LocaleExtension.swift in Sources */, + 85C503F92CEF78C10075DF6F /* DataStoreIDManager.swift in Sources */, 98F6EA472863124100720957 /* ContentBlockerRulesLists.swift in Sources */, 566B73732BECE4F200FF1959 /* SyncErrorHandling.swift in Sources */, F1134EB01F40AC6300B73467 /* AtbParser.swift in Sources */, @@ -8486,6 +8510,7 @@ 1D8F727F2BA86D8000E31493 /* PixelExperiment.swift in Sources */, 56D8556C2BEA91C4009F9698 /* SyncAlertsPresenting.swift in Sources */, 37FD780F2A29E28B00B36DB1 /* SyncErrorHandler.swift in Sources */, + 85C503FB2CF0ADCE0075DF6F /* ObservationsDataCleaning.swift in Sources */, 85F21DC621145DD5002631A6 /* global.swift in Sources */, 372A0FF02B2389590033BF7F /* SyncMetricsEventsHandler.swift in Sources */, F41C2DA326C1925700F9A760 /* BookmarksAndFolders.xcdatamodeld in Sources */, @@ -8518,6 +8543,7 @@ B652DF0D287C2A6300C12A9C /* PrivacyFeatures.swift in Sources */, F10E522D1E946F8800CE1253 /* NSAttributedStringExtension.swift in Sources */, 9F678B892C9BAA4800CA0E19 /* Debouncer.swift in Sources */, + 85AB84C32CF624D8007E679F /* HTTPCookieExtension.swift in Sources */, 9887DC252354D2AA005C85F5 /* Database.swift in Sources */, F143C3171E4A99D200CFDE3A /* AppURLs.swift in Sources */, C1963863283794A000298D4D /* BookmarksCachingSearch.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 840a3ceb31..8ef177dd59 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -311,7 +311,8 @@ import os.log syncService.initializeIfNeeded() self.syncService = syncService - privacyProDataReporter = PrivacyProDataReporter() + let fireproofing = UserDefaultsFireproofing.xshared + privacyProDataReporter = PrivacyProDataReporter(fireproofing: fireproofing) isSyncInProgressCancellable = syncService.isSyncInProgressPublisher .filter { $0 } @@ -383,8 +384,10 @@ import os.log subscriptionFeatureAvailability: subscriptionFeatureAvailability, voiceSearchHelper: voiceSearchHelper, featureFlagger: AppDependencyProvider.shared.featureFlagger, + fireproofing: fireproofing, subscriptionCookieManager: subscriptionCookieManager, textZoomCoordinator: makeTextZoomCoordinator(), + websiteDataManager: makeWebsiteDataManager(fireproofing: fireproofing), appDidFinishLaunchingStartTime: didFinishLaunchingStartTime) main.loadViewIfNeeded() @@ -438,6 +441,13 @@ import os.log return true } + private func makeWebsiteDataManager(fireproofing: Fireproofing, + dataStoreIDManager: DataStoreIDManaging = DataStoreIDManager.shared) -> WebsiteDataManaging { + return WebCacheManager(cookieStorage: MigratableCookieStorage(), + fireproofing: fireproofing, + dataStoreIDManager: dataStoreIDManager) + } + private func makeTextZoomCoordinator() -> TextZoomCoordinator { let provider = AppDependencyProvider.shared let storage = TextZoomStorage() diff --git a/DuckDuckGo/ContentBlockingUpdating.swift b/DuckDuckGo/ContentBlockingUpdating.swift index dc3b8a720d..4147bbef44 100644 --- a/DuckDuckGo/ContentBlockingUpdating.swift +++ b/DuckDuckGo/ContentBlockingUpdating.swift @@ -57,12 +57,14 @@ public final class ContentBlockingUpdating { init(appSettings: AppSettings = AppUserDefaults(), contentBlockerRulesManager: ContentBlockerRulesManagerProtocol = ContentBlocking.shared.contentBlockingManager, - privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager) { + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + fireproofing: Fireproofing = UserDefaultsFireproofing.xshared) { let makeValue: (Update) -> NewContent = { rulesUpdate in let sourceProvider = DefaultScriptSourceProvider(appSettings: appSettings, privacyConfigurationManager: privacyConfigurationManager, - contentBlockingManager: contentBlockerRulesManager) + contentBlockingManager: contentBlockerRulesManager, + fireproofing: fireproofing) return NewContent(rulesUpdate: rulesUpdate, sourceProvider: sourceProvider) } diff --git a/DuckDuckGo/Favicons.swift b/DuckDuckGo/Favicons.swift index 719b1fa2cd..fafe8ef3a6 100644 --- a/DuckDuckGo/Favicons.swift +++ b/DuckDuckGo/Favicons.swift @@ -57,7 +57,7 @@ public class Favicons { init(sourcesProvider: FaviconSourcesProvider = DefaultFaviconSourcesProvider(), downloader: NotFoundCachingDownloader = NotFoundCachingDownloader(), - fireproofing: Fireproofing = UserDefaultsFireproofing.shared) { + fireproofing: Fireproofing = UserDefaultsFireproofing.xshared) { self.sourcesProvider = sourcesProvider self.downloader = downloader self.fireproofing = fireproofing diff --git a/DuckDuckGo/FireproofingSettingsViewController.swift b/DuckDuckGo/FireproofingSettingsViewController.swift index 08bc88b0c7..0fe2175144 100644 --- a/DuckDuckGo/FireproofingSettingsViewController.swift +++ b/DuckDuckGo/FireproofingSettingsViewController.swift @@ -37,9 +37,13 @@ class FireproofingSettingsViewController: UITableViewController { private var shouldShowRemoveAll = false private let fireproofing: Fireproofing + private let websiteDataManager: WebsiteDataManaging - init?(coder: NSCoder, fireproofing: Fireproofing) { + init?(coder: NSCoder, + fireproofing: Fireproofing, + websiteDataManager: WebsiteDataManaging) { self.fireproofing = fireproofing + self.websiteDataManager = websiteDataManager super.init(coder: coder) } @@ -166,7 +170,7 @@ class FireproofingSettingsViewController: UITableViewController { } Task { @MainActor in - await WebCacheManager.shared.removeCookies(forDomains: [domain], dataStore: WKWebsiteDataStore.current()) + await websiteDataManager.removeCookies(forDomains: [domain], fromDataStore: WKWebsiteDataStore.current()) } } @@ -225,7 +229,7 @@ class FireproofingSettingsViewController: UITableViewController { self?.refreshModel() }, confirmed: { [weak self] in Task { @MainActor in - await WebCacheManager.shared.removeCookies(forDomains: self?.model ?? [], dataStore: WKWebsiteDataStore.current()) + await self?.websiteDataManager.removeCookies(forDomains: self?.model ?? [], fromDataStore: WKWebsiteDataStore.current()) self?.fireproofing.clearAll() self?.refreshModel() self?.endEditing() diff --git a/DuckDuckGo/ImageCacheDebugViewController.swift b/DuckDuckGo/ImageCacheDebugViewController.swift index 37c6a838fe..f7be2275bd 100644 --- a/DuckDuckGo/ImageCacheDebugViewController.swift +++ b/DuckDuckGo/ImageCacheDebugViewController.swift @@ -61,7 +61,7 @@ class ImageCacheDebugViewController: UITableViewController { init?(coder: NSCoder, bookmarksDatabase: CoreDataDatabase, - fireproofing: Fireproofing = UserDefaultsFireproofing.shared) { + fireproofing: Fireproofing) { bookmarksContext = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType) self.fireproofing = fireproofing diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 3ab8722e7f..5eafe2cdea 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -292,7 +292,9 @@ extension MainViewController { appSettings: appSettings, bookmarksDatabase: bookmarksDatabase, tabManager: tabManager, - syncPausedStateManager: syncPausedStateManager) + syncPausedStateManager: syncPausedStateManager, + fireproofing: fireproofing, + websiteDataManager: websiteDataManager) let settingsViewModel = SettingsViewModel(legacyViewProvider: legacyViewProvider, subscriptionManager: AppDependencyProvider.shared.subscriptionManager, @@ -331,7 +333,8 @@ extension MainViewController { sync: self.syncService, bookmarksDatabase: self.bookmarksDatabase, internalUserDecider: AppDependencyProvider.shared.internalUserDecider, - tabManager: self.tabManager) + tabManager: self.tabManager, + fireproofing: self.fireproofing) } let controller = UINavigationController(rootViewController: settings) diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 29ed297fec..ab0835156d 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -178,6 +178,7 @@ class MainViewController: UIViewController { } let fireproofing: Fireproofing + let websiteDataManager: WebsiteDataManaging let textZoomCoordinator: TextZoomCoordinating var historyManager: HistoryManaging @@ -206,9 +207,10 @@ class MainViewController: UIViewController { subscriptionFeatureAvailability: SubscriptionFeatureAvailability, voiceSearchHelper: VoiceSearchHelperProtocol, featureFlagger: FeatureFlagger, - fireproofing: Fireproofing = UserDefaultsFireproofing.shared, + fireproofing: Fireproofing, subscriptionCookieManager: SubscriptionCookieManaging, textZoomCoordinator: TextZoomCoordinating, + websiteDataManager: WebsiteDataManaging, appDidFinishLaunchingStartTime: CFAbsoluteTime? ) { self.bookmarksDatabase = bookmarksDatabase @@ -235,7 +237,9 @@ class MainViewController: UIViewController { featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, appSettings: appSettings, - textZoomCoordinator: textZoomCoordinator) + textZoomCoordinator: textZoomCoordinator, + websiteDataManager: websiteDataManager, + fireproofing: fireproofing) self.syncPausedStateManager = syncPausedStateManager self.privacyProDataReporter = privacyProDataReporter self.homeTabManager = NewTabPageManager() @@ -249,6 +253,7 @@ class MainViewController: UIViewController { self.fireproofing = fireproofing self.subscriptionCookieManager = subscriptionCookieManager self.textZoomCoordinator = textZoomCoordinator + self.websiteDataManager = websiteDataManager self.appDidFinishLaunchingStartTime = appDidFinishLaunchingStartTime super.init(nibName: nil, bundle: nil) @@ -2673,7 +2678,7 @@ extension MainViewController: AutoClearWorker { URLSession.shared.configuration.urlCache?.removeAllCachedResponses() let pixel = TimedPixel(.forgetAllDataCleared) - await WebCacheManager.shared.clear() + await websiteDataManager.clear(dataStore: WKWebsiteDataStore.default()) pixel.fire(withAdditionalParameters: [PixelParameters.tabCount: "\(self.tabManager.count)"]) AutoconsentManagement.shared.clearCache() diff --git a/DuckDuckGo/RootDebugViewController+VanillaBrowser.swift b/DuckDuckGo/RootDebugViewController+VanillaBrowser.swift index c2900f153e..1de4437194 100644 --- a/DuckDuckGo/RootDebugViewController+VanillaBrowser.swift +++ b/DuckDuckGo/RootDebugViewController+VanillaBrowser.swift @@ -29,7 +29,7 @@ extension RootDebugViewController { fileprivate static let ddgURL = URL(string: "https://duckduckgo.com/")! @objc func openVanillaBrowser(_ sender: Any?) { - let homeURL = tabManager?.current()?.tabModel.link?.url ?? RootDebugViewController.ddgURL + let homeURL = tabManager.current()?.tabModel.link?.url ?? RootDebugViewController.ddgURL openVanillaBrowser(url: homeURL) } diff --git a/DuckDuckGo/RootDebugViewController.swift b/DuckDuckGo/RootDebugViewController.swift index d056ebb057..ac9ac306d4 100644 --- a/DuckDuckGo/RootDebugViewController.swift +++ b/DuckDuckGo/RootDebugViewController.swift @@ -55,14 +55,15 @@ class RootDebugViewController: UITableViewController { weak var reportGatheringActivity: UIView? @IBAction func onShareTapped() { - presentShareSheet(withItems: [DiagnosticReportDataSource(delegate: self)], fromButtonItem: shareButton) + presentShareSheet(withItems: [DiagnosticReportDataSource(delegate: self, fireproofing: fireproofing)], fromButtonItem: shareButton) } - private var bookmarksDatabase: CoreDataDatabase? - private var sync: DDGSyncing? - private var internalUserDecider: DefaultInternalUserDecider? - var tabManager: TabManager? - private var tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling? + private let bookmarksDatabase: CoreDataDatabase + private let sync: DDGSyncing + private let internalUserDecider: InternalUserDecider + let tabManager: TabManager + private let tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling + private let fireproofing: Fireproofing @UserDefaultsWrapper(key: .lastConfigurationRefreshDate, defaultValue: .distantPast) private var lastConfigurationRefreshDate: Date @@ -72,33 +73,27 @@ class RootDebugViewController: UITableViewController { bookmarksDatabase: CoreDataDatabase, internalUserDecider: InternalUserDecider, tabManager: TabManager, - tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling = TipKitDebugOptionsUIActionHandler()) { + tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling = TipKitDebugOptionsUIActionHandler(), + fireproofing: Fireproofing) { self.sync = sync self.bookmarksDatabase = bookmarksDatabase - self.internalUserDecider = internalUserDecider as? DefaultInternalUserDecider + self.internalUserDecider = internalUserDecider self.tabManager = tabManager self.tipKitUIActionHandler = tipKitUIActionHandler + self.fireproofing = fireproofing super.init(coder: coder) } required init?(coder: NSCoder) { - super.init(coder: coder) - } - - func configure(sync: DDGSyncing, bookmarksDatabase: CoreDataDatabase, internalUserDecider: InternalUserDecider, tabManager: TabManager, tipKitUIActionHandler: TipKitDebugOptionsUIActionHandling = TipKitDebugOptionsUIActionHandler()) { - - self.sync = sync - self.bookmarksDatabase = bookmarksDatabase - self.internalUserDecider = internalUserDecider as? DefaultInternalUserDecider - self.tabManager = tabManager - self.tipKitUIActionHandler = tipKitUIActionHandler + fatalError("init not implemented") } @IBSegueAction func onCreateImageCacheDebugScreen(_ coder: NSCoder) -> ImageCacheDebugViewController? { guard let controller = ImageCacheDebugViewController(coder: coder, - bookmarksDatabase: self.bookmarksDatabase!) else { + bookmarksDatabase: self.bookmarksDatabase, + fireproofing: fireproofing) else { fatalError("Failed to create controller") } @@ -107,8 +102,8 @@ class RootDebugViewController: UITableViewController { @IBSegueAction func onCreateSyncDebugScreen(_ coder: NSCoder, sender: Any?, segueIdentifier: String?) -> SyncDebugViewController { guard let controller = SyncDebugViewController(coder: coder, - sync: self.sync!, - bookmarksDatabase: self.bookmarksDatabase!) else { + sync: self.sync, + bookmarksDatabase: self.bookmarksDatabase) else { fatalError("Failed to create controller") } @@ -124,7 +119,7 @@ class RootDebugViewController: UITableViewController { } @IBSegueAction func onCreateCookieDebugScreen(_ coder: NSCoder) -> CookieDebugViewController? { - guard let controller = CookieDebugViewController(coder: coder, fireproofing: UserDefaultsFireproofing.shared) else { + guard let controller = CookieDebugViewController(coder: coder, fireproofing: fireproofing) else { fatalError("Failed to create controller") } @@ -136,7 +131,7 @@ class RootDebugViewController: UITableViewController { if cell.tag == Row.toggleInspectableWebViews.rawValue { cell.accessoryType = AppUserDefaults().inspectableWebViewEnabled ? .checkmark : .none } else if cell.tag == Row.toggleInternalUserState.rawValue { - cell.accessoryType = (internalUserDecider?.isInternalUser ?? false) ? .checkmark : .none + cell.accessoryType = (internalUserDecider.isInternalUser) ? .checkmark : .none } } @@ -172,8 +167,8 @@ class RootDebugViewController: UITableViewController { cell.accessoryType = defaults.inspectableWebViewEnabled ? .checkmark : .none NotificationCenter.default.post(Notification(name: AppUserDefaults.Notifications.inspectableWebViewsToggled)) case .toggleInternalUserState: - let newState = !(internalUserDecider?.isInternalUser ?? false) - internalUserDecider?.debugSetInternalUserState(newState) + let newState = !internalUserDecider.isInternalUser + (internalUserDecider as? DefaultInternalUserDecider)?.debugSetInternalUserState(newState) cell.accessoryType = newState ? .checkmark : .none NotificationCenter.default.post(Notification(name: AppUserDefaults.Notifications.inspectableWebViewsToggled)) case .openVanillaBrowser: @@ -193,12 +188,11 @@ class RootDebugViewController: UITableViewController { let controller = UIHostingController(rootView: OnboardingDebugView(onNewOnboardingIntroStartAction: action)) show(controller, sender: nil) case .resetSyncPromoPrompts: - guard let sync = sync else { return } let syncPromoPresenter = SyncPromoManager(syncService: sync) syncPromoPresenter.resetPromos() ActionMessageView.present(message: "Sync Promos reset") case .resetTipKit: - tipKitUIActionHandler?.resetTipKitTapped() + tipKitUIActionHandler.resetTipKitTapped() } } } @@ -264,7 +258,7 @@ class DiagnosticReportDataSource: UIActivityItemProvider { @UserDefaultsWrapper(key: .lastConfigurationRefreshDate, defaultValue: .distantPast) private var lastRefreshDate: Date - convenience init(delegate: DiagnosticReportDataSourceDelegate, fireproofing: Fireproofing = UserDefaultsFireproofing.shared) { + convenience init(delegate: DiagnosticReportDataSourceDelegate, fireproofing: Fireproofing) { self.init(placeholderItem: "") self.delegate = delegate self.fireproofing = fireproofing diff --git a/DuckDuckGo/ScriptSourceProviding.swift b/DuckDuckGo/ScriptSourceProviding.swift index 61a47e794f..549b538b45 100644 --- a/DuckDuckGo/ScriptSourceProviding.swift +++ b/DuckDuckGo/ScriptSourceProviding.swift @@ -55,7 +55,7 @@ struct DefaultScriptSourceProvider: ScriptSourceProviding { init(appSettings: AppSettings = AppDependencyProvider.shared.appSettings, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, contentBlockingManager: ContentBlockerRulesManagerProtocol = ContentBlocking.shared.contentBlockingManager, - fireproofing: Fireproofing = UserDefaultsFireproofing.shared) { + fireproofing: Fireproofing) { sendDoNotSell = appSettings.sendDoNotSell diff --git a/DuckDuckGo/SettingsLegacyViewProvider.swift b/DuckDuckGo/SettingsLegacyViewProvider.swift index 3b74346f78..2ce76ef5d6 100644 --- a/DuckDuckGo/SettingsLegacyViewProvider.swift +++ b/DuckDuckGo/SettingsLegacyViewProvider.swift @@ -41,6 +41,7 @@ class SettingsLegacyViewProvider: ObservableObject { let tabManager: TabManager let syncPausedStateManager: any SyncPausedStateManaging let fireproofing: Fireproofing + let websiteDataManager: WebsiteDataManaging init(syncService: any DDGSyncing, syncDataProviders: SyncDataProviders, @@ -48,7 +49,8 @@ class SettingsLegacyViewProvider: ObservableObject { bookmarksDatabase: CoreDataDatabase, tabManager: TabManager, syncPausedStateManager: any SyncPausedStateManaging, - fireproofing: Fireproofing = UserDefaultsFireproofing.shared) { + fireproofing: Fireproofing, + websiteDataManager: WebsiteDataManaging) { self.syncService = syncService self.syncDataProviders = syncDataProviders self.appSettings = appSettings @@ -56,6 +58,7 @@ class SettingsLegacyViewProvider: ObservableObject { self.tabManager = tabManager self.syncPausedStateManager = syncPausedStateManager self.fireproofing = fireproofing + self.websiteDataManager = websiteDataManager } enum LegacyView { @@ -81,7 +84,7 @@ class SettingsLegacyViewProvider: ObservableObject { private func instantiateFireproofingController() -> UIViewController { let storyboard = UIStoryboard(name: StoryboardName.settings, bundle: nil) return storyboard.instantiateViewController(identifier: "FireProofSites") { coder in - return FireproofingSettingsViewController(coder: coder, fireproofing: self.fireproofing) + return FireproofingSettingsViewController(coder: coder, fireproofing: self.fireproofing, websiteDataManager: self.websiteDataManager) } } @@ -92,6 +95,18 @@ class SettingsLegacyViewProvider: ObservableObject { }) } + private func instantiateDebugController() -> UIViewController { + let storyboard = UIStoryboard(name: "Debug", bundle: nil) + return storyboard.instantiateViewController(identifier: "DebugMenu") { coder in + RootDebugViewController(coder: coder, + sync: self.syncService, + bookmarksDatabase: self.bookmarksDatabase, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider, + tabManager: self.tabManager, + fireproofing: self.fireproofing) + } + } + // Legacy UIKit Views (Pushed unmodified) var addToDock: UIViewController { instantiate( "instructions", fromStoryboard: StoryboardName.homeRow) } var appIcon: UIViewController { instantiate("AppIcon", fromStoryboard: StoryboardName.settings) } @@ -102,6 +117,8 @@ class SettingsLegacyViewProvider: ObservableObject { var keyboard: UIViewController { instantiate("Keyboard", fromStoryboard: StoryboardName.settings) } var feedback: UIViewController { instantiate("Feedback", fromStoryboard: StoryboardName.feedback) } var autoclearData: UIViewController { instantiateAutoClearController() } + var debug: UIViewController { instantiateDebugController() } + @MainActor func syncSettings(source: String? = nil) -> SyncSettingsViewController { @@ -121,17 +138,5 @@ class SettingsLegacyViewProvider: ObservableObject { selectedAccount: selectedAccount, source: .settings) } - - var debug: UIViewController { - let storyboard = UIStoryboard(name: "Debug", bundle: nil) - if let viewController = storyboard.instantiateViewController(withIdentifier: "DebugMenu") as? RootDebugViewController { - viewController.configure(sync: syncService, - bookmarksDatabase: bookmarksDatabase, - internalUserDecider: AppDependencyProvider.shared.internalUserDecider, - tabManager: tabManager) - return viewController - } - return UIViewController() - } - + } diff --git a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift index 5af147b4a5..f7246b44f0 100644 --- a/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift +++ b/DuckDuckGo/Subscription/AsyncHeadlessWebview/HeadlessWebView.swift @@ -22,6 +22,7 @@ import SwiftUI import WebKit import UserScript import BrowserServicesKit +import Core struct HeadlessWebView: UIViewRepresentable { weak var userScript: UserScriptMessaging? @@ -85,7 +86,7 @@ struct HeadlessWebView: UIViewRepresentable { // Enable content blocking rules if settings.contentBlocking { - let sourceProvider = DefaultScriptSourceProvider() + let sourceProvider = DefaultScriptSourceProvider(fireproofing: UserDefaultsFireproofing.xshared) let contentBlockerUserScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig) let contentScopeUserScript = ContentScopeUserScript(sourceProvider.privacyConfigurationManager, properties: sourceProvider.contentScopeProperties) diff --git a/DuckDuckGo/Subscription/PrivacyProDataReporting.swift b/DuckDuckGo/Subscription/PrivacyProDataReporting.swift index 5c8dfb7790..02f06abd64 100644 --- a/DuckDuckGo/Subscription/PrivacyProDataReporting.swift +++ b/DuckDuckGo/Subscription/PrivacyProDataReporting.swift @@ -129,7 +129,7 @@ final class PrivacyProDataReporter: PrivacyProDataReporting { secureVaultMaker: @escaping () -> (any AutofillSecureVault)? = { try? AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter()) }, syncService: DDGSyncing? = nil, tabsModel: TabsModel? = nil, - fireproofing: Fireproofing = UserDefaultsFireproofing.shared, + fireproofing: Fireproofing, dateGenerator: @escaping () -> Date = Date.init) { self.configurationManager = configurationManager self.variantManager = variantManager diff --git a/DuckDuckGo/TabManager.swift b/DuckDuckGo/TabManager.swift index 2690c7b518..998ff5c25a 100644 --- a/DuckDuckGo/TabManager.swift +++ b/DuckDuckGo/TabManager.swift @@ -44,6 +44,8 @@ class TabManager { private let onboardingPixelReporter: OnboardingPixelReporting private let featureFlagger: FeatureFlagger private let textZoomCoordinator: TextZoomCoordinating + private let fireproofing: Fireproofing + private let websiteDataManager: WebsiteDataManaging private let subscriptionCookieManager: SubscriptionCookieManaging private let appSettings: AppSettings @@ -66,7 +68,9 @@ class TabManager { featureFlagger: FeatureFlagger, subscriptionCookieManager: SubscriptionCookieManaging, appSettings: AppSettings, - textZoomCoordinator: TextZoomCoordinating) { + textZoomCoordinator: TextZoomCoordinating, + websiteDataManager: WebsiteDataManaging, + fireproofing: Fireproofing) { self.model = model self.previewsSource = previewsSource self.bookmarksDatabase = bookmarksDatabase @@ -81,6 +85,8 @@ class TabManager { self.subscriptionCookieManager = subscriptionCookieManager self.appSettings = appSettings self.textZoomCoordinator = textZoomCoordinator + self.websiteDataManager = websiteDataManager + self.fireproofing = fireproofing registerForNotifications() } @@ -105,7 +111,9 @@ class TabManager { onboardingPixelReporter: onboardingPixelReporter, featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: textZoomCoordinator) + textZoomCoordinator: textZoomCoordinator, + websiteDataManager: websiteDataManager, + fireproofing: fireproofing) controller.applyInheritedAttribution(inheritedAttribution) controller.attachWebView(configuration: configuration, andLoadRequest: url == nil ? nil : URLRequest.userInitiated(url!), @@ -185,7 +193,9 @@ class TabManager { onboardingPixelReporter: onboardingPixelReporter, featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: textZoomCoordinator) + textZoomCoordinator: textZoomCoordinator, + websiteDataManager: websiteDataManager, + fireproofing: fireproofing) controller.attachWebView(configuration: configCopy, andLoadRequest: request, consumeCookies: !model.hasActiveTabs, diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index a5bbd41846..529f49da2b 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -332,7 +332,9 @@ class TabViewController: UIViewController { urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), featureFlagger: FeatureFlagger, subscriptionCookieManager: SubscriptionCookieManaging, - textZoomCoordinator: TextZoomCoordinating) -> TabViewController { + textZoomCoordinator: TextZoomCoordinating, + websiteDataManager: WebsiteDataManaging, + fireproofing: Fireproofing) -> TabViewController { let storyboard = UIStoryboard(name: "Tab", bundle: nil) let controller = storyboard.instantiateViewController(identifier: "TabViewController", creator: { coder in @@ -350,7 +352,9 @@ class TabViewController: UIViewController { urlCredentialCreator: urlCredentialCreator, featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: textZoomCoordinator + textZoomCoordinator: textZoomCoordinator, + fireproofing: fireproofing, + websiteDataManager: websiteDataManager ) }) return controller @@ -371,6 +375,7 @@ class TabViewController: UIViewController { let onboardingPixelReporter: OnboardingCustomInteractionPixelReporting let textZoomCoordinator: TextZoomCoordinating let fireproofing: Fireproofing + let websiteDataManager: WebsiteDataManaging required init?(coder aDecoder: NSCoder, tabModel: Tab, @@ -388,7 +393,8 @@ class TabViewController: UIViewController { featureFlagger: FeatureFlagger, subscriptionCookieManager: SubscriptionCookieManaging, textZoomCoordinator: TextZoomCoordinating, - fireproofing: Fireproofing = UserDefaultsFireproofing.shared) { + fireproofing: Fireproofing, + websiteDataManager: WebsiteDataManaging) { self.tabModel = tabModel self.appSettings = appSettings self.bookmarksDatabase = bookmarksDatabase @@ -410,6 +416,7 @@ class TabViewController: UIViewController { self.subscriptionCookieManager = subscriptionCookieManager self.textZoomCoordinator = textZoomCoordinator self.fireproofing = fireproofing + self.websiteDataManager = websiteDataManager super.init(coder: aDecoder) @@ -664,7 +671,7 @@ class TabViewController: UIViewController { Task { @MainActor in await webView.configuration.websiteDataStore.dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) let cookieStore = webView.configuration.websiteDataStore.httpCookieStore - await WebCacheManager.shared.consumeCookies(httpCookieStore: cookieStore) + await websiteDataManager.consumeCookies(into: cookieStore) subscriptionCookieManager.resetLastRefreshDate() await subscriptionCookieManager.refreshSubscriptionCookie() doLoad() diff --git a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index 974fbbf895..0227757b55 100644 --- a/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -114,7 +114,9 @@ extension TabViewController { onboardingPixelReporter: onboardingPixelReporter, featureFlagger: featureFlagger, subscriptionCookieManager: subscriptionCookieManager, - textZoomCoordinator: textZoomCoordinator) + textZoomCoordinator: textZoomCoordinator, + websiteDataManager: websiteDataManager, + fireproofing: fireproofing) tabController.isLinkPreview = true let configuration = WKWebViewConfiguration.nonPersistent() diff --git a/DuckDuckGoTests/FireButtonReferenceTests.swift b/DuckDuckGoTests/FireButtonReferenceTests.swift index 9f1e322674..66d1c32688 100644 --- a/DuckDuckGoTests/FireButtonReferenceTests.swift +++ b/DuckDuckGoTests/FireButtonReferenceTests.swift @@ -49,7 +49,7 @@ final class FireButtonReferenceTests: XCTestCase { func testClearDataUsingLegacyContainer() async throws { // Using WKWebsiteDataStore(forIdentifier:) doesn't persist cookies in a testable way, so use the legacy container here. - let fireproofing = UserDefaultsFireproofing.shared + let fireproofing = UserDefaultsFireproofing.xshared fireproofing.clearAll() for site in testData.fireButtonFireproofing.fireproofedSites { @@ -61,8 +61,6 @@ final class FireButtonReferenceTests: XCTestCase { let referenceTests = testData.fireButtonFireproofing.tests.filter { $0.exceptPlatforms.contains("ios-browser") == false } - - let cookieStorage = CookieStorage() for test in referenceTests { let cookie = try XCTUnwrap(cookie(for: test)) @@ -73,15 +71,13 @@ final class FireButtonReferenceTests: XCTestCase { await fulfillment(of: [warmupCompleted], timeout: 5) - let cookieStore = WKWebsiteDataStore.default().httpCookieStore + let dataStore = WKWebsiteDataStore.default() + let cookieStore = dataStore.httpCookieStore await cookieStore.setCookie(cookie) - - // Pretend the webview was loaded and the cookies were previously consumed - cookieStorage.isConsumed = true - - await WebCacheManager.shared.clear(cookieStorage: cookieStorage, fireproofing: fireproofing, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) - let testCookie = cookieStorage.cookies.filter { $0.name == test.cookieName }.first + await WebCacheManager(cookieStorage: MigratableCookieStorage(), fireproofing: fireproofing, dataStoreIDManager: DataStoreIDManager(store: MockKeyValueStore())).clear(dataStore: dataStore) + + let testCookie = await cookieStore.allCookies().filter { $0.name == test.cookieName }.first if test.expectCookieRemoved { XCTAssertNil(testCookie, "Cookie should not exist for test: \(test.name)") @@ -90,52 +86,11 @@ final class FireButtonReferenceTests: XCTestCase { } // Reset cache - cookieStorage.cookies = [] + // cookieStorage.cookies = [] } } - - func testCookieStorage() throws { - let fireproofing = UserDefaultsFireproofing.shared - fireproofing.clearAll() - - for site in testData.fireButtonFireproofing.fireproofedSites { - let sanitizedSite = sanitizedSite(site) - print("Adding %s to fireproofed sites", sanitizedSite) - fireproofing.addToAllowed(domain: sanitizedSite) - } - - let referenceTests = testData.fireButtonFireproofing.tests.filter { - $0.exceptPlatforms.contains("ios-browser") == false - } - - let cookieStorage = CookieStorage() - cookieStorage.isConsumed = true - for test in referenceTests { - let cookie = try XCTUnwrap(cookie(for: test)) - - // Pretend the webview was loaded and the cookies were previously consumed - cookieStorage.isConsumed = true - - // This simulates loading the cookies from the current web view data stores and updating the storage - cookieStorage.updateCookies([ - cookie - ], preservingFireproofedDomains: fireproofing) - let testCookie = cookieStorage.cookies.filter { $0.name == test.cookieName }.first - - if test.expectCookieRemoved { - XCTAssertNil(testCookie, "Cookie should not exist for test: \(test.name)") - } else { - XCTAssertNotNil(testCookie, "Cookie should exist for test: \(test.name)") - } - - // Reset cache - cookieStorage.cookies = [] - cookieStorage.isConsumed = true - } - } - private func cookie(for test: Test) -> HTTPCookie? { HTTPCookie(properties: [.name: test.cookieName, .path: "", diff --git a/DuckDuckGoTests/MockFireproofing.swift b/DuckDuckGoTests/MockFireproofing.swift new file mode 100644 index 0000000000..ced235d287 --- /dev/null +++ b/DuckDuckGoTests/MockFireproofing.swift @@ -0,0 +1,36 @@ +// +// MockFireproofing.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import Core + +// MARK: Mocks + +class MockFireproofing: UserDefaultsFireproofing { + override var allowedDomains: [String] { + return domains + } + + let domains: [String] + init(domains: [String] = []) { + self.domains = domains + } + +} diff --git a/DuckDuckGoTests/MockTabDelegate.swift b/DuckDuckGoTests/MockTabDelegate.swift index acb7db0e53..7878b46cbd 100644 --- a/DuckDuckGoTests/MockTabDelegate.swift +++ b/DuckDuckGoTests/MockTabDelegate.swift @@ -146,7 +146,9 @@ extension TabViewController { urlCredentialCreator: MockCredentialCreator(), featureFlagger: featureFlagger, subscriptionCookieManager: SubscriptionCookieManagerMock(), - textZoomCoordinator: MockTextZoomCoordinator() + textZoomCoordinator: MockTextZoomCoordinator(), + websiteDataManager: MockWebsiteDataManager(), + fireproofing: MockFireproofing() ) tab.attachWebView(configuration: .nonPersistent(), andLoadRequest: nil, consumeCookies: false, customWebView: customWebView) return tab diff --git a/DuckDuckGoTests/MockWebsiteDataManager.swift b/DuckDuckGoTests/MockWebsiteDataManager.swift new file mode 100644 index 0000000000..c64c71f456 --- /dev/null +++ b/DuckDuckGoTests/MockWebsiteDataManager.swift @@ -0,0 +1,35 @@ +// +// MockWebsiteDataManager.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import WebKit + +@testable import Core + +class MockWebsiteDataManager: WebsiteDataManaging { + + func removeCookies(forDomains domains: [String], fromDataStore: WKWebsiteDataStore) async { + } + + func consumeCookies(into httpCookieStore: WKHTTPCookieStore) async { + } + + func clear(dataStore: WKWebsiteDataStore) async { + } + +} diff --git a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift index 41398f8fc2..3cbcc0c9e7 100644 --- a/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift +++ b/DuckDuckGoTests/OnboardingDaxFavouritesTests.swift @@ -34,6 +34,8 @@ final class OnboardingDaxFavouritesTests: XCTestCase { private var tutorialSettingsMock: MockTutorialSettings! private var contextualOnboardingLogicMock: ContextualOnboardingLogicMock! + let mockWebsiteDataManager = MockWebsiteDataManager() + override func setUpWithError() throws { try super.setUpWithError() let db = CoreDataDatabase.bookmarksMock @@ -82,8 +84,10 @@ final class OnboardingDaxFavouritesTests: XCTestCase { subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock.enabled, voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true), featureFlagger: MockFeatureFlagger(), + fireproofing: MockFireproofing(), subscriptionCookieManager: SubscriptionCookieManagerMock(), textZoomCoordinator: MockTextZoomCoordinator(), + websiteDataManager: mockWebsiteDataManager, appDidFinishLaunchingStartTime: nil ) let window = UIWindow(frame: UIScreen.main.bounds) diff --git a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift index 1afdd4153f..c65348aa11 100644 --- a/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift +++ b/DuckDuckGoTests/OnboardingNavigationDelegateTests.swift @@ -82,8 +82,10 @@ final class OnboardingNavigationDelegateTests: XCTestCase { subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock.enabled, voiceSearchHelper: MockVoiceSearchHelper(isSpeechRecognizerAvailable: true, voiceSearchEnabled: true), featureFlagger: MockFeatureFlagger(), + fireproofing: MockFireproofing(), subscriptionCookieManager: SubscriptionCookieManagerMock(), textZoomCoordinator: MockTextZoomCoordinator(), + websiteDataManager: MockWebsiteDataManager(), appDidFinishLaunchingStartTime: nil) let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = UIViewController() diff --git a/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift b/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift index 65597fd1a0..b54c2f20fa 100644 --- a/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift +++ b/DuckDuckGoTests/Subscription/PrivacyProDataReporterTests.swift @@ -73,6 +73,7 @@ final class PrivacyProDataReporterTests: XCTestCase { autofillCheck: { true }, secureVaultMaker: { nil }, tabsModel: TabsModel(tabs: [], desktop: false), + fireproofing: MockFireproofing(), dateGenerator: mockCalendar.now ) anotherReporter = PrivacyProDataReporter( @@ -85,7 +86,8 @@ final class PrivacyProDataReporterTests: XCTestCase { featureFlagger: MockFeatureFlagger(), autofillCheck: { true }, secureVaultMaker: { nil }, - tabsModel: TabsModel(tabs: [Tab(), Tab(), Tab(), Tab()], desktop: false) + tabsModel: TabsModel(tabs: [Tab(), Tab(), Tab(), Tab()], desktop: false), + fireproofing: MockFireproofing() ) } diff --git a/WebViewUnitTests/CookieStorageTests.swift b/WebViewUnitTests/CookieStorageTests.swift index f200fc251e..62d56e30aa 100644 --- a/WebViewUnitTests/CookieStorageTests.swift +++ b/WebViewUnitTests/CookieStorageTests.swift @@ -20,195 +20,62 @@ import XCTest @testable import Core import WebKit +import Persistence +import TestUtils public class CookieStorageTests: XCTestCase { - - var storage: CookieStorage! - - // This is updated by the `make` function which preserves any cookies added as part of this test - let fireproofing = UserDefaultsFireproofing.shared - static let userDefaultsSuiteName = "test" - - public override func setUp() { - super.setUp() - let defaults = UserDefaults(suiteName: Self.userDefaultsSuiteName)! - defaults.removePersistentDomain(forName: Self.userDefaultsSuiteName) - storage = CookieStorage(userDefaults: defaults) - storage.isConsumed = true - fireproofing.clearAll() + func testLoadCookiesFromDefaultsAndRemovalWhenMigrationCompletes() { + let store = MockKeyValueStore() + MigratableCookieStorage.addCookies([ + .make(name: "test1", value: "value1", domain: "example.com"), + .make(name: "test2", value: "value2", domain: "example.com"), + .make(name: "test3", value: "value3", domain: "facebook.com"), + ], store) + + let storage = MigratableCookieStorage(store: store) + XCTAssertEqual(storage.cookies.count, 3) + + XCTAssertTrue(storage.cookies.contains(where: { + $0.domain == "example.com" && + $0.name == "test1" && + $0.value == "value1" + })) + + XCTAssertTrue(storage.cookies.contains(where: { + $0.domain == "example.com" && + $0.name == "test2" && + $0.value == "value2" + })) + + XCTAssertTrue(storage.cookies.contains(where: { + $0.domain == "facebook.com" && + $0.name == "test3" && + $0.value == "value3" + })) + + // Now remove them all + storage.migrationComplete() + + XCTAssertTrue(storage.cookies.isEmpty) } - - func testWhenDomainRemovesAllCookesThenTheyAreClearedFromPersisted() { - fireproofing.addToAllowed(domain: "example.com") - - XCTAssertEqual(storage.updateCookies([ - make("example.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing), .empty) - XCTAssertEqual(1, storage.cookies.count) - - storage.isConsumed = true - storage.updateCookies([], preservingFireproofedDomains: fireproofing) - XCTAssertEqual(0, storage.cookies.count) - - } - - func testWhenUpdatedThenDuckDuckGoCookiesAreNotRemoved() { - storage.updateCookies([ - make("duckduckgo.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(1, storage.cookies.count) - - storage.isConsumed = true - storage.updateCookies([ - make("duckduckgo.com", name: "x", value: "1"), - make("test.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(2, storage.cookies.count) - - storage.isConsumed = true - storage.updateCookies([ - make("usedev1.duckduckgo.com", name: "x", value: "1"), - make("duckduckgo.com", name: "x", value: "1"), - make("test.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(3, storage.cookies.count) - - } - - func testWhenUpdatedThenCookiesWithFutureExpirationAreNotRemoved() { - storage.updateCookies([ - make("test.com", name: "x", value: "1", expires: .distantFuture), - make("example.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(2, storage.cookies.count) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "test.com" })) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "example.com" })) - - } - - func testWhenUpdatingThenExistingExpiredCookiesAreRemoved() { - storage.cookies = [ - make("test.com", name: "x", value: "1", expires: Date(timeIntervalSinceNow: -100)), - ] - XCTAssertEqual(1, storage.cookies.count) - - storage.isConsumed = true - storage.updateCookies([ - make("example.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(1, storage.cookies.count) - XCTAssertFalse(storage.cookies.contains(where: { $0.domain == "test.com" })) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "example.com" })) - - } - - func testWhenExpiredCookieIsAddedThenItIsNotPersisted() { - - storage.updateCookies([ - make("example.com", name: "x", value: "1", expires: Date(timeIntervalSinceNow: -100)), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(0, storage.cookies.count) - - } - - func testWhenUpdatedThenNoLongerFireproofedDomainsAreCleared() { - storage.updateCookies([ - make("test.com", name: "x", value: "1"), - make("example.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - fireproofing.remove(domain: "test.com") - - storage.isConsumed = true - storage.updateCookies([ - make("example.com", name: "x", value: "1"), - ], preservingFireproofedDomains: fireproofing) - - XCTAssertEqual(1, storage.cookies.count) - XCTAssertFalse(storage.cookies.contains(where: { $0.domain == "test.com" })) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "example.com" })) - } - - func testWhenStorageInitialiedThenItIsEmptyAndIsReadyToBeUpdated() { - XCTAssertEqual(0, storage.cookies.count) - XCTAssertTrue(storage.isConsumed) - } - - func testWhenStorageIsUpdatedThenConsumedIsResetToFalse() { - storage.isConsumed = true - XCTAssertTrue(storage.isConsumed) - storage.updateCookies([ - make("test.com", name: "x", value: "1") - ], preservingFireproofedDomains: fireproofing) - XCTAssertFalse(storage.isConsumed) - } - - func testWhenStorageIsReinstanciatedThenUsesStoredData() { - storage.updateCookies([ - make("test.com", name: "x", value: "1") - ], preservingFireproofedDomains: fireproofing) - storage.isConsumed = true - - let otherStorage = CookieStorage(userDefaults: UserDefaults(suiteName: Self.userDefaultsSuiteName)!) - XCTAssertEqual(1, otherStorage.cookies.count) - XCTAssertTrue(otherStorage.isConsumed) - } - - func testWhenStorageIsUpdatedThenUpdatingAddsNewCookies() { - storage.updateCookies([ - make("test.com", name: "x", value: "1") - ], preservingFireproofedDomains: fireproofing) - XCTAssertEqual(1, storage.cookies.count) - } +} - func testWhenStorageHasMatchingDOmainThenUpdatingReplacesCookies() { - storage.updateCookies([ - make("test.com", name: "x", value: "1") - ], preservingFireproofedDomains: fireproofing) +extension MigratableCookieStorage { - storage.isConsumed = true - storage.updateCookies([ - make("test.com", name: "x", value: "2"), - make("test.com", name: "y", value: "3"), - ], preservingFireproofedDomains: fireproofing) + static func addCookies(_ cookies: [HTTPCookie], _ store: KeyValueStoring) { - XCTAssertEqual(2, storage.cookies.count) - XCTAssertFalse(storage.cookies.contains(where: { $0.domain == "test.com" && $0.name == "x" && $0.value == "1" })) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "test.com" && $0.name == "x" && $0.value == "2" })) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "test.com" && $0.name == "y" && $0.value == "3" })) + var cookieData = [[String: Any?]]() + cookies.forEach { cookie in + var mappedCookie = [String: Any?]() + cookie.properties?.forEach { + mappedCookie[$0.key.rawValue] = $0.value + } + cookieData.append(mappedCookie) + } + store.set(cookieData, forKey: MigratableCookieStorage.Keys.allowedCookies) } - - func testWhenStorageUpdatedAndNotConsumedThenNothingHappens() { - storage.updateCookies([ - make("test.com", name: "x", value: "1") - ], preservingFireproofedDomains: fireproofing) - - storage.updateCookies([ - make("example.com", name: "y", value: "3"), - ], preservingFireproofedDomains: fireproofing) - XCTAssertEqual(1, storage.cookies.count) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "test.com" && $0.name == "x" && $0.value == "1" })) - } - - func make(_ domain: String, name: String, value: String, expires: Date? = nil) -> HTTPCookie { - fireproofing.addToAllowed(domain: domain) - return HTTPCookie(properties: [ - .domain: domain, - .name: name, - .value: value, - .path: "/", - .expires: expires as Any - ])! - } - } diff --git a/WebViewUnitTests/DataStoreIDManagerTests.swift b/WebViewUnitTests/DataStoreIDManagerTests.swift new file mode 100644 index 0000000000..ea32e99b48 --- /dev/null +++ b/WebViewUnitTests/DataStoreIDManagerTests.swift @@ -0,0 +1,51 @@ +// +// DataStoreIDManagerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +import XCTest +@testable import Core +import WebKit +import TestUtils + +class DataStoreIDManagerTests: XCTestCase { + + func testWhenFreshlyInstalledThenNoIDIsAllocated() { + let manager = DataStoreIDManager(store: MockKeyValueStore()) + XCTAssertNil(manager.currentID) + } + + func testWhenIDIsInvalidatedThenNoNewIDIsCreated() { + let manager = DataStoreIDManager(store: MockKeyValueStore()) + XCTAssertNil(manager.currentID) + manager.invalidateCurrentID() + XCTAssertNil(manager.currentID) + } + + func testWhenIDAlreadyExistsThenItIsRedFromTheStore() { + let storedUUID = UUID().uuidString + let store = MockKeyValueStore() + store.set(storedUUID, forKey: DataStoreIDManager.Constants.currentWebContainerID.rawValue) + let manager = DataStoreIDManager(store: store) + XCTAssertEqual(manager.currentID?.uuidString, storedUUID) + manager.invalidateCurrentID() + XCTAssertNil(manager.currentID) + } + +} diff --git a/WebViewUnitTests/DataStoreIdManagerTests.swift b/WebViewUnitTests/DataStoreIdManagerTests.swift deleted file mode 100644 index f4d1e3d2df..0000000000 --- a/WebViewUnitTests/DataStoreIdManagerTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// DataStoreIdManagerTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -import XCTest -@testable import Core -import WebKit -import TestUtils - -class DataStoreIdManagerTests: XCTestCase { - - func testWhenFreshlyInstalledThenIdIsAllocated() { - let manager = DataStoreIdManager(store: MockKeyValueStore()) - - XCTAssertNil(manager.currentId) - manager.invalidateCurrentIdAndAllocateNew() - XCTAssertNotNil(manager.currentId) - } - - func testWhenIdIsInvalidatedThenNewIsCreated() { - - let manager = DataStoreIdManager(store: MockKeyValueStore()) - - XCTAssertNil(manager.currentId) - manager.invalidateCurrentIdAndAllocateNew() - - let firstID = manager.currentId - XCTAssertNotNil(firstID) - - manager.invalidateCurrentIdAndAllocateNew() - let secondID = manager.currentId - - XCTAssertNotEqual(firstID, secondID) - - manager.invalidateCurrentIdAndAllocateNew() - let thirdID = manager.currentId - - XCTAssertNotEqual(firstID, thirdID) - XCTAssertNotEqual(secondID, thirdID) - } - - func testWhenIdAlreadyExistsThenItIsRedFromTheStore() { - - let storedUUID = UUID().uuidString - let store = MockKeyValueStore() - store.set(storedUUID, forKey: DataStoreIdManager.Constants.currentWebContainerId.rawValue) - - let manager = DataStoreIdManager(store: store) - - XCTAssertEqual(manager.currentId?.uuidString, storedUUID) - } - -} diff --git a/WebViewUnitTests/UserDefaultsFireproofingTests.swift b/WebViewUnitTests/UserDefaultsFireproofingTests.swift index 7c138f16e8..1fdb9be04b 100644 --- a/WebViewUnitTests/UserDefaultsFireproofingTests.swift +++ b/WebViewUnitTests/UserDefaultsFireproofingTests.swift @@ -19,6 +19,7 @@ import XCTest @testable import Core +@testable import Subscription // TODO: More broad shared TestUtils extension UserDefaultsWrapper { @@ -72,10 +73,27 @@ class UserDefaultsFireproofingTests: XCTestCase { fireproofing.addToAllowed(domain: "example.com") XCTAssertTrue(fireproofing.isAllowed(fireproofDomain: "example.com")) } - + + func testAllowedCookieDomains() { + let fireproofing = UserDefaultsFireproofing() + XCTAssertFalse(fireproofing.isAllowed(fireproofDomain: "example.com")) + fireproofing.addToAllowed(domain: "example.com") + XCTAssertTrue(fireproofing.isAllowed(cookieDomain: ".example.com")) + XCTAssertFalse(fireproofing.isAllowed(cookieDomain: "subdomain.example.com")) + XCTAssertFalse(fireproofing.isAllowed(cookieDomain: ".subdomain.example.com")) + } + func testWhenNewThenAllowedDomainsIsEmpty() { let fireproofing = UserDefaultsFireproofing() XCTAssertTrue(fireproofing.allowedDomains.isEmpty) } + func testDuckDuckGoIsFireproofed() { + let fireproofing = UserDefaultsFireproofing() + XCTAssertTrue(fireproofing.isAllowed(fireproofDomain: "duckduckgo.com")) + XCTAssertTrue(fireproofing.isAllowed(cookieDomain: "duckduckgo.com")) + XCTAssertTrue(fireproofing.isAllowed(cookieDomain: SubscriptionCookieManager.cookieDomain)) + XCTAssertFalse(fireproofing.isAllowed(cookieDomain: "test.duckduckgo.com")) + } + } diff --git a/WebViewUnitTests/WebCacheManagerTests.swift b/WebViewUnitTests/WebCacheManagerTests.swift index 785f513ffb..6fa7be86c1 100644 --- a/WebViewUnitTests/WebCacheManagerTests.swift +++ b/WebViewUnitTests/WebCacheManagerTests.swift @@ -47,189 +47,174 @@ extension HTTPCookie { class WebCacheManagerTests: XCTestCase { - override func setUp() { - super.setUp() - CookieStorage().cookies = [] - CookieStorage().isConsumed = true - - if #available(iOS 17, *) { - WKWebsiteDataStore.fetchAllDataStoreIdentifiers { uuids in - uuids.forEach { - WKWebsiteDataStore.remove(forIdentifier: $0, completionHandler: { _ in }) - } - } - } + let keyValueStore = MockKeyValueStore() + + lazy var cookieStorage = MigratableCookieStorage(store: keyValueStore) + lazy var fireproofing = MockFireproofing() + lazy var dataStoreIDManager = DataStoreIDManager(store: keyValueStore) + let dataStoreCleaner = MockDataStoreCleaner() + let observationsCleaner = MockObservationsCleaner() + + func test_whenNewInstall_ThenUsesDefaultPersistence() async { + let dataStore = await WKWebsiteDataStore.current() + let defaultStore = await WKWebsiteDataStore.default() + XCTAssertTrue(dataStore === defaultStore) } - @available(iOS 17, *) - @MainActor - func testEnsureIdAllocatedAfterClearing() async throws { - let fireproofing = MockFireproofing(domains: []) + func test_whenClearingData_ThenCookiesAreRemoved() async { + let dataStore = await WKWebsiteDataStore.default() + await dataStore.httpCookieStore.setCookie(.make(name: "Test", value: "Value", domain: "example.com")) + + var cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(1, cookies.count) + + let webCacheManager = await makeWebCacheManager() + await webCacheManager.clear(dataStore: dataStore) - let storage = CookieStorage() + cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(0, cookies.count) + } - let inMemoryDataStoreIdManager = DataStoreIdManager(store: MockKeyValueStore()) - XCTAssertNil(inMemoryDataStoreIdManager.currentId) + func test_WhenClearingDefaultPersistence_ThenLeaveFireproofedCookies() async { + fireproofing = MockFireproofing(domains: ["example.com"]) - await WebCacheManager.shared.clear(cookieStorage: storage, fireproofing: fireproofing, dataStoreIdManager: inMemoryDataStoreIdManager) + let dataStore = await WKWebsiteDataStore.default() + await dataStore.httpCookieStore.setCookie(.make(name: "Test1", value: "Value", domain: "example.com")) + await dataStore.httpCookieStore.setCookie(.make(name: "Test2", value: "Value", domain: ".example.com")) + await dataStore.httpCookieStore.setCookie(.make(name: "Test3", value: "Value", domain: "facebook.com")) - XCTAssertNotNil(inMemoryDataStoreIdManager.currentId) - let oldId = inMemoryDataStoreIdManager.currentId?.uuidString - XCTAssertNotNil(oldId) + var cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(3, cookies.count) - await WebCacheManager.shared.clear(cookieStorage: storage, fireproofing: fireproofing, dataStoreIdManager: inMemoryDataStoreIdManager) + let webCacheManager = await makeWebCacheManager() + await webCacheManager.clear(dataStore: dataStore) - XCTAssertNotNil(inMemoryDataStoreIdManager.currentId) - XCTAssertNotEqual(inMemoryDataStoreIdManager.currentId?.uuidString, oldId) + cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(2, cookies.count) + XCTAssertTrue(cookies.contains(where: { $0.domain == "example.com" })) + XCTAssertTrue(cookies.contains(where: { $0.domain == ".example.com" })) } - @available(iOS 17, *) - @MainActor - func testWhenCookiesHaveSubDomainsOnSubDomainsAndWidlcardsThenOnlyMatchingCookiesRetained() async throws { - let fireproofing = MockFireproofing(domains: ["mobile.twitter.com"]) + func test_WhenClearingDataAfterUsingContainer_ThenCookiesAreMigratedAndOldContainersAreRemoved() async { + // Mock having a single container so we can validate cleaning it gets called + dataStoreCleaner.countContainersReturnValue = 1 - let defaultStore = WKWebsiteDataStore.default() - await defaultStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) + // Mock a data store id to force migration to happen + keyValueStore.store = [DataStoreIDManager.Constants.currentWebContainerID.rawValue: UUID().uuidString] + dataStoreIDManager = DataStoreIDManager(store: keyValueStore) - let initialCount = await defaultStore.httpCookieStore.allCookies().count - XCTAssertEqual(0, initialCount) + fireproofing = MockFireproofing(domains: ["example.com"]) - await defaultStore.httpCookieStore.setCookie(.make(domain: "twitter.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: ".twitter.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: "mobile.twitter.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: "fake.mobile.twitter.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: ".fake.mobile.twitter.com")) + MigratableCookieStorage.addCookies([ + .make(name: "Test1", value: "Value", domain: "example.com"), + .make(name: "Test2", value: "Value", domain: ".example.com"), + .make(name: "Test3", value: "Value", domain: "facebook.com"), + ], keyValueStore) - let loadedCount = await defaultStore.httpCookieStore.allCookies().count - XCTAssertEqual(5, loadedCount) + let dataStore = await WKWebsiteDataStore.default() + var cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(0, cookies.count) - let cookieStore = CookieStorage() - await WebCacheManager.shared.clear(cookieStorage: cookieStore, fireproofing: fireproofing, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) + let webCacheManager = await makeWebCacheManager() + await webCacheManager.clear(dataStore: dataStore) - let cookies = await defaultStore.httpCookieStore.allCookies() - XCTAssertEqual(cookies.count, 0) + cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(2, cookies.count) + XCTAssertTrue(cookies.contains(where: { $0.domain == "example.com" })) + XCTAssertTrue(cookies.contains(where: { $0.domain == ".example.com" })) - XCTAssertEqual(2, cookieStore.cookies.count) - XCTAssertTrue(cookieStore.cookies.contains(where: { $0.domain == ".twitter.com" })) - XCTAssertTrue(cookieStore.cookies.contains(where: { $0.domain == "mobile.twitter.com" })) + XCTAssertEqual(1, dataStoreCleaner.removeAllContainersAfterDelayCalls.count) + XCTAssertEqual(1, dataStoreCleaner.removeAllContainersAfterDelayCalls[0]) } - - @MainActor - func testWhenRemovingCookieForDomainThenItIsRemovedFromCookieStorage() async { - let defaultStore = WKWebsiteDataStore.default() - await defaultStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) - - let initialCount = await defaultStore.httpCookieStore.allCookies().count - XCTAssertEqual(0, initialCount) - - await defaultStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) - await defaultStore.httpCookieStore.setCookie(.make(domain: "www.example.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: ".example.com")) - let cookies = await defaultStore.httpCookieStore.allCookies() - XCTAssertEqual(cookies.count, 2) - - await WebCacheManager.shared.removeCookies(forDomains: ["www.example.com"], dataStore: WKWebsiteDataStore.default()) - let cleanCookies = await defaultStore.httpCookieStore.allCookies() - XCTAssertEqual(cleanCookies.count, 0) + + func test_WhenClearingData_ThenOldContainersAreRemoved() async { + // Mock existence of 5 containers so we can validate that cleaning it is called even without migrations + dataStoreCleaner.countContainersReturnValue = 5 + await makeWebCacheManager().clear(dataStore: .default()) + XCTAssertEqual(1, dataStoreCleaner.removeAllContainersAfterDelayCalls.count) + XCTAssertEqual(5, dataStoreCleaner.removeAllContainersAfterDelayCalls[0]) } - @MainActor - func testWhenClearedThenCookiesWithParentDomainsAreRetained() async { - let fireproofing = MockFireproofing(domains: ["www.example.com"]) + func test_WhenClearingData_ThenObservationsDatabaseIsCleared() async { + XCTAssertEqual(0, observationsCleaner.removeObservationsDataCallCount) + await makeWebCacheManager().clear(dataStore: .default()) + XCTAssertEqual(1, observationsCleaner.removeObservationsDataCallCount) + } - let defaultStore = WKWebsiteDataStore.default() - await defaultStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) + func test_WhenCookiesAreFromPreviousAppWithContainers_ThenTheyAreConsumed() async { - let initialCount = await defaultStore.httpCookieStore.allCookies().count - XCTAssertEqual(0, initialCount) + MigratableCookieStorage.addCookies([ + .make(name: "Test1", value: "Value", domain: "example.com"), + .make(name: "Test2", value: "Value", domain: ".example.com"), + .make(name: "Test3", value: "Value", domain: "facebook.com"), + ], keyValueStore) - await defaultStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) - await defaultStore.httpCookieStore.setCookie(.make(domain: "example.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: ".example.com")) + keyValueStore.set(false, forKey: MigratableCookieStorage.Keys.consumed) - let cookieStorage = CookieStorage() - - await WebCacheManager.shared.clear(cookieStorage: cookieStorage, - fireproofing: fireproofing, - dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) - let cookies = await defaultStore.httpCookieStore.allCookies() + cookieStorage = MigratableCookieStorage(store: keyValueStore) - XCTAssertEqual(cookies.count, 0) - XCTAssertEqual(cookieStorage.cookies.count, 1) - XCTAssertEqual(cookieStorage.cookies[0].domain, ".example.com") - } + let dataStore = await WKWebsiteDataStore.default() + let httpCookieStore = await dataStore.httpCookieStore + await makeWebCacheManager().consumeCookies(into: httpCookieStore) - @MainActor - @available(iOS 17, *) - func testWhenClearedWithDataStoreContainerThenDDGCookiesAreRetained() async throws { - throw XCTSkip("WKWebsiteDataStore(forIdentifier:) does not persist cookies properly until attached to a running webview") - - // This test should look like `testWhenClearedWithLegacyContainerThenDDGCookiesAreRetained` but - // with a container ID set on the `dataStoreIdManager`. + XCTAssertTrue(self.cookieStorage.isConsumed) + XCTAssertTrue(self.cookieStorage.cookies.isEmpty) + + let cookies = await httpCookieStore.allCookies() + XCTAssertEqual(3, cookies.count) } - @MainActor - func testWhenClearedWithLegacyContainerThenDDGCookiesAreRetained() async { - let fireproofing = MockFireproofing(domains: ["www.example.com"]) + func test_WhenRemoveCookiesForDomains_ThenUnaffectedLeftBehind() async { + let dataStore = await WKWebsiteDataStore.default() + await dataStore.httpCookieStore.setCookie(.make(name: "Test1", value: "Value", domain: "example.com")) + await dataStore.httpCookieStore.setCookie(.make(name: "Test4", value: "Value", domain: "sample.com")) + await dataStore.httpCookieStore.setCookie(.make(name: "Test2", value: "Value", domain: ".example.com")) + await dataStore.httpCookieStore.setCookie(.make(name: "Test3", value: "Value", domain: "facebook.com")) - let cookieStore = WKWebsiteDataStore.default().httpCookieStore - await cookieStore.setCookie(.make(name: "name", value: "value", domain: "duckduckgo.com")) - await cookieStore.setCookie(.make(name: "name", value: "value", domain: "subdomain.duckduckgo.com")) + var cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(4, cookies.count) - let storage = CookieStorage() - storage.isConsumed = true - - await WebCacheManager.shared.clear(cookieStorage: storage, fireproofing: fireproofing, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) + let webCacheManager = await makeWebCacheManager() + await webCacheManager.removeCookies(forDomains: ["example.com", "sample.com"], fromDataStore: dataStore) - XCTAssertEqual(storage.cookies.count, 2) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "duckduckgo.com" })) - XCTAssertTrue(storage.cookies.contains(where: { $0.domain == "subdomain.duckduckgo.com" })) + cookies = await dataStore.httpCookieStore.allCookies() + XCTAssertEqual(1, cookies.count) + XCTAssertTrue(cookies.contains(where: { $0.domain == "facebook.com" })) } - + @MainActor - func testWhenClearedThenCookiesForLoginsAreRetained() async { - let fireproofing = MockFireproofing(domains: ["www.example.com"]) + private func makeWebCacheManager() -> WebCacheManager { + return WebCacheManager( + cookieStorage: cookieStorage, + fireproofing: fireproofing, + dataStoreIDManager: dataStoreIDManager, + dataStoreCleaner: dataStoreCleaner, + observationsCleaner: observationsCleaner + ) + } +} - let defaultStore = WKWebsiteDataStore.default() - await defaultStore.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: .distantPast) +class MockDataStoreCleaner: WebsiteDataStoreCleaning { - let initialCount = await defaultStore.httpCookieStore.allCookies().count - XCTAssertEqual(0, initialCount) + var countContainersReturnValue = 0 + var removeAllContainersAfterDelayCalls: [Int] = [] - await defaultStore.httpCookieStore.setCookie(.make(domain: "www.example.com")) - await defaultStore.httpCookieStore.setCookie(.make(domain: "facebook.com")) + func countContainers() async -> Int { + return countContainersReturnValue + } + + func removeAllContainersAfterDelay(previousCount: Int) { + removeAllContainersAfterDelayCalls.append(previousCount) + } - let loadedCount = await defaultStore.httpCookieStore.allCookies().count - XCTAssertEqual(2, loadedCount) +} - let cookieStore = CookieStorage() - - await WebCacheManager.shared.clear(cookieStorage: cookieStore, fireproofing: fireproofing, dataStoreIdManager: DataStoreIdManager(store: MockKeyValueStore())) +class MockObservationsCleaner: ObservationsDataCleaning { - let cookies = await defaultStore.httpCookieStore.allCookies() - XCTAssertEqual(cookies.count, 0) - - XCTAssertEqual(1, cookieStore.cookies.count) - XCTAssertEqual(cookieStore.cookies[0].domain, "www.example.com") - } + var removeObservationsDataCallCount = 0 - @MainActor - func x_testWhenAccessingObservationsDbThenValidDatabasePoolIsReturned() { - let pool = WebCacheManager.shared.getValidDatabasePool() - XCTAssertNotNil(pool, "DatabasePool should not be nil") + func removeObservationsData() async { + removeObservationsDataCallCount += 1 } - // MARK: Mocks - - class MockFireproofing: UserDefaultsFireproofing { - override var allowedDomains: [String] { - return domains - } - - let domains: [String] - init(domains: [String]) { - self.domains = domains - } - } - } From 414460b3cdc58e0a6416bfc62702228a68659622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Tue, 3 Dec 2024 11:42:07 +0100 Subject: [PATCH 7/8] Make a simple state machine to identify incorrect transitions (#3660) Task/Issue URL: https://app.asana.com/0/0/1208878879182791/f Tech Design URL: https://app.asana.com/0/481882893211075/1208859623176995/f CC: @bwaresiak **Description**: 1. Implement simple state machine with empty states and AppDelegate lifecycle event triggers 2. Fire a pixel on incorrect transition **Steps to test this PR**: 1. Duplicate any event, e.g. write appStateMachine.handle(.backgrounding(application)) twice 2. Go to background and see the pixel `m_debug_app-did-transition-to-unexpected-state` is being sent --- Core/Pixel.swift | 3 + Core/PixelEvent.swift | 6 + DuckDuckGo.xcodeproj/project.pbxproj | 44 +++++++ DuckDuckGo/AppDelegate.swift | 8 ++ DuckDuckGo/AppLifecycle/AppStateMachine.swift | 53 ++++++++ .../AppLifecycle/AppStateTransitions.swift | 118 ++++++++++++++++++ .../AppLifecycle/AppStates/Active.swift | 28 +++++ .../AppLifecycle/AppStates/Background.swift | 28 +++++ .../AppLifecycle/AppStates/Inactive.swift | 28 +++++ DuckDuckGo/AppLifecycle/AppStates/Init.swift | 22 ++++ .../AppLifecycle/AppStates/Launched.swift | 28 +++++ 11 files changed, 366 insertions(+) create mode 100644 DuckDuckGo/AppLifecycle/AppStateMachine.swift create mode 100644 DuckDuckGo/AppLifecycle/AppStateTransitions.swift create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Active.swift create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Background.swift create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Inactive.swift create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Init.swift create mode 100644 DuckDuckGo/AppLifecycle/AppStates/Launched.swift diff --git a/Core/Pixel.swift b/Core/Pixel.swift index c9d4e38470..04f48ee03f 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -161,6 +161,9 @@ public struct PixelParameters { public static let retriedPixel = "retriedPixel" public static let time = "time" + + public static let appState = "state" + public static let appEvent = "event" } public struct PixelValues { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index d45a37bd61..b98773df96 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -895,6 +895,9 @@ extension Pixel { case appDidShowUITime(time: BucketAggregation) case appDidBecomeActiveTime(time: BucketAggregation) + // MARK: Lifecycle + case appDidTransitionToUnexpectedState + } } @@ -1784,6 +1787,9 @@ extension Pixel.Event { case .appDidShowUITime(let time): return "m_debug_app-did-show-ui-time-\(time)" case .appDidBecomeActiveTime(let time): return "m_debug_app-did-become-active-time-\(time)" + // MARK: Lifecycle + case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state" + } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3e7dec040f..90ef0ac981 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -987,6 +987,13 @@ CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9B873B278C8FEA001F4906 /* WidgetEducationView.swift */; }; CB9B873E278C93C2001F4906 /* HomeMessage.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CB9B873D278C93C2001F4906 /* HomeMessage.xcassets */; }; CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; + CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; + CBAD0EFB2CFE1D41006267B8 /* Launched.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */; }; + CBAD0EFD2CFE1D4B006267B8 /* Active.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFC2CFE1D48006267B8 /* Active.swift */; }; + CBAD0EFF2CFE1D50006267B8 /* Inactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */; }; + CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F002CFE1D54006267B8 /* Background.swift */; }; + CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; + CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; @@ -2824,6 +2831,13 @@ CBA1DE942AF6D579007C9457 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextExtension.swift; sourceTree = ""; }; CBAA195B27C3982A00A4BD49 /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; + CBAD0EF82CFE1D35006267B8 /* Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Init.swift; sourceTree = ""; }; + CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Launched.swift; sourceTree = ""; }; + CBAD0EFC2CFE1D48006267B8 /* Active.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Active.swift; sourceTree = ""; }; + CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inactive.swift; sourceTree = ""; }; + CBAD0F002CFE1D54006267B8 /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; + CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateMachine.swift; sourceTree = ""; }; + CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTransitions.swift; sourceTree = ""; }; CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; @@ -5447,6 +5461,28 @@ name = Resources; sourceTree = ""; }; + CBAD0EF72CFE1D14006267B8 /* AppStates */ = { + isa = PBXGroup; + children = ( + CBAD0EF82CFE1D35006267B8 /* Init.swift */, + CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */, + CBAD0EFC2CFE1D48006267B8 /* Active.swift */, + CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */, + CBAD0F002CFE1D54006267B8 /* Background.swift */, + ); + path = AppStates; + sourceTree = ""; + }; + CBAD0F042CFE1DA2006267B8 /* AppLifecycle */ = { + isa = PBXGroup; + children = ( + CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */, + CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */, + CBAD0EF72CFE1D14006267B8 /* AppStates */, + ); + path = AppLifecycle; + sourceTree = ""; + }; D62EC3B72C24695800FC9D04 /* DuckPlayer */ = { isa = PBXGroup; children = ( @@ -6336,6 +6372,7 @@ F1C5ECF31E37812900C599A4 /* Application */ = { isa = PBXGroup; children = ( + CBAD0F042CFE1DA2006267B8 /* AppLifecycle */, 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */, CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, @@ -7518,6 +7555,7 @@ BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, + CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */, 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */, 6FE1273D2C204C2500EB5724 /* FavoritesView.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, @@ -7555,6 +7593,7 @@ 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */, + CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 31DE43C42C2C60E800F8C51F /* DuckPlayerModalPresenter.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, @@ -7621,6 +7660,7 @@ BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */, F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */, 1EE52ABB28FB1D6300B750C1 /* UIImageExtension.swift in Sources */, + CBAD0EFF2CFE1D50006267B8 /* Inactive.swift in Sources */, 858650D12469BCDE00C36F8A /* DaxDialogs.swift in Sources */, 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, @@ -7661,6 +7701,7 @@ 859DB8132CE6263C001F7210 /* TextZoomStorage.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, 8C4838B5221C8F7F008A6739 /* GestureToolbarButton.swift in Sources */, + CBAD0EFD2CFE1D4B006267B8 /* Active.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, 859DB8172CE6263C001F7210 /* TextZoomLevel.swift in Sources */, BDE91CD62C6294020005CB74 /* FeedbackCategoryProviding.swift in Sources */, @@ -7756,6 +7797,7 @@ D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 1D200C9B2BA31A6A00108701 /* AboutView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, + CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */, D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, @@ -7948,6 +7990,7 @@ 311BD1B12836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift in Sources */, B652DF13287C373A00C12A9C /* ScriptSourceProviding.swift in Sources */, 854A012B2A54412600FCC628 /* ActivityViewController.swift in Sources */, + CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */, F1CA3C391F045885005FADB3 /* PrivacyUserDefaults.swift in Sources */, 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */, 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */, @@ -8034,6 +8077,7 @@ 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */, F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */, + CBAD0EFB2CFE1D41006267B8 /* Launched.swift in Sources */, 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */, 85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */, D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 8ef177dd59..1f6cb0cf60 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -117,6 +117,8 @@ import os.log private var didFinishLaunchingStartTime: CFAbsoluteTime? + private let appStateMachine = AppStateMachine() + override init() { super.init() @@ -131,6 +133,7 @@ import os.log // swiftlint:disable:next cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + appStateMachine.handle(.launching(application, launchOptions: launchOptions)) didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() defer { if let didFinishLaunchingStartTime { @@ -597,6 +600,8 @@ import os.log func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } + appStateMachine.handle(.activating(application)) + defer { if let didFinishLaunchingStartTime { let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime @@ -700,6 +705,7 @@ import os.log } func applicationWillResignActive(_ application: UIApplication) { + appStateMachine.handle(.suspending(application)) Task { @MainActor in await refreshShortcuts() await vpnWorkaround.removeRedditSessionWorkaround() @@ -792,6 +798,7 @@ import os.log } func applicationDidEnterBackground(_ application: UIApplication) { + appStateMachine.handle(.backgrounding(application)) displayBlankSnapshotWindow() autoClear?.startClearingTimer() lastBackgroundDate = Date() @@ -837,6 +844,7 @@ import os.log func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { Logger.sync.debug("App launched with url \(url.absoluteString)") + appStateMachine.handle(.openURL(url)) // If showing the onboarding intro ignore deeplinks guard mainViewController?.needsToShowOnboardingIntro() == false else { diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift new file mode 100644 index 0000000000..53fb3b1bf0 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -0,0 +1,53 @@ +// +// AppStateMachine.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +enum AppEvent { + + case launching(UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) + case activating(UIApplication) + case backgrounding(UIApplication) + case suspending(UIApplication) + + case openURL(URL) + +} + +protocol AppState { + + func apply(event: AppEvent) -> any AppState + +} + +protocol AppEventHandler { + + func handle(_ event: AppEvent) + +} + +final class AppStateMachine: AppEventHandler { + + private(set) var currentState: any AppState = Init() + + func handle(_ event: AppEvent) { + currentState = currentState.apply(event: event) + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift new file mode 100644 index 0000000000..8e30596fee --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -0,0 +1,118 @@ +// +// AppStateTransitions.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import os.log +import Core + +extension Init { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .launching(let application, let launchOptions): + return Launched(application: application, launchOptions: launchOptions) + default: + return handleUnexpectedEvent(event) + } + } + +} + +extension Launched { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .activating(let application): + return Active(application: application) + case .openURL: + return self + case .launching, .suspending, .backgrounding: + return handleUnexpectedEvent(event) + } + } + +} + +extension Active { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .suspending(let application): + return Inactive(application: application) + case .launching, .activating, .backgrounding, .openURL: + return handleUnexpectedEvent(event) + } + } + +} + +extension Inactive { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .backgrounding(let application): + return Background(application: application) + case .activating(let application): + return Active(application: application) + case .launching, .suspending, .openURL: + return handleUnexpectedEvent(event) + } + } + +} + +extension Background { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .activating(let application): + return Active(application: application) + case .openURL: + return self + case .launching, .suspending, .backgrounding: + return handleUnexpectedEvent(event) + } + } + +} + +extension AppEvent { + + var rawValue: String { + switch self { + case .launching: return "launching" + case .activating: return "activating" + case .backgrounding: return "backgrounding" + case .suspending: return "suspending" + case .openURL: return "openURL" + } + } + +} + +extension AppState { + + func handleUnexpectedEvent(_ event: AppEvent) -> Self { + Logger.lifecycle.error("Invalid transition (\(event.rawValue)) for state (\(type(of: self)))") + DailyPixel.fireDailyAndCount(pixel: .appDidTransitionToUnexpectedState, + withAdditionalParameters: [PixelParameters.appState: String(describing: type(of: self)), + PixelParameters.appEvent: event.rawValue]) + return self + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift new file mode 100644 index 0000000000..df99c36d50 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -0,0 +1,28 @@ +// +// Active.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Active: AppState { + + init(application: UIApplication) { + + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift new file mode 100644 index 0000000000..9332e41b6b --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -0,0 +1,28 @@ +// +// Background.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Background: AppState { + + init(application: UIApplication) { + + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift new file mode 100644 index 0000000000..888ef34e09 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -0,0 +1,28 @@ +// +// Inactive.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Inactive: AppState { + + init(application: UIApplication) { + + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Init.swift new file mode 100644 index 0000000000..d68d714ea5 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Init.swift @@ -0,0 +1,22 @@ +// +// Init.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +struct Init: AppState { + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift new file mode 100644 index 0000000000..674bcf0b91 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -0,0 +1,28 @@ +// +// Launched.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Launched: AppState { + + init(application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { + + } + +} From a5dab9869c04d8834321ae63e7f58cf078aa5e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Tue, 3 Dec 2024 12:43:17 +0100 Subject: [PATCH 8/8] Release 7.148.0-1 (#3661) Please make sure all GH checks passed before merging. It can take around 20 minutes. Briefly review this PR to see if there are no issues or red flags and then merge it. --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 90ef0ac981..74091cfaf2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -9431,7 +9431,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9468,7 +9468,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9558,7 +9558,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9585,7 +9585,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9731,7 +9731,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9755,7 +9755,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9822,7 +9822,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9856,7 +9856,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9889,7 +9889,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -9919,7 +9919,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10308,7 +10308,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10339,7 +10339,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10367,7 +10367,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -10400,7 +10400,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -10430,7 +10430,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProviderAlpha.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -10463,11 +10463,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -10697,7 +10697,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10725,7 +10725,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10757,7 +10757,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10794,7 +10794,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -10829,7 +10829,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10864,11 +10864,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -11040,11 +11040,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -11073,10 +11073,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 0; + DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";