diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ab626b81fc..1035013e92 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -7,14 +7,39 @@ objects = { /* Begin PBXBuildFile section */ + 020807B22C6CFF95006F94C4 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 020807B12C6CFF95006F94C4 /* Configuration */; }; 021EA0802BD2A9D500772C9A /* TabsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */; }; 021EA0812BD2A9D500772C9A /* TabsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA07F2BD2A9D500772C9A /* TabsPreferences.swift */; }; 021EA0842BD6E01A00772C9A /* TabsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA0822BD6DF1B00772C9A /* TabsPreferencesTests.swift */; }; 021EA0852BD6E0EB00772C9A /* TabsPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EA0822BD6DF1B00772C9A /* TabsPreferencesTests.swift */; }; 0230C0A3272080090018F728 /* KeyedCodingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */; }; + 02589D9D2C88E8210093940D /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 02589D9C2C88E8210093940D /* Configuration */; }; + 02589D9F2C88E8270093940D /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02589D9E2C88E8270093940D /* Persistence */; }; + 02589DA12C88EB570093940D /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 02589DA02C88EB570093940D /* Configuration */; }; + 02589DA32C88EB5D0093940D /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02589DA22C88EB5D0093940D /* Persistence */; }; + 02589DA52C88ED190093940D /* BrowserServicesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02589DA42C88ED190093940D /* BrowserServicesKit */; }; + 02589DA72C88F8E90093940D /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02589DA62C88F8E90093940D /* PixelKit */; }; + 02589DA92C89F0420093940D /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 02589DA82C89F0420093940D /* Configuration */; }; + 02589DAB2C89F0490093940D /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02589DAA2C89F0490093940D /* Persistence */; }; 026ADE1426C3010C002518EE /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 028904202A7B25380028369C /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */; }; 028904212A7B25770028369C /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */; }; + 02A15D8C2C88D763001A4237 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D8B2C88D763001A4237 /* Configuration */; }; + 02A15D8E2C88D76A001A4237 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D8D2C88D76A001A4237 /* Persistence */; }; + 02A15D902C88D773001A4237 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D8F2C88D773001A4237 /* Persistence */; }; + 02A15D922C88D789001A4237 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D912C88D789001A4237 /* Configuration */; }; + 02A15D942C88D78F001A4237 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D932C88D78F001A4237 /* Persistence */; }; + 02A15D962C88D797001A4237 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D952C88D797001A4237 /* Configuration */; }; + 02A15D982C88D79D001A4237 /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = 02A15D972C88D79D001A4237 /* Persistence */; }; + 02FDA6592C764B970024CD8B /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA6582C764B970024CD8B /* ConfigurationManager.swift */; }; + 02FDA65B2C764C200024CD8B /* ConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA65A2C764C200024CD8B /* ConfigurationStore.swift */; }; + 02FDA65C2C764CB00024CD8B /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA6582C764B970024CD8B /* ConfigurationManager.swift */; }; + 02FDA65D2C764CB30024CD8B /* ConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA65A2C764C200024CD8B /* ConfigurationStore.swift */; }; + 02FDA65F2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA65E2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift */; }; + 02FDA6602C764E220024CD8B /* VPNPrivacyConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA65E2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift */; }; + 02FDA6622C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA6612C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift */; }; + 02FDA6632C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA6612C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift */; }; + 02FDA6642C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FDA6612C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift */; }; 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142879D924CE1179005419BB /* SuggestionViewModelTests.swift */; }; 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142879DB24CE1185005419BB /* SuggestionContainerViewModelTests.swift */; }; 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1430DFF424D0580F00B8978C /* TabBarViewController.swift */; }; @@ -3141,6 +3166,10 @@ 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedCodingExtension.swift; sourceTree = ""; }; 026ADE1326C3010C002518EE /* macos-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "macos-config.json"; sourceTree = ""; }; 0289041E2A7B23CE0028369C /* AppConfigurationURLProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProviderTests.swift; sourceTree = ""; }; + 02FDA6582C764B970024CD8B /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; + 02FDA65A2C764C200024CD8B /* ConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationStore.swift; sourceTree = ""; }; + 02FDA65E2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNPrivacyConfigurationManager.swift; sourceTree = ""; }; + 02FDA6612C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNAgentConfigurationURLProvider.swift; sourceTree = ""; }; 142879D924CE1179005419BB /* SuggestionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionViewModelTests.swift; sourceTree = ""; }; 142879DB24CE1185005419BB /* SuggestionContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionContainerViewModelTests.swift; sourceTree = ""; }; 1430DFF424D0580F00B8978C /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; @@ -4700,10 +4729,12 @@ 9D9DE57B2C63AA1F00D20B15 /* AppKitExtensions in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 7B37C7A52BAA32A50062546A /* Subscription in Frameworks */, + 02589DA12C88EB570093940D /* Configuration in Frameworks */, F198C7182BD18A4C000BF24D /* PixelKit in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */, 4B25375B2A11BE7300610219 /* NetworkExtension.framework in Frameworks */, + 02589DA32C88EB5D0093940D /* Persistence in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4712,10 +4743,12 @@ buildActionMask = 2147483647; files = ( F198C71A2BD18A5B000BF24D /* PixelKit in Frameworks */, + 02A15D922C88D789001A4237 /* Configuration in Frameworks */, 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, + 02A15D942C88D78F001A4237 /* Persistence in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, 7B8594192B5B26230007EB3E /* UDSHelper in Frameworks */, BDADBDC92BD2BC2200421B9B /* Lottie in Frameworks */, @@ -4735,7 +4768,9 @@ 7B23668E2C09FAFA002D393F /* VPNAppLauncher in Frameworks */, 7BA7CC612AD1211C0042E5CE /* Networking in Frameworks */, F198C71C2BD18A61000BF24D /* PixelKit in Frameworks */, + 02A15D982C88D79D001A4237 /* Persistence in Frameworks */, 7BEEA5142AD1236300A9E72B /* NetworkProtectionIPC in Frameworks */, + 02A15D962C88D797001A4237 /* Configuration in Frameworks */, BDADBDCB2BD2BC2800421B9B /* Lottie in Frameworks */, 9D9DE5832C63BE9600D20B15 /* AppKitExtensions in Frameworks */, 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */, @@ -4751,7 +4786,11 @@ files = ( 7B23668A2C09FAE8002D393F /* VPNAppLauncher in Frameworks */, 7B624F172BA25C1F00A6C544 /* NetworkProtectionUI in Frameworks */, + 02589DAB2C89F0490093940D /* Persistence in Frameworks */, + 02589DA52C88ED190093940D /* BrowserServicesKit in Frameworks */, 37269F052B3332C2005E8E46 /* Common in Frameworks */, + 02589DA72C88F8E90093940D /* PixelKit in Frameworks */, + 02589DA92C89F0420093940D /* Configuration in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4759,11 +4798,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 02589D9F2C88E8270093940D /* Persistence in Frameworks */, F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */, 4B5235472C7BB15700AFAF64 /* WireGuard in Frameworks */, 37269EFF2B332FBB005E8E46 /* Common in Frameworks */, EE7295E72A545BBB008C0991 /* NetworkProtection in Frameworks */, F198C7162BD18A44000BF24D /* PixelKit in Frameworks */, + 02589D9D2C88E8210093940D /* Configuration in Frameworks */, 4B4D60AF2A0C837F00BCD287 /* Networking in Frameworks */, 7B2366882C09FADA002D393F /* VPNAppLauncher in Frameworks */, 7B25856E2BA2F2ED00D49F79 /* NetworkProtectionUI in Frameworks */, @@ -4805,7 +4846,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 02A15D902C88D773001A4237 /* Persistence in Frameworks */, 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */, + 020807B22C6CFF95006F94C4 /* Configuration in Frameworks */, F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */, C18BF9D02C736C9100ED6B8A /* Freemium in Frameworks */, 9D9AE8F92AAA3AD00026E7DC /* DataBrokerProtection in Frameworks */, @@ -4816,6 +4859,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 02A15D8E2C88D76A001A4237 /* Persistence in Frameworks */, + 02A15D8C2C88D763001A4237 /* Configuration in Frameworks */, 315A023F2B6421AE00BFA577 /* Networking in Frameworks */, F1D042902BFB9FA300A31506 /* Subscription in Frameworks */, C18BF9D22C736C9700ED6B8A /* Freemium in Frameworks */, @@ -6596,6 +6641,10 @@ isa = PBXGroup; children = ( 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, + 02FDA6582C764B970024CD8B /* ConfigurationManager.swift */, + 02FDA65A2C764C200024CD8B /* ConfigurationStore.swift */, + 02FDA65E2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift */, + 02FDA6612C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift */, 7B60AFF92C511B65008E32A3 /* VPNUIActionHandler.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, @@ -9297,6 +9346,8 @@ F198C7172BD18A4C000BF24D /* PixelKit */, 9D9DE57A2C63AA1F00D20B15 /* AppKitExtensions */, 4B5235442C7BB14D00AFAF64 /* WireGuard */, + 02589DA02C88EB570093940D /* Configuration */, + 02589DA22C88EB5D0093940D /* Persistence */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -9332,6 +9383,8 @@ BDADBDC82BD2BC2200421B9B /* Lottie */, 7B23668B2C09FAF1002D393F /* VPNAppLauncher */, 9D9DE5802C63BA0B00D20B15 /* AppKitExtensions */, + 02A15D912C88D789001A4237 /* Configuration */, + 02A15D932C88D78F001A4237 /* Persistence */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -9367,6 +9420,8 @@ BDADBDCA2BD2BC2800421B9B /* Lottie */, 7B23668D2C09FAFA002D393F /* VPNAppLauncher */, 9D9DE5822C63BE9600D20B15 /* AppKitExtensions */, + 02A15D952C88D797001A4237 /* Configuration */, + 02A15D972C88D79D001A4237 /* Persistence */, ); productName = DuckDuckGoAgentAppStore; productReference = 4B2D06692A13318400DE1F49 /* DuckDuckGo VPN App Store.app */; @@ -9390,6 +9445,10 @@ 37269F042B3332C2005E8E46 /* Common */, 7B624F162BA25C1F00A6C544 /* NetworkProtectionUI */, 7B2366892C09FAE8002D393F /* VPNAppLauncher */, + 02589DA42C88ED190093940D /* BrowserServicesKit */, + 02589DA62C88F8E90093940D /* PixelKit */, + 02589DA82C89F0420093940D /* Configuration */, + 02589DAA2C89F0490093940D /* Persistence */, ); productName = DuckDuckGoNotifications; productReference = 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */; @@ -9419,6 +9478,8 @@ 7B2366872C09FADA002D393F /* VPNAppLauncher */, 9D9DE5762C63AA1600D20B15 /* AppKitExtensions */, 4B5235462C7BB15700AFAF64 /* WireGuard */, + 02589D9C2C88E8210093940D /* Configuration */, + 02589D9E2C88E8270093940D /* Persistence */, ); productName = NetworkProtectionAppExtension; productReference = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; @@ -9505,7 +9566,9 @@ 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */, 9DEF97E02B06C4EE00764F03 /* Networking */, F1D0428D2BFB9F9C00A31506 /* Subscription */, + 020807B12C6CFF95006F94C4 /* Configuration */, C18BF9CF2C736C9100ED6B8A /* Freemium */, + 02A15D8F2C88D773001A4237 /* Persistence */, ); productName = DuckDuckGoAgent; productReference = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; @@ -9530,6 +9593,8 @@ 315A023E2B6421AE00BFA577 /* Networking */, F1D0428F2BFB9FA300A31506 /* Subscription */, C18BF9D12C736C9700ED6B8A /* Freemium */, + 02A15D8B2C88D763001A4237 /* Configuration */, + 02A15D8D2C88D76A001A4237 /* Persistence */, ); productName = DuckDuckGoAgent; productReference = 9D9AE8F22AAA39D30026E7DC /* DuckDuckGo Personal Information Removal App Store.app */; @@ -11734,6 +11799,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 02FDA6632C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift in Sources */, B6F92BA22A691580002ABA6B /* UserDefaultsWrapper.swift in Sources */, 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, @@ -11742,9 +11808,11 @@ F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, + 02FDA6592C764B970024CD8B /* ConfigurationManager.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, + 02FDA65F2C764E220024CD8B /* VPNPrivacyConfigurationManager.swift in Sources */, 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, @@ -11755,6 +11823,7 @@ F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, F1C70D802BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, + 02FDA65B2C764C200024CD8B /* ConfigurationStore.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, 7B60AFFA2C511B65008E32A3 /* VPNUIActionHandler.swift in Sources */, @@ -11773,6 +11842,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 02FDA6642C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift in Sources */, 7B2DDCFB2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, @@ -11781,9 +11851,11 @@ F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + 02FDA65C2C764CB00024CD8B /* ConfigurationManager.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, 4BF0E5082AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, + 02FDA6602C764E220024CD8B /* VPNPrivacyConfigurationManager.swift in Sources */, 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, @@ -11794,6 +11866,7 @@ 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, + 02FDA65D2C764CB30024CD8B /* ConfigurationStore.swift in Sources */, 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7B60AFFB2C511C68008E32A3 /* VPNUIActionHandler.swift in Sources */, 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, @@ -11817,6 +11890,7 @@ 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */, + 02FDA6622C765D0F0024CD8B /* VPNAgentConfigurationURLProvider.swift in Sources */, B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, ); @@ -14152,7 +14226,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 196.0.0; + version = 196.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -14222,6 +14296,86 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 020807B12C6CFF95006F94C4 /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02589D9C2C88E8210093940D /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02589D9E2C88E8270093940D /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; + 02589DA02C88EB570093940D /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02589DA22C88EB5D0093940D /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; + 02589DA42C88ED190093940D /* BrowserServicesKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = BrowserServicesKit; + }; + 02589DA62C88F8E90093940D /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; + 02589DA82C89F0420093940D /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02589DAA2C89F0490093940D /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; + 02A15D8B2C88D763001A4237 /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02A15D8D2C88D76A001A4237 /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; + 02A15D8F2C88D773001A4237 /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; + 02A15D912C88D789001A4237 /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02A15D932C88D78F001A4237 /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; + 02A15D952C88D797001A4237 /* Configuration */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Configuration; + }; + 02A15D972C88D79D001A4237 /* Persistence */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Persistence; + }; 08D4923DC968236E22E373E2 /* Crashes */ = { isa = XCSwiftPackageProductDependency; package = FAE06B199CA1F209B55B34E9 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ffee4ceb9..9df44c6b18 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" : { - "branch" : "196.0.0", - "revision" : "ae3dbec01b8b72dc2ea4c510aecbc802862eab63" + "revision" : "f7083a3c74a4aa1f6a0f4ab65265eb2f422a2cf0", + "version" : "196.1.0" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index d6e36522c7..6431b1ca29 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -98,6 +98,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { public let vpnSettings = VPNSettings(defaults: .netP) + var configurationStore = ConfigurationStore() + var configurationManager: ConfigurationManager + // MARK: - VPN private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler? @@ -174,6 +177,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) internalUserDecider = DefaultInternalUserDecider(store: internalUserDeciderStore) + configurationManager = ConfigurationManager(store: configurationStore) + if NSApplication.runType.requiresEnvironment { Self.configurePixelKit() @@ -221,10 +226,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #if DEBUG AppPrivacyFeatures.shared = NSApplication.runType.requiresEnvironment // runtime mock-replacement for Unit Tests, to be redone when we‘ll be doing Dependency Injection - ? AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider), database: Database.shared) + ? AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider, configurationStore: configurationStore), database: Database.shared) : AppPrivacyFeatures(contentBlocking: ContentBlockingMock(), httpsUpgradeStore: HTTPSUpgradeStoreMock()) #else - AppPrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider), database: Database.shared) + AppPrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider, configurationStore: configurationStore), database: Database.shared) #endif if NSApplication.runType.requiresEnvironment { remoteMessagingClient = RemoteMessagingClient( @@ -233,7 +238,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { appearancePreferences: .shared, pinnedTabsManager: pinnedTabsManager, internalUserDecider: internalUserDecider, - configurationStore: ConfigurationStore.shared, + configurationStore: configurationStore, remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider( privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager ) @@ -310,7 +315,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if case .normal = NSApp.runType { FaviconManager.shared.loadFavicons() } - ConfigurationManager.shared.start() + configurationManager.start() _ = DownloadListCoordinator.shared _ = RecentlyClosedCoordinator.shared diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 60f753a1c5..e529fa78b3 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -44,17 +44,6 @@ public struct UserDefaultsWrapper { /// system setting defining window title double-click action case appleActionOnDoubleClick = "AppleActionOnDoubleClick" - case configLastUpdated = "config.last.updated" - case configStorageTrackerRadarEtag = "config.storage.trackerradar.etag" - case configStorageBloomFilterSpecEtag = "config.storage.bloomfilter.spec.etag" - case configStorageBloomFilterBinaryEtag = "config.storage.bloomfilter.binary.etag" - case configStorageBloomFilterExclusionsEtag = "config.storage.bloomfilter.exclusions.etag" - case configStorageSurrogatesEtag = "config.storage.surrogates.etag" - case configStoragePrivacyConfigurationEtag = "config.storage.privacyconfiguration.etag" - case configStorageRemoteMessagingConfigEtag = "config.storage.remotemessagingconfig.etag" - - case configLastInstalled = "config.last.installed" - case fireproofDomains = "com.duckduckgo.fireproofing.allowedDomains" case areDomainsMigratedToETLDPlus1 = "com.duckduckgo.are-domains-migrated-to-etldplus1" case unprotectedDomains = "com.duckduckgo.contentblocker.unprotectedDomains" diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index 4f912030f5..f193bb836a 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -17,60 +17,32 @@ // import Foundation +import os.log import Combine import BrowserServicesKit +import Persistence import Configuration import Common import Networking import PixelKit -import os.log - -final class ConfigurationManager { - enum Error: Swift.Error { - - case timeout - case bloomFilterSpecNotFound - case bloomFilterBinaryNotFound - case bloomFilterPersistenceFailed - case bloomFilterExclusionsNotFound - case bloomFilterExclusionsPersistenceFailed - - func withUnderlyingError(_ underlyingError: Swift.Error) -> Swift.Error { - let nsError = self as NSError - return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSUnderlyingErrorKey: underlyingError]) - } +final class ConfigurationManager: DefaultConfigurationManager { + private enum Constants { + static let lastConfigurationInstallDateKey = "config.last.installed" } - enum Constants { - - static let downloadTimeoutSeconds = 60.0 * 5 -#if DEBUG - static let refreshPeriodSeconds = 60.0 * 2 // 2 minutes -#else - static let refreshPeriodSeconds = 60.0 * 30 // 30 minutes -#endif - static let retryDelaySeconds = 60.0 * 60 * 1 // 1 hour delay before checking again if something went wrong last time - static let refreshCheckIntervalSeconds = 60.0 // check if we need a refresh every minute + private var defaults: KeyValueStoring + private(set) var lastConfigurationInstallDate: Date? { + get { + defaults.object(forKey: Constants.lastConfigurationInstallDateKey) as? Date + } + set { + defaults.set(newValue, forKey: Constants.lastConfigurationInstallDateKey) + } } - static let shared = ConfigurationManager() - static let queue: DispatchQueue = DispatchQueue(label: "Configuration Manager") - - @UserDefaultsWrapper(key: .configLastUpdated, defaultValue: .distantPast) - private(set) var lastUpdateTime: Date - - @UserDefaultsWrapper(key: .configLastInstalled, defaultValue: nil) - private(set) var lastConfigurationInstallDate: Date? - - private var timerCancellable: AnyCancellable? - private var lastRefreshCheckTime: Date = Date() - - private lazy var fetcher = ConfigurationFetcher(store: ConfigurationStore.shared, - eventMapping: Self.configurationDebugEvents) - static let configurationDebugEvents = EventMapping { event, error, _, _ in let domainEvent: GeneralPixel switch event { @@ -81,21 +53,19 @@ final class ConfigurationManager { PixelKit.fire(DebugEvent(domainEvent, error: error)) } - func start() { - Logger.config.debug("Starting configuration refresh timer") - timerCancellable = Timer.publish(every: Constants.refreshCheckIntervalSeconds, on: .main, in: .default) - .autoconnect() - .receive(on: Self.queue) - .sink(receiveValue: { _ in - self.lastRefreshCheckTime = Date() - self.refreshIfNeeded() - }) - Task { - await refreshNow() - } + override init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), + store: ConfigurationStoring = ConfigurationStore(), + defaults: KeyValueStoring = UserDefaults.appConfiguration) { + self.defaults = defaults + super.init(fetcher: fetcher, store: store, defaults: defaults) + } + + func log() { + Logger.config.log("last update \(String(describing: self.lastUpdateTime), privacy: .public)") + Logger.config.log("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)") } - private func refreshNow(isDebug: Bool = false) async { + override public func refreshNow(isDebug: Bool = false) async { let updateTrackerBlockingDependenciesTask = Task { let didFetchAnyTrackerBlockingDependencies = await fetchTrackerBlockingDependencies(isDebug: isDebug) if didFetchAnyTrackerBlockingDependencies { @@ -116,7 +86,7 @@ final class ConfigurationManager { let updateBloomFilterExclusionsTask = Task { do { - try await fetcher.fetch(.bloomFilterExcludedDomains) + try await fetcher.fetch(.bloomFilterExcludedDomains, isDebug: isDebug) try await updateBloomFilterExclusions() tryAgainLater() } catch { @@ -128,7 +98,7 @@ final class ConfigurationManager { await updateBloomFilterTask.value await updateBloomFilterExclusionsTask.value - ConfigurationStore.shared.log() + (store as? ConfigurationStore)?.log() Logger.config.info("last update \(String(describing: self.lastUpdateTime), privacy: .public)") Logger.config.info("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)") @@ -138,8 +108,8 @@ final class ConfigurationManager { var didFetchAnyTrackerBlockingDependencies = false var tasks = [Configuration: Task<(), Swift.Error>]() - tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet) } - tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates) } + tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet, isDebug: isDebug) } + tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates, isDebug: isDebug) } tasks[.privacyConfiguration] = Task { try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) } for (configuration, task) in tasks { @@ -147,7 +117,9 @@ final class ConfigurationManager { try await task.value didFetchAnyTrackerBlockingDependencies = true } catch { - Logger.config.error("Failed to complete configuration update to \(configuration.rawValue): \(error.localizedDescription, privacy: .public)") + Logger.config.error( + "Failed to complete configuration update to \(configuration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) tryAgainSoon() } } @@ -167,48 +139,20 @@ final class ConfigurationManager { tryAgainSoon() } - @discardableResult - public func refreshIfNeeded() -> Task? { - guard isReadyToRefresh else { - Logger.config.debug("Configuration refresh is not needed at this time") - return nil - } - return Task { - await refreshNow() - } - } - - private var isReadyToRefresh: Bool { Date().timeIntervalSince(lastUpdateTime) > Constants.refreshPeriodSeconds } - - public func forceRefresh(isDebug: Bool = false) { - Task { - await refreshNow(isDebug: isDebug) - } - } - - private func tryAgainLater() { - lastUpdateTime = Date() - } - - private func tryAgainSoon() { - // Set the last update time to in the past so it triggers again sooner - lastUpdateTime = Date(timeIntervalSinceNow: Constants.refreshPeriodSeconds - Constants.retryDelaySeconds) - } - private func updateTrackerBlockingDependencies() { lastConfigurationInstallDate = Date() - ContentBlocking.shared.trackerDataManager.reload(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), - data: ConfigurationStore.shared.loadData(for: .trackerDataSet)) - ContentBlocking.shared.privacyConfigurationManager.reload(etag: ConfigurationStore.shared.loadEtag(for: .privacyConfiguration), - data: ConfigurationStore.shared.loadData(for: .privacyConfiguration)) + ContentBlocking.shared.trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet), + data: store.loadData(for: .trackerDataSet)) + ContentBlocking.shared.privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration)) ContentBlocking.shared.contentBlockingManager.scheduleCompilation() } private func updateBloomFilter() async throws { - guard let specData = ConfigurationStore.shared.loadData(for: .bloomFilterSpec) else { + guard let specData = store.loadData(for: .bloomFilterSpec) else { throw Error.bloomFilterSpecNotFound } - guard let bloomFilterData = ConfigurationStore.shared.loadData(for: .bloomFilterBinary) else { + guard let bloomFilterData = store.loadData(for: .bloomFilterBinary) else { throw Error.bloomFilterBinaryNotFound } try await Task.detached { @@ -224,7 +168,7 @@ final class ConfigurationManager { } private func updateBloomFilterExclusions() async throws { - guard let bloomFilterExclusions = ConfigurationStore.shared.loadData(for: .bloomFilterExcludedDomains) else { + guard let bloomFilterExclusions = store.loadData(for: .bloomFilterExcludedDomains) else { throw Error.bloomFilterExclusionsNotFound } try await Task.detached { @@ -239,3 +183,19 @@ final class ConfigurationManager { } } + +extension ConfigurationManager { + override var presentedItemURL: URL? { + store.fileUrl(for: .privacyConfiguration).deletingLastPathComponent() + } + + override func presentedSubitemDidAppear(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateTrackerBlockingDependencies() + } + + override func presentedSubitemDidChange(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateTrackerBlockingDependencies() + } +} diff --git a/DuckDuckGo/Configuration/ConfigurationStore.swift b/DuckDuckGo/Configuration/ConfigurationStore.swift index 0f9b5ddcfd..404daa77ad 100644 --- a/DuckDuckGo/Configuration/ConfigurationStore.swift +++ b/DuckDuckGo/Configuration/ConfigurationStore.swift @@ -17,10 +17,11 @@ // import Common +import os.log import Foundation import Configuration +import Persistence import PixelKit -import os.log final class ConfigurationStore: ConfigurationStoring { @@ -34,30 +35,84 @@ final class ConfigurationStore: ConfigurationStoring { .remoteMessagingConfig: "remote-messaging-config.json" ] - static let shared = ConfigurationStore() + private enum Etag { + static let configStorageTrackerRadarEtag = "config.storage.trackerradar.etag" + static let configStorageBloomFilterSpecEtag = "config.storage.bloomfilter.spec.etag" + static let configStorageBloomFilterBinaryEtag = "config.storage.bloomfilter.binary.etag" + static let configStorageBloomFilterExclusionsEtag = "config.storage.bloomfilter.exclusions.etag" + static let configStorageSurrogatesEtag = "config.storage.surrogates.etag" + static let configStoragePrivacyConfigurationEtag = "config.storage.privacyconfiguration.etag" + static let configStorageRemoteMessagingConfigEtag = "config.storage.remotemessagingconfig.etag" + } - @UserDefaultsWrapper(key: .configStorageTrackerRadarEtag, defaultValue: nil) - private var trackerRadarEtag: String? + private let defaults: KeyValueStoring - @UserDefaultsWrapper(key: .configStorageBloomFilterSpecEtag, defaultValue: nil) - private var bloomFilterSpecEtag: String? + private var trackerRadarEtag: String? { + get { + defaults.object(forKey: Etag.configStorageTrackerRadarEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStorageTrackerRadarEtag) + } + } - @UserDefaultsWrapper(key: .configStorageBloomFilterBinaryEtag, defaultValue: nil) - private var bloomFilterBinaryEtag: String? + private var bloomFilterSpecEtag: String? { + get { + defaults.object(forKey: Etag.configStorageBloomFilterSpecEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStorageBloomFilterSpecEtag) + } + } - @UserDefaultsWrapper(key: .configStorageBloomFilterExclusionsEtag, defaultValue: nil) - private var bloomFilterExcludedDomainsEtag: String? + private var bloomFilterBinaryEtag: String? { + get { + defaults.object(forKey: Etag.configStorageBloomFilterBinaryEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStorageBloomFilterBinaryEtag) + } + } - @UserDefaultsWrapper(key: .configStorageSurrogatesEtag, defaultValue: nil) - private var surrogatesEtag: String? + private var bloomFilterExcludedDomainsEtag: String? { + get { + defaults.object(forKey: Etag.configStorageBloomFilterExclusionsEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStorageBloomFilterExclusionsEtag) + } + } - @UserDefaultsWrapper(key: .configStoragePrivacyConfigurationEtag, defaultValue: nil) - private var privacyConfigurationEtag: String? + private var surrogatesEtag: String? { + get { + defaults.object(forKey: Etag.configStorageSurrogatesEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStorageSurrogatesEtag) + } + } - @UserDefaultsWrapper(key: .configStorageRemoteMessagingConfigEtag, defaultValue: nil) - private var remoteMessagingConfigEtag: String? + private var privacyConfigurationEtag: String? { + get { + defaults.object(forKey: Etag.configStoragePrivacyConfigurationEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStoragePrivacyConfigurationEtag) + } + } + + private var remoteMessagingConfigEtag: String? { + get { + defaults.object(forKey: Etag.configStorageRemoteMessagingConfigEtag) as? String + } + set { + defaults.set(newValue, forKey: Etag.configStorageRemoteMessagingConfigEtag) + } + } - private init() { } + init(defaults: KeyValueStoring = UserDefaults.appConfiguration) { + self.defaults = defaults + } func loadEtag(for configuration: Configuration) -> String? { switch configuration { @@ -93,24 +148,46 @@ final class ConfigurationStore: ConfigurationStoring { func loadData(for config: Configuration) -> Data? { let file = fileUrl(for: config) - do { - return try Data(contentsOf: file) - } catch { - guard NSApp.runType.requiresEnvironment else { return nil } + var data: Data? + var coordinatorError: NSError? - let nserror = error as NSError + NSFileCoordinator().coordinate(readingItemAt: file, error: &coordinatorError) { fileUrl in + do { + data = try Data(contentsOf: fileUrl) + } catch { + guard NSApp.runType.requiresEnvironment else { return } - if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { - PixelKit.fire(DebugEvent(GeneralPixel.trackerDataCouldNotBeLoaded, error: error)) + let nserror = error as NSError + + if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { + PixelKit.fire(DebugEvent(GeneralPixel.trackerDataCouldNotBeLoaded, error: error)) + } } + } - return nil + if let coordinatorError { + PixelKit.fire(DebugEvent(GeneralPixel.configurationFileCoordinatorError, error: coordinatorError)) + Logger.config.error("Unable to read \(config.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") } + + return data } func saveData(_ data: Data, for config: Configuration) throws { let file = fileUrl(for: config) - try data.write(to: file, options: .atomic) + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(writingItemAt: file, options: .forReplacing, error: &coordinatorError) { fileUrl in + do { + try data.write(to: fileUrl, options: .atomic) + } catch { + Logger.config.error("Unable to write \(config.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + } + + if let coordinatorError { + Logger.config.error("Unable to write \(config.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } } func log() { @@ -126,7 +203,9 @@ final class ConfigurationStore: ConfigurationStoring { func fileUrl(for config: Configuration) -> URL { let fm = FileManager.default - let dir = URL.sandboxApplicationSupportURL + guard let dir = fm.containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.appGroup(bundle: .appConfiguration)) else { + fatalError("Failed to get application group URL") + } let subDir = dir.appendingPathComponent("Configuration") var isDir: ObjCBool = false diff --git a/DuckDuckGo/ContentBlocker/ContentBlocking.swift b/DuckDuckGo/ContentBlocker/ContentBlocking.swift index 973622888a..7a3186b95e 100644 --- a/DuckDuckGo/ContentBlocker/ContentBlocking.swift +++ b/DuckDuckGo/ContentBlocker/ContentBlocking.swift @@ -59,17 +59,16 @@ final class AppContentBlocking { // keeping whole ContentBlocking state initialization in one place to avoid races between updates publishing and rules storing @MainActor - init(internalUserDecider: InternalUserDecider) { - let configStorage = ConfigurationStore.shared - privacyConfigurationManager = PrivacyConfigurationManager(fetchedETag: configStorage.loadEtag(for: .privacyConfiguration), - fetchedData: configStorage.loadData(for: .privacyConfiguration), + init(internalUserDecider: InternalUserDecider, configurationStore: ConfigurationStore) { + privacyConfigurationManager = PrivacyConfigurationManager(fetchedETag: configurationStore.loadEtag(for: .privacyConfiguration), + fetchedData: configurationStore.loadData(for: .privacyConfiguration), embeddedDataProvider: AppPrivacyConfigurationDataProvider(), localProtection: LocalUnprotectedDomains.shared, errorReporting: Self.debugEvents, internalUserDecider: internalUserDecider) - trackerDataManager = TrackerDataManager(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), - data: ConfigurationStore.shared.loadData(for: .trackerDataSet), + trackerDataManager = TrackerDataManager(etag: configurationStore.loadEtag(for: .trackerDataSet), + data: configurationStore.loadData(for: .trackerDataSet), embeddedDataProvider: AppTrackerDataSetProvider(), errorReporting: Self.debugEvents) @@ -85,7 +84,7 @@ final class AppContentBlocking { userContentUpdating = UserContentUpdating(contentBlockerRulesManager: contentBlockingManager, privacyConfigurationManager: privacyConfigurationManager, trackerDataManager: trackerDataManager, - configStorage: configStorage, + configStorage: configurationStore, webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, tld: tld) diff --git a/DuckDuckGo/ContentBlocker/Mocks/ContentBlockerRulesManagerMock.swift b/DuckDuckGo/ContentBlocker/Mocks/ContentBlockerRulesManagerMock.swift index 7df870692b..60a95541ea 100644 --- a/DuckDuckGo/ContentBlocker/Mocks/ContentBlockerRulesManagerMock.swift +++ b/DuckDuckGo/ContentBlocker/Mocks/ContentBlockerRulesManagerMock.swift @@ -23,7 +23,7 @@ import Combine final class ContentBlockerRulesManagerMock: NSObject, ContentBlockerRulesManagerProtocol { func scheduleCompilation() -> BrowserServicesKit.ContentBlockerRulesManager.CompletionToken { - fatalError() + BrowserServicesKit.ContentBlockerRulesManager.CompletionToken() } var currentMainRules: BrowserServicesKit.ContentBlockerRulesManager.Rules? diff --git a/DuckDuckGo/ContentBlocker/Mocks/ContentBlockingMock.swift b/DuckDuckGo/ContentBlocker/Mocks/ContentBlockingMock.swift index 6da3d7024c..fb9e5620d5 100644 --- a/DuckDuckGo/ContentBlocker/Mocks/ContentBlockingMock.swift +++ b/DuckDuckGo/ContentBlocker/Mocks/ContentBlockingMock.swift @@ -30,8 +30,8 @@ final class ContentBlockingMock: NSObject, ContentBlockingProtocol, AdClickAttri var embeddedDataEtag: String = "" var embeddedData: Data = .init() } - var trackerDataManager = TrackerDataManager(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), - data: ConfigurationStore.shared.loadData(for: .trackerDataSet), + var trackerDataManager = TrackerDataManager(etag: ConfigurationStore().loadEtag(for: .trackerDataSet), + data: ConfigurationStore().loadData(for: .trackerDataSet), embeddedDataProvider: AppTrackerDataSetProvider(), errorReporting: nil) diff --git a/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift b/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift index f47619c5f1..0422106de2 100644 --- a/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift +++ b/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift @@ -92,7 +92,7 @@ final class MockPrivacyConfigurationManager: NSObject, PrivacyConfigurationManag } func reload(etag: String?, data: Data?) -> BrowserServicesKit.PrivacyConfigurationManager.ReloadResult { - fatalError("not implemented") + return .embedded } var updatesPublisher: AnyPublisher = Just(()).eraseToAnyPublisher() diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index 8f76e1596c..4e3cad6696 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -38,7 +38,7 @@ protocol ScriptSourceProviding { // refactor: ScriptSourceProvider to be passed to init methods as `some ScriptSourceProviding`, DefaultScriptSourceProvider to be killed // swiftlint:disable:next identifier_name @MainActor func DefaultScriptSourceProvider() -> ScriptSourceProviding { - ScriptSourceProvider(configStorage: ConfigurationStore.shared, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) + ScriptSourceProvider(configStorage: Application.appDelegate.configurationStore, privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, contentBlockingManager: ContentBlocking.shared.contentBlockingManager, trackerDataManager: ContentBlocking.shared.trackerDataManager, tld: ContentBlocking.shared.tld) } struct ScriptSourceProvider: ScriptSourceProviding { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index e35e718150..31983f62aa 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -32,7 +32,9 @@ public class DataBrokerProtectionPixelsHandler: EventMapping { event, error, _, _ in + let domainEvent: NetworkProtectionPixelEvent + switch event { + case .invalidPayload(let configuration): + domainEvent = .networkProtectionConfigurationInvalidPayload(configuration: configuration) + } + + PixelKit.fire(DebugEvent(domainEvent, error: error)) + } + + func log() { + Logger.config.log("last update \(String(describing: self.lastUpdateTime), privacy: .public)") + Logger.config.log("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)") + } + + override public func refreshNow(isDebug: Bool = false) async { + let updateConfigDependenciesTask = Task { + let didFetchConfig = await fetchConfigDependencies(isDebug: isDebug) + if didFetchConfig { + updateConfigDependencies() + tryAgainLater() + } + } + + await updateConfigDependenciesTask.value + + (store as? ConfigurationStore)?.log() + log() + } + + func fetchConfigDependencies(isDebug: Bool) async -> Bool { + do { + try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) + return true + } catch { + Logger.config.error( + "Failed to complete configuration update to \(Configuration.privacyConfiguration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) + tryAgainSoon() + } + + return false + } + + func updateConfigDependencies() { + privacyConfigManager.reload( + etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration) + ) + } +} + +extension ConfigurationManager { + override var presentedItemURL: URL? { + store.fileUrl(for: .privacyConfiguration).deletingLastPathComponent() + } + + override func presentedSubitemDidAppear(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateConfigDependencies() + } + + override func presentedSubitemDidChange(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateConfigDependencies() + } +} diff --git a/DuckDuckGoVPN/ConfigurationStore.swift b/DuckDuckGoVPN/ConfigurationStore.swift new file mode 100644 index 0000000000..844d4f3950 --- /dev/null +++ b/DuckDuckGoVPN/ConfigurationStore.swift @@ -0,0 +1,151 @@ +// +// ConfigurationStore.swift +// +// 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 os.log +import BrowserServicesKit +import Persistence +import Common +import Configuration +import PixelKit + +final class ConfigurationStore: ConfigurationStoring { + + private static let fileLocations: [Configuration: String] = [ + .privacyConfiguration: "macos-config.json", + ] + + enum Error: Swift.Error { + + case unsupportedConfig + + func withUnderlyingError(_ underlyingError: Swift.Error) -> Swift.Error { + let nsError = self as NSError + return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSUnderlyingErrorKey: underlyingError]) + } + + } + + private var privacyConfigurationEtagKey: String { + return "configurationPrivacyConfigurationEtag" + } + + let defaults: KeyValueStoring + + var privacyConfigurationEtag: String? { + get { + defaults.object(forKey: privacyConfigurationEtagKey) as? String + } + set { + defaults.set(newValue, forKey: privacyConfigurationEtagKey) + } + } + + init(defaults: KeyValueStoring = UserDefaults.appConfiguration) { + self.defaults = defaults + } + + func log() { + Logger.config.log("privacyConfigurationEtag \(self.privacyConfigurationEtag ?? "", privacy: .public)") + } + + func loadData(for config: Configuration) -> Data? { + let file = fileUrl(for: config) + var data: Data? + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(readingItemAt: file, error: &coordinatorError) { fileUrl in + do { + data = try Data(contentsOf: fileUrl) + } catch { + let nserror = error as NSError + + if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { + PixelKit.fire(DebugEvent(NetworkProtectionPixelEvent.networkProtectionConfigurationErrorLoadingCachedConfig(error))) + } + } + } + + if let coordinatorError { + Logger.config.error("Unable to read \(config.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } + + return data + } + + func loadEtag(for configuration: Configuration) -> String? { + guard configuration == .privacyConfiguration else { return nil } + + return privacyConfigurationEtag + } + + func loadEmbeddedEtag(for configuration: Configuration) -> String? { + // If we embed the full config some day we need to load the etag for it here + return nil + } + + func saveData(_ data: Data, for configuration: Configuration) throws { + guard configuration == .privacyConfiguration else { throw Error.unsupportedConfig } + let file = fileUrl(for: configuration) + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(writingItemAt: file, options: .forReplacing, error: &coordinatorError) { fileUrl in + do { + try data.write(to: fileUrl, options: .atomic) + } catch { + Logger.config.error("Unable to write \(configuration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + } + + if let coordinatorError { + Logger.config.error("Unable to write \(configuration.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } + } + + func saveEtag(_ etag: String, for configuration: Configuration) throws { + guard configuration == .privacyConfiguration else { throw Error.unsupportedConfig } + + privacyConfigurationEtag = etag + } + + func fileUrl(for config: Configuration) -> URL { + let fm = FileManager.default + + guard let dir = fm.containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.appGroup(bundle: .appConfiguration)) else { + fatalError("Failed to get application group URL") + } + let subDir = dir.appendingPathComponent("Configuration") + + var isDir: ObjCBool = false + if !fm.fileExists(atPath: subDir.path, isDirectory: &isDir) { + do { + try fm.createDirectory(at: subDir, withIntermediateDirectories: true, attributes: nil) + isDir = true + } catch { + fatalError("Failed to create directory at \(subDir.path)") + } + } + + if !isDir.boolValue { + fatalError("Configuration folder at \(subDir.path) is not a directory") + } + + return subDir.appendingPathComponent(Self.fileLocations[config]!) + } + +} diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 441975044d..0d1a02448e 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -19,7 +19,9 @@ import AppLauncher import Cocoa import Combine +import BrowserServicesKit import Common +import Configuration import LoginItems import Networking import NetworkExtension @@ -129,6 +131,11 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private let accountManager: AccountManager private let accessTokenStorage: SubscriptionTokenKeychainStorage + private let configurationStore = ConfigurationStore() + private let configurationManager: ConfigurationManager + private let privacyConfigurationManager = VPNPrivacyConfigurationManager() + private var configurationSubscription: AnyCancellable? + public init(accountManager: AccountManager, accessTokenStorage: SubscriptionTokenKeychainStorage, subscriptionEnvironment: SubscriptionEnvironment) { @@ -137,6 +144,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { self.accessTokenStorage = accessTokenStorage self.tunnelSettings = VPNSettings(defaults: .netP) self.tunnelSettings.alignTo(subscriptionEnvironment: subscriptionEnvironment) + self.configurationManager = ConfigurationManager(privacyConfigManager: privacyConfigurationManager, store: configurationStore) } private var cancellables = Set() @@ -357,6 +365,12 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) Logger.networkProtection.info("DuckDuckGoVPN started") + // Setup Remote Configuration + Configuration.setURLProvider(VPNAgentConfigurationURLProvider()) + configurationManager.start() + // Load cached config (if any) + privacyConfigurationManager.reload(etag: configurationStore.loadEtag(for: .privacyConfiguration), data: configurationStore.loadData(for: .privacyConfiguration)) + setupMenuVisibility() Task { @MainActor in @@ -372,6 +386,13 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { setUpSubscriptionMonitoring() + configurationSubscription = privacyConfigurationManager.updatesPublisher + .sink { [weak self] in + if self?.privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(BackgroundAgentPixelTestSubfeature.pixelTest) ?? false { + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionConfigurationPixelTest, frequency: .daily) + } + } + if launchedOnStartup { Task { let isConnected = await tunnelController.isConnected diff --git a/DuckDuckGoVPN/VPNAgentConfigurationURLProvider.swift b/DuckDuckGoVPN/VPNAgentConfigurationURLProvider.swift new file mode 100644 index 0000000000..0068cf3291 --- /dev/null +++ b/DuckDuckGoVPN/VPNAgentConfigurationURLProvider.swift @@ -0,0 +1,29 @@ +// +// VPNAgentConfigurationURLProvider.swift +// +// 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 BrowserServicesKit +import Configuration + +struct VPNAgentConfigurationURLProvider: ConfigurationURLProviding { + func url(for configuration: Configuration) -> URL { + guard configuration == .privacyConfiguration else { fatalError("\(configuration.rawValue) is not supported on this target") } + + return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json")! + } +} diff --git a/DuckDuckGoVPN/VPNPrivacyConfigurationManager.swift b/DuckDuckGoVPN/VPNPrivacyConfigurationManager.swift new file mode 100644 index 0000000000..e25274db35 --- /dev/null +++ b/DuckDuckGoVPN/VPNPrivacyConfigurationManager.swift @@ -0,0 +1,133 @@ +// +// VPNPrivacyConfigurationManager.swift +// +// Copyright © 2023 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 BrowserServicesKit +import Combine +import Common +import PixelKit + +public final class VPNPrivacyConfigurationManager: PrivacyConfigurationManaging { + + private let lock = NSLock() + + var embeddedConfigData: Data { + let configString = """ + { + "readme": "https://github.com/duckduckgo/privacy-configuration", + "version": 1693838894358, + "features": { + "networkProtection": { + "state": "enabled", + "exceptions": [], + "settings": {} + } + }, + "unprotectedTemporary": [] + } + """ + let data = configString.data(using: .utf8) + return data! + } + + private var _fetchedConfigData: PrivacyConfigurationManager.ConfigurationData? + private(set) public var fetchedConfigData: PrivacyConfigurationManager.ConfigurationData? { + get { + lock.lock() + let data = _fetchedConfigData + lock.unlock() + return data + } + set { + lock.lock() + _fetchedConfigData = newValue + lock.unlock() + } + } + + public var currentConfig: Data { + if let fetchedData = fetchedConfigData { + return fetchedData.rawData + } + return embeddedConfigData + } + + private let updatesSubject = PassthroughSubject() + public var updatesPublisher: AnyPublisher { + updatesSubject.eraseToAnyPublisher() + } + + public var privacyConfig: BrowserServicesKit.PrivacyConfiguration { + guard let privacyConfigurationData = try? PrivacyConfigurationData(data: currentConfig) else { + fatalError("Could not retrieve privacy configuration data") + } + let privacyConfig = privacyConfiguration(withData: privacyConfigurationData, + internalUserDecider: internalUserDecider) + return privacyConfig + } + + public var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) + + @discardableResult + public func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + let result: PrivacyConfigurationManager.ReloadResult + + if let etag = etag, let data = data { + result = .downloaded + + do { + let configData = try PrivacyConfigurationData(data: data) + fetchedConfigData = (data, configData, etag) + } catch { + PixelKit.fire(NetworkProtectionPixelEvent.networkProtectionConfigurationFailedToParse(error)) + fetchedConfigData = nil + return .embeddedFallback + } + } else { + fetchedConfigData = nil + result = .embedded + } + + return result + } +} + +func privacyConfiguration(withData data: PrivacyConfigurationData, + internalUserDecider: InternalUserDecider) -> PrivacyConfiguration { + let domain = MockDomainsProtectionStore() + return AppPrivacyConfiguration(data: data, + identifier: UUID().uuidString, + localProtection: domain, + internalUserDecider: internalUserDecider) +} + +final class MockDomainsProtectionStore: DomainsProtectionStore { + var unprotectedDomains = Set() + + func disableProtection(forDomain domain: String) { + unprotectedDomains.insert(domain) + } + + func enableProtection(forDomain domain: String) { + unprotectedDomains.remove(domain) + } +} + +final class InternalUserDeciderStoreMock: InternalUserStoring { + var isInternalUser: Bool = false +} diff --git a/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift b/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift index 9d8f51425d..2609eb2f98 100644 --- a/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift +++ b/IntegrationTests/HTTPSUpgrade/HTTPSUpgradeIntegrationTests.swift @@ -43,7 +43,6 @@ class HTTPSUpgradeIntegrationTests: XCTestCase { window = WindowsManager.openNewWindow(with: .none)! XCTAssertTrue(AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig.isFeature(.httpsUpgrade, enabledForDomain: "privacy-test-pages.site")) - await ConfigurationManager.shared.refreshIfNeeded()?.value } @MainActor diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 3231f53dff..7323dce45a 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "196.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "196.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], @@ -41,6 +41,8 @@ let package = Package( .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), .byName(name: "XPCHelper"), .product(name: "PixelKit", package: "BrowserServicesKit"), + .product(name: "Configuration", package: "BrowserServicesKit"), + .product(name: "Persistence", package: "BrowserServicesKit") ], resources: [.process("Resources")], swiftSettings: [ diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Bundle/BundleExtension.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Bundle/BundleExtension.swift index 4dc092e9b3..6818f2f23d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Bundle/BundleExtension.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Bundle/BundleExtension.swift @@ -25,6 +25,7 @@ protocol GroupNameProviding { extension Bundle: GroupNameProviding { static let dbpAppGroupName = "DBP_APP_GROUP" + static let configAppGroupName = "APP_CONFIGURATION_APP_GROUP" var appGroupName: String { guard let appGroup = object(forInfoDictionaryKey: Bundle.dbpAppGroupName) as? String else { @@ -32,4 +33,11 @@ extension Bundle: GroupNameProviding { } return appGroup } + + var configAppGroupName: String { + guard let appGroup = object(forInfoDictionaryKey: Bundle.configAppGroupName) as? String else { + fatalError("Info.plist is missing \(Bundle.configAppGroupName)") + } + return appGroup + } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/ConfigurationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/ConfigurationManager.swift new file mode 100644 index 0000000000..380619c261 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/ConfigurationManager.swift @@ -0,0 +1,110 @@ +// +// ConfigurationManager.swift +// +// 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 os.log +import BrowserServicesKit +import Persistence +import Configuration +import Common +import Networking +import PixelKit + +public extension Logger { + static var config: Logger = { Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Configuration") }() +} + +final class ConfigurationManager: DefaultConfigurationManager { + + private let privacyConfigManager: DBPPrivacyConfigurationManager + + init(privacyConfigManager: DBPPrivacyConfigurationManager, + fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), + store: ConfigurationStoring = ConfigurationStore(), + defaults: KeyValueStoring = UserDefaults.config) { + self.privacyConfigManager = privacyConfigManager + super.init(fetcher: fetcher, store: store, defaults: defaults) + } + + static let configurationDebugEvents = EventMapping { event, error, _, _ in + let domainEvent: DataBrokerProtectionPixels + switch event { + case .invalidPayload(let configuration): + domainEvent = .invalidPayload(configuration) + } + + PixelKit.fire(DebugEvent(domainEvent, error: error)) + } + + func log() { + Logger.config.log("last update \(String(describing: self.lastUpdateTime), privacy: .public)") + Logger.config.log("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)") + } + + override public func refreshNow(isDebug: Bool = false) async { + let updateConfigDependenciesTask = Task { + let didFetchConfig = await fetchConfigDependencies(isDebug: isDebug) + if didFetchConfig { + updateConfigDependencies() + tryAgainLater() + } + } + + await updateConfigDependenciesTask.value + + (store as? ConfigurationStore)?.log() + log() + } + + func fetchConfigDependencies(isDebug: Bool) async -> Bool { + do { + try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) + return true + } catch { + Logger.config.error( + "Failed to complete configuration update to \(Configuration.privacyConfiguration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) + tryAgainSoon() + } + + return false + } + + func updateConfigDependencies() { + privacyConfigManager.reload( + etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration) + ) + } +} + +extension ConfigurationManager { + override var presentedItemURL: URL? { + store.fileUrl(for: .privacyConfiguration).deletingLastPathComponent() + } + + override func presentedSubitemDidAppear(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateConfigDependencies() + } + + override func presentedSubitemDidChange(at url: URL) { + guard url == store.fileUrl(for: .privacyConfiguration) else { return } + updateConfigDependencies() + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/ConfigurationStore.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/ConfigurationStore.swift new file mode 100644 index 0000000000..a870961d3c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/ConfigurationStore.swift @@ -0,0 +1,151 @@ +// +// ConfigurationStore.swift +// +// 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 os.log +import BrowserServicesKit +import Persistence +import Common +import Configuration +import PixelKit + +final class ConfigurationStore: ConfigurationStoring { + + private static let fileLocations: [Configuration: String] = [ + .privacyConfiguration: "macos-config.json", + ] + + enum Error: Swift.Error { + + case unsupportedConfig + + func withUnderlyingError(_ underlyingError: Swift.Error) -> Swift.Error { + let nsError = self as NSError + return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSUnderlyingErrorKey: underlyingError]) + } + + } + + private var privacyConfigurationEtagKey: String { + return "configurationPrivacyConfigurationEtag" + } + + let defaults: KeyValueStoring + + var privacyConfigurationEtag: String? { + get { + defaults.object(forKey: privacyConfigurationEtagKey) as? String + } + set { + defaults.set(newValue, forKey: privacyConfigurationEtagKey) + } + } + + init(defaults: KeyValueStoring = UserDefaults.config) { + self.defaults = defaults + } + + func log() { + Logger.config.log("privacyConfigurationEtag \(self.privacyConfigurationEtag ?? "", privacy: .public)") + } + + func loadData(for config: Configuration) -> Data? { + let file = fileUrl(for: config) + var data: Data? + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(readingItemAt: file, error: &coordinatorError) { fileUrl in + do { + data = try Data(contentsOf: fileUrl) + } catch { + let nserror = error as NSError + + if nserror.domain != NSCocoaErrorDomain || nserror.code != NSFileReadNoSuchFileError { + PixelKit.fire(DebugEvent(DataBrokerProtectionPixels.errorLoadingCachedConfig(error))) + } + } + } + + if let coordinatorError { + Logger.config.error("Unable to read \(config.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } + + return data + } + + func loadEtag(for configuration: Configuration) -> String? { + guard configuration == .privacyConfiguration else { return nil } + + return privacyConfigurationEtag + } + + func loadEmbeddedEtag(for configuration: Configuration) -> String? { + // If we embed the full config some day we need to load the etag for it here + return nil + } + + func saveData(_ data: Data, for configuration: Configuration) throws { + guard configuration == .privacyConfiguration else { throw Error.unsupportedConfig } + let file = fileUrl(for: configuration) + var coordinatorError: NSError? + + NSFileCoordinator().coordinate(writingItemAt: file, options: .forReplacing, error: &coordinatorError) { fileUrl in + do { + try data.write(to: fileUrl, options: .atomic) + } catch { + Logger.config.error("Unable to write \(configuration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + } + + if let coordinatorError { + Logger.config.error("Unable to write \(configuration.rawValue, privacy: .public): \(coordinatorError.localizedDescription, privacy: .public)") + } + } + + func saveEtag(_ etag: String, for configuration: Configuration) throws { + guard configuration == .privacyConfiguration else { throw Error.unsupportedConfig } + + privacyConfigurationEtag = etag + } + + func fileUrl(for config: Configuration) -> URL { + let fm = FileManager.default + + guard let dir = fm.containerURL(forSecurityApplicationGroupIdentifier: Bundle.main.configAppGroup) else { + fatalError("Failed to get application group URL") + } + let subDir = dir.appendingPathComponent("Configuration") + + var isDir: ObjCBool = false + if !fm.fileExists(atPath: subDir.path, isDirectory: &isDir) { + do { + try fm.createDirectory(at: subDir, withIntermediateDirectories: true, attributes: nil) + isDir = true + } catch { + fatalError("Failed to create directory at \(subDir.path)") + } + } + + if !isDir.boolValue { + fatalError("Configuration folder at \(subDir.path) is not a directory") + } + + return subDir.appendingPathComponent(Self.fileLocations[config]!) + } + +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/DBPAgentConfigurationURLProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/DBPAgentConfigurationURLProvider.swift new file mode 100644 index 0000000000..78f38b6f05 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/DBPAgentConfigurationURLProvider.swift @@ -0,0 +1,29 @@ +// +// DBPAgentConfigurationURLProvider.swift +// +// 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 BrowserServicesKit +import Configuration + +struct DBPAgentConfigurationURLProvider: ConfigurationURLProviding { + func url(for configuration: Configuration) -> URL { + guard configuration == .privacyConfiguration else { fatalError("\(configuration.rawValue) is not supported on this target") } + + return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json")! + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/DBPPrivacyConfigurationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/DBPPrivacyConfigurationManager.swift new file mode 100644 index 0000000000..10a734da85 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Configuration/DBPPrivacyConfigurationManager.swift @@ -0,0 +1,118 @@ +// +// DBPPrivacyConfigurationManager.swift +// +// Copyright © 2023 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 BrowserServicesKit +import Combine +import Common +import PixelKit + +public final class DBPPrivacyConfigurationManager: PrivacyConfigurationManaging { + + private let lock = NSLock() + + var embeddedConfigData: Data { + let configString = """ + { + "readme": "https://github.com/duckduckgo/privacy-configuration", + "version": 1693838894358, + "features": { + "brokerProtection": { + "state": "enabled", + "exceptions": [], + "settings": {} + } + }, + "unprotectedTemporary": [] + } + """ + let data = configString.data(using: .utf8) + return data! + } + + private var _fetchedConfigData: PrivacyConfigurationManager.ConfigurationData? + private(set) public var fetchedConfigData: PrivacyConfigurationManager.ConfigurationData? { + get { + lock.lock() + let data = _fetchedConfigData + lock.unlock() + return data + } + set { + lock.lock() + _fetchedConfigData = newValue + lock.unlock() + } + } + + public var currentConfig: Data { + if let fetchedData = fetchedConfigData { + return fetchedData.rawData + } + return embeddedConfigData + } + + private let updatesSubject = PassthroughSubject() + public var updatesPublisher: AnyPublisher { + updatesSubject.eraseToAnyPublisher() + } + + public var privacyConfig: BrowserServicesKit.PrivacyConfiguration { + guard let privacyConfigurationData = try? PrivacyConfigurationData(data: currentConfig) else { + fatalError("Could not retrieve privacy configuration data") + } + let privacyConfig = privacyConfiguration(withData: privacyConfigurationData, + internalUserDecider: internalUserDecider) + return privacyConfig + } + + public var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) + + @discardableResult + public func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + let result: PrivacyConfigurationManager.ReloadResult + + if let etag = etag, let data = data { + result = .downloaded + + do { + let configData = try PrivacyConfigurationData(data: data) + fetchedConfigData = (data, configData, etag) + updatesSubject.send(()) + } catch { + PixelKit.fire(DebugEvent(DataBrokerProtectionPixels.failedToParsePrivacyConfig(error), error: error)) + fetchedConfigData = nil + return .embeddedFallback + } + } else { + fetchedConfigData = nil + result = .embedded + } + + return result + } +} + +func privacyConfiguration(withData data: PrivacyConfigurationData, + internalUserDecider: InternalUserDecider) -> PrivacyConfiguration { + let domain = MockDomainsProtectionStore() + return AppPrivacyConfiguration(data: data, + identifier: UUID().uuidString, + localProtection: domain, + internalUserDecider: internalUserDecider) +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 11f3086983..b2e8656204 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -155,7 +155,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { private let authenticationManager: DataBrokerProtectionAuthenticationManaging init(authenticationManager: DataBrokerProtectionAuthenticationManaging) { - let privacyConfigurationManager = PrivacyConfigurationManagingMock() + let privacyConfigurationManager = DBPPrivacyConfigurationManager() let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, credentialsAutofill: false, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index d008a454df..d7b4cbd67c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -19,6 +19,7 @@ import Foundation import Common import BrowserServicesKit +import Configuration import PixelKit enum ErrorCategory: Equatable { @@ -54,6 +55,7 @@ public enum DataBrokerProtectionPixels { static let triesKey = "tries" static let errorCategoryKey = "error_category" static let errorDetailsKey = "error_details" + static let errorDomainKey = "error_domain" static let pattern = "pattern" static let isParent = "is_parent" static let actionIDKey = "action_id" @@ -189,6 +191,12 @@ public enum DataBrokerProtectionPixels { case entitlementCheckInvalid case entitlementCheckError + // Configuration + case invalidPayload(Configuration) + case errorLoadingCachedConfig(Error) + case pixelTest + case failedToParsePrivacyConfig(Error) + // Measure success/failure rate of Personal Information Removal Pixels // https://app.asana.com/0/1204006570077678/1206889724879222/f case globalMetricsWeeklyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int) @@ -331,6 +339,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .gatekeeperNotAuthenticated: return "m_mac_dbp_gatekeeper_not_authenticated" case .gatekeeperEntitlementsInvalid: return "m_mac_dbp_gatekeeper_entitlements_invalid" + // Configuration + case .invalidPayload(let configuration): return "m_mac_dbp_\(configuration.rawValue)_invalid_payload".lowercased() + case .errorLoadingCachedConfig: return "m_mac_dbp_configuration_error_loading_cached_config" + case .pixelTest: return "m_mac_dbp_configuration_pixel_test" + case .failedToParsePrivacyConfig: return "m_mac_dbp_configuration_failed_to_parse" + case .customDataBrokerStatsOptoutSubmit: return "m_mac_dbp_databroker_custom_stats_optoutsubmit" case .customGlobalStatsOptoutSubmit: return "m_mac_dbp_custom_stats_optoutsubmit" } @@ -442,7 +456,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .secureVaultKeyStoreUpdateError, .secureVaultError, .gatekeeperNotAuthenticated, - .gatekeeperEntitlementsInvalid: + .gatekeeperEntitlementsInvalid, + .invalidPayload, + .pixelTest, + .failedToParsePrivacyConfig: return [:] case .ipcServerProfileSavedCalledByApp, .ipcServerProfileSavedReceivedByAgent, @@ -498,6 +515,8 @@ extension DataBrokerProtectionPixels: PixelKitEvent { Consts.durationOfFirstOptOut: String(durationOfFirstOptOut), Consts.numberOfNewRecordsFound: String(numberOfNewRecordsFound), Consts.numberOfReappereances: String(numberOfReappereances)] + case .errorLoadingCachedConfig(let error): + return [Consts.errorDomainKey: (error as NSError).domain] case .customDataBrokerStatsOptoutSubmit(let dataBrokerName, let optOutSubmitSuccessRate): return [Consts.dataBrokerParamKey: dataBrokerName, Consts.optOutSubmitSuccessRate: String(optOutSubmitSuccessRate)] @@ -520,10 +539,13 @@ public class DataBrokerProtectionPixelsHandler: EventMapping = .init(Just(())) - - public var privacyConfig: BrowserServicesKit.PrivacyConfiguration { - guard let privacyConfigurationData = try? PrivacyConfigurationData(data: data) else { - fatalError("Could not retrieve privacy configuration data") - } - let privacyConfig = privacyConfiguration(withData: privacyConfigurationData, - internalUserDecider: internalUserDecider) - return privacyConfig - } - - public var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: InternalUserDeciderStoreMock()) - - public func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { - .downloaded - } -} - -func privacyConfiguration(withData data: PrivacyConfigurationData, - internalUserDecider: InternalUserDecider) -> PrivacyConfiguration { - let domain = MockDomainsProtectionStore() - return AppPrivacyConfiguration(data: data, - identifier: UUID().uuidString, - localProtection: domain, - internalUserDecider: internalUserDecider) -} final class MockDomainsProtectionStore: DomainsProtectionStore { var unprotectedDomains = Set() diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index 7c7630ca93..22df56a835 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -17,8 +17,10 @@ // import Foundation +import Combine import Common import BrowserServicesKit +import Configuration import PixelKit import os.log @@ -32,7 +34,13 @@ public class DataBrokerProtectionAgentManagerProvider { let activityScheduler = DefaultDataBrokerProtectionBackgroundActivityScheduler(config: executionConfig) let notificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) - let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned + Configuration.setURLProvider(DBPAgentConfigurationURLProvider()) + let configStore = ConfigurationStore() + let privacyConfigurationManager = DBPPrivacyConfigurationManager() + let configurationManager = ConfigurationManager(privacyConfigManager: privacyConfigurationManager, store: configStore) + configurationManager.start() + // Load cached config (if any) + privacyConfigurationManager.reload(etag: configStore.loadEtag(for: .privacyConfiguration), data: configStore.loadData(for: .privacyConfiguration)) let ipcServer = DefaultDataBrokerProtectionIPCServer(machServiceName: Bundle.main.bundleIdentifier!) let features = ContentScopeFeatureToggles(emailProtection: false, @@ -95,7 +103,9 @@ public class DataBrokerProtectionAgentManagerProvider { dataManager: dataManager, operationDependencies: operationDependencies, pixelHandler: pixelHandler, - agentStopper: agentstopper) + agentStopper: agentstopper, + configurationManager: configurationManager, + privacyConfigurationManager: privacyConfigurationManager) } } @@ -109,12 +119,16 @@ public final class DataBrokerProtectionAgentManager { private let operationDependencies: DataBrokerOperationDependencies private let pixelHandler: EventMapping private let agentStopper: DataBrokerProtectionAgentStopper + private let configurationManger: DefaultConfigurationManager + private let privacyConfigurationManager: DBPPrivacyConfigurationManager // Used for debug functions only, so not injected private lazy var browserWindowManager = BrowserWindowManager() private var didStartActivityScheduler = false + private var configurationSubscription: AnyCancellable? + init(userNotificationService: DataBrokerProtectionUserNotificationService, activityScheduler: DataBrokerProtectionBackgroundActivityScheduler, ipcServer: DataBrokerProtectionIPCServer, @@ -122,7 +136,9 @@ public final class DataBrokerProtectionAgentManager { dataManager: DataBrokerProtectionDataManaging, operationDependencies: DataBrokerOperationDependencies, pixelHandler: EventMapping, - agentStopper: DataBrokerProtectionAgentStopper + agentStopper: DataBrokerProtectionAgentStopper, + configurationManager: DefaultConfigurationManager, + privacyConfigurationManager: DBPPrivacyConfigurationManager ) { self.userNotificationService = userNotificationService self.activityScheduler = activityScheduler @@ -132,6 +148,8 @@ public final class DataBrokerProtectionAgentManager { self.operationDependencies = operationDependencies self.pixelHandler = pixelHandler self.agentStopper = agentStopper + self.configurationManger = configurationManager + self.privacyConfigurationManager = privacyConfigurationManager self.activityScheduler.delegate = self self.ipcServer.serverDelegate = self @@ -154,6 +172,13 @@ public final class DataBrokerProtectionAgentManager { /// Monitors entitlement changes every 60 minutes to optimize system performance and resource utilization by avoiding unnecessary operations when entitlement is invalid. /// While keeping the agent active with invalid entitlement has no significant risk, setting the monitoring interval at 60 minutes is a good balance to minimize backend checks. agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: .minutes(60)) + + configurationSubscription = privacyConfigurationManager.updatesPublisher + .sink { [weak self] _ in + if self?.privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(BackgroundAgentPixelTestSubfeature.pixelTest) ?? false { + PixelKit.fire(DataBrokerProtectionPixels.pixelTest, frequency: .daily) + } + } } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionBundleExtension.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionBundleExtension.swift index d6f4b9a011..87047625ee 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionBundleExtension.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionBundleExtension.swift @@ -20,6 +20,7 @@ import Foundation extension UserDefaults { static let dbp = UserDefaults(suiteName: Bundle.main.dbpAppGroup)! + static let config = UserDefaults(suiteName: Bundle.main.configAppGroup)! } extension Bundle { @@ -30,4 +31,11 @@ extension Bundle { } return appGroup } + + var configAppGroup: String { + guard let appGroup = object(forInfoDictionaryKey: Bundle.configAppGroupName) as? String else { + fatalError("Info.plist is missing \(Bundle.configAppGroupName)") + } + return appGroup + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift index 185e21f2f8..d0ccaec883 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift @@ -17,6 +17,8 @@ // import XCTest +import Configuration +import Persistence @testable import DataBrokerProtection final class DataBrokerProtectionAgentManagerTests: XCTestCase { @@ -32,6 +34,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { private var mockDependencies: DefaultDataBrokerOperationDependencies! private var mockProfile: DataBrokerProtectionProfile! private var mockAgentStopper: MockAgentStopper! + private var mockConfigurationManager: MockConfigurationManager! + private var mockPrivacyConfigurationManager: DBPPrivacyConfigurationManager! override func setUpWithError() throws { @@ -39,6 +43,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { mockActivityScheduler = MockDataBrokerProtectionBackgroundActivityScheduler() mockNotificationService = MockUserNotificationService() mockAgentStopper = MockAgentStopper() + mockConfigurationManager = MockConfigurationManager() + mockPrivacyConfigurationManager = DBPPrivacyConfigurationManager() let mockDatabase = MockDatabase() let mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) @@ -78,7 +84,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockDataManager.profileToReturn = mockProfile @@ -121,7 +129,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: agentStopper) + agentStopper: agentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockDataManager.profileToReturn = nil @@ -153,7 +163,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockDataManager.profileToReturn = nil @@ -190,7 +202,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockDataManager.profileToReturn = mockProfile @@ -216,7 +230,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockDataManager.profileToReturn = mockProfile @@ -242,7 +258,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockNotificationService.reset() @@ -263,7 +281,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockNotificationService.reset() @@ -284,7 +304,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockNotificationService.reset() mockQueueManager.startImmediateOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) @@ -306,7 +328,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = true @@ -328,7 +352,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = false @@ -350,7 +376,9 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { dataManager: mockDataManager, operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, - agentStopper: mockAgentStopper) + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager) var startScheduledScansCalled = false mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in @@ -364,3 +392,48 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(startScheduledScansCalled) } } + +struct MockConfigurationFetcher: ConfigurationFetching { + func fetch(_ configuration: Configuration, isDebug: Bool) async throws { + return + } + + func fetch(all configurations: [Configuration]) async throws { + return + } +} + +struct MockConfigurationStore: ConfigurationStoring { + func loadData(for configuration: Configuration) -> Data? { + return nil + } + + func loadEtag(for configuration: Configuration) -> String? { + return nil + } + + func loadEmbeddedEtag(for configuration: Configuration) -> String? { + return nil + } + + mutating func saveData(_ data: Data, for configuration: Configuration) throws { + return + } + + mutating func saveEtag(_ etag: String, for configuration: Configuration) throws { + return + } + + func fileUrl(for configuration: Configuration) -> URL { + return URL(string: "file:///\(configuration.rawValue)")! + } + +} + +final class MockConfigurationManager: DefaultConfigurationManager { + override init(fetcher: ConfigurationFetching = MockConfigurationFetcher(), + store: ConfigurationStoring = MockConfigurationStore(), + defaults: KeyValueStoring = UserDefaults()) { + super.init(fetcher: fetcher, store: store, defaults: defaults) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 368598cf8c..7eda40ac91 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -19,6 +19,7 @@ import BrowserServicesKit import Combine import Common +import Configuration import Foundation import GRDB import SecureStorage diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 293af5fb1c..d9ab035a2d 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "196.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "196.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 25967a68c6..16a305fb81 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "196.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "196.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift b/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift index cdc70e2ca2..e19f21d6b9 100644 --- a/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift +++ b/UnitTests/Autoconsent/AutoconsentMessageProtocolTests.swift @@ -29,8 +29,8 @@ class AutoconsentMessageProtocolTests: XCTestCase { privacyConfigurationManager: MockPrivacyConfigurationManager(), webTrackingProtectionPreferences: WebTrackingProtectionPreferences.shared, // mock contentBlockingManager: ContentBlockerRulesManagerMock(), - trackerDataManager: TrackerDataManager(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), - data: ConfigurationStore.shared.loadData(for: .trackerDataSet), + trackerDataManager: TrackerDataManager(etag: ConfigurationStore().loadEtag(for: .trackerDataSet), + data: ConfigurationStore().loadData(for: .trackerDataSet), embeddedDataProvider: AppTrackerDataSetProvider(), errorReporting: nil), tld: TLD()), diff --git a/UnitTests/Common/UserDefaultsWrapperTests.swift b/UnitTests/Common/UserDefaultsWrapperTests.swift index daf2f90f89..8d4e2f2f12 100644 --- a/UnitTests/Common/UserDefaultsWrapperTests.swift +++ b/UnitTests/Common/UserDefaultsWrapperTests.swift @@ -304,24 +304,24 @@ final class UserDefaultsWrapperTests: XCTestCase { // MARK: Optional with enum Key func testOptionalRawRepresentableValueDefaultValueWithEnumKey() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaultValue: MyRawRepresentable(rawValue: "value"), defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: MyRawRepresentable(rawValue: "value"), defaults: defaults) XCTAssertEqual(wrapper.wrappedValue, MyRawRepresentable(rawValue: "value")) } func testOptionalRawRepresentableValueNilValueWithEnumKey() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) XCTAssertNil(wrapper.wrappedValue) } func testOptionalRawRepresentableValueUpdatingWithEnumKey() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) wrapper.wrappedValue = MyRawRepresentable(rawValue: "new") XCTAssertEqual(wrapper.wrappedValue, MyRawRepresentable(rawValue: "new")) - XCTAssertEqual(defaults.dictionary as! [String: String], [UserDefaultsWrapper.Key.configLastUpdated.rawValue: "new"]) + XCTAssertEqual(defaults.dictionary as! [String: String], [UserDefaultsWrapper.Key.lastCrashReportCheckDate.rawValue: "new"]) } func testOptionalRawRepresentableValueRemovalWithEnumKey() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) wrapper.wrappedValue = MyRawRepresentable(rawValue: "new") wrapper.wrappedValue = nil XCTAssertNil(wrapper.wrappedValue) @@ -329,7 +329,7 @@ final class UserDefaultsWrapperTests: XCTestCase { } func testOptionalRawRepresentableValueClearWithEnumKey() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) wrapper.wrappedValue = MyRawRepresentable(rawValue: "new") wrapper.clear() XCTAssertNil(wrapper.wrappedValue) @@ -337,14 +337,14 @@ final class UserDefaultsWrapperTests: XCTestCase { } func testOptionalRawRepresentableValueSharedDefaultsWithEnumKey() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate) XCTAssertNil(wrapper.wrappedValue) wrapper.wrappedValue = MyRawRepresentable(rawValue: "new") XCTAssertEqual(wrapper.wrappedValue, MyRawRepresentable(rawValue: "new")) - UserDefaultsWrapper.clear(.configLastUpdated) + UserDefaultsWrapper.clear(.lastCrashReportCheckDate) XCTAssertNil(wrapper.wrappedValue) - XCTAssertNil(UserDefaultsWrapper.sharedDefaults.object(forKey: UserDefaultsWrapper.Key.configLastUpdated.rawValue)) + XCTAssertNil(UserDefaultsWrapper.sharedDefaults.object(forKey: UserDefaultsWrapper.Key.lastCrashReportCheckDate.rawValue)) } // MARK: Date @@ -353,19 +353,19 @@ final class UserDefaultsWrapperTests: XCTestCase { let date2 = Date().addingTimeInterval(1) func testDateValueDefaultValue() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaultValue: date1, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: date1, defaults: defaults) XCTAssertEqual(wrapper.wrappedValue, date1) } func testDateValueUpdating() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaultValue: date1, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: date1, defaults: defaults) wrapper.wrappedValue = date2 XCTAssertEqual(wrapper.wrappedValue, date2) - XCTAssertEqual(defaults.dictionary as! [String: Date], [UserDefaultsWrapper.Key.configLastUpdated.rawValue: date2]) + XCTAssertEqual(defaults.dictionary as! [String: Date], [UserDefaultsWrapper.Key.lastCrashReportCheckDate.rawValue: date2]) } func testDateValueClear() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaultValue: date1, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: date1, defaults: defaults) wrapper.wrappedValue = date2 wrapper.clear() XCTAssertEqual(wrapper.wrappedValue, date1) @@ -373,37 +373,37 @@ final class UserDefaultsWrapperTests: XCTestCase { } func testDateValueSharedDefaults() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaultValue: date1) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: date1) XCTAssertEqual(wrapper.wrappedValue, date1) wrapper.wrappedValue = date2 XCTAssertEqual(wrapper.wrappedValue, date2) - UserDefaultsWrapper.clear(.configLastUpdated) + UserDefaultsWrapper.clear(.lastCrashReportCheckDate) XCTAssertEqual(wrapper.wrappedValue, date1) - XCTAssertNil(UserDefaultsWrapper.sharedDefaults.object(forKey: UserDefaultsWrapper.Key.configLastUpdated.rawValue)) + XCTAssertNil(UserDefaultsWrapper.sharedDefaults.object(forKey: UserDefaultsWrapper.Key.lastCrashReportCheckDate.rawValue)) } // MARK: Optional func testOptionalDateValueDefaultValue() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaultValue: date1, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: date1, defaults: defaults) XCTAssertEqual(wrapper.wrappedValue, date1) } func testOptionalDateValueNilValue() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) XCTAssertNil(wrapper.wrappedValue) } func testOptionalDateValueUpdating() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) wrapper.wrappedValue = date2 XCTAssertEqual(wrapper.wrappedValue, date2) - XCTAssertEqual(defaults.dictionary as! [String: Date], [UserDefaultsWrapper.Key.configLastUpdated.rawValue: date2]) + XCTAssertEqual(defaults.dictionary as! [String: Date], [UserDefaultsWrapper.Key.lastCrashReportCheckDate.rawValue: date2]) } func testOptionalDateValueRemoval() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) wrapper.wrappedValue = date2 wrapper.wrappedValue = nil XCTAssertNil(wrapper.wrappedValue) @@ -411,7 +411,7 @@ final class UserDefaultsWrapperTests: XCTestCase { } func testOptionalDateValueClear() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated, defaults: defaults) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaults: defaults) wrapper.wrappedValue = date2 wrapper.clear() XCTAssertNil(wrapper.wrappedValue) @@ -419,14 +419,14 @@ final class UserDefaultsWrapperTests: XCTestCase { } func testOptionalDateValueSharedDefaults() { - let wrapper = UserDefaultsWrapper(key: .configLastUpdated) + let wrapper = UserDefaultsWrapper(key: .lastCrashReportCheckDate) XCTAssertNil(wrapper.wrappedValue) wrapper.wrappedValue = date2 XCTAssertEqual(wrapper.wrappedValue, date2) - UserDefaultsWrapper.clear(.configLastUpdated) + UserDefaultsWrapper.clear(.lastCrashReportCheckDate) XCTAssertNil(wrapper.wrappedValue) - XCTAssertNil(UserDefaultsWrapper.sharedDefaults.object(forKey: UserDefaultsWrapper.Key.configLastUpdated.rawValue)) + XCTAssertNil(UserDefaultsWrapper.sharedDefaults.object(forKey: UserDefaultsWrapper.Key.lastCrashReportCheckDate.rawValue)) } } diff --git a/UnitTests/Configuration/ConfigurationStorageTests.swift b/UnitTests/Configuration/ConfigurationStorageTests.swift index 81d4b7a890..9b2876ddfb 100644 --- a/UnitTests/Configuration/ConfigurationStorageTests.swift +++ b/UnitTests/Configuration/ConfigurationStorageTests.swift @@ -23,10 +23,12 @@ import Configuration final class ConfigurationStorageTests: XCTestCase { + var configurationStore: ConfigurationStore = ConfigurationStore() + override func tearDown() { super.tearDown() for config in Configuration.allCases { - let url = ConfigurationStore.shared.fileUrl(for: config) + let url = configurationStore.fileUrl(for: config) try? FileManager.default.removeItem(at: url) } } @@ -34,16 +36,16 @@ final class ConfigurationStorageTests: XCTestCase { func test_when_data_is_saved_for_config_then_it_can_be_loaded_correctly() { for config in Configuration.allCases { let uuid = UUID().uuidString - try? ConfigurationStore.shared.saveData(uuid.data(using: .utf8)!, for: config) - XCTAssertEqual(uuid, ConfigurationStore.shared.loadData(for: config)?.utf8String()) + try? configurationStore.saveData(uuid.data(using: .utf8)!, for: config) + XCTAssertEqual(uuid, configurationStore.loadData(for: config)?.utf8String()) } } func test_when_etag_is_saved_for_config_then_it_can_be_loaded_correctly() { for config in Configuration.allCases { let etag = UUID().uuidString - try? ConfigurationStore.shared.saveEtag(etag, for: config) - XCTAssertEqual(etag, ConfigurationStore.shared.loadEtag(for: config)) + try? configurationStore.saveEtag(etag, for: config) + XCTAssertEqual(etag, configurationStore.loadEtag(for: config)) } } diff --git a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift index 524c1501a5..44e256cd74 100644 --- a/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift +++ b/UnitTests/ContentBlocker/ContentBlockingUpdatingTests.swift @@ -31,12 +31,13 @@ final class ContentBlockingUpdatingTests: XCTestCase { @MainActor override func setUp() { + let configStore = ConfigurationStore() updating = UserContentUpdating(contentBlockerRulesManager: rulesManager, privacyConfigurationManager: MockPrivacyConfigurationManager(), - trackerDataManager: TrackerDataManager(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet), - data: ConfigurationStore.shared.loadData(for: .trackerDataSet), - embeddedDataProvider: AppTrackerDataSetProvider(), - errorReporting: nil), + trackerDataManager: TrackerDataManager(etag: configStore.loadEtag(for: .trackerDataSet), + data: configStore.loadData(for: .trackerDataSet), + embeddedDataProvider: AppTrackerDataSetProvider(), + errorReporting: nil), configStorage: MockConfigurationStore(), webTrackingProtectionPreferences: preferences, tld: TLD()) diff --git a/UnitTests/ContentBlocker/MockConfigurationStore.swift b/UnitTests/ContentBlocker/MockConfigurationStore.swift index f7d19af95d..1204b3cf8c 100644 --- a/UnitTests/ContentBlocker/MockConfigurationStore.swift +++ b/UnitTests/ContentBlocker/MockConfigurationStore.swift @@ -60,4 +60,8 @@ final class MockConfigurationStore: ConfigurationStoring { func log() { } + func fileUrl(for configuration: Configuration) -> URL { + return URL(string: "file///\(configuration.rawValue)")! + } + }